@aws-blocks/bb-distributed-table 0.1.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.
- package/LICENSE +174 -0
- package/README.md +292 -0
- package/dist/errors.d.ts +111 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +135 -0
- package/dist/gsi-manager-lambda/index.js +221 -0
- package/dist/gsi-manager-lambda.d.ts +26 -0
- package/dist/gsi-manager-lambda.d.ts.map +1 -0
- package/dist/gsi-manager-lambda.js +244 -0
- package/dist/index.aws.d.ts +59 -0
- package/dist/index.aws.d.ts.map +1 -0
- package/dist/index.aws.js +310 -0
- package/dist/index.browser.d.ts +5 -0
- package/dist/index.browser.d.ts.map +1 -0
- package/dist/index.browser.js +7 -0
- package/dist/index.cdk.d.ts +27 -0
- package/dist/index.cdk.d.ts.map +1 -0
- package/dist/index.cdk.js +180 -0
- package/dist/index.cdk.test.d.ts +2 -0
- package/dist/index.cdk.test.d.ts.map +1 -0
- package/dist/index.cdk.test.js +93 -0
- package/dist/index.mock.d.ts +101 -0
- package/dist/index.mock.d.ts.map +1 -0
- package/dist/index.mock.js +301 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +555 -0
- package/dist/parity.test.d.ts +2 -0
- package/dist/parity.test.d.ts.map +1 -0
- package/dist/parity.test.js +557 -0
- package/dist/types.d.ts +143 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/package.json +49 -0
- package/src/errors.ts +145 -0
- package/src/gsi-manager-lambda.ts +305 -0
- package/src/index.aws.ts +400 -0
- package/src/index.browser.ts +8 -0
- package/src/index.cdk.test.ts +107 -0
- package/src/index.cdk.ts +220 -0
- package/src/index.mock.ts +363 -0
- package/src/index.test.ts +657 -0
- package/src/parity.test.ts +763 -0
- package/src/types.ts +163 -0
- package/src/version.ts +3 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Scope } from '@aws-blocks/core';
|
|
2
|
+
import type { ScopeParent } from '@aws-blocks/core';
|
|
3
|
+
export { DistributedTableErrors } from './errors.js';
|
|
4
|
+
export type { TableKeyConfig, DistributedTableOptions, ExternalTableRef, TableKey, PartitionKeyCondition, SortKeyCondition, KeyCondition, QueryOptions, ScanOptions, PutOptions, DeleteOptions, } from './types.js';
|
|
5
|
+
import type { TableKeyConfig, DistributedTableOptions, ExternalTableRef, ScanOptions, PutOptions, DeleteOptions, TableKey } from './types.js';
|
|
6
|
+
import type { QueryOptions } from './types.js';
|
|
7
|
+
import type { ChildLogger } from '@aws-blocks/bb-logger';
|
|
8
|
+
export declare class DistributedTable<T, K extends TableKeyConfig<T> = TableKeyConfig<T>, Indexes extends Record<string, TableKeyConfig<T>> = Record<string, TableKeyConfig<T>>> extends Scope {
|
|
9
|
+
options: DistributedTableOptions<T, K, Indexes>;
|
|
10
|
+
readonly bbName = "DistributedTable";
|
|
11
|
+
private schema;
|
|
12
|
+
private keyConfig;
|
|
13
|
+
private indexes;
|
|
14
|
+
private docClient;
|
|
15
|
+
/** @internal Logger for internal operations. Defaults to error-level when not provided. */
|
|
16
|
+
protected log: ChildLogger;
|
|
17
|
+
constructor(scope: ScopeParent, id: string, options: DistributedTableOptions<T, K, Indexes>);
|
|
18
|
+
get(key: TableKey<T, K>): Promise<T | null>;
|
|
19
|
+
put(item: T, options?: PutOptions<T>): Promise<void>;
|
|
20
|
+
delete(key: TableKey<T, K>, options?: DeleteOptions<T>): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Query items by index, yielding matches as an async stream with automatic
|
|
23
|
+
* pagination over DynamoDB's `LastEvaluatedKey`.
|
|
24
|
+
*
|
|
25
|
+
* @throws {DistributedTableErrors.InvalidQuery} If `options.index` does not exist,
|
|
26
|
+
* the `where` clause is missing, the partition key is not given as
|
|
27
|
+
* `{ equals: value }`, or more than one sort-key condition is supplied
|
|
28
|
+
* (DynamoDB allows only one per query — use `between` for ranges).
|
|
29
|
+
*/
|
|
30
|
+
query(options: QueryOptions<T, K, Indexes>): AsyncIterable<T>;
|
|
31
|
+
scan(options?: ScanOptions): AsyncIterable<T>;
|
|
32
|
+
getBatch(keys: TableKey<T, K>[]): Promise<(T | null)[]>;
|
|
33
|
+
putBatch(items: T[]): Promise<void>;
|
|
34
|
+
deleteBatch(keys: TableKey<T, K>[]): Promise<void>;
|
|
35
|
+
static fromExisting(tableName: string): ExternalTableRef;
|
|
36
|
+
private validateItem;
|
|
37
|
+
private buildKey;
|
|
38
|
+
private backoff;
|
|
39
|
+
/**
|
|
40
|
+
* Run a DynamoDB batch operation, resubmitting any unprocessed entries with
|
|
41
|
+
* exponential backoff. DynamoDB batch APIs can succeed partially (HTTP 200 with
|
|
42
|
+
* UnprocessedKeys/UnprocessedItems) under throttling or the 16 MB response cap,
|
|
43
|
+
* so the leftovers must be retried by the caller.
|
|
44
|
+
*
|
|
45
|
+
* If entries remain unprocessed after MAX_BATCH_ATTEMPTS, this throws a
|
|
46
|
+
* BatchIncomplete error rather than returning quietly: for writes/deletes a
|
|
47
|
+
* silent return would drop data, and for reads it would be indistinguishable
|
|
48
|
+
* from a missing item. Callers should back off and resubmit.
|
|
49
|
+
* See: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Programming.Errors.html#Programming.Errors.BatchOperations
|
|
50
|
+
*
|
|
51
|
+
* @param operation Human-readable operation name used in the exhaustion error.
|
|
52
|
+
* @param initial Entries to submit on the first attempt.
|
|
53
|
+
* @param send Performs one batch call and returns the entries DynamoDB did not process.
|
|
54
|
+
*/
|
|
55
|
+
private retryUnprocessed;
|
|
56
|
+
private applyFieldEqualsCondition;
|
|
57
|
+
private buildQueryCommand;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=index.aws.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.aws.d.ts","sourceRoot":"","sources":["../src/index.aws.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,KAAK,EAA6C,MAAM,kBAAkB,CAAC;AACpF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAIpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,YAAY,EACX,cAAc,EACd,uBAAuB,EACvB,gBAAgB,EAChB,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,WAAW,EACX,UAAU,EACV,aAAa,EAGb,QAAQ,EACR,MAAM,YAAY,CAAC;AAEpB,OAAO,KAAK,EAAgB,YAAY,EAAE,MAAM,YAAY,CAAC;AAE7D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAwBzD,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;IAUqC,OAAO,EAAE,uBAAuB,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC;IATlG,QAAQ,CAAC,MAAM,sBAAW;IAC1B,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,SAAS,CAAyB;IAE1C,2FAA2F;IAC3F,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;IAc5F,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAQ3C,GAAG,CAAC,IAAI,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBpD,MAAM,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAa5E;;;;;;;;OAQG;IACI,KAAK,CACX,OAAO,EAAE,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC,GAClC,aAAa,CAAC,CAAC,CAAC;IAsCZ,IAAI,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,aAAa,CAAC,CAAC,CAAC;IAoB9C,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;IAsBvD,QAAQ,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBnC,WAAW,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBxD,MAAM,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB;YAM1C,YAAY;IAQ1B,OAAO,CAAC,QAAQ;IAMhB,OAAO,CAAC,OAAO;IAWf;;;;;;;;;;;;;;;OAeG;YACW,gBAAgB;IAkB9B,OAAO,CAAC,yBAAyB;IAkBjC,OAAO,CAAC,iBAAiB;CA4CzB"}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
4
|
+
import { DynamoDBDocumentClient, GetCommand, PutCommand, DeleteCommand, QueryCommand, ScanCommand, BatchGetCommand, BatchWriteCommand, } from '@aws-sdk/lib-dynamodb';
|
|
5
|
+
import { Scope, registerSdkIdentifiers, getSdkIdentifiers } from '@aws-blocks/core';
|
|
6
|
+
import { BB_NAME, BB_VERSION } from './version.js';
|
|
7
|
+
export { DistributedTableErrors } from './errors.js';
|
|
8
|
+
import { DistributedTableErrors, DistributedTableMessages, blocksError, normalizeSortKeyCondition, remapItemTooLarge } from './errors.js';
|
|
9
|
+
import { Logger } from '@aws-blocks/bb-logger';
|
|
10
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
11
|
+
// DynamoDB batch API limits and retry tuning.
|
|
12
|
+
// BatchGetItem accepts up to 100 keys per call; BatchWriteItem up to 25 requests.
|
|
13
|
+
// Both can return UnprocessedKeys/UnprocessedItems on partial success (e.g. throttling
|
|
14
|
+
// or the 16 MB response cap), which the caller must resubmit with backoff.
|
|
15
|
+
// See: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Programming.Errors.html#Programming.Errors.BatchOperations
|
|
16
|
+
const BATCH_GET_MAX_KEYS = 100;
|
|
17
|
+
const BATCH_WRITE_MAX_REQUESTS = 25;
|
|
18
|
+
const MAX_BATCH_ATTEMPTS = 5;
|
|
19
|
+
const BASE_BACKOFF_MS = 50;
|
|
20
|
+
const MAX_BACKOFF_MS = 5000;
|
|
21
|
+
/** Split an array into chunks of at most `size` elements. */
|
|
22
|
+
function chunked(items, size) {
|
|
23
|
+
const chunks = [];
|
|
24
|
+
for (let i = 0; i < items.length; i += size) {
|
|
25
|
+
chunks.push(items.slice(i, i + size));
|
|
26
|
+
}
|
|
27
|
+
return chunks;
|
|
28
|
+
}
|
|
29
|
+
export class DistributedTable extends Scope {
|
|
30
|
+
options;
|
|
31
|
+
bbName = BB_NAME;
|
|
32
|
+
schema;
|
|
33
|
+
keyConfig;
|
|
34
|
+
indexes;
|
|
35
|
+
docClient;
|
|
36
|
+
/** @internal Logger for internal operations. Defaults to error-level when not provided. */
|
|
37
|
+
log;
|
|
38
|
+
constructor(scope, id, options) {
|
|
39
|
+
super(id, { parent: scope, bbName: BB_NAME, bbVersion: BB_VERSION });
|
|
40
|
+
this.options = options;
|
|
41
|
+
this.log = options?.logger ?? new Logger(this, 'logger', { level: 'error' });
|
|
42
|
+
const tableName = options.table?.tableName ?? this.fullId.substring(0, 255);
|
|
43
|
+
this.schema = options.schema;
|
|
44
|
+
this.keyConfig = options.key;
|
|
45
|
+
this.indexes = (options.indexes ?? {});
|
|
46
|
+
const client = new DynamoDBClient({
|
|
47
|
+
customUserAgent: this.buildUserAgentChain(),
|
|
48
|
+
});
|
|
49
|
+
this.docClient = DynamoDBDocumentClient.from(client);
|
|
50
|
+
registerSdkIdentifiers(this.fullId, { tableName });
|
|
51
|
+
}
|
|
52
|
+
async get(key) {
|
|
53
|
+
const result = await this.docClient.send(new GetCommand({
|
|
54
|
+
TableName: getSdkIdentifiers(this).tableName,
|
|
55
|
+
Key: this.buildKey(key),
|
|
56
|
+
}));
|
|
57
|
+
return result.Item ?? null;
|
|
58
|
+
}
|
|
59
|
+
async put(item, options) {
|
|
60
|
+
await this.validateItem(item);
|
|
61
|
+
const command = { TableName: getSdkIdentifiers(this).tableName, Item: item };
|
|
62
|
+
if (options?.ifNotExists) {
|
|
63
|
+
command.ConditionExpression = 'attribute_not_exists(#pk)';
|
|
64
|
+
command.ExpressionAttributeNames = { '#pk': this.keyConfig.partitionKey };
|
|
65
|
+
}
|
|
66
|
+
else if (options?.ifFieldEquals) {
|
|
67
|
+
this.applyFieldEqualsCondition(command, options.ifFieldEquals);
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
await this.docClient.send(new PutCommand(command));
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
throw remapItemTooLarge(err);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async delete(key, options) {
|
|
77
|
+
const command = { TableName: getSdkIdentifiers(this).tableName, Key: this.buildKey(key) };
|
|
78
|
+
if (options?.ifExists) {
|
|
79
|
+
command.ConditionExpression = 'attribute_exists(#pk)';
|
|
80
|
+
command.ExpressionAttributeNames = { '#pk': this.keyConfig.partitionKey };
|
|
81
|
+
}
|
|
82
|
+
else if (options?.ifFieldEquals) {
|
|
83
|
+
this.applyFieldEqualsCondition(command, options.ifFieldEquals);
|
|
84
|
+
}
|
|
85
|
+
await this.docClient.send(new DeleteCommand(command));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Query items by index, yielding matches as an async stream with automatic
|
|
89
|
+
* pagination over DynamoDB's `LastEvaluatedKey`.
|
|
90
|
+
*
|
|
91
|
+
* @throws {DistributedTableErrors.InvalidQuery} If `options.index` does not exist,
|
|
92
|
+
* the `where` clause is missing, the partition key is not given as
|
|
93
|
+
* `{ equals: value }`, or more than one sort-key condition is supplied
|
|
94
|
+
* (DynamoDB allows only one per query — use `between` for ranges).
|
|
95
|
+
*/
|
|
96
|
+
async *query(options) {
|
|
97
|
+
const indexConfig = options.index ? this.indexes[options.index] : this.keyConfig;
|
|
98
|
+
if (!indexConfig)
|
|
99
|
+
throw blocksError(DistributedTableErrors.InvalidQuery, DistributedTableMessages.indexNotFound(options.index));
|
|
100
|
+
const pkField = indexConfig.partitionKey;
|
|
101
|
+
const skField = indexConfig.sortKey;
|
|
102
|
+
if (!options.where) {
|
|
103
|
+
throw blocksError(DistributedTableErrors.InvalidQuery, DistributedTableMessages.whereRequired(pkField));
|
|
104
|
+
}
|
|
105
|
+
const pkValue = options.where[pkField]?.equals;
|
|
106
|
+
if (pkValue === undefined) {
|
|
107
|
+
throw blocksError(DistributedTableErrors.InvalidQuery, DistributedTableMessages.partitionKeyEqualsRequired(pkField));
|
|
108
|
+
}
|
|
109
|
+
// Normalize the sort-key condition before building the query: a present-but-
|
|
110
|
+
// empty condition ({} / all-undefined) becomes "no filter" (otherwise we'd
|
|
111
|
+
// register #sk in ExpressionAttributeNames with no clause and DynamoDB would
|
|
112
|
+
// reject it), and more than one condition is rejected before issuing a call.
|
|
113
|
+
const skCondition = skField
|
|
114
|
+
? normalizeSortKeyCondition(options.where[skField])
|
|
115
|
+
: undefined;
|
|
116
|
+
let lastEvaluatedKey;
|
|
117
|
+
let count = 0;
|
|
118
|
+
do {
|
|
119
|
+
const command = this.buildQueryCommand(options.index, pkField, pkValue, skField, skCondition, lastEvaluatedKey, options);
|
|
120
|
+
const result = await this.docClient.send(command);
|
|
121
|
+
for (const item of result.Items ?? []) {
|
|
122
|
+
yield item;
|
|
123
|
+
if (options.limit && ++count >= options.limit)
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
lastEvaluatedKey = result.LastEvaluatedKey;
|
|
127
|
+
} while (lastEvaluatedKey);
|
|
128
|
+
}
|
|
129
|
+
async *scan(options) {
|
|
130
|
+
let lastEvaluatedKey;
|
|
131
|
+
let count = 0;
|
|
132
|
+
do {
|
|
133
|
+
const result = await this.docClient.send(new ScanCommand({
|
|
134
|
+
TableName: getSdkIdentifiers(this).tableName,
|
|
135
|
+
ExclusiveStartKey: lastEvaluatedKey,
|
|
136
|
+
Limit: options?.limit,
|
|
137
|
+
}));
|
|
138
|
+
for (const item of result.Items ?? []) {
|
|
139
|
+
yield item;
|
|
140
|
+
if (options?.limit && ++count >= options.limit)
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
lastEvaluatedKey = result.LastEvaluatedKey;
|
|
144
|
+
} while (lastEvaluatedKey);
|
|
145
|
+
}
|
|
146
|
+
async getBatch(keys) {
|
|
147
|
+
const results = new Map();
|
|
148
|
+
const tableName = getSdkIdentifiers(this).tableName;
|
|
149
|
+
for (const chunk of chunked(keys, BATCH_GET_MAX_KEYS)) {
|
|
150
|
+
await this.retryUnprocessed('getBatch', chunk.map(k => this.buildKey(k)), async (pendingKeys) => {
|
|
151
|
+
const resp = await this.docClient.send(new BatchGetCommand({
|
|
152
|
+
RequestItems: { [tableName]: { Keys: pendingKeys } },
|
|
153
|
+
}));
|
|
154
|
+
for (const item of resp.Responses?.[tableName] ?? []) {
|
|
155
|
+
results.set(JSON.stringify(this.buildKey(item)), item);
|
|
156
|
+
}
|
|
157
|
+
return resp.UnprocessedKeys?.[tableName]?.Keys;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return keys.map(key => results.get(JSON.stringify(this.buildKey(key))) ?? null);
|
|
161
|
+
}
|
|
162
|
+
async putBatch(items) {
|
|
163
|
+
for (const item of items)
|
|
164
|
+
await this.validateItem(item);
|
|
165
|
+
const tableName = getSdkIdentifiers(this).tableName;
|
|
166
|
+
for (const chunk of chunked(items, BATCH_WRITE_MAX_REQUESTS)) {
|
|
167
|
+
await this.retryUnprocessed('putBatch', chunk.map(item => ({ PutRequest: { Item: item } })), async (requests) => {
|
|
168
|
+
try {
|
|
169
|
+
const resp = await this.docClient.send(new BatchWriteCommand({
|
|
170
|
+
RequestItems: { [tableName]: requests },
|
|
171
|
+
}));
|
|
172
|
+
return resp.UnprocessedItems?.[tableName];
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
throw remapItemTooLarge(err);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async deleteBatch(keys) {
|
|
181
|
+
const tableName = getSdkIdentifiers(this).tableName;
|
|
182
|
+
for (const chunk of chunked(keys, BATCH_WRITE_MAX_REQUESTS)) {
|
|
183
|
+
await this.retryUnprocessed('deleteBatch', chunk.map(key => ({ DeleteRequest: { Key: this.buildKey(key) } })), async (requests) => {
|
|
184
|
+
const resp = await this.docClient.send(new BatchWriteCommand({
|
|
185
|
+
RequestItems: { [tableName]: requests },
|
|
186
|
+
}));
|
|
187
|
+
return resp.UnprocessedItems?.[tableName];
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
static fromExisting(tableName) {
|
|
192
|
+
return { __brand: 'ExternalTableRef', tableName };
|
|
193
|
+
}
|
|
194
|
+
// ── Internal ────────────────────────────────────────────────────────────
|
|
195
|
+
async validateItem(item) {
|
|
196
|
+
const result = this.schema['~standard'].validate(item);
|
|
197
|
+
const resolved = result instanceof Promise ? await result : result;
|
|
198
|
+
if (resolved.issues) {
|
|
199
|
+
throw blocksError(DistributedTableErrors.ValidationFailed, resolved.issues[0].message);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
buildKey(key) {
|
|
203
|
+
const result = { [this.keyConfig.partitionKey]: key[this.keyConfig.partitionKey] };
|
|
204
|
+
if (this.keyConfig.sortKey)
|
|
205
|
+
result[this.keyConfig.sortKey] = key[this.keyConfig.sortKey];
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
backoff(attempt) {
|
|
209
|
+
// Exponential backoff with equal jitter: keep half the delay as a floor and
|
|
210
|
+
// randomise the other half. Full jitter (random * cap) can collapse to ~0ms
|
|
211
|
+
// and lets concurrent callers re-collide; equal jitter preserves a minimum
|
|
212
|
+
// spacing while still de-synchronising retries under shared throttling.
|
|
213
|
+
// See: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Programming.Errors.html#Programming.Errors.BatchOperations
|
|
214
|
+
const capped = Math.min(BASE_BACKOFF_MS * Math.pow(2, attempt), MAX_BACKOFF_MS);
|
|
215
|
+
const ms = capped / 2 + Math.random() * (capped / 2);
|
|
216
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Run a DynamoDB batch operation, resubmitting any unprocessed entries with
|
|
220
|
+
* exponential backoff. DynamoDB batch APIs can succeed partially (HTTP 200 with
|
|
221
|
+
* UnprocessedKeys/UnprocessedItems) under throttling or the 16 MB response cap,
|
|
222
|
+
* so the leftovers must be retried by the caller.
|
|
223
|
+
*
|
|
224
|
+
* If entries remain unprocessed after MAX_BATCH_ATTEMPTS, this throws a
|
|
225
|
+
* BatchIncomplete error rather than returning quietly: for writes/deletes a
|
|
226
|
+
* silent return would drop data, and for reads it would be indistinguishable
|
|
227
|
+
* from a missing item. Callers should back off and resubmit.
|
|
228
|
+
* See: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Programming.Errors.html#Programming.Errors.BatchOperations
|
|
229
|
+
*
|
|
230
|
+
* @param operation Human-readable operation name used in the exhaustion error.
|
|
231
|
+
* @param initial Entries to submit on the first attempt.
|
|
232
|
+
* @param send Performs one batch call and returns the entries DynamoDB did not process.
|
|
233
|
+
*/
|
|
234
|
+
async retryUnprocessed(operation, initial, send) {
|
|
235
|
+
let pending = initial;
|
|
236
|
+
for (let attempt = 0; attempt < MAX_BATCH_ATTEMPTS && pending && pending.length > 0; attempt++) {
|
|
237
|
+
if (attempt > 0)
|
|
238
|
+
await this.backoff(attempt);
|
|
239
|
+
pending = await send(pending);
|
|
240
|
+
}
|
|
241
|
+
if (pending && pending.length > 0) {
|
|
242
|
+
throw blocksError(DistributedTableErrors.BatchIncomplete, DistributedTableMessages.batchIncomplete(operation, pending.length, MAX_BATCH_ATTEMPTS));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
applyFieldEqualsCondition(command, fields) {
|
|
246
|
+
const entries = Object.entries(fields).filter(([, v]) => v !== undefined);
|
|
247
|
+
if (entries.length === 0) {
|
|
248
|
+
throw blocksError(DistributedTableErrors.InvalidQuery, DistributedTableMessages.emptyIfFieldEquals);
|
|
249
|
+
}
|
|
250
|
+
const conditions = [];
|
|
251
|
+
const names = {};
|
|
252
|
+
const values = {};
|
|
253
|
+
entries.forEach(([field, value], i) => {
|
|
254
|
+
conditions.push(`#field${i} = :val${i}`);
|
|
255
|
+
names[`#field${i}`] = field;
|
|
256
|
+
values[`:val${i}`] = value;
|
|
257
|
+
});
|
|
258
|
+
command.ConditionExpression = conditions.join(' AND ');
|
|
259
|
+
command.ExpressionAttributeNames = names;
|
|
260
|
+
command.ExpressionAttributeValues = values;
|
|
261
|
+
}
|
|
262
|
+
buildQueryCommand(indexName, pkField, pkValue, skField, skCondition, exclusiveStartKey, options) {
|
|
263
|
+
let expr = '#pk = :pkval';
|
|
264
|
+
const names = { '#pk': pkField };
|
|
265
|
+
const values = { ':pkval': pkValue };
|
|
266
|
+
if (skField && skCondition) {
|
|
267
|
+
names['#sk'] = skField;
|
|
268
|
+
if ('equals' in skCondition && skCondition.equals !== undefined) {
|
|
269
|
+
expr += ' AND #sk = :skval';
|
|
270
|
+
values[':skval'] = skCondition.equals;
|
|
271
|
+
}
|
|
272
|
+
else if ('greaterThan' in skCondition && skCondition.greaterThan !== undefined) {
|
|
273
|
+
expr += ' AND #sk > :skval';
|
|
274
|
+
values[':skval'] = skCondition.greaterThan;
|
|
275
|
+
}
|
|
276
|
+
else if ('greaterThanOrEqual' in skCondition && skCondition.greaterThanOrEqual !== undefined) {
|
|
277
|
+
expr += ' AND #sk >= :skval';
|
|
278
|
+
values[':skval'] = skCondition.greaterThanOrEqual;
|
|
279
|
+
}
|
|
280
|
+
else if ('lessThan' in skCondition && skCondition.lessThan !== undefined) {
|
|
281
|
+
expr += ' AND #sk < :skval';
|
|
282
|
+
values[':skval'] = skCondition.lessThan;
|
|
283
|
+
}
|
|
284
|
+
else if ('lessThanOrEqual' in skCondition && skCondition.lessThanOrEqual !== undefined) {
|
|
285
|
+
expr += ' AND #sk <= :skval';
|
|
286
|
+
values[':skval'] = skCondition.lessThanOrEqual;
|
|
287
|
+
}
|
|
288
|
+
else if ('between' in skCondition && skCondition.between) {
|
|
289
|
+
expr += ' AND #sk BETWEEN :skval1 AND :skval2';
|
|
290
|
+
values[':skval1'] = skCondition.between[0];
|
|
291
|
+
values[':skval2'] = skCondition.between[1];
|
|
292
|
+
}
|
|
293
|
+
else if ('beginsWith' in skCondition && skCondition.beginsWith !== undefined) {
|
|
294
|
+
expr += ' AND begins_with(#sk, :skval)';
|
|
295
|
+
values[':skval'] = skCondition.beginsWith;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return new QueryCommand({
|
|
299
|
+
TableName: getSdkIdentifiers(this).tableName,
|
|
300
|
+
IndexName: indexName || undefined,
|
|
301
|
+
KeyConditionExpression: expr,
|
|
302
|
+
ExpressionAttributeNames: names,
|
|
303
|
+
ExpressionAttributeValues: values,
|
|
304
|
+
ExclusiveStartKey: exclusiveStartKey,
|
|
305
|
+
Limit: options?.limit,
|
|
306
|
+
ScanIndexForward: options?.order === 'desc' ? false : undefined,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// ── Query input helper type ─────────────────────────────────────────────────
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.browser.d.ts","sourceRoot":"","sources":["../src/index.browser.ts"],"names":[],"mappings":"AAIA,qBAAa,gBAAgB;gBAChB,GAAG,IAAI,EAAE,GAAG,EAAE;CAC1B;AACD,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// Browser stub - DistributedTable runs server-side only
|
|
4
|
+
export class DistributedTable {
|
|
5
|
+
constructor(...args) { }
|
|
6
|
+
}
|
|
7
|
+
export { DistributedTableErrors } from './errors.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Scope } from '@aws-blocks/core/cdk';
|
|
2
|
+
import type { ScopeParent } from '@aws-blocks/core';
|
|
3
|
+
import type { ExternalTableRef } from './types.js';
|
|
4
|
+
export { DistributedTableErrors } from './errors.js';
|
|
5
|
+
export type { DistributedTableOptions, TableKeyConfig, TableKey, PutOptions, DeleteOptions, QueryOptions, ScanOptions, ExternalTableRef } from './types.js';
|
|
6
|
+
export declare class DistributedTable<T = any> extends Scope {
|
|
7
|
+
options: any;
|
|
8
|
+
private table;
|
|
9
|
+
/**
|
|
10
|
+
* Reference an existing DynamoDB table instead of provisioning a new one.
|
|
11
|
+
* Mirrors the same factory exposed by the runtime build so the same code
|
|
12
|
+
* works in both contexts. The customer is responsible for ensuring the
|
|
13
|
+
* pre-existing table already has any required GSIs configured — Blocks
|
|
14
|
+
* will not modify the table when this factory is used.
|
|
15
|
+
*/
|
|
16
|
+
static fromExisting(tableName: string): ExternalTableRef;
|
|
17
|
+
constructor(scope: ScopeParent, id: string, options: any);
|
|
18
|
+
get(..._args: unknown[]): never;
|
|
19
|
+
put(..._args: unknown[]): never;
|
|
20
|
+
delete(..._args: unknown[]): never;
|
|
21
|
+
query(..._args: unknown[]): never;
|
|
22
|
+
scan(..._args: unknown[]): never;
|
|
23
|
+
getBatch(..._args: unknown[]): never;
|
|
24
|
+
putBatch(..._args: unknown[]): never;
|
|
25
|
+
deleteBatch(..._args: unknown[]): never;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=index.cdk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cdk.d.ts","sourceRoot":"","sources":["../src/index.cdk.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,KAAK,EAAoC,MAAM,sBAAsB,CAAC;AAC/E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAInD,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,YAAY,EAAE,uBAAuB,EAAE,cAAc,EAAE,QAAQ,EAAE,UAAU,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE5J,qBAAa,gBAAgB,CAAC,CAAC,GAAG,GAAG,CAAE,SAAQ,KAAK;IAcA,OAAO,EAAE,GAAG;IAb/D,OAAO,CAAC,KAAK,CAAS;IAEtB;;;;;;OAMG;IACH,MAAM,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB;gBAI5C,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAS,OAAO,EAAE,GAAG;IAmG/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"}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { Table, AttributeType, BillingMode } from 'aws-cdk-lib/aws-dynamodb';
|
|
4
|
+
import * as cdk from 'aws-cdk-lib';
|
|
5
|
+
import { CustomResource, Duration } from 'aws-cdk-lib';
|
|
6
|
+
import { Code, Function as LambdaFunction } from 'aws-cdk-lib/aws-lambda';
|
|
7
|
+
import { Provider } from 'aws-cdk-lib/custom-resources';
|
|
8
|
+
import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
|
|
9
|
+
import { Scope, synthGuard, DEFAULT_NODE_RUNTIME } from '@aws-blocks/core/cdk';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { dirname, join } from 'node:path';
|
|
12
|
+
export { DistributedTableErrors } from './errors.js';
|
|
13
|
+
export class DistributedTable extends Scope {
|
|
14
|
+
options;
|
|
15
|
+
table;
|
|
16
|
+
/**
|
|
17
|
+
* Reference an existing DynamoDB table instead of provisioning a new one.
|
|
18
|
+
* Mirrors the same factory exposed by the runtime build so the same code
|
|
19
|
+
* works in both contexts. The customer is responsible for ensuring the
|
|
20
|
+
* pre-existing table already has any required GSIs configured — Blocks
|
|
21
|
+
* will not modify the table when this factory is used.
|
|
22
|
+
*/
|
|
23
|
+
static fromExisting(tableName) {
|
|
24
|
+
return { __brand: 'ExternalTableRef', tableName };
|
|
25
|
+
}
|
|
26
|
+
constructor(scope, id, options) {
|
|
27
|
+
super(id, { parent: scope });
|
|
28
|
+
this.options = options;
|
|
29
|
+
const config = options;
|
|
30
|
+
if (config?.table) {
|
|
31
|
+
// `fromExisting`: don't provision; bind to the pre-existing table by name
|
|
32
|
+
// and grant the runtime Lambda read/write + index query access.
|
|
33
|
+
// We deliberately skip the GSI custom resource — the customer owns the
|
|
34
|
+
// table's index lifecycle when they bring their own.
|
|
35
|
+
this.table = Table.fromTableName(this, 'table', config.table.tableName);
|
|
36
|
+
this.table.grantReadWriteData(this.handler);
|
|
37
|
+
this.handler.addToRolePolicy(new PolicyStatement({
|
|
38
|
+
actions: ['dynamodb:Query'],
|
|
39
|
+
resources: [`${this.table.tableArn}/index/*`],
|
|
40
|
+
}));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const tableName = this.fullId.substring(0, 255);
|
|
44
|
+
const isSandbox = this.node.tryGetContext('sandboxMode') === 'true';
|
|
45
|
+
// Probe the schema's validate() to determine if a key field is numeric.
|
|
46
|
+
// Sends a test value of 0 for the field — if validation doesn't flag it,
|
|
47
|
+
// the field accepts numbers. Uses only the StandardSchemaV1 interface.
|
|
48
|
+
const isNumericField = (fieldName) => {
|
|
49
|
+
const probe = { [fieldName]: 0 };
|
|
50
|
+
const result = config.schema['~standard'].validate(probe);
|
|
51
|
+
// validate may return sync or async; at synth time schemas are sync
|
|
52
|
+
if (result && 'issues' in result && result.issues) {
|
|
53
|
+
return !result.issues.some((i) => i.path?.length === 1 && i.path[0] === fieldName);
|
|
54
|
+
}
|
|
55
|
+
return true; // no issues for this field → numeric
|
|
56
|
+
};
|
|
57
|
+
const getDdbType = (fieldName) => isNumericField(fieldName) ? AttributeType.NUMBER : AttributeType.STRING;
|
|
58
|
+
this.table = new Table(this, 'table', {
|
|
59
|
+
tableName,
|
|
60
|
+
partitionKey: {
|
|
61
|
+
name: config.key.partitionKey,
|
|
62
|
+
type: getDdbType(config.key.partitionKey),
|
|
63
|
+
},
|
|
64
|
+
sortKey: config.key.sortKey ? {
|
|
65
|
+
name: config.key.sortKey,
|
|
66
|
+
type: getDdbType(config.key.sortKey),
|
|
67
|
+
} : undefined,
|
|
68
|
+
billingMode: BillingMode.PAY_PER_REQUEST,
|
|
69
|
+
timeToLiveAttribute: config.ttl || undefined,
|
|
70
|
+
});
|
|
71
|
+
this.table.grantReadWriteData(this.handler);
|
|
72
|
+
// Explicit index query permissions
|
|
73
|
+
this.handler.addToRolePolicy(new PolicyStatement({
|
|
74
|
+
actions: ['dynamodb:Query'],
|
|
75
|
+
resources: [`${this.table.tableArn}/index/*`],
|
|
76
|
+
}));
|
|
77
|
+
// Add GSI manager if indexes are defined
|
|
78
|
+
if (config.indexes && Object.keys(config.indexes).length > 0) {
|
|
79
|
+
const gsiProvider = getOrCreateGsiProvider(cdk.Stack.of(this));
|
|
80
|
+
gsiProvider.addTableArn(this.table.tableArn, isSandbox);
|
|
81
|
+
const indexesWithTypes = {};
|
|
82
|
+
for (const [indexName, indexConfig] of Object.entries(config.indexes)) {
|
|
83
|
+
indexesWithTypes[indexName] = {
|
|
84
|
+
partitionKey: indexConfig.partitionKey,
|
|
85
|
+
sortKey: indexConfig.sortKey,
|
|
86
|
+
partitionKeyType: getDdbType(indexConfig.partitionKey) === AttributeType.NUMBER ? 'N' : 'S',
|
|
87
|
+
sortKeyType: indexConfig.sortKey
|
|
88
|
+
? (getDdbType(indexConfig.sortKey) === AttributeType.NUMBER ? 'N' : 'S')
|
|
89
|
+
: undefined,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const gsiResource = new CustomResource(this, 'gsi-resource', {
|
|
93
|
+
serviceToken: gsiProvider.serviceToken,
|
|
94
|
+
properties: {
|
|
95
|
+
TableName: this.table.tableName,
|
|
96
|
+
Indexes: indexesWithTypes,
|
|
97
|
+
SandboxMode: isSandbox ? 'true' : 'false',
|
|
98
|
+
Version: '3',
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
gsiResource.node.addDependency(this.table);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// ── Runtime methods are not available during CDK synth ────────────────
|
|
105
|
+
// Under `--conditions=cdk` a DistributedTable resolves to this construct,
|
|
106
|
+
// which only provisions infrastructure. The data methods live in the runtime
|
|
107
|
+
// build; calling them at module top-level (which runs during synth) would
|
|
108
|
+
// otherwise fail with a cryptic `X is not a function`. These stubs turn that
|
|
109
|
+
// into an actionable message.
|
|
110
|
+
get(..._args) { return synthGuard('DistributedTable', 'get'); }
|
|
111
|
+
put(..._args) { return synthGuard('DistributedTable', 'put'); }
|
|
112
|
+
delete(..._args) { return synthGuard('DistributedTable', 'delete'); }
|
|
113
|
+
query(..._args) { return synthGuard('DistributedTable', 'query'); }
|
|
114
|
+
scan(..._args) { return synthGuard('DistributedTable', 'scan'); }
|
|
115
|
+
getBatch(..._args) { return synthGuard('DistributedTable', 'getBatch'); }
|
|
116
|
+
putBatch(..._args) { return synthGuard('DistributedTable', 'putBatch'); }
|
|
117
|
+
deleteBatch(..._args) { return synthGuard('DistributedTable', 'deleteBatch'); }
|
|
118
|
+
}
|
|
119
|
+
// ── Shared GSI Manager Provider (one per stack) ─────────────────────────────
|
|
120
|
+
const GSI_PROVIDER_KEY = Symbol.for('BLOCKS_GSI_MANAGER_PROVIDER');
|
|
121
|
+
function getOrCreateGsiProvider(stack) {
|
|
122
|
+
const existing = stack[GSI_PROVIDER_KEY];
|
|
123
|
+
if (existing)
|
|
124
|
+
return existing;
|
|
125
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
126
|
+
const tableArns = [];
|
|
127
|
+
const sandboxTableArns = [];
|
|
128
|
+
const gsiManagerLambda = new LambdaFunction(stack, 'KitGsiManager', {
|
|
129
|
+
runtime: DEFAULT_NODE_RUNTIME,
|
|
130
|
+
handler: 'index.handler',
|
|
131
|
+
code: Code.fromAsset(join(__dirname, 'gsi-manager-lambda')),
|
|
132
|
+
timeout: Duration.minutes(15),
|
|
133
|
+
});
|
|
134
|
+
const gsiIsCompleteLambda = new LambdaFunction(stack, 'KitGsiIsComplete', {
|
|
135
|
+
runtime: DEFAULT_NODE_RUNTIME,
|
|
136
|
+
handler: 'index.isCompleteHandler',
|
|
137
|
+
code: Code.fromAsset(join(__dirname, 'gsi-manager-lambda')),
|
|
138
|
+
timeout: Duration.minutes(1),
|
|
139
|
+
});
|
|
140
|
+
// Production permissions — lazily resolved so ARNs accumulate as tables register
|
|
141
|
+
gsiManagerLambda.addToRolePolicy(new PolicyStatement({
|
|
142
|
+
actions: ['dynamodb:DescribeTable', 'dynamodb:UpdateTable'],
|
|
143
|
+
resources: cdk.Lazy.list({ produce: () => tableArns }),
|
|
144
|
+
}));
|
|
145
|
+
gsiIsCompleteLambda.addToRolePolicy(new PolicyStatement({
|
|
146
|
+
actions: ['dynamodb:DescribeTable', 'dynamodb:UpdateTable'],
|
|
147
|
+
resources: cdk.Lazy.list({ produce: () => tableArns }),
|
|
148
|
+
}));
|
|
149
|
+
// Sandbox permissions — only added if any table requests sandbox mode
|
|
150
|
+
let sandboxPolicyAdded = false;
|
|
151
|
+
const provider = new Provider(stack, 'KitGsiProvider', {
|
|
152
|
+
onEventHandler: gsiManagerLambda,
|
|
153
|
+
isCompleteHandler: gsiIsCompleteLambda,
|
|
154
|
+
queryInterval: Duration.seconds(10),
|
|
155
|
+
totalTimeout: Duration.hours(2),
|
|
156
|
+
});
|
|
157
|
+
const shared = {
|
|
158
|
+
serviceToken: provider.serviceToken,
|
|
159
|
+
addTableArn: (tableArn, isSandbox) => {
|
|
160
|
+
tableArns.push(tableArn);
|
|
161
|
+
if (isSandbox) {
|
|
162
|
+
sandboxTableArns.push(tableArn);
|
|
163
|
+
if (!sandboxPolicyAdded) {
|
|
164
|
+
sandboxPolicyAdded = true;
|
|
165
|
+
gsiManagerLambda.addToRolePolicy(new PolicyStatement({
|
|
166
|
+
actions: [
|
|
167
|
+
'dynamodb:DeleteTable',
|
|
168
|
+
'dynamodb:CreateTable',
|
|
169
|
+
'dynamodb:Scan',
|
|
170
|
+
'dynamodb:BatchWriteItem',
|
|
171
|
+
],
|
|
172
|
+
resources: cdk.Lazy.list({ produce: () => sandboxTableArns }),
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
stack[GSI_PROVIDER_KEY] = shared;
|
|
179
|
+
return shared;
|
|
180
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cdk.test.d.ts","sourceRoot":"","sources":["../src/index.cdk.test.ts"],"names":[],"mappings":""}
|