@squiz/db-lib 1.76.0 → 1.77.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/lib/dynamodb/AbstractDynamoDbRepository.d.ts +1 -1
  3. package/lib/dynamodb/AbstractDynamoDbRepository.d.ts.map +1 -1
  4. package/lib/dynamodb/AbstractDynamoDbRepository.js +6 -5
  5. package/lib/dynamodb/AbstractDynamoDbRepository.js.map +1 -1
  6. package/lib/dynamodb/AbstractDynamoDbRepository.spec.d.ts.map +1 -1
  7. package/lib/dynamodb/AbstractDynamoDbRepository.spec.js +15 -14
  8. package/lib/dynamodb/AbstractDynamoDbRepository.spec.js.map +1 -1
  9. package/lib/dynamodb/getDynamoDbOptions.d.ts.map +1 -1
  10. package/lib/externalized/ExternalizedDynamoDbRepository.d.ts +151 -0
  11. package/lib/externalized/ExternalizedDynamoDbRepository.d.ts.map +1 -0
  12. package/lib/externalized/ExternalizedDynamoDbRepository.js +463 -0
  13. package/lib/externalized/ExternalizedDynamoDbRepository.js.map +1 -0
  14. package/lib/externalized/ExternalizedDynamoDbRepository.spec.d.ts +2 -0
  15. package/lib/externalized/ExternalizedDynamoDbRepository.spec.d.ts.map +1 -0
  16. package/lib/externalized/ExternalizedDynamoDbRepository.spec.js +218 -0
  17. package/lib/externalized/ExternalizedDynamoDbRepository.spec.js.map +1 -0
  18. package/lib/index.d.ts +2 -0
  19. package/lib/index.d.ts.map +1 -1
  20. package/lib/index.js +3 -0
  21. package/lib/index.js.map +1 -1
  22. package/lib/s3/S3ExternalStorage.d.ts +66 -0
  23. package/lib/s3/S3ExternalStorage.d.ts.map +1 -0
  24. package/lib/s3/S3ExternalStorage.js +84 -0
  25. package/lib/s3/S3ExternalStorage.js.map +1 -0
  26. package/lib/s3/S3ExternalStorage.spec.d.ts +12 -0
  27. package/lib/s3/S3ExternalStorage.spec.d.ts.map +1 -0
  28. package/lib/s3/S3ExternalStorage.spec.js +130 -0
  29. package/lib/s3/S3ExternalStorage.spec.js.map +1 -0
  30. package/package.json +7 -6
  31. package/src/dynamodb/AbstractDynamoDbRepository.spec.ts +2 -1
  32. package/src/dynamodb/AbstractDynamoDbRepository.ts +3 -1
  33. package/src/externalized/ExternalizedDynamoDbRepository.spec.ts +274 -0
  34. package/src/externalized/ExternalizedDynamoDbRepository.ts +545 -0
  35. package/src/index.ts +4 -0
  36. package/src/s3/S3ExternalStorage.spec.ts +181 -0
  37. package/src/s3/S3ExternalStorage.ts +118 -0
  38. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const ExternalizedDynamoDbRepository_1 = require("./ExternalizedDynamoDbRepository");
4
+ class TestItem {
5
+ constructor(input = {}) {
6
+ this.id = input.id || '';
7
+ this.name = input.name || '';
8
+ this.data = input.data || [];
9
+ // Note: storageLocation is intentionally not set from input to simulate model stripping it
10
+ }
11
+ }
12
+ // Test entity definition
13
+ const testEntityDefinition = {
14
+ keys: {
15
+ pk: {
16
+ attributeName: 'pk',
17
+ format: 'ITEM#${id}',
18
+ },
19
+ sk: {
20
+ attributeName: 'sk',
21
+ format: 'ITEM#${id}',
22
+ },
23
+ },
24
+ indexes: {},
25
+ fieldsAsJsonString: ['data'],
26
+ };
27
+ // Concrete test repository
28
+ class TestRepo extends ExternalizedDynamoDbRepository_1.ExternalizedDynamoDbRepository {
29
+ constructor(dbManager, storage, overrides = {}) {
30
+ super('test-table', dbManager, 'test_item', { ...testEntityDefinition, ...overrides }, TestItem, storage);
31
+ }
32
+ // Override updateItem to allow mocking super.updateItem
33
+ async updateItem(value) {
34
+ if (this.mockSuperUpdateItem) {
35
+ // Use mock if provided
36
+ const previousLocation = await this.fetchStoredLocation(value);
37
+ const updatedItem = await this.mockSuperUpdateItem(value);
38
+ if (!updatedItem) {
39
+ return undefined;
40
+ }
41
+ const newLocation = updatedItem.storageLocation;
42
+ if (previousLocation && previousLocation.key !== (newLocation === null || newLocation === void 0 ? void 0 : newLocation.key)) {
43
+ await this.storage.delete(previousLocation);
44
+ }
45
+ return updatedItem;
46
+ }
47
+ return super.updateItem(value);
48
+ }
49
+ }
50
+ const createDbManager = (clientOverrides = {}) => ({
51
+ client: {
52
+ get: jest.fn(),
53
+ put: jest.fn(),
54
+ delete: jest.fn(),
55
+ update: jest.fn(),
56
+ ...clientOverrides,
57
+ },
58
+ repositories: {},
59
+ executeInTransaction: async (fn) => fn({}),
60
+ addWriteTransactionItem: jest.fn(),
61
+ });
62
+ const createStorage = () => ({
63
+ save: jest.fn(),
64
+ load: jest.fn(),
65
+ delete: jest.fn(),
66
+ });
67
+ const createTestItem = (overrides = {}) => {
68
+ const item = new TestItem({
69
+ id: 'item-1',
70
+ name: 'Test',
71
+ data: [{ key: 'sample', value: { main: [] } }],
72
+ ...overrides,
73
+ });
74
+ return item;
75
+ };
76
+ describe('ExternalizedDynamoDbRepository', () => {
77
+ it('externalizes data to S3 when prepareValueForStorage is called', async () => {
78
+ const storage = {
79
+ ...createStorage(),
80
+ save: jest.fn().mockResolvedValue({ location: { type: 's3', key: 'item-key' }, size: 500 }),
81
+ };
82
+ const repo = new TestRepo(createDbManager(), storage);
83
+ const item = createTestItem();
84
+ const storedValue = await repo.prepareValueForStorage(item);
85
+ expect(storage.save).toHaveBeenCalled();
86
+ // storedValue is a minimal payload - original item is NOT mutated
87
+ expect(storedValue.data).toEqual([]);
88
+ expect(storedValue.storageLocation).toEqual({ type: 's3', key: 'item-key' });
89
+ });
90
+ it('creates item with storageLocation preserved', async () => {
91
+ const client = createDbManager().client;
92
+ client.put.mockResolvedValue({});
93
+ const storage = {
94
+ ...createStorage(),
95
+ save: jest.fn().mockResolvedValue({ location: { type: 's3', key: 'item-key' }, size: 500 }),
96
+ };
97
+ const repo = new TestRepo({ client }, storage);
98
+ const item = createTestItem();
99
+ // Use prepareValueForStorage to externalize data and set storageLocation
100
+ const preparedItem = await repo.prepareValueForStorage(item);
101
+ // Verify the prepared value has minimal payload (empty data)
102
+ expect(preparedItem.data).toEqual([]);
103
+ expect(preparedItem.storageLocation).toEqual({ type: 's3', key: 'item-key' });
104
+ // Original item should NOT be mutated
105
+ expect(item.storageLocation).toBeUndefined();
106
+ });
107
+ it('loads payload from external storage when storageLocation is set', async () => {
108
+ const storage = createStorage();
109
+ const fullData = {
110
+ id: 'item-1',
111
+ name: 'From S3',
112
+ data: [{ key: 'sample', value: { main: [] } }],
113
+ };
114
+ storage.load.mockResolvedValue(fullData);
115
+ const repo = new TestRepo(createDbManager({ get: jest.fn() }), storage);
116
+ const result = await repo.hydrateFromExternalStorage({
117
+ id: 'item-1',
118
+ name: 'Placeholder',
119
+ data: [],
120
+ storageLocation: { type: 's3', key: 'item-key' },
121
+ });
122
+ expect(storage.load).toHaveBeenCalledWith({ type: 's3', key: 'item-key' });
123
+ expect(result === null || result === void 0 ? void 0 : result.name).toBe('From S3');
124
+ expect(result === null || result === void 0 ? void 0 : result.data).toEqual([{ key: 'sample', value: { main: [] } }]);
125
+ });
126
+ it('deletes externalized payloads when deleting items', async () => {
127
+ const client = createDbManager({
128
+ get: jest.fn().mockResolvedValue({ Item: { storageLocation: { type: 's3', key: 'item-key' } } }),
129
+ delete: jest.fn().mockResolvedValue({}),
130
+ }).client;
131
+ const storage = createStorage();
132
+ const repo = new TestRepo({ client }, storage);
133
+ await repo.deleteItem({ id: 'item-1' });
134
+ expect(storage.delete).toHaveBeenCalledWith({ type: 's3', key: 'item-key' });
135
+ });
136
+ it('returns inline data when no storageLocation is present', async () => {
137
+ const storage = createStorage();
138
+ const repo = new TestRepo(createDbManager({ get: jest.fn() }), storage);
139
+ const inlineRecord = {
140
+ id: 'item-1',
141
+ name: 'Inline Item',
142
+ data: [{ key: 'sample', value: { main: [] } }],
143
+ };
144
+ const result = await repo.hydrateFromExternalStorage(inlineRecord);
145
+ expect(storage.load).not.toHaveBeenCalled();
146
+ expect(result).toBeDefined();
147
+ expect(result === null || result === void 0 ? void 0 : result.name).toBe('Inline Item');
148
+ expect(result === null || result === void 0 ? void 0 : result.data).toEqual([{ key: 'sample', value: { main: [] } }]);
149
+ });
150
+ it('cleans up old S3 file when updating with new storageLocation', async () => {
151
+ const oldLocation = { type: 's3', key: 'old-key' };
152
+ const newLocation = { type: 's3', key: 'new-key' };
153
+ const client = createDbManager({
154
+ get: jest.fn().mockResolvedValue({
155
+ Item: {
156
+ id: 'item-1',
157
+ name: 'Test',
158
+ storageLocation: oldLocation,
159
+ },
160
+ }),
161
+ update: jest.fn().mockResolvedValue({
162
+ Attributes: {
163
+ id: 'item-1',
164
+ name: 'Updated',
165
+ storageLocation: newLocation,
166
+ },
167
+ }),
168
+ }).client;
169
+ const storage = createStorage();
170
+ const repo = new TestRepo({ client }, storage);
171
+ // Update without storageLocation (it will be fetched from DynamoDB)
172
+ const updateValue = {
173
+ id: 'item-1',
174
+ name: 'Updated',
175
+ };
176
+ // Create an updated item with the new storageLocation
177
+ const updatedItem = createTestItem({ id: 'item-1', name: 'Updated' });
178
+ updatedItem.storageLocation = newLocation;
179
+ // Mock super.updateItem using the custom mock hook
180
+ repo.mockSuperUpdateItem = jest.fn().mockResolvedValue(updatedItem);
181
+ await repo.updateItem(updateValue);
182
+ expect(storage.delete).toHaveBeenCalledWith(oldLocation);
183
+ });
184
+ it('does not delete S3 file when storageLocation is unchanged', async () => {
185
+ const location = { type: 's3', key: 'same-key' };
186
+ const client = createDbManager({
187
+ get: jest.fn().mockResolvedValue({
188
+ Item: {
189
+ id: 'item-1',
190
+ name: 'Test',
191
+ storageLocation: location,
192
+ },
193
+ }),
194
+ update: jest.fn().mockResolvedValue({
195
+ Attributes: {
196
+ id: 'item-1',
197
+ name: 'Updated',
198
+ storageLocation: location,
199
+ },
200
+ }),
201
+ }).client;
202
+ const storage = createStorage();
203
+ const repo = new TestRepo({ client }, storage);
204
+ // Update without storageLocation (it will be fetched from DynamoDB)
205
+ const updateValue = {
206
+ id: 'item-1',
207
+ name: 'Updated',
208
+ };
209
+ // Create an updated item with the same storageLocation
210
+ const updatedItem = createTestItem({ id: 'item-1', name: 'Updated' });
211
+ updatedItem.storageLocation = location;
212
+ // Mock super.updateItem using the custom mock hook
213
+ repo.mockSuperUpdateItem = jest.fn().mockResolvedValue(updatedItem);
214
+ await repo.updateItem(updateValue);
215
+ expect(storage.delete).not.toHaveBeenCalled();
216
+ });
217
+ });
218
+ //# sourceMappingURL=ExternalizedDynamoDbRepository.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExternalizedDynamoDbRepository.spec.js","sourceRoot":"","sources":["../../src/externalized/ExternalizedDynamoDbRepository.spec.ts"],"names":[],"mappings":"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"}
package/lib/index.d.ts CHANGED
@@ -12,4 +12,6 @@ export * from './error/MissingKeyValuesError';
12
12
  export * from './error/InvalidDbSchemaError';
13
13
  export * from './PostgresErrorCodes';
14
14
  export { Pool, PoolClient } from 'pg';
15
+ export * from './s3/S3ExternalStorage';
16
+ export * from './externalized/ExternalizedDynamoDbRepository';
15
17
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uCAAuC,CAAC;AACtD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC;AACpC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,0BAA0B,CAAC;AACzC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,8BAA8B,CAAC;AAG7C,cAAc,sBAAsB,CAAC;AAErC,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uCAAuC,CAAC;AACtD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC;AACpC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,0BAA0B,CAAC;AACzC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,8BAA8B,CAAC;AAG7C,cAAc,sBAAsB,CAAC;AAErC,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAGtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,+CAA+C,CAAC"}
package/lib/index.js CHANGED
@@ -31,4 +31,7 @@ __exportStar(require("./error/InvalidDbSchemaError"), exports);
31
31
  __exportStar(require("./PostgresErrorCodes"), exports);
32
32
  var pg_1 = require("pg");
33
33
  Object.defineProperty(exports, "Pool", { enumerable: true, get: function () { return pg_1.Pool; } });
34
+ // S3 External Storage
35
+ __exportStar(require("./s3/S3ExternalStorage"), exports);
36
+ __exportStar(require("./externalized/ExternalizedDynamoDbRepository"), exports);
34
37
  //# sourceMappingURL=index.js.map
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,uDAAqC;AACrC,sDAAoC;AACpC,6DAA2C;AAC3C,wEAAsD;AACtD,gEAA8C;AAC9C,6CAA2B;AAC3B,iDAA+B;AAC/B,sDAAoC;AACpC,6DAA2C;AAC3C,2DAAyC;AACzC,gEAA8C;AAC9C,+DAA6C;AAE7C,WAAW;AACX,uDAAqC;AAErC,yBAAsC;AAA7B,0FAAA,IAAI,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,uDAAqC;AACrC,sDAAoC;AACpC,6DAA2C;AAC3C,wEAAsD;AACtD,gEAA8C;AAC9C,6CAA2B;AAC3B,iDAA+B;AAC/B,sDAAoC;AACpC,6DAA2C;AAC3C,2DAAyC;AACzC,gEAA8C;AAC9C,+DAA6C;AAE7C,WAAW;AACX,uDAAqC;AAErC,yBAAsC;AAA7B,0FAAA,IAAI,OAAA;AAEb,sBAAsB;AACtB,yDAAuC;AACvC,gFAA8D"}
@@ -0,0 +1,66 @@
1
+ /*!
2
+ * @file S3ExternalStorage.ts
3
+ * @description This file contains the S3ExternalStorage class, which is used to store and retrieve page contents from S3.
4
+ * @author Dean Heffernan
5
+ * @copyright 2025 Squiz
6
+ */
7
+ import { S3Client } from '@aws-sdk/client-s3';
8
+ /**
9
+ * Represents the storage location of page contents in S3.
10
+ */
11
+ export interface S3StorageLocation {
12
+ type: 's3';
13
+ key: string;
14
+ }
15
+ /**
16
+ * The S3ExternalStorage class is used to store and retrieve page contents from S3.
17
+ * @class S3ExternalStorage
18
+ * @constructor
19
+ * @param {string} bucket - The name of the S3 bucket to use for storing page contents.
20
+ * @param {number} thresholdBytes - The threshold in bytes above which page contents will be stored externally.
21
+ * @param {S3Client} s3Client - The S3 client to use for storing and retrieving page contents.
22
+ * @returns {S3ExternalStorage} A new instance of the S3ExternalStorage class.
23
+ */
24
+ export declare class S3ExternalStorage {
25
+ /**
26
+ * The config to use for storing and retrieving page contents.
27
+ * @type {Config}
28
+ */
29
+ private readonly tenantId;
30
+ /**
31
+ * The name of the S3 bucket to use for storing page contents.
32
+ * @type {string}
33
+ */
34
+ private readonly bucket;
35
+ /**
36
+ * The S3 client to use for storing and retrieving page contents.
37
+ * @type {S3Client}
38
+ */
39
+ private readonly s3Client;
40
+ /**
41
+ * Creates a new instance of the S3ExternalStorage class.
42
+ * @constructor
43
+ * @param {S3Client} s3Client - The S3 client to use for storing and retrieving page contents.
44
+ * @returns {S3ExternalStorage} A new instance of the S3ExternalStorage class.
45
+ */
46
+ constructor(tenantId: string, s3Client: S3Client, bucket: string);
47
+ /**
48
+ * Saves a payload to S3.
49
+ * @param {string} entityName - The name of the entity to save the payload for.
50
+ * @param {string} referenceId - The reference ID of the payload.
51
+ * @param {object} payload - The payload to save.
52
+ * @returns {Promise<{ location: S3StorageLocation; size: number }>} A promise that resolves to the location and size of the saved payload.
53
+ */
54
+ save(entityName: string, referenceId: string, payload: object): Promise<{
55
+ location: S3StorageLocation;
56
+ size: number;
57
+ }>;
58
+ /**
59
+ * Loads a payload from S3.
60
+ * @param location - The location of the payload to load.
61
+ * @returns {Promise<Record<string, unknown>>} A promise that resolves to the loaded payload.
62
+ */
63
+ load(location: S3StorageLocation): Promise<Record<string, unknown>>;
64
+ delete(location?: S3StorageLocation): Promise<void>;
65
+ }
66
+ //# sourceMappingURL=S3ExternalStorage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"S3ExternalStorage.d.ts","sourceRoot":"","sources":["../../src/s3/S3ExternalStorage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAA2D,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEvG;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,IAAI,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;GAQG;AACH,qBAAa,iBAAiB;IAC5B;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAW;IAEpC;;;;;OAKG;gBACS,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM;IAMhE;;;;;;OAMG;IACU,IAAI,CACf,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;QAAE,QAAQ,EAAE,iBAAiB,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAoBzD;;;;OAIG;IACU,IAAI,CAAC,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAcnE,MAAM,CAAC,QAAQ,CAAC,EAAE,iBAAiB;CAWjD"}
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ /*!
3
+ * @file S3ExternalStorage.ts
4
+ * @description This file contains the S3ExternalStorage class, which is used to store and retrieve page contents from S3.
5
+ * @author Dean Heffernan
6
+ * @copyright 2025 Squiz
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.S3ExternalStorage = void 0;
10
+ // External
11
+ const client_s3_1 = require("@aws-sdk/client-s3");
12
+ /**
13
+ * The S3ExternalStorage class is used to store and retrieve page contents from S3.
14
+ * @class S3ExternalStorage
15
+ * @constructor
16
+ * @param {string} bucket - The name of the S3 bucket to use for storing page contents.
17
+ * @param {number} thresholdBytes - The threshold in bytes above which page contents will be stored externally.
18
+ * @param {S3Client} s3Client - The S3 client to use for storing and retrieving page contents.
19
+ * @returns {S3ExternalStorage} A new instance of the S3ExternalStorage class.
20
+ */
21
+ class S3ExternalStorage {
22
+ /**
23
+ * Creates a new instance of the S3ExternalStorage class.
24
+ * @constructor
25
+ * @param {S3Client} s3Client - The S3 client to use for storing and retrieving page contents.
26
+ * @returns {S3ExternalStorage} A new instance of the S3ExternalStorage class.
27
+ */
28
+ constructor(tenantId, s3Client, bucket) {
29
+ this.tenantId = tenantId;
30
+ this.bucket = bucket;
31
+ this.s3Client = s3Client;
32
+ }
33
+ /**
34
+ * Saves a payload to S3.
35
+ * @param {string} entityName - The name of the entity to save the payload for.
36
+ * @param {string} referenceId - The reference ID of the payload.
37
+ * @param {object} payload - The payload to save.
38
+ * @returns {Promise<{ location: S3StorageLocation; size: number }>} A promise that resolves to the location and size of the saved payload.
39
+ */
40
+ async save(entityName, referenceId, payload) {
41
+ const body = JSON.stringify(payload);
42
+ const key = `${this.tenantId}/${entityName}/${referenceId}.json`;
43
+ await this.s3Client.send(new client_s3_1.PutObjectCommand({
44
+ Bucket: this.bucket,
45
+ Key: key,
46
+ Body: body,
47
+ ContentType: 'application/json',
48
+ }));
49
+ return {
50
+ location: {
51
+ type: 's3',
52
+ key,
53
+ },
54
+ size: Buffer.byteLength(body, 'utf8'),
55
+ };
56
+ }
57
+ /**
58
+ * Loads a payload from S3.
59
+ * @param location - The location of the payload to load.
60
+ * @returns {Promise<Record<string, unknown>>} A promise that resolves to the loaded payload.
61
+ */
62
+ async load(location) {
63
+ const response = await this.s3Client.send(new client_s3_1.GetObjectCommand({
64
+ Bucket: this.bucket,
65
+ Key: location.key,
66
+ }));
67
+ if (!response.Body) {
68
+ throw new Error(`Failed to load externalised items from S3 object ${location.key}`);
69
+ }
70
+ const contents = await response.Body.transformToString();
71
+ return JSON.parse(contents);
72
+ }
73
+ async delete(location) {
74
+ if (!location || !this.bucket) {
75
+ return;
76
+ }
77
+ await this.s3Client.send(new client_s3_1.DeleteObjectCommand({
78
+ Bucket: this.bucket,
79
+ Key: location.key,
80
+ }));
81
+ }
82
+ }
83
+ exports.S3ExternalStorage = S3ExternalStorage;
84
+ //# sourceMappingURL=S3ExternalStorage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"S3ExternalStorage.js","sourceRoot":"","sources":["../../src/s3/S3ExternalStorage.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAEH,WAAW;AACX,kDAAuG;AAUvG;;;;;;;;GAQG;AACH,MAAa,iBAAiB;IAiB5B;;;;;OAKG;IACH,YAAY,QAAgB,EAAE,QAAkB,EAAE,MAAc;QAC9D,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;IAED;;;;;;OAMG;IACI,KAAK,CAAC,IAAI,CACf,UAAkB,EAClB,WAAmB,EACnB,OAAe;QAEf,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QACrC,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,QAAQ,IAAI,UAAU,IAAI,WAAW,OAAO,CAAC;QACjE,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CACtB,IAAI,4BAAgB,CAAC;YACnB,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,GAAG,EAAE,GAAG;YACR,IAAI,EAAE,IAAI;YACV,WAAW,EAAE,kBAAkB;SAChC,CAAC,CACH,CAAC;QACF,OAAO;YACL,QAAQ,EAAE;gBACR,IAAI,EAAE,IAAI;gBACV,GAAG;aACJ;YACD,IAAI,EAAE,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC;SACtC,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACI,KAAK,CAAC,IAAI,CAAC,QAA2B;QAC3C,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CACvC,IAAI,4BAAgB,CAAC;YACnB,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,GAAG,EAAE,QAAQ,CAAC,GAAG;SAClB,CAAC,CACH,CAAC;QACF,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CAAC,oDAAoD,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC;QACtF,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,iBAAiB,EAAE,CAAC;QACzD,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC9B,CAAC;IAEM,KAAK,CAAC,MAAM,CAAC,QAA4B;QAC9C,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YAC9B,OAAO;QACT,CAAC;QACD,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CACtB,IAAI,+BAAmB,CAAC;YACtB,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,GAAG,EAAE,QAAQ,CAAC,GAAG;SAClB,CAAC,CACH,CAAC;IACJ,CAAC;CACF;AA1FD,8CA0FC"}
@@ -0,0 +1,12 @@
1
+ type S3MockSet = {
2
+ sendMock: jest.Mock;
3
+ putObjectCommandMock: jest.Mock;
4
+ getObjectCommandMock: jest.Mock;
5
+ deleteObjectCommandMock: jest.Mock;
6
+ headBucketCommandMock: jest.Mock;
7
+ };
8
+ declare global {
9
+ var __s3MockSet: S3MockSet | undefined;
10
+ }
11
+ export {};
12
+ //# sourceMappingURL=S3ExternalStorage.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"S3ExternalStorage.spec.d.ts","sourceRoot":"","sources":["../../src/s3/S3ExternalStorage.spec.ts"],"names":[],"mappings":"AAMA,KAAK,SAAS,GAAG;IACf,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC;IACpB,oBAAoB,EAAE,IAAI,CAAC,IAAI,CAAC;IAChC,oBAAoB,EAAE,IAAI,CAAC,IAAI,CAAC;IAChC,uBAAuB,EAAE,IAAI,CAAC,IAAI,CAAC;IACnC,qBAAqB,EAAE,IAAI,CAAC,IAAI,CAAC;CAClC,CAAC;AAEF,OAAO,CAAC,MAAM,CAAC;IAEb,IAAI,WAAW,EAAE,SAAS,GAAG,SAAS,CAAC;CACxC"}
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const S3ExternalStorage_1 = require("./S3ExternalStorage");
4
+ const client_s3_1 = require("@aws-sdk/client-s3");
5
+ const TEST_TENANT_ID = 'test-tenant';
6
+ const TEST_BUCKET = 'dx-test-us-db-fallback';
7
+ jest.mock('@aws-sdk/client-s3', () => {
8
+ const buildCommandMock = (name) => jest.fn().mockImplementation((input) => ({
9
+ name,
10
+ input,
11
+ }));
12
+ const state = {
13
+ sendMock: jest.fn(),
14
+ putObjectCommandMock: buildCommandMock('PutObjectCommand'),
15
+ getObjectCommandMock: buildCommandMock('GetObjectCommand'),
16
+ deleteObjectCommandMock: buildCommandMock('DeleteObjectCommand'),
17
+ headBucketCommandMock: buildCommandMock('HeadBucketCommand'),
18
+ };
19
+ global.__s3MockSet = state;
20
+ return {
21
+ S3Client: jest.fn().mockImplementation(() => ({
22
+ send: state.sendMock,
23
+ })),
24
+ PutObjectCommand: state.putObjectCommandMock,
25
+ GetObjectCommand: state.getObjectCommandMock,
26
+ DeleteObjectCommand: state.deleteObjectCommandMock,
27
+ HeadBucketCommand: state.headBucketCommandMock,
28
+ };
29
+ });
30
+ const { sendMock, putObjectCommandMock, getObjectCommandMock, deleteObjectCommandMock, headBucketCommandMock } = global.__s3MockSet;
31
+ jest.mock('crypto', () => ({
32
+ randomUUID: jest.fn(() => 'fixed-uuid'),
33
+ }));
34
+ describe('S3ExternalStorage', () => {
35
+ beforeEach(() => {
36
+ sendMock.mockReset();
37
+ putObjectCommandMock.mockClear();
38
+ getObjectCommandMock.mockClear();
39
+ deleteObjectCommandMock.mockClear();
40
+ headBucketCommandMock.mockClear();
41
+ });
42
+ const createStorage = () => new S3ExternalStorage_1.S3ExternalStorage(TEST_TENANT_ID, new client_s3_1.S3Client({ region: 'us-west-2' }), TEST_BUCKET);
43
+ it('saves payloads to S3 and returns location metadata', async () => {
44
+ const storage = createStorage();
45
+ const payload = { foo: 'bar' };
46
+ sendMock.mockResolvedValueOnce({}); // put object
47
+ const result = await storage.save('test_entity', 'abc', payload);
48
+ expect(putObjectCommandMock).toHaveBeenCalledWith(expect.objectContaining({
49
+ Bucket: 'dx-test-us-db-fallback',
50
+ Body: JSON.stringify(payload),
51
+ ContentType: 'application/json',
52
+ Key: result.location.key,
53
+ }));
54
+ expect(result.location).toMatchObject({
55
+ type: 's3',
56
+ });
57
+ expect(result.location.key).toBe('test-tenant/test_entity/abc.json');
58
+ expect(result.size).toBe(JSON.stringify(payload).length);
59
+ });
60
+ it('loads payloads from S3 and parses JSON', async () => {
61
+ const storage = createStorage();
62
+ const storedPayload = { baz: 'qux' };
63
+ const transformer = jest.fn().mockResolvedValue(JSON.stringify(storedPayload));
64
+ sendMock.mockResolvedValueOnce({
65
+ Body: {
66
+ transformToString: transformer,
67
+ },
68
+ });
69
+ const location = { type: 's3', key: 'items/foo/bar.json' };
70
+ const result = await storage.load(location);
71
+ expect(getObjectCommandMock).toHaveBeenCalledWith({
72
+ Bucket: 'dx-test-us-db-fallback',
73
+ Key: 'items/foo/bar.json',
74
+ });
75
+ expect(transformer).toHaveBeenCalled();
76
+ expect(result).toEqual(storedPayload);
77
+ });
78
+ it('deletes payloads from S3 when a location is provided', async () => {
79
+ const storage = createStorage();
80
+ const location = { type: 's3', key: 'items/foo.json' };
81
+ sendMock.mockResolvedValueOnce({}); // delete
82
+ await storage.delete(location);
83
+ expect(deleteObjectCommandMock).toHaveBeenCalledWith({
84
+ Bucket: 'dx-test-us-db-fallback',
85
+ Key: 'items/foo.json',
86
+ });
87
+ });
88
+ it('handles S3 errors when saving', async () => {
89
+ const storage = createStorage();
90
+ const payload = { foo: 'bar' };
91
+ const s3Error = Object.assign(new Error('AccessDenied'), {
92
+ name: 'AccessDenied',
93
+ $metadata: { httpStatusCode: 403 },
94
+ });
95
+ sendMock.mockRejectedValueOnce(s3Error);
96
+ await expect(storage.save('test_entity', 'first', payload)).rejects.toThrow('AccessDenied');
97
+ });
98
+ it('does not delete when location is undefined', async () => {
99
+ const storage = createStorage();
100
+ await storage.delete(undefined);
101
+ expect(deleteObjectCommandMock).not.toHaveBeenCalled();
102
+ });
103
+ it('generates unique S3 keys for different entity types', async () => {
104
+ const storage = createStorage();
105
+ sendMock.mockResolvedValue({});
106
+ const result1 = await storage.save('entity_type_a', 'id-1', { data: 'test' });
107
+ const result2 = await storage.save('entity_type_b', 'id-2', { data: 'test' });
108
+ expect(result1.location.key).toContain('entity_type_a/id-1');
109
+ expect(result2.location.key).toContain('entity_type_b/id-2');
110
+ });
111
+ it('throws error when S3 response has no body', async () => {
112
+ const storage = createStorage();
113
+ sendMock.mockResolvedValueOnce({ Body: undefined });
114
+ const location = { type: 's3', key: 'items/foo/bar.json' };
115
+ await expect(storage.load(location)).rejects.toThrow('Failed to load externalised items from S3 object items/foo/bar.json');
116
+ });
117
+ it('handles S3 errors gracefully when loading', async () => {
118
+ const storage = createStorage();
119
+ sendMock.mockRejectedValueOnce(new Error('S3 GetObject failed'));
120
+ const location = { type: 's3', key: 'items/foo/bar.json' };
121
+ await expect(storage.load(location)).rejects.toThrow('S3 GetObject failed');
122
+ });
123
+ it('handles S3 errors gracefully when deleting', async () => {
124
+ const storage = createStorage();
125
+ sendMock.mockRejectedValueOnce(new Error('S3 DeleteObject failed'));
126
+ const location = { type: 's3', key: 'items/foo.json' };
127
+ await expect(storage.delete(location)).rejects.toThrow('S3 DeleteObject failed');
128
+ });
129
+ });
130
+ //# sourceMappingURL=S3ExternalStorage.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"S3ExternalStorage.spec.js","sourceRoot":"","sources":["../../src/s3/S3ExternalStorage.spec.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/db-lib",
3
- "version": "1.76.0",
3
+ "version": "1.77.0",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "private": false,
@@ -19,19 +19,20 @@
19
19
  "@types/node": "22.10.5",
20
20
  "@types/pg": "^8.11.8",
21
21
  "aws-sdk-client-mock": "^4.0.0",
22
- "aws-sdk-client-mock-jest": "4.0.0",
22
+ "aws-sdk-client-mock-jest": "4.1.0",
23
23
  "jest": "29.4.1",
24
24
  "ts-jest": "29.0.5",
25
25
  "typescript": "^5.7.2"
26
26
  },
27
27
  "dependencies": {
28
- "@aws-sdk/client-dynamodb": "^3.651.1",
29
- "@aws-sdk/client-secrets-manager": "3.651.1",
30
- "@aws-sdk/lib-dynamodb": "^3.651.1",
28
+ "@aws-sdk/client-dynamodb": "^3.946.0",
29
+ "@aws-sdk/client-s3": "^3.946.0",
30
+ "@aws-sdk/client-secrets-manager": "^3.946.0",
31
+ "@aws-sdk/lib-dynamodb": "^3.946.0",
31
32
  "@opentelemetry/api": "^1.6.0",
32
33
  "@squiz/dx-common-lib": "^1.69.1",
33
34
  "@squiz/dx-logger-lib": "^1.65.1",
34
- "dotenv": "16.0.3",
35
+ "dotenv": "16.6.1",
35
36
  "pg": "^8.12.0"
36
37
  }
37
38
  }
@@ -21,7 +21,8 @@ import {
21
21
  } from '@aws-sdk/lib-dynamodb';
22
22
  import { ConditionalCheckFailedException } from '@aws-sdk/client-dynamodb';
23
23
  import { DynamoDbManager, Transaction } from './DynamoDbManager';
24
- import { MissingKeyValuesError, InvalidDbSchemaError } from '..';
24
+ import { MissingKeyValuesError } from '../error/MissingKeyValuesError';
25
+ import { InvalidDbSchemaError } from '../error/InvalidDbSchemaError';
25
26
 
26
27
  import { mockClient } from 'aws-sdk-client-mock';
27
28
  import 'aws-sdk-client-mock-jest';
@@ -10,7 +10,9 @@ import {
10
10
  BatchGetCommandOutput,
11
11
  } from '@aws-sdk/lib-dynamodb';
12
12
 
13
- import { Transaction, DynamoDbManager, MissingKeyValuesError, InvalidDbSchemaError } from '..';
13
+ import { Transaction, DynamoDbManager } from './DynamoDbManager';
14
+ import { MissingKeyValuesError } from '../error/MissingKeyValuesError';
15
+ import { InvalidDbSchemaError } from '../error/InvalidDbSchemaError';
14
16
  import { InvalidDataFormatError } from '../error/InvalidDataFormatError';
15
17
  import { UnknownKeyAttributeError } from '../error/UnknownKeyAttributeError';
16
18