diff --git a/.eslintignore b/.eslintignore
index c95db6dce80..33f0e28918b 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -36,3 +36,5 @@ _book/**
/packages/object-store/*.js
/packages/lzards-api-client/*.d.ts
/packages/lzards-api-client/*.js
+/tasks/move-granule-collections/*.js
+/tasks/move-granule-collections/*.d.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e1d09cbd68..4200356d362 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,6 +30,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
### Added
+- **CUMULUS-3757**
+ - Added a database helper function to assist with updating a granule and its files PG fields when moving the granule(s) across collections
- **CUMULUS-3919**
- Added terraform variables `disableSSL` and `rejectUnauthorized` to `tf-modules/cumulus-rds-tf` module.
diff --git a/docs/tasks.md b/docs/tasks.md
index e0a9b7a4f34..081b488fc65 100644
--- a/docs/tasks.md
+++ b/docs/tasks.md
@@ -80,6 +80,15 @@ Move granule files from staging to final location
---
+### [@cumulus/move-granule-collections](https://github.com/nasa/cumulus/tree/master/tasks/move-granule-collections)
+
+Move granule files and records in cumulus datastores from one collection to another
+
+- Schemas: See this module's [schema definitions](https://github.com/nasa/cumulus/tree/master/tasks/move-granule-collections/schemas).
+- Resources: [npm](https://npmjs.com/package/@cumulus/move-granule-collections) | [source](https://github.com/nasa/cumulus) | [web](https://github.com/nasa/cumulus/tree/master/tasks/move-granule-collections)
+
+---
+
### [@cumulus/orca-copy-to-archive-adapter](https://github.com/nasa/cumulus/tree/master/tasks/orca-copy-to-archive-adapter)
Adapter to invoke orca copy-to-archive lambda
diff --git a/example/cumulus-tf/ecs_move_granule_collections.asl.json b/example/cumulus-tf/ecs_move_granule_collections.asl.json
new file mode 100644
index 00000000000..1f2730a3af2
--- /dev/null
+++ b/example/cumulus-tf/ecs_move_granule_collections.asl.json
@@ -0,0 +1,30 @@
+{
+ "Comment": "Moves granules across collections",
+ "StartAt": "EcsTaskMoveGranuleCollections",
+ "States": {
+ "EcsTaskMoveGranuleCollections": {
+ "Parameters": {
+ "cma": {
+ "event.$": "$",
+ "task_config": {
+ "buckets": "{$.meta.buckets}",
+ "provider": "{$.meta.provider}",
+ "collection": "{$.meta.collection}"
+ }
+ }
+ },
+ "Type": "Task",
+ "Resource": "${ecs_task_move_granule_collections}",
+ "TimeoutSeconds": 60,
+ "Retry": [
+ {
+ "ErrorEquals": [
+ "States.Timeout"
+ ],
+ "MaxAttempts": 1
+ }
+ ],
+ "End": true
+ }
+ }
+}
diff --git a/example/cumulus-tf/ecs_move_granule_collections.tf b/example/cumulus-tf/ecs_move_granule_collections.tf
new file mode 100644
index 00000000000..7b6265c1949
--- /dev/null
+++ b/example/cumulus-tf/ecs_move_granule_collections.tf
@@ -0,0 +1,17 @@
+module "ecs_hello_world_workflow" {
+ source = "../../tf-modules/workflow"
+
+ prefix = var.prefix
+ name = "ECSMoveGranuleCollectionsWorkflow"
+ workflow_config = module.cumulus.workflow_config
+ system_bucket = var.system_bucket
+ tags = local.tags
+
+
+ state_machine_definition = templatefile(
+ "${path.module}/ecs_hello_world_workflow.asl.json",
+ {
+ ecs_task_move_granule_collections: aws_sfn_activity.move_granule_collections_ecs_task.id
+ }
+ )
+}
diff --git a/example/spec/parallel/moveGranuleCollections/MoveGranuleCollectionWorkflowSpec.js b/example/spec/parallel/moveGranuleCollections/MoveGranuleCollectionWorkflowSpec.js
new file mode 100644
index 00000000000..ee543fa4823
--- /dev/null
+++ b/example/spec/parallel/moveGranuleCollections/MoveGranuleCollectionWorkflowSpec.js
@@ -0,0 +1,61 @@
+const { deleteExecution } = require('@cumulus/api-client/executions');
+const { ActivityStep } = require('@cumulus/integration-tests/sfnStep');
+const { getExecution } = require('@cumulus/api-client/executions');
+
+const { buildAndExecuteWorkflow } = require('../../helpers/workflowUtils');
+const { loadConfig } = require('../../helpers/testUtils');
+const { waitForApiStatus } = require('../../helpers/apiUtils');
+
+const activityStep = new ActivityStep();
+
+describe('The MoveGranuleCollection workflow using ECS', () => {
+ let workflowExecution;
+ let config;
+
+ beforeAll(async () => {
+ config = await loadConfig();
+
+ workflowExecution = await buildAndExecuteWorkflow(
+ config.stackName,
+ config.bucket,
+ 'ECSMoveGranuleCollections'
+ );
+ });
+
+ afterAll(async () => {
+ await deleteExecution({ prefix: config.stackName, executionArn: workflowExecution.executionArn });
+ });
+
+ it('executes successfully', () => {
+ expect(workflowExecution.status).toEqual('completed');
+ });
+
+ describe('the HelloWorld ECS', () => {
+ let activityOutput;
+
+ beforeAll(async () => {
+ activityOutput = await activityStep.getStepOutput(
+ workflowExecution.executionArn,
+ 'EcsTaskHelloWorld'
+ );
+ });
+
+ it('output is Hello World', () => {
+ expect(activityOutput.payload).toEqual({ hello: 'Hello World' });
+ });
+ });
+
+ describe('the reporting lambda has received the cloudwatch stepfunction event and', () => {
+ it('the execution record is added to the PostgreSQL database', async () => {
+ const record = await waitForApiStatus(
+ getExecution,
+ {
+ prefix: config.stackName,
+ arn: workflowExecution.executionArn,
+ },
+ 'completed'
+ );
+ expect(record.status).toEqual('completed');
+ });
+ });
+});
diff --git a/example/spec/parallel/moveGranuleCollections/MoveGranuleCollectionsSpec.js b/example/spec/parallel/moveGranuleCollections/MoveGranuleCollectionsSpec.js
new file mode 100644
index 00000000000..fff27013f2a
--- /dev/null
+++ b/example/spec/parallel/moveGranuleCollections/MoveGranuleCollectionsSpec.js
@@ -0,0 +1,296 @@
+'use strict';
+
+const { InvokeCommand } = require('@aws-sdk/client-lambda');
+const { lambda } = require('@cumulus/aws-client/services');
+const {
+ promiseS3Upload,
+ deleteS3Object,
+} = require('@cumulus/aws-client/S3');
+const { waitForListObjectsV2ResultCount } = require('@cumulus/integration-tests');
+const {
+ granules,
+ collections,
+} = require('@cumulus/api-client');
+
+const path = require('path');
+const fs = require('fs');
+const { v4: uuidv4 } = require('uuid');
+const { loadConfig } = require('../../helpers/testUtils');
+const { constructCollectionId } = require('../../../../packages/message/Collections');
+describe('when moveGranulesCollection is called', () => {
+ let stackName;
+ const sourceUrlPrefix = `source_path/${uuidv4()}`;
+ const targetUrlPrefix = `target_path/${uuidv4()}`;
+ const originalCollection = {
+ files: [
+ {
+ regex: '^MOD11A1\\.A[\\d]{7}\\.[\\S]{6}\\.006.[\\d]{13}\\.hdf$',
+ sampleFileName: 'MOD11A1.A2017200.h19v04.006.2017201090724.hdf',
+ bucket: 'protected',
+ },
+ {
+ regex: '^BROWSE\\.MOD11A1\\.A[\\d]{7}\\.[\\S]{6}\\.006.[\\d]{13}\\.hdf$',
+ sampleFileName: 'BROWSE.MOD11A1.A2017200.h19v04.006.2017201090724.hdf',
+ bucket: 'private',
+ },
+ {
+ regex: '^MOD11A1\\.A[\\d]{7}\\.[\\S]{6}\\.006.[\\d]{13}\\.hdf\\.met$',
+ sampleFileName: 'MOD11A1.A2017200.h19v04.006.2017201090724.hdf.met',
+ bucket: 'private',
+ },
+ {
+ regex: '^MOD11A1\\.A[\\d]{7}\\.[\\S]{6}\\.006.[\\d]{13}\\.cmr\\.xml$',
+ sampleFileName: 'MOD11A1.A2017200.h19v04.006.2017201090724.cmr.xml',
+ bucket: 'protected',
+ },
+ {
+ regex: '^MOD11A1\\.A[\\d]{7}\\.[\\S]{6}\\.006.[\\d]{13}_2\\.jpg$',
+ sampleFileName: 'MOD11A1.A2017200.h19v04.006.2017201090724_2.jpg',
+ bucket: 'public',
+ },
+ {
+ regex: '^MOD11A1\\.A[\\d]{7}\\.[\\S]{6}\\.006.[\\d]{13}_1\\.jpg$',
+ sampleFileName: 'MOD11A1.A2017200.h19v04.006.2017201090724_1.jpg',
+ bucket: 'private',
+ },
+ ],
+ url_path: targetUrlPrefix,
+ name: 'MOD11A1',
+ granuleIdExtraction: '(MOD11A1\\.(.*))\\.hdf',
+ granuleId: '^MOD11A1\\.A[\\d]{7}\\.[\\S]{6}\\.006.[\\d]{13}$',
+ dataType: 'MOD11A1',
+ process: 'modis',
+ version: '006',
+ sampleFileName: 'MOD11A1.A2017200.h19v04.006.2017201090724.hdf',
+ id: 'MOD11A1',
+ };
+ const targetCollection = {
+ files: [
+ {
+ regex: '^MOD11A1\\.A[\\d]{7}\\.[\\S]{6}\\.006.[\\d]{13}\\.hdf$',
+ sampleFileName: 'MOD11A1.A2017200.h19v04.006.2017201090724.hdf',
+ bucket: 'protected',
+ },
+ {
+ regex: '^BROWSE\\.MOD11A1\\.A[\\d]{7}\\.[\\S]{6}\\.006.[\\d]{13}\\.hdf$',
+ sampleFileName: 'BROWSE.MOD11A1.A2017200.h19v04.006.2017201090724.hdf',
+ bucket: 'private',
+ },
+ {
+ regex: '^MOD11A1\\.A[\\d]{7}\\.[\\S]{6}\\.006.[\\d]{13}\\.hdf\\.met$',
+ sampleFileName: 'MOD11A1.A2017200.h19v04.006.2017201090724.hdf.met',
+ bucket: 'private',
+ },
+ {
+ regex: '^MOD11A1\\.A[\\d]{7}\\.[\\S]{6}\\.006.[\\d]{13}\\.cmr\\.xml$',
+ sampleFileName: 'MOD11A1.A2017200.h19v04.006.2017201090724.cmr.xml',
+ bucket: 'public',
+ },
+ {
+ regex: '^MOD11A1\\.A[\\d]{7}\\.[\\S]{6}\\.006.[\\d]{13}_2\\.jpg$',
+ sampleFileName: 'MOD11A1.A2017200.h19v04.006.2017201090724_2.jpg',
+ bucket: 'public',
+ },
+ {
+ regex: '^MOD11A1\\.A[\\d]{7}\\.[\\S]{6}\\.006.[\\d]{13}_1\\.jpg$',
+ sampleFileName: 'MOD11A1.A2017200.h19v04.006.2017201090724_1.jpg',
+ bucket: 'public',
+ url_path: `${targetUrlPrefix}/jpg/example2/`,
+ },
+ ],
+ url_path: targetUrlPrefix,
+ name: 'MOD11A2',
+ granuleIdExtraction: '(MOD11A1\\.(.*))\\.hdf',
+ granuleId: '^MOD11A1\\.A[\\d]{7}\\.[\\S]{6}\\.006.[\\d]{13}$',
+ dataType: 'MOD11A2',
+ process: 'modis',
+ version: '006',
+ sampleFileName: 'MOD11A1.A2017200.h19v04.006.2017201090724.hdf',
+ id: 'MOD11A2',
+ };
+ const processGranule = {
+ status: 'completed',
+ collectionId: 'MOD11A1___006',
+ granuleId: 'MOD11A1.A2017200.h19v04.006.2017201090724',
+ files: [
+ {
+ key: `${sourceUrlPrefix}/MOD11A1.A2017200.h19v04.006.2017201090724.hdf`,
+ bucket: 'cumulus-test-sandbox-protected',
+ type: 'data',
+ fileName: 'MOD11A1.A2017200.h19v04.006.2017201090724.hdf',
+ },
+ {
+ key: `${sourceUrlPrefix}/MOD11A1.A2017200.h19v04.006.2017201090724_1.jpg`,
+ bucket: 'cumulus-test-sandbox-private',
+ type: 'browse',
+ fileName: 'MOD11A1.A2017200.h19v04.006.2017201090724_1.jpg',
+ },
+ {
+ key: `${sourceUrlPrefix}/MOD11A1.A2017200.h19v04.006.2017201090724_2.jpg`,
+ bucket: 'cumulus-test-sandbox-public',
+ type: 'browse',
+ fileName: 'MOD11A1.A2017200.h19v04.006.2017201090724_2.jpg',
+ },
+ {
+ key: `${sourceUrlPrefix}/MOD11A1.A2017200.h19v04.006.2017201090724.cmr.xml`,
+ bucket: 'cumulus-test-sandbox-protected',
+ type: 'metadata',
+ fileName: 'MOD11A1.A2017200.h19v04.006.2017201090724.cmr.xml',
+ },
+ ],
+ };
+ // let systemBucket;
+ beforeAll(async () => {
+ const config = await loadConfig();
+ stackName = config.stackName;
+ });
+
+ describe('under normal circumstances', () => {
+ let beforeAllFailed;
+ let finalFiles;
+ afterAll(async () => {
+ await Promise.all(finalFiles.map((fileObj) => deleteS3Object(
+ fileObj.bucket,
+ fileObj.key
+ )));
+ });
+ beforeAll(async () => {
+ finalFiles = [
+ {
+ bucket: 'cumulus-test-sandbox-protected',
+ key: `${targetUrlPrefix}/MOD11A1.A2017200.h19v04.006.2017201090724.hdf`,
+ },
+ {
+ bucket: 'cumulus-test-sandbox-public',
+ key: `${targetUrlPrefix}/jpg/example2/MOD11A1.A2017200.h19v04.006.2017201090724_1.jpg`,
+ },
+ {
+ bucket: 'cumulus-test-sandbox-public',
+ key: `${targetUrlPrefix}/MOD11A1.A2017200.h19v04.006.2017201090724_2.jpg`,
+ },
+ {
+ bucket: 'cumulus-test-sandbox-public',
+ key: `${targetUrlPrefix}/MOD11A1.A2017200.h19v04.006.2017201090724.cmr.xml`,
+ },
+ ];
+
+ const payload = {
+ meta: {
+ collection: targetCollection,
+ buckets: {
+ internal: {
+ type: 'cumulus-test-sandbox-internal',
+ },
+ private: {
+ name: 'cumulus-test-sandbox-private',
+ type: 'private',
+ },
+ protected: {
+ name: 'cumulus-test-sandbox-protected',
+ type: 'protected',
+ },
+ public: {
+ name: 'cumulus-test-sandbox-public',
+ type: 'public',
+ },
+ },
+ },
+ config: {
+ buckets: '{$.meta.buckets}',
+ distribution_endpoint: 'https://something.api.us-east-1.amazonaws.com/',
+ collection: '{$.meta.collection}',
+ },
+ input: {
+ granules: [
+ processGranule,
+ ],
+ },
+ };
+ //upload to cumulus
+ try {
+ try {
+ await collections.createCollection({
+ prefix: stackName,
+ collection: originalCollection,
+ });
+ } catch {
+ console.log(`collection ${constructCollectionId(originalCollection.name, originalCollection.version)} already exists`);
+ }
+ try {
+ await collections.createCollection({
+ prefix: stackName,
+ collection: targetCollection,
+ });
+ } catch {
+ console.log(`collection ${constructCollectionId(targetCollection.name, targetCollection.version)} already exists`);
+ }
+ try {
+ await granules.createGranule({
+ prefix: stackName,
+ body: processGranule,
+ });
+ } catch {
+ console.log(`granule ${processGranule.granuleId} already exists`);
+ }
+ await Promise.all(processGranule.files.map(async (file) => {
+ let body;
+ if (file.type === 'metadata') {
+ body = fs.createReadStream(path.join(__dirname, 'data/meta.xml'));
+ } else {
+ body = file.key;
+ }
+ await promiseS3Upload({
+ params: {
+ Bucket: file.bucket,
+ Key: file.key,
+ Body: body,
+ },
+ });
+ }));
+ const { $metadata } = await lambda().send(new InvokeCommand({
+ FunctionName: `${stackName}-MoveGranuleCollections`,
+ InvocationType: 'RequestResponse',
+ Payload: JSON.stringify({
+ cma: {
+ meta: payload.meta,
+ task_config: payload.config,
+ event: {
+ payload: payload.input,
+ },
+ },
+ }),
+ }));
+ console.log($metadata.httpStatusCode);
+ if ($metadata.httpStatusCode >= 400) {
+ console.log(`lambda invocation to set up failed, code ${$metadata.httpStatusCode}`);
+ beforeAllFailed = true;
+ }
+
+ await Promise.all(finalFiles.map((file) => expectAsync(
+ waitForListObjectsV2ResultCount({
+ bucket: file.bucket,
+ prefix: file.key,
+ desiredCount: 1,
+ interval: 5 * 1000,
+ timeout: 30 * 1000,
+ })
+ ).toBeResolved()));
+ } catch (error) {
+ console.log(`files do not appear to have been moved: error: ${error}`);
+ beforeAllFailed = false;
+ }
+ });
+ it('moves the granule data in s3', () => {
+ if (beforeAllFailed) fail('beforeAllFailed');
+ });
+ it('updates the granule data in cumulus', async () => {
+ if (beforeAllFailed) fail('beforeAllFailed');
+ const cumulusGranule = await granules.getGranule({
+ prefix: stackName,
+ granuleId: processGranule.granuleId,
+ });
+ expect(cumulusGranule.granuleId).toEqual(processGranule.granuleId);
+ expect(cumulusGranule.collectionId).toEqual(constructCollectionId(targetCollection.name, targetCollection.version));
+ });
+ });
+});
diff --git a/example/spec/parallel/moveGranuleCollections/data/meta.xml b/example/spec/parallel/moveGranuleCollections/data/meta.xml
new file mode 100644
index 00000000000..8c2a07bf41d
--- /dev/null
+++ b/example/spec/parallel/moveGranuleCollections/data/meta.xml
@@ -0,0 +1,210 @@
+
+
+ MOD11A1.A2017200.h19v04.006.2017201090724
+ 2017-11-20T23:02:40.055807
+ 2017-11-20T23:02:40.055814
+
+ MOD11A1
+ 006
+
+
+ further update is anticipated
+ reprocessed
+ MOD11A1.A2017200.h19v04.006.2017201090724.hdf
+ BOTH
+ 2015-07-02T16:47:38.000Z
+ 6.4.4AS
+
+
+ 6.4.11
+
+
+
+ 2003-02-19T00:00:00Z
+ 2003-02-19T23:59:59Z
+
+
+
+
+
+
+
+
+ -70.004161028804404
+ -0.004166666666662
+
+
+ -60.004177439215297
+ -0.004166666666662
+
+
+ -60.929844150213498
+ -9.995833333333330
+
+
+ -71.084093041393103
+ -9.995833333333330
+
+
+
+
+
+
+
+
+ MOD 1KM L3 LST
+
+ 0
+ 0
+ 0
+ 92
+
+
+ Passed
+ No automatic quality assessment is performed in the PGE.
+ Not Investigated
+ See http://landweb.nascom.nasa.gov/cgi-bin/QA_WWW/qaFlagPage.cgi?sat
+
+
+
+
+
+ Terra
+
+
+ MODIS
+
+
+ MODIS
+
+
+
+
+
+
+
+
+ QAFRACTIONGOODQUALITY
+
+ 0.0285983
+
+
+
+ QAPERCENTNOTPRODUCEDOTHER
+
+ 0
+
+
+
+ CLOUD_CONTAMINATED_LST_SCREENED
+
+ YES
+
+
+
+ VERTICALTILENUMBER
+
+ 09
+
+
+
+ QAFRACTIONOTHERQUALITY
+
+ 0.0543250
+
+
+
+ QAPERCENTGOODQUALITY
+
+ 3
+
+
+
+ HORIZONTALTILENUMBER
+
+ 11
+
+
+
+ QAFRACTIONNOTPRODUCEDCLOUD
+
+ 0.9170767
+
+
+
+ TileID
+
+ 51011009
+
+
+
+ QAFRACTIONNOTPRODUCEDOTHER
+
+ 0.0000000
+
+
+
+ identifier_product_doi
+
+ 10.5067/MODIS/MOD11A1.006
+
+
+
+ N_GRAN_POINTERS
+
+ 28
+
+
+
+ QAPERCENTNOTPRODUCEDCLOUD
+
+ 92
+
+
+
+ identifier_product_doi_authority
+
+ http://dx.doi.org
+
+
+
+ QAPERCENTOTHERQUALITY
+
+ 5
+
+
+
+
+ MOD03.A2003050.0315.006.2012268032410.hdf
+ MOD021KM.A2003050.0315.006.2014220090715.hdf
+ MOD35_L2.A2003050.0315.006.2014320160924.hdf
+ MOD07_L2.A2003050.0315.006.2014320161105.hdf
+ MOD03.A2003050.1355.006.2012268034643.hdf
+
+
+ 11
+ 09
+ MODIS Tile SIN
+
+
+
+ https://fvk4vim143.execute-api.us-east-1.amazonaws.com/dev/MOD11A1.A2017200.h19v04.006.2017201090724.hdf
+ Download MOD11A1.A2017200.h19v04.006.2017201090724.hdf
+
+
+ http://cumulus-test-sandbox-public.s3.amazonaws.com/MOD11A1.A2017200.h19v04.006.2017201090724_1.jpg
+ Download MOD11A1.A2017200.h19v04.006.2017201090724_1.jpg
+
+
+ http://cumulus-test-sandbox-public.s3.amazonaws.com/MOD11A1.A2017200.h19v04.006.2017201090724_2.jpg
+ Download MOD11A1.A2017200.h19v04.006.2017201090724_2.jpg
+
+
+ https://fvk4vim143.execute-api.us-east-1.amazonaws.com/dev/MOD11A1.A2017200.h19v04.006.2017201090724.cmr.xml
+ Download MOD11A1.A2017200.h19v04.006.2017201090724.cmr.xml
+
+
+ true
+ true
+ 92
+
diff --git a/package.json b/package.json
index 149e9ae5efb..52d234e3437 100644
--- a/package.json
+++ b/package.json
@@ -33,7 +33,7 @@
"bootstrap-no-build-no-scripts": "lerna bootstrap --no-ci --force-local --ignore-scripts",
"bootstrap-no-build-no-concurrency": "lerna bootstrap --no-ci --force-local --concurrency 1",
"bootstrap-no-build-quiet": "lerna bootstrap --no-ci --force-local --loglevel=error",
- "ci:bootstrap": "lerna bootstrap --no-ci --force-local --ignore-scripts && lerna run prepublish",
+ "ci:bootstrap": "lerna bootstrap --no-ci --force-local && lerna run prepublish",
"ci:bootstrap-no-scripts": "lerna bootstrap --no-ci --force-local --ignore-scripts",
"ci:bootstrap-no-scripts-quiet": "lerna bootstrap --no-ci --force-local --ignore-scripts --loglevel=error",
"update": "lerna version --exact --force-publish --no-git-tag-version --no-push",
diff --git a/packages/api-client/src/granules.ts b/packages/api-client/src/granules.ts
index 305a157defb..2cbd9c7936b 100644
--- a/packages/api-client/src/granules.ts
+++ b/packages/api-client/src/granules.ts
@@ -602,6 +602,40 @@ export const associateExecutionWithGranule = async (params: {
});
};
+/**
+ * Update granules to new details in cumulus
+ * POST /granules/bulk
+ *
+ * @param params - params
+ * @param params.prefix - the prefix configured for the stack
+ * @param params.body - body to pass the API lambda
+ * @param params.callback - async function to invoke the api lambda
+ * that takes a prefix / user payload. Defaults
+ * to cumulusApiClient.invokeApifunction to invoke the
+ * api lambda
+ * @returns - the response from the callback
+ */
+export const updateGranules = async (params: {
+ prefix: string,
+ body: ApiGranule[],
+ callback?: InvokeApiFunction
+}): Promise => {
+ const { prefix, body, callback = invokeApi } = params;
+ return await callback({
+ prefix: prefix,
+ payload: {
+ httpMethod: 'PATCH',
+ resource: '/{proxy+}',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ path: '/granules/',
+ body: JSON.stringify(body),
+ },
+ expectedStatusCodes: 200,
+ });
+};
+
/**
* Bulk operations on granules stored in cumulus
* POST /granules/bulk
diff --git a/packages/api/endpoints/granules.js b/packages/api/endpoints/granules.js
index f3b3ed47544..94db3e9ef14 100644
--- a/packages/api/endpoints/granules.js
+++ b/packages/api/endpoints/granules.js
@@ -25,6 +25,7 @@ const {
translatePostgresCollectionToApiCollection,
translatePostgresGranuleToApiGranule,
getGranuleAndCollection,
+ updateGranulesAndFiles,
} = require('@cumulus/db');
const { deleteGranuleAndFiles } = require('../src/lib/granule-delete');
@@ -846,6 +847,34 @@ async function getByGranuleId(req, res) {
return res.send({ ...result, recoveryStatus });
}
+/**
+ * Based on a move-collections-task, will update a list of
+ * moved granules' records in PG and ES
+ *
+ * @param {Object} req - express request object
+ * @param {Object} res - express response object
+ * @returns {Promise