diff --git a/app/src/docs/v1.api-spec.yaml b/app/src/docs/v1.api-spec.yaml index beb9a560..47f140fe 100644 --- a/app/src/docs/v1.api-spec.yaml +++ b/app/src/docs/v1.api-spec.yaml @@ -778,6 +778,8 @@ paths: This endpoint does not guarantee immediately synchronized results. Synchronization latency will be affected by the remaining number of objects awaiting synchronization. + The `lastSyncedDate` attribute will be asynchronously updated when it + performs a successful synchronization with the S3 bucket tags: - Sync operationId: syncObject diff --git a/app/src/services/object.js b/app/src/services/object.js index 34461a8a..3b9c1735 100644 --- a/app/src/services/object.js +++ b/app/src/services/object.js @@ -218,6 +218,8 @@ const service = { * @param {string} data.path The relative S3 key/path of the object * @param {boolean} [data.public] The optional public flag - defaults to true if undefined * @param {boolean} [data.active] The optional active flag - defaults to true if undefined + * @param {string} [data.lastSyncedDate] The last time a sync request was made for the object. + * Should be left undefined if not part of a sync operation * @param {object} [etrx=undefined] An optional Objection Transaction object * @returns {Promise} The result of running the patch operation * @throws The error encountered upon db transaction failure @@ -232,7 +234,8 @@ const service = { path: data.path, public: data.public, active: data.active, - updatedBy: data.userId ?? SYSTEM_USER + updatedBy: data.userId ?? SYSTEM_USER, + lastSyncedDate: data.lastSyncedDate }); if (!etrx) await trx.commit(); diff --git a/app/src/services/sync.js b/app/src/services/sync.js index 655aa46a..82ef5d02 100644 --- a/app/src/services/sync.js +++ b/app/src/services/sync.js @@ -160,11 +160,19 @@ const service = { // Case: already synced - record & update public status as needed if (comsObject) { if (s3Public === undefined || s3Public === comsObject.public) { - response = comsObject; + response = await objectService.update({ + id: comsObject.id, + userId: userId, + lastSyncedDate: new Date().toISOString() + }, trx); } else { response = await objectService.update({ - id: comsObject.id, userId: userId, path: comsObject.path, public: s3Public - }); + id: comsObject.id, + userId: userId, + path: comsObject.path, + public: s3Public, + lastSyncedDate: new Date().toISOString() + }, trx); modified = true; } } @@ -179,7 +187,8 @@ const service = { path: path, public: s3Public, bucketId: bucketId, - userId: userId + userId: userId, + lastSyncedDate: new Date().toISOString() }, trx); modified = true; diff --git a/app/tests/unit/services/object.spec.js b/app/tests/unit/services/object.spec.js index e9d49a6b..f13fcdef 100644 --- a/app/tests/unit/services/object.spec.js +++ b/app/tests/unit/services/object.spec.js @@ -33,7 +33,8 @@ const data = { public: 'true', active: 'true', createdBy: SYSTEM_USER, - userId: SYSTEM_USER + userId: SYSTEM_USER, + lastSyncedDate: undefined }; beforeEach(() => { @@ -227,6 +228,7 @@ describe('read', () => { describe('update', () => { it('Update an object DB record', async () => { + await service.update({ ...data }); expect(ObjectModel.startTransaction).toHaveBeenCalledTimes(1); @@ -236,7 +238,27 @@ describe('update', () => { path: data.path, public: data.public, active: data.active, - updatedBy: data.userId + updatedBy: data.userId, + lastSyncedDate: undefined + }); + expect(objectModelTrx.commit).toHaveBeenCalledTimes(1); + }); + + it('Update an object DB record as part of a sync operation', async () => { + + const testDateString = new Date('2024-01-01T00:00:00').toISOString(); + + await service.update({ ...data, lastSyncedDate: testDateString }); + + expect(ObjectModel.startTransaction).toHaveBeenCalledTimes(1); + expect(ObjectModel.query).toHaveBeenCalledTimes(1); + expect(ObjectModel.patchAndFetchById).toHaveBeenCalledTimes(1); + expect(ObjectModel.patchAndFetchById).toBeCalledWith(data.id, { + path: data.path, + public: data.public, + active: data.active, + updatedBy: data.userId, + lastSyncedDate: testDateString }); expect(objectModelTrx.commit).toHaveBeenCalledTimes(1); }); diff --git a/app/tests/unit/services/sync.spec.js b/app/tests/unit/services/sync.spec.js index 5008a8ef..8ff047df 100644 --- a/app/tests/unit/services/sync.spec.js +++ b/app/tests/unit/services/sync.spec.js @@ -1,3 +1,5 @@ +const { NIL: SYSTEM_USER } = require('uuid'); + const { resetModel, trxBuilder } = require('../../common/helper'); const utils = require('../../../src/db/models/utils'); const ObjectModel = require('../../../src/db/models/tables/objectModel'); @@ -371,7 +373,7 @@ describe('syncObject', () => { pruneOrphanedMetadataSpy.mockRestore(); pruneOrphanedTagsSpy.mockRestore(); searchObjectsSpy.mockRestore(); - updateSpy.mockReset(); + updateSpy.mockRestore(); }); it('should return object when already synced', async () => { @@ -379,6 +381,7 @@ describe('syncObject', () => { headObjectSpy.mockResolvedValue({}); searchObjectsSpy.mockResolvedValue({ total: 1, data: [comsObject] }); getObjectPublicSpy.mockResolvedValue(true); + updateSpy.mockResolvedValue(comsObject); const result = await service.syncObject(path, bucketId); @@ -405,7 +408,10 @@ describe('syncObject', () => { path: path, bucketId: bucketId }), expect.any(Object)); expect(objectModelTrx.commit).toHaveBeenCalledTimes(1); - expect(updateSpy).toHaveBeenCalledTimes(0); + expect(updateSpy).toHaveBeenCalledTimes(1); + expect(updateSpy).toHaveBeenCalledWith(expect.objectContaining({ + id: comsObject.id, lastSyncedDate: expect.anything() + }), expect.any(Object)); }); it('should return object when already synced but public mismatch', async () => { @@ -442,8 +448,8 @@ describe('syncObject', () => { expect(objectModelTrx.commit).toHaveBeenCalledTimes(1); expect(updateSpy).toHaveBeenCalledTimes(1); expect(updateSpy).toHaveBeenCalledWith(expect.objectContaining({ - id: validUuidv4, path: path, public: false - })); + id: validUuidv4, path: path, public: false, lastSyncedDate: expect.anything(), userId: SYSTEM_USER + }), expect.any(Object)); }); it('should return object when already synced but S3 ACL errors out', async () => { @@ -451,6 +457,7 @@ describe('syncObject', () => { headObjectSpy.mockResolvedValue({}); searchObjectsSpy.mockResolvedValue({ total: 1, data: [comsObject] }); getObjectPublicSpy.mockImplementation(() => { throw new Error(); }); + updateSpy.mockResolvedValue(comsObject); const result = await service.syncObject(path, bucketId); @@ -477,7 +484,7 @@ describe('syncObject', () => { path: path, bucketId: bucketId }), expect.any(Object)); expect(objectModelTrx.commit).toHaveBeenCalledTimes(1); - expect(updateSpy).toHaveBeenCalledTimes(0); + expect(updateSpy).toHaveBeenCalledTimes(1); }); it('should insert new object when not in COMS', async () => {