@aws-blocks/bb-distributed-table 0.1.6 → 0.1.7
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.
- package/DESIGN.md +4 -0
- package/dist/index.cdk.d.ts.map +1 -1
- package/dist/index.cdk.js +14 -2
- package/dist/index.cdk.test.js +13 -0
- package/dist/index.mock.d.ts +7 -0
- package/dist/index.mock.d.ts.map +1 -1
- package/dist/index.mock.js +30 -1
- package/dist/index.test.js +42 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/src/index.cdk.test.ts +14 -0
- package/src/index.cdk.ts +14 -2
- package/src/index.mock.ts +29 -1
- package/src/index.test.ts +45 -0
- package/src/version.ts +1 -1
package/DESIGN.md
CHANGED
|
@@ -81,6 +81,10 @@ orders.query({
|
|
|
81
81
|
|
|
82
82
|
The `order` field maps to DynamoDB's `ScanIndexForward` parameter (`'desc'` → `ScanIndexForward: false`). It defaults to `'asc'`.
|
|
83
83
|
|
|
84
|
+
**Ordering ties (GSI):** a GSI sort key need not be unique, so multiple items can share one sort-key value. The mock tie-breaks on the base-table primary key (partition key, then sort key) rather than on Map insertion order — otherwise results would depend on write order / disk-reload order. This **matches DynamoDB's observed ordering** for index rows with equal sort keys; AWS does not document a contractual guarantee for the tie order, so the mock is intentionally **deterministic even where DynamoDB's contract is unspecified** (a plus for reproducible tests — just don't rely on a specific tie order in production logic). Under `order: 'desc'` the whole index order, ties included, reverses.
|
|
85
|
+
|
|
86
|
+
**String-ordering boundary:** the mock compares sort/base keys with JavaScript `<`/`>` (UTF-16 code-unit order), which can differ from DynamoDB's UTF-8 byte order for non-ASCII string keys. This applies to both the index sort-key comparison and the base-key tie-break, and is a pre-existing property of the mock — surfaced here so the parity discussion is complete.
|
|
87
|
+
|
|
84
88
|
### D-DT-7: TTL via options field
|
|
85
89
|
|
|
86
90
|
**Decision:** TTL is configured via `ttl: 'fieldName'` in the constructor options, not as a separate method or decorator.
|
package/dist/index.cdk.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cdk.d.ts","sourceRoot":"","sources":["../src/index.cdk.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.cdk.d.ts","sourceRoot":"","sources":["../src/index.cdk.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,KAAK,EAAoC,MAAM,sBAAsB,CAAC;AAC/E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAItE,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,YAAY,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,cAAc,EAAE,QAAQ,EAAE,UAAU,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEnM,qBAAa,gBAAgB,CAAC,CAAC,GAAG,GAAG,CAAE,SAAQ,KAAK;IA2BA,OAAO,EAAE,GAAG;IA1B/D,OAAO,CAAC,KAAK,CAAS;IAEtB;;;;;;OAMG;IACH,MAAM,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB;IAIxD;;;;;;;;OAQG;IACH,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB;gBAIxC,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAS,OAAO,EAAE,GAAG;IAqO/D,GAAG,CAAC,GAAG,KAAK,EAAE,OAAO,EAAE,GAAG,KAAK;IAC/B,GAAG,CAAC,GAAG,KAAK,EAAE,OAAO,EAAE,GAAG,KAAK;IAC/B,MAAM,CAAC,GAAG,KAAK,EAAE,OAAO,EAAE,GAAG,KAAK;IAClC,KAAK,CAAC,GAAG,KAAK,EAAE,OAAO,EAAE,GAAG,KAAK;IACjC,IAAI,CAAC,GAAG,KAAK,EAAE,OAAO,EAAE,GAAG,KAAK;IAChC,QAAQ,CAAC,GAAG,KAAK,EAAE,OAAO,EAAE,GAAG,KAAK;IACpC,QAAQ,CAAC,GAAG,KAAK,EAAE,OAAO,EAAE,GAAG,KAAK;IACpC,WAAW,CAAC,GAAG,KAAK,EAAE,OAAO,EAAE,GAAG,KAAK;CACvC"}
|
package/dist/index.cdk.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Table, AttributeType, BillingMode, TableEncryption } from 'aws-cdk-lib/
|
|
|
4
4
|
import * as cdk from 'aws-cdk-lib';
|
|
5
5
|
import { Annotations, CustomResource, Duration, RemovalPolicy } from 'aws-cdk-lib';
|
|
6
6
|
import { Code, Function as LambdaFunction } from 'aws-cdk-lib/aws-lambda';
|
|
7
|
+
import { LogGroup } from 'aws-cdk-lib/aws-logs';
|
|
7
8
|
import { Provider } from 'aws-cdk-lib/custom-resources';
|
|
8
9
|
import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
|
|
9
10
|
import { Key } from 'aws-cdk-lib/aws-kms';
|
|
@@ -207,7 +208,7 @@ export class DistributedTable extends Scope {
|
|
|
207
208
|
}));
|
|
208
209
|
// Add GSI manager if indexes are defined
|
|
209
210
|
if (config.indexes && Object.keys(config.indexes).length > 0) {
|
|
210
|
-
const gsiProvider = getOrCreateGsiProvider(cdk.Stack.of(this));
|
|
211
|
+
const gsiProvider = getOrCreateGsiProvider(cdk.Stack.of(this), this.defaults.logRetention);
|
|
211
212
|
gsiProvider.addTableArn(this.table.tableArn, isSandbox);
|
|
212
213
|
const indexesWithTypes = {};
|
|
213
214
|
for (const [indexName, indexConfig] of Object.entries(config.indexes)) {
|
|
@@ -249,24 +250,35 @@ export class DistributedTable extends Scope {
|
|
|
249
250
|
}
|
|
250
251
|
// ── Shared GSI Manager Provider (one per stack) ─────────────────────────────
|
|
251
252
|
const GSI_PROVIDER_KEY = Symbol.for('BLOCKS_GSI_MANAGER_PROVIDER');
|
|
252
|
-
function getOrCreateGsiProvider(stack) {
|
|
253
|
+
function getOrCreateGsiProvider(stack, logRetention) {
|
|
253
254
|
const existing = stack[GSI_PROVIDER_KEY];
|
|
254
255
|
if (existing)
|
|
255
256
|
return existing;
|
|
256
257
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
257
258
|
const tableArns = [];
|
|
258
259
|
const sandboxTableArns = [];
|
|
260
|
+
// Own the GSI-manager Lambdas' log groups so their retention follows the
|
|
261
|
+
// stack-wide default instead of AWS's infinite retention. Torn down with the
|
|
262
|
+
// stack (logs are not durable state).
|
|
259
263
|
const gsiManagerLambda = new LambdaFunction(stack, 'BlocksGsiManager', {
|
|
260
264
|
runtime: DEFAULT_NODE_RUNTIME,
|
|
261
265
|
handler: 'index.handler',
|
|
262
266
|
code: Code.fromAsset(join(__dirname, 'gsi-manager-lambda')),
|
|
263
267
|
timeout: Duration.minutes(15),
|
|
268
|
+
logGroup: new LogGroup(stack, 'BlocksGsiManagerLogs', {
|
|
269
|
+
retention: logRetention,
|
|
270
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
271
|
+
}),
|
|
264
272
|
});
|
|
265
273
|
const gsiIsCompleteLambda = new LambdaFunction(stack, 'BlocksGsiIsComplete', {
|
|
266
274
|
runtime: DEFAULT_NODE_RUNTIME,
|
|
267
275
|
handler: 'index.isCompleteHandler',
|
|
268
276
|
code: Code.fromAsset(join(__dirname, 'gsi-manager-lambda')),
|
|
269
277
|
timeout: Duration.minutes(1),
|
|
278
|
+
logGroup: new LogGroup(stack, 'BlocksGsiIsCompleteLogs', {
|
|
279
|
+
retention: logRetention,
|
|
280
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
281
|
+
}),
|
|
270
282
|
});
|
|
271
283
|
// Production permissions — lazily resolved so ARNs accumulate as tables register
|
|
272
284
|
gsiManagerLambda.addToRolePolicy(new PolicyStatement({
|
package/dist/index.cdk.test.js
CHANGED
|
@@ -332,6 +332,19 @@ test('CDK: a per-block protection option overrides the stack defaults', () => {
|
|
|
332
332
|
template.hasResource('AWS::DynamoDB::Table', { DeletionPolicy: 'Retain' });
|
|
333
333
|
template.hasResourceProperties('AWS::DynamoDB::Table', { DeletionProtectionEnabled: true });
|
|
334
334
|
});
|
|
335
|
+
test('CDK: GSI manager Lambda log groups adopt defaults.logRetention', () => {
|
|
336
|
+
const { stack, parent } = setup(BlocksPresets.sandbox);
|
|
337
|
+
new DistributedTable(parent, 'users', {
|
|
338
|
+
schema: userSchema,
|
|
339
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
340
|
+
indexes: { byEmail: { partitionKey: 'email' } },
|
|
341
|
+
});
|
|
342
|
+
const template = Template.fromStack(stack);
|
|
343
|
+
// The GSI manager + isComplete Lambdas now own explicit log groups whose
|
|
344
|
+
// retention follows the stack-wide default (sandbox → one week), instead of
|
|
345
|
+
// AWS's infinite default.
|
|
346
|
+
template.hasResourceProperties('AWS::Logs::LogGroup', { RetentionInDays: 7 });
|
|
347
|
+
});
|
|
335
348
|
test('CDK: DistributedTable.fromExisting does NOT provision a table (regression)', () => {
|
|
336
349
|
const { stack, parent } = setup();
|
|
337
350
|
new DistributedTable(parent, 'users', {
|
package/dist/index.mock.d.ts
CHANGED
|
@@ -101,6 +101,13 @@ export declare class DistributedTable<T, K extends TableKeyConfig<T> = TableKeyC
|
|
|
101
101
|
static fromExisting(tableName: string): ExternalTableRef;
|
|
102
102
|
static fromKmsKey(keyArn: string): ExternalKmsKeyRef;
|
|
103
103
|
private checkFieldEquals;
|
|
104
|
+
/**
|
|
105
|
+
* Deterministic tie-break for `query` ordering: compare two items by the
|
|
106
|
+
* base-table primary key (partition key, then sort key). Used when an index
|
|
107
|
+
* sort-key value is shared by multiple items, so results don't depend on Map
|
|
108
|
+
* insertion order. Returns a stable -1/0/1.
|
|
109
|
+
*/
|
|
110
|
+
private compareByBaseKey;
|
|
104
111
|
private serializeKey;
|
|
105
112
|
private loadFromDisk;
|
|
106
113
|
private flushToDisk;
|
package/dist/index.mock.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mock.d.ts","sourceRoot":"","sources":["../src/index.mock.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,EAA0B,MAAM,kBAAkB,CAAC;AAEjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAMpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,YAAY,EACX,cAAc,EACd,uBAAuB,EACvB,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,QAAQ,EACR,qBAAqB,EACrB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,UAAU,EACV,aAAa,GACb,MAAM,YAAY,CAAC;AAEpB,OAAO,KAAK,EACX,cAAc,EACd,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,EAEjB,WAAW,EACX,UAAU,EACV,aAAa,EACb,QAAQ,EAER,MAAM,YAAY,CAAC;AA4DpB;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,gBAAgB,CAC5B,CAAC,EACD,CAAC,SAAS,cAAc,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,EAC/C,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,CACpF,SAAQ,KAAK;IAWqC,OAAO,EAAE,uBAAuB,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC;IAVlG,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,IAAI,CAAiB;IAC7B,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,cAAc,CAAqB;IAE3C,0FAA0F;IAC1F,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC;gBAEf,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAS,OAAO,EAAE,uBAAuB,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC;IAe5F,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAIjD;;;;;OAKG;IACH,OAAO,CAAC,aAAa;IAIf,GAAG,CAAC,IAAI,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBpD,MAAM,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAc5E;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACI,KAAK,CACX,OAAO,EAAE,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC,GAClC,aAAa,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.mock.d.ts","sourceRoot":"","sources":["../src/index.mock.ts"],"names":[],"mappings":"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"}
|
package/dist/index.mock.js
CHANGED
|
@@ -212,7 +212,14 @@ export class DistributedTable extends Scope {
|
|
|
212
212
|
const dir = options.order === 'desc' ? -1 : 1;
|
|
213
213
|
items.sort((a, b) => {
|
|
214
214
|
const av = a[skField], bv = b[skField];
|
|
215
|
-
|
|
215
|
+
const primary = av < bv ? -1 : av > bv ? 1 : 0;
|
|
216
|
+
// Tie-break on the base-table primary key. On a GSI the sort key need
|
|
217
|
+
// not be unique, so equal sort-key values must NOT fall back to Map
|
|
218
|
+
// insertion order (which varies by write order / disk reload) — that's
|
|
219
|
+
// the mock-vs-DynamoDB divergence. DynamoDB orders index ties by the
|
|
220
|
+
// base-table key, and the whole index (ties included) reverses under
|
|
221
|
+
// `order: 'desc'`.
|
|
222
|
+
return (primary !== 0 ? primary : this.compareByBaseKey(a, b)) * dir;
|
|
216
223
|
});
|
|
217
224
|
}
|
|
218
225
|
let count = 0;
|
|
@@ -295,6 +302,28 @@ export class DistributedTable extends Scope {
|
|
|
295
302
|
}
|
|
296
303
|
}
|
|
297
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Deterministic tie-break for `query` ordering: compare two items by the
|
|
307
|
+
* base-table primary key (partition key, then sort key). Used when an index
|
|
308
|
+
* sort-key value is shared by multiple items, so results don't depend on Map
|
|
309
|
+
* insertion order. Returns a stable -1/0/1.
|
|
310
|
+
*/
|
|
311
|
+
compareByBaseKey(a, b) {
|
|
312
|
+
const pk = this.keyConfig.partitionKey;
|
|
313
|
+
const ap = a[pk], bp = b[pk];
|
|
314
|
+
if (ap !== bp)
|
|
315
|
+
return ap < bp ? -1 : 1;
|
|
316
|
+
const sk = this.keyConfig.sortKey;
|
|
317
|
+
if (sk) {
|
|
318
|
+
const as = a[sk], bs = b[sk];
|
|
319
|
+
if (as !== bs)
|
|
320
|
+
return as < bs ? -1 : 1;
|
|
321
|
+
}
|
|
322
|
+
// Unreachable for distinct rows: every item is keyed by its serialized base
|
|
323
|
+
// primary key, so two different entries always differ in base PK or SK.
|
|
324
|
+
// (String comparisons above use JS UTF-16 order — see DESIGN.md D-DT-6.)
|
|
325
|
+
return 0;
|
|
326
|
+
}
|
|
298
327
|
serializeKey(key) {
|
|
299
328
|
const parts = [key[this.keyConfig.partitionKey]];
|
|
300
329
|
if (this.keyConfig.sortKey)
|
package/dist/index.test.js
CHANGED
|
@@ -724,4 +724,46 @@ describe('DistributedTable', () => {
|
|
|
724
724
|
async () => { for await (const _ of table.query({ index: 'doesNotExist', where: { userId: { equals: 'u1' } } })) { } }, /Index 'doesNotExist' not found/);
|
|
725
725
|
});
|
|
726
726
|
});
|
|
727
|
+
// ── Query: index sort-key ties are ordered deterministically ────────────
|
|
728
|
+
describe('query (index sort-key ties)', () => {
|
|
729
|
+
const cardSchema = z.object({ boardId: z.string(), cardId: z.string(), position: z.number() });
|
|
730
|
+
// Base key is (boardId, cardId); the GSI sorts by `position`, which is NOT
|
|
731
|
+
// unique — several cards can share a position. DynamoDB tie-breaks such
|
|
732
|
+
// index rows by the base-table key, so the mock must too (not by Map
|
|
733
|
+
// insertion order, which varies by write order / disk reload).
|
|
734
|
+
function cardTable() {
|
|
735
|
+
return new DistributedTable(testScope(), 'cards', {
|
|
736
|
+
schema: cardSchema,
|
|
737
|
+
key: { partitionKey: 'boardId', sortKey: 'cardId' },
|
|
738
|
+
indexes: { byPosition: { partitionKey: 'boardId', sortKey: 'position' } },
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
const q = (t, order) => collect(t.query({ index: 'byPosition', where: { boardId: { equals: 'b1' } }, ...(order ? { order } : {}) }));
|
|
742
|
+
test('ties on the index sort key order by the base-table key, regardless of write order', async () => {
|
|
743
|
+
const t1 = cardTable();
|
|
744
|
+
// All position=1; insert cardIds out of order.
|
|
745
|
+
for (const cardId of ['c3', 'c1', 'c2'])
|
|
746
|
+
await t1.put({ boardId: 'b1', cardId, position: 1 });
|
|
747
|
+
assert.deepEqual((await q(t1)).map((c) => c.cardId), ['c1', 'c2', 'c3']);
|
|
748
|
+
// A different write order must yield the SAME result (deterministic).
|
|
749
|
+
const t2 = cardTable();
|
|
750
|
+
for (const cardId of ['c2', 'c3', 'c1'])
|
|
751
|
+
await t2.put({ boardId: 'b1', cardId, position: 1 });
|
|
752
|
+
assert.deepEqual((await q(t2)).map((c) => c.cardId), ['c1', 'c2', 'c3']);
|
|
753
|
+
});
|
|
754
|
+
test('desc reverses ties too (whole index order flips)', async () => {
|
|
755
|
+
const t = cardTable();
|
|
756
|
+
for (const cardId of ['c1', 'c3', 'c2'])
|
|
757
|
+
await t.put({ boardId: 'b1', cardId, position: 1 });
|
|
758
|
+
assert.deepEqual((await q(t, 'desc')).map((c) => c.cardId), ['c3', 'c2', 'c1']);
|
|
759
|
+
});
|
|
760
|
+
test('primary order stays by index sort key; base key only breaks ties', async () => {
|
|
761
|
+
const t = cardTable();
|
|
762
|
+
await t.put({ boardId: 'b1', cardId: 'zzz', position: 1 });
|
|
763
|
+
await t.put({ boardId: 'b1', cardId: 'aaa', position: 2 });
|
|
764
|
+
await t.put({ boardId: 'b1', cardId: 'mmm', position: 1 });
|
|
765
|
+
// position asc first (1,1,2); within position=1, base key (cardId) breaks the tie.
|
|
766
|
+
assert.deepEqual((await q(t)).map((c) => [c.position, c.cardId]), [[1, 'mmm'], [1, 'zzz'], [2, 'aaa']]);
|
|
767
|
+
});
|
|
768
|
+
});
|
|
727
769
|
});
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aws-blocks/bb-distributed-table",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/aws-devtools-labs/aws-blocks.git",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"test": "node --test --test-concurrency=1 dist/index.test.js dist/parity.test.js dist/index.cdk.test.js"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@aws-blocks/core": "^0.
|
|
43
|
-
"@aws-blocks/bb-logger": "^0.1.
|
|
42
|
+
"@aws-blocks/core": "^0.4.0",
|
|
43
|
+
"@aws-blocks/bb-logger": "^0.1.6",
|
|
44
44
|
"@aws-sdk/client-dynamodb": "^3.0.0",
|
|
45
45
|
"@aws-sdk/lib-dynamodb": "^3.0.0",
|
|
46
46
|
"@standard-schema/spec": "^1.0.0",
|
package/src/index.cdk.test.ts
CHANGED
|
@@ -363,6 +363,20 @@ test('CDK: a per-block protection option overrides the stack defaults', () => {
|
|
|
363
363
|
template.hasResourceProperties('AWS::DynamoDB::Table', { DeletionProtectionEnabled: true });
|
|
364
364
|
});
|
|
365
365
|
|
|
366
|
+
test('CDK: GSI manager Lambda log groups adopt defaults.logRetention', () => {
|
|
367
|
+
const { stack, parent } = setup(BlocksPresets.sandbox);
|
|
368
|
+
new DistributedTable(parent, 'users', {
|
|
369
|
+
schema: userSchema,
|
|
370
|
+
key: { partitionKey: 'userId', sortKey: 'createdAt' },
|
|
371
|
+
indexes: { byEmail: { partitionKey: 'email' } },
|
|
372
|
+
});
|
|
373
|
+
const template = Template.fromStack(stack);
|
|
374
|
+
// The GSI manager + isComplete Lambdas now own explicit log groups whose
|
|
375
|
+
// retention follows the stack-wide default (sandbox → one week), instead of
|
|
376
|
+
// AWS's infinite default.
|
|
377
|
+
template.hasResourceProperties('AWS::Logs::LogGroup', { RetentionInDays: 7 });
|
|
378
|
+
});
|
|
379
|
+
|
|
366
380
|
test('CDK: DistributedTable.fromExisting does NOT provision a table (regression)', () => {
|
|
367
381
|
const { stack, parent } = setup();
|
|
368
382
|
new DistributedTable(parent, 'users', {
|
package/src/index.cdk.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { Table, type ITable, AttributeType, BillingMode, TableEncryption } from
|
|
|
6
6
|
import * as cdk from 'aws-cdk-lib';
|
|
7
7
|
import { Annotations, CustomResource, Duration, RemovalPolicy } from 'aws-cdk-lib';
|
|
8
8
|
import { Code, Function as LambdaFunction } from 'aws-cdk-lib/aws-lambda';
|
|
9
|
+
import { LogGroup, type RetentionDays } from 'aws-cdk-lib/aws-logs';
|
|
9
10
|
import { Provider } from 'aws-cdk-lib/custom-resources';
|
|
10
11
|
import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
|
|
11
12
|
import { Key, type IKey } from 'aws-cdk-lib/aws-kms';
|
|
@@ -239,7 +240,7 @@ export class DistributedTable<T = any> extends Scope {
|
|
|
239
240
|
|
|
240
241
|
// Add GSI manager if indexes are defined
|
|
241
242
|
if (config.indexes && Object.keys(config.indexes).length > 0) {
|
|
242
|
-
const gsiProvider = getOrCreateGsiProvider(cdk.Stack.of(this));
|
|
243
|
+
const gsiProvider = getOrCreateGsiProvider(cdk.Stack.of(this), this.defaults.logRetention);
|
|
243
244
|
gsiProvider.addTableArn(this.table.tableArn, isSandbox);
|
|
244
245
|
|
|
245
246
|
const indexesWithTypes: Record<string, any> = {};
|
|
@@ -293,7 +294,7 @@ interface SharedGsiProvider {
|
|
|
293
294
|
addTableArn: (tableArn: string, isSandbox: boolean) => void;
|
|
294
295
|
}
|
|
295
296
|
|
|
296
|
-
function getOrCreateGsiProvider(stack: cdk.Stack): SharedGsiProvider {
|
|
297
|
+
function getOrCreateGsiProvider(stack: cdk.Stack, logRetention: RetentionDays): SharedGsiProvider {
|
|
297
298
|
const existing = (stack as any)[GSI_PROVIDER_KEY] as SharedGsiProvider | undefined;
|
|
298
299
|
if (existing) return existing;
|
|
299
300
|
|
|
@@ -302,11 +303,18 @@ function getOrCreateGsiProvider(stack: cdk.Stack): SharedGsiProvider {
|
|
|
302
303
|
const tableArns: string[] = [];
|
|
303
304
|
const sandboxTableArns: string[] = [];
|
|
304
305
|
|
|
306
|
+
// Own the GSI-manager Lambdas' log groups so their retention follows the
|
|
307
|
+
// stack-wide default instead of AWS's infinite retention. Torn down with the
|
|
308
|
+
// stack (logs are not durable state).
|
|
305
309
|
const gsiManagerLambda = new LambdaFunction(stack, 'BlocksGsiManager', {
|
|
306
310
|
runtime: DEFAULT_NODE_RUNTIME,
|
|
307
311
|
handler: 'index.handler',
|
|
308
312
|
code: Code.fromAsset(join(__dirname, 'gsi-manager-lambda')),
|
|
309
313
|
timeout: Duration.minutes(15),
|
|
314
|
+
logGroup: new LogGroup(stack, 'BlocksGsiManagerLogs', {
|
|
315
|
+
retention: logRetention,
|
|
316
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
317
|
+
}),
|
|
310
318
|
});
|
|
311
319
|
|
|
312
320
|
const gsiIsCompleteLambda = new LambdaFunction(stack, 'BlocksGsiIsComplete', {
|
|
@@ -314,6 +322,10 @@ function getOrCreateGsiProvider(stack: cdk.Stack): SharedGsiProvider {
|
|
|
314
322
|
handler: 'index.isCompleteHandler',
|
|
315
323
|
code: Code.fromAsset(join(__dirname, 'gsi-manager-lambda')),
|
|
316
324
|
timeout: Duration.minutes(1),
|
|
325
|
+
logGroup: new LogGroup(stack, 'BlocksGsiIsCompleteLogs', {
|
|
326
|
+
retention: logRetention,
|
|
327
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
328
|
+
}),
|
|
317
329
|
});
|
|
318
330
|
|
|
319
331
|
// Production permissions — lazily resolved so ARNs accumulate as tables register
|
package/src/index.mock.ts
CHANGED
|
@@ -266,7 +266,14 @@ export class DistributedTable<
|
|
|
266
266
|
const dir = options.order === 'desc' ? -1 : 1;
|
|
267
267
|
items.sort((a, b) => {
|
|
268
268
|
const av = (a as any)[skField], bv = (b as any)[skField];
|
|
269
|
-
|
|
269
|
+
const primary = av < bv ? -1 : av > bv ? 1 : 0;
|
|
270
|
+
// Tie-break on the base-table primary key. On a GSI the sort key need
|
|
271
|
+
// not be unique, so equal sort-key values must NOT fall back to Map
|
|
272
|
+
// insertion order (which varies by write order / disk reload) — that's
|
|
273
|
+
// the mock-vs-DynamoDB divergence. DynamoDB orders index ties by the
|
|
274
|
+
// base-table key, and the whole index (ties included) reverses under
|
|
275
|
+
// `order: 'desc'`.
|
|
276
|
+
return (primary !== 0 ? primary : this.compareByBaseKey(a, b)) * dir;
|
|
270
277
|
});
|
|
271
278
|
}
|
|
272
279
|
|
|
@@ -359,6 +366,27 @@ export class DistributedTable<
|
|
|
359
366
|
}
|
|
360
367
|
}
|
|
361
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Deterministic tie-break for `query` ordering: compare two items by the
|
|
371
|
+
* base-table primary key (partition key, then sort key). Used when an index
|
|
372
|
+
* sort-key value is shared by multiple items, so results don't depend on Map
|
|
373
|
+
* insertion order. Returns a stable -1/0/1.
|
|
374
|
+
*/
|
|
375
|
+
private compareByBaseKey(a: T, b: T): number {
|
|
376
|
+
const pk = this.keyConfig.partitionKey;
|
|
377
|
+
const ap = (a as any)[pk], bp = (b as any)[pk];
|
|
378
|
+
if (ap !== bp) return ap < bp ? -1 : 1;
|
|
379
|
+
const sk = this.keyConfig.sortKey;
|
|
380
|
+
if (sk) {
|
|
381
|
+
const as = (a as any)[sk], bs = (b as any)[sk];
|
|
382
|
+
if (as !== bs) return as < bs ? -1 : 1;
|
|
383
|
+
}
|
|
384
|
+
// Unreachable for distinct rows: every item is keyed by its serialized base
|
|
385
|
+
// primary key, so two different entries always differ in base PK or SK.
|
|
386
|
+
// (String comparisons above use JS UTF-16 order — see DESIGN.md D-DT-6.)
|
|
387
|
+
return 0;
|
|
388
|
+
}
|
|
389
|
+
|
|
362
390
|
private serializeKey(key: TableKey<T, K>): string {
|
|
363
391
|
const parts = [(key as any)[this.keyConfig.partitionKey]];
|
|
364
392
|
if (this.keyConfig.sortKey) parts.push((key as any)[this.keyConfig.sortKey]);
|
package/src/index.test.ts
CHANGED
|
@@ -859,4 +859,49 @@ describe('DistributedTable', () => {
|
|
|
859
859
|
);
|
|
860
860
|
});
|
|
861
861
|
});
|
|
862
|
+
|
|
863
|
+
// ── Query: index sort-key ties are ordered deterministically ────────────
|
|
864
|
+
describe('query (index sort-key ties)', () => {
|
|
865
|
+
const cardSchema = z.object({ boardId: z.string(), cardId: z.string(), position: z.number() });
|
|
866
|
+
// Base key is (boardId, cardId); the GSI sorts by `position`, which is NOT
|
|
867
|
+
// unique — several cards can share a position. DynamoDB tie-breaks such
|
|
868
|
+
// index rows by the base-table key, so the mock must too (not by Map
|
|
869
|
+
// insertion order, which varies by write order / disk reload).
|
|
870
|
+
function cardTable() {
|
|
871
|
+
return new DistributedTable(testScope(), 'cards', {
|
|
872
|
+
schema: cardSchema,
|
|
873
|
+
key: { partitionKey: 'boardId', sortKey: 'cardId' },
|
|
874
|
+
indexes: { byPosition: { partitionKey: 'boardId', sortKey: 'position' } },
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
const q = (t: ReturnType<typeof cardTable>, order?: 'asc' | 'desc') =>
|
|
878
|
+
collect(t.query({ index: 'byPosition', where: { boardId: { equals: 'b1' } }, ...(order ? { order } : {}) }));
|
|
879
|
+
|
|
880
|
+
test('ties on the index sort key order by the base-table key, regardless of write order', async () => {
|
|
881
|
+
const t1 = cardTable();
|
|
882
|
+
// All position=1; insert cardIds out of order.
|
|
883
|
+
for (const cardId of ['c3', 'c1', 'c2']) await t1.put({ boardId: 'b1', cardId, position: 1 });
|
|
884
|
+
assert.deepEqual((await q(t1)).map((c) => c.cardId), ['c1', 'c2', 'c3']);
|
|
885
|
+
|
|
886
|
+
// A different write order must yield the SAME result (deterministic).
|
|
887
|
+
const t2 = cardTable();
|
|
888
|
+
for (const cardId of ['c2', 'c3', 'c1']) await t2.put({ boardId: 'b1', cardId, position: 1 });
|
|
889
|
+
assert.deepEqual((await q(t2)).map((c) => c.cardId), ['c1', 'c2', 'c3']);
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
test('desc reverses ties too (whole index order flips)', async () => {
|
|
893
|
+
const t = cardTable();
|
|
894
|
+
for (const cardId of ['c1', 'c3', 'c2']) await t.put({ boardId: 'b1', cardId, position: 1 });
|
|
895
|
+
assert.deepEqual((await q(t, 'desc')).map((c) => c.cardId), ['c3', 'c2', 'c1']);
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
test('primary order stays by index sort key; base key only breaks ties', async () => {
|
|
899
|
+
const t = cardTable();
|
|
900
|
+
await t.put({ boardId: 'b1', cardId: 'zzz', position: 1 });
|
|
901
|
+
await t.put({ boardId: 'b1', cardId: 'aaa', position: 2 });
|
|
902
|
+
await t.put({ boardId: 'b1', cardId: 'mmm', position: 1 });
|
|
903
|
+
// position asc first (1,1,2); within position=1, base key (cardId) breaks the tie.
|
|
904
|
+
assert.deepEqual((await q(t)).map((c) => [c.position, c.cardId]), [[1, 'mmm'], [1, 'zzz'], [2, 'aaa']]);
|
|
905
|
+
});
|
|
906
|
+
});
|
|
862
907
|
});
|
package/src/version.ts
CHANGED