@aws-blocks/bb-distributed-table 0.1.5 → 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 +22 -1
- package/README.md +57 -1
- package/dist/index.aws.d.ts +3 -2
- package/dist/index.aws.d.ts.map +1 -1
- package/dist/index.aws.js +3 -0
- package/dist/index.cdk.d.ts +12 -2
- package/dist/index.cdk.d.ts.map +1 -1
- package/dist/index.cdk.js +151 -13
- package/dist/index.cdk.test.js +313 -1
- package/dist/index.mock.d.ts +10 -2
- package/dist/index.mock.d.ts.map +1 -1
- package/dist/index.mock.js +33 -1
- package/dist/index.test.js +42 -0
- package/dist/types.d.ts +86 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/src/index.aws.ts +6 -0
- package/src/index.cdk.test.ts +341 -1
- package/src/index.cdk.ts +166 -15
- package/src/index.mock.ts +35 -1
- package/src/index.test.ts +45 -0
- package/src/types.ts +85 -0
- package/src/version.ts +1 -1
package/src/index.mock.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type {
|
|
|
15
15
|
DistributedTableOptions,
|
|
16
16
|
ReadValidationMode,
|
|
17
17
|
ExternalTableRef,
|
|
18
|
+
ExternalKmsKeyRef,
|
|
18
19
|
TableKey,
|
|
19
20
|
PartitionKeyCondition,
|
|
20
21
|
SortKeyCondition,
|
|
@@ -29,6 +30,7 @@ import type {
|
|
|
29
30
|
TableKeyConfig,
|
|
30
31
|
DistributedTableOptions,
|
|
31
32
|
ExternalTableRef,
|
|
33
|
+
ExternalKmsKeyRef,
|
|
32
34
|
SortKeyCondition,
|
|
33
35
|
ScanOptions,
|
|
34
36
|
PutOptions,
|
|
@@ -264,7 +266,14 @@ export class DistributedTable<
|
|
|
264
266
|
const dir = options.order === 'desc' ? -1 : 1;
|
|
265
267
|
items.sort((a, b) => {
|
|
266
268
|
const av = (a as any)[skField], bv = (b as any)[skField];
|
|
267
|
-
|
|
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;
|
|
268
277
|
});
|
|
269
278
|
}
|
|
270
279
|
|
|
@@ -334,6 +343,10 @@ export class DistributedTable<
|
|
|
334
343
|
return { __brand: 'ExternalTableRef' as const, tableName };
|
|
335
344
|
}
|
|
336
345
|
|
|
346
|
+
static fromKmsKey(keyArn: string): ExternalKmsKeyRef {
|
|
347
|
+
return { __brand: 'ExternalKmsKeyRef' as const, keyArn };
|
|
348
|
+
}
|
|
349
|
+
|
|
337
350
|
// ── Internal ────────────────────────────────────────────────────────────
|
|
338
351
|
|
|
339
352
|
private checkFieldEquals(keyStr: string, fields: Partial<T>): void {
|
|
@@ -353,6 +366,27 @@ export class DistributedTable<
|
|
|
353
366
|
}
|
|
354
367
|
}
|
|
355
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
|
+
|
|
356
390
|
private serializeKey(key: TableKey<T, K>): string {
|
|
357
391
|
const parts = [(key as any)[this.keyConfig.partitionKey]];
|
|
358
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/types.ts
CHANGED
|
@@ -57,6 +57,80 @@ export interface DistributedTableOptions<
|
|
|
57
57
|
* ```
|
|
58
58
|
*/
|
|
59
59
|
ttl?: keyof T & string;
|
|
60
|
+
/**
|
|
61
|
+
* DynamoDB Point-in-Time Recovery (continuous backups) — restore the table
|
|
62
|
+
* to any second within a retention window, protecting against accidental
|
|
63
|
+
* writes/deletes and logical corruption.
|
|
64
|
+
*
|
|
65
|
+
* A single knob, since the recovery window only means anything when PITR is
|
|
66
|
+
* on:
|
|
67
|
+
* - `true` — enable PITR with the default 35-day window.
|
|
68
|
+
* - `false` — disable PITR.
|
|
69
|
+
* - `{ retentionDays: n }` — enable PITR and keep `n` days of continuous
|
|
70
|
+
* backups (**1–35**). A shorter window reduces backup-storage cost at the
|
|
71
|
+
* expense of how far back you can restore.
|
|
72
|
+
*
|
|
73
|
+
* When omitted, the stack-wide default applies (`defaults.pointInTimeRecovery`
|
|
74
|
+
* from `BlocksPresets` — on under `production`, off under `sandbox`). A
|
|
75
|
+
* per-block value always wins.
|
|
76
|
+
*
|
|
77
|
+
* Note: PITR bills for continuous-backup storage (per GB-month of table
|
|
78
|
+
* size), so it is not free on large tables.
|
|
79
|
+
*/
|
|
80
|
+
pointInTimeRecovery?: boolean | { retentionDays: number };
|
|
81
|
+
/**
|
|
82
|
+
* How hard the table is to destroy — a single knob spanning DynamoDB
|
|
83
|
+
* deletion protection and the CloudFormation removal policy, which together
|
|
84
|
+
* answer one question: "can this table be destroyed?"
|
|
85
|
+
*
|
|
86
|
+
* - `'disposable'`: `RemovalPolicy.DESTROY`, deletion protection **off**.
|
|
87
|
+
* Deleting the stack deletes the table. The **sandbox default** — keeps
|
|
88
|
+
* `sandbox:destroy` a one-command teardown.
|
|
89
|
+
* - `'retained'`: `RemovalPolicy.RETAIN`, deletion protection **off**.
|
|
90
|
+
* Deleting the stack orphans (keeps) the table, but a direct
|
|
91
|
+
* `DeleteTable`/console delete still works. Use when you want the data to
|
|
92
|
+
* survive stack teardown without blocking intentional deletes.
|
|
93
|
+
* - `'locked'`: `RemovalPolicy.RETAIN` **and** deletion protection **on**.
|
|
94
|
+
* The table survives stack deletion and DynamoDB refuses a direct delete
|
|
95
|
+
* until protection is turned off. The **production default**.
|
|
96
|
+
*
|
|
97
|
+
* When omitted, removal policy and deletion protection follow the stack-wide
|
|
98
|
+
* `defaults` (`BlocksPresets.production` ≈ `'locked'`, `BlocksPresets.sandbox`
|
|
99
|
+
* ≈ `'disposable'`). A per-block value always wins.
|
|
100
|
+
*
|
|
101
|
+
* Replaces the separate `deletionProtection` + `removalPolicy` booleans:
|
|
102
|
+
* those two knobs could encode the contradictory `deletionProtection: true`
|
|
103
|
+
* + `removalPolicy: 'destroy'` state, which wedges stack deletion (CFN
|
|
104
|
+
* issues `DeleteTable`, DynamoDB refuses it, the stack lands in
|
|
105
|
+
* `DELETE_FAILED`). A single enum makes that state unrepresentable.
|
|
106
|
+
*/
|
|
107
|
+
protection?: 'disposable' | 'retained' | 'locked';
|
|
108
|
+
/**
|
|
109
|
+
* Server-side encryption at rest.
|
|
110
|
+
*
|
|
111
|
+
* - `'aws-managed'` (default): SSE with the AWS-managed `aws/dynamodb` KMS
|
|
112
|
+
* key. Auditable via CloudTrail with no per-key monthly charge.
|
|
113
|
+
* - `'customer-managed'`: provisions a **dedicated** customer-managed KMS
|
|
114
|
+
* key (CMK) for this table, giving you full control over rotation and key
|
|
115
|
+
* policy. Incurs standard KMS key + request charges — and note this mints
|
|
116
|
+
* a **separate key per table**, so a dozen tables means a dozen keys.
|
|
117
|
+
* - a {@link ExternalKmsKeyRef} from {@link DistributedTable.fromKmsKey}:
|
|
118
|
+
* uses an **existing** CMK you already own, so several tables can share one
|
|
119
|
+
* key (and one monthly charge) instead of each provisioning its own.
|
|
120
|
+
*
|
|
121
|
+
* DynamoDB is always encrypted at rest; this only selects the key.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* // Share one key across several tables
|
|
126
|
+
* const key = DistributedTable.fromKmsKey(
|
|
127
|
+
* 'arn:aws:kms:us-east-1:111122223333:key/abcd-1234',
|
|
128
|
+
* );
|
|
129
|
+
* new DistributedTable(scope, 'orders', { schema, key: { partitionKey: 'id' }, encryption: key });
|
|
130
|
+
* new DistributedTable(scope, 'events', { schema, key: { partitionKey: 'id' }, encryption: key });
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
encryption?: 'aws-managed' | 'customer-managed' | ExternalKmsKeyRef;
|
|
60
134
|
/**
|
|
61
135
|
* How reads (`get`, `getBatch`, `query`, `scan`) reconcile a stored item with
|
|
62
136
|
* the configured `schema`. Writes (`put`/`putBatch`) always validate; this
|
|
@@ -109,6 +183,17 @@ export interface ExternalTableRef {
|
|
|
109
183
|
readonly tableName: string;
|
|
110
184
|
}
|
|
111
185
|
|
|
186
|
+
/**
|
|
187
|
+
* A reference to an existing customer-managed KMS key, produced by
|
|
188
|
+
* {@link DistributedTable.fromKmsKey}. Pass it as the `encryption` option to
|
|
189
|
+
* encrypt the table with a CMK you already own — letting several tables share
|
|
190
|
+
* one key instead of each provisioning its own dedicated key.
|
|
191
|
+
*/
|
|
192
|
+
export interface ExternalKmsKeyRef {
|
|
193
|
+
readonly __brand: 'ExternalKmsKeyRef';
|
|
194
|
+
readonly keyArn: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
112
197
|
// ── Key type for get/delete ─────────────────────────────────────────────────
|
|
113
198
|
|
|
114
199
|
/**
|
package/src/version.ts
CHANGED