@webda/aws 4.0.0-beta.2 → 4.0.0-beta.3
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/README.md +64 -7
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/services/aws-parameters.d.ts +33 -0
- package/lib/services/aws-parameters.d.ts.map +1 -0
- package/lib/services/aws-parameters.js +18 -0
- package/lib/services/aws-parameters.js.map +1 -0
- package/lib/services/awscommands.service.d.ts +39 -0
- package/lib/services/awscommands.service.d.ts.map +1 -0
- package/lib/services/awscommands.service.js +127 -0
- package/lib/services/awscommands.service.js.map +1 -0
- package/lib/services/cloudwatchlogger.service.d.ts +131 -0
- package/lib/services/cloudwatchlogger.service.d.ts.map +1 -0
- package/lib/services/{cloudwatchlogger.js → cloudwatchlogger.service.js} +60 -28
- package/lib/services/cloudwatchlogger.service.js.map +1 -0
- package/lib/services/contributors.d.ts +37 -0
- package/lib/services/contributors.d.ts.map +1 -0
- package/lib/services/contributors.js +2 -0
- package/lib/services/contributors.js.map +1 -0
- package/lib/services/dynamodb.service.d.ts +297 -0
- package/lib/services/dynamodb.service.d.ts.map +1 -0
- package/lib/services/dynamodb.service.js +985 -0
- package/lib/services/dynamodb.service.js.map +1 -0
- package/lib/services/index.d.ts +11 -31
- package/lib/services/index.d.ts.map +1 -1
- package/lib/services/index.js +11 -8
- package/lib/services/index.js.map +1 -1
- package/lib/services/lambdacaller.service.d.ts +81 -0
- package/lib/services/lambdacaller.service.d.ts.map +1 -0
- package/lib/services/lambdacaller.service.js +75 -0
- package/lib/services/lambdacaller.service.js.map +1 -0
- package/lib/services/lambdaserver.service.d.ts +127 -0
- package/lib/services/lambdaserver.service.d.ts.map +1 -0
- package/lib/services/lambdaserver.service.js +294 -0
- package/lib/services/lambdaserver.service.js.map +1 -0
- package/lib/services/{route53.d.ts → route53.service.d.ts} +21 -12
- package/lib/services/route53.service.d.ts.map +1 -0
- package/lib/services/{route53.js → route53.service.js} +25 -28
- package/lib/services/route53.service.js.map +1 -0
- package/lib/services/s3binary.service.d.ts +215 -0
- package/lib/services/s3binary.service.d.ts.map +1 -0
- package/lib/services/{s3binary.js → s3binary.service.js} +93 -55
- package/lib/services/s3binary.service.js.map +1 -0
- package/lib/services/secretsmanager.service.d.ts +102 -0
- package/lib/services/secretsmanager.service.d.ts.map +1 -0
- package/lib/services/{secretsmanager.js → secretsmanager.service.js} +40 -22
- package/lib/services/secretsmanager.service.js.map +1 -0
- package/lib/services/{sqsqueue.d.ts → sqsqueue.service.d.ts} +60 -35
- package/lib/services/sqsqueue.service.d.ts.map +1 -0
- package/lib/services/{sqsqueue.js → sqsqueue.service.js} +53 -28
- package/lib/services/sqsqueue.service.js.map +1 -0
- package/package.json +58 -63
- package/webda.module.json +743 -280
- package/lib/deployers/cloudformation.d.ts +0 -317
- package/lib/deployers/cloudformation.d.ts.map +0 -1
- package/lib/deployers/cloudformation.js +0 -837
- package/lib/deployers/cloudformation.js.map +0 -1
- package/lib/deployers/cloudformation.spec.d.ts +0 -2
- package/lib/deployers/cloudformation.spec.d.ts.map +0 -1
- package/lib/deployers/cloudformation.spec.js +0 -530
- package/lib/deployers/cloudformation.spec.js.map +0 -1
- package/lib/deployers/index.d.ts +0 -255
- package/lib/deployers/index.d.ts.map +0 -1
- package/lib/deployers/index.js +0 -543
- package/lib/deployers/index.js.map +0 -1
- package/lib/deployers/index.spec.d.ts +0 -9
- package/lib/deployers/index.spec.d.ts.map +0 -1
- package/lib/deployers/index.spec.js +0 -595
- package/lib/deployers/index.spec.js.map +0 -1
- package/lib/deployers/lambda-entrypoint.d.ts +0 -2
- package/lib/deployers/lambda-entrypoint.d.ts.map +0 -1
- package/lib/deployers/lambda-entrypoint.js +0 -13
- package/lib/deployers/lambda-entrypoint.js.map +0 -1
- package/lib/deployers/lambdapackager.d.ts +0 -28
- package/lib/deployers/lambdapackager.d.ts.map +0 -1
- package/lib/deployers/lambdapackager.js +0 -26
- package/lib/deployers/lambdapackager.js.map +0 -1
- package/lib/deployers/lambdapackager.spec.d.ts +0 -2
- package/lib/deployers/lambdapackager.spec.d.ts.map +0 -1
- package/lib/deployers/lambdapackager.spec.js +0 -44
- package/lib/deployers/lambdapackager.spec.js.map +0 -1
- package/lib/index.spec.d.ts +0 -11
- package/lib/index.spec.d.ts.map +0 -1
- package/lib/index.spec.js +0 -42
- package/lib/index.spec.js.map +0 -1
- package/lib/services/aws-mixin.d.ts +0 -25
- package/lib/services/aws-mixin.d.ts.map +0 -1
- package/lib/services/aws-mixin.js +0 -18
- package/lib/services/aws-mixin.js.map +0 -1
- package/lib/services/cloudwatchlogger.d.ts +0 -82
- package/lib/services/cloudwatchlogger.d.ts.map +0 -1
- package/lib/services/cloudwatchlogger.js.map +0 -1
- package/lib/services/cloudwatchlogger.spec.d.ts +0 -2
- package/lib/services/cloudwatchlogger.spec.d.ts.map +0 -1
- package/lib/services/cloudwatchlogger.spec.js +0 -104
- package/lib/services/cloudwatchlogger.spec.js.map +0 -1
- package/lib/services/dynamodb.d.ts +0 -174
- package/lib/services/dynamodb.d.ts.map +0 -1
- package/lib/services/dynamodb.js +0 -711
- package/lib/services/dynamodb.js.map +0 -1
- package/lib/services/dynamodb.spec.d.ts +0 -24
- package/lib/services/dynamodb.spec.d.ts.map +0 -1
- package/lib/services/dynamodb.spec.js +0 -344
- package/lib/services/dynamodb.spec.js.map +0 -1
- package/lib/services/lambdacaller.d.ts +0 -69
- package/lib/services/lambdacaller.d.ts.map +0 -1
- package/lib/services/lambdacaller.js +0 -63
- package/lib/services/lambdacaller.js.map +0 -1
- package/lib/services/lambdacaller.spec.d.ts +0 -2
- package/lib/services/lambdacaller.spec.d.ts.map +0 -1
- package/lib/services/lambdacaller.spec.js +0 -70
- package/lib/services/lambdacaller.spec.js.map +0 -1
- package/lib/services/lambdaserver.d.ts +0 -87
- package/lib/services/lambdaserver.d.ts.map +0 -1
- package/lib/services/lambdaserver.js +0 -263
- package/lib/services/lambdaserver.js.map +0 -1
- package/lib/services/lambdaserver.spec.d.ts +0 -2
- package/lib/services/lambdaserver.spec.d.ts.map +0 -1
- package/lib/services/lambdaserver.spec.js +0 -417
- package/lib/services/lambdaserver.spec.js.map +0 -1
- package/lib/services/route53.d.ts.map +0 -1
- package/lib/services/route53.js.map +0 -1
- package/lib/services/route53.spec.d.ts +0 -2
- package/lib/services/route53.spec.d.ts.map +0 -1
- package/lib/services/route53.spec.js +0 -102
- package/lib/services/route53.spec.js.map +0 -1
- package/lib/services/s3binary.d.ts +0 -163
- package/lib/services/s3binary.d.ts.map +0 -1
- package/lib/services/s3binary.js.map +0 -1
- package/lib/services/s3binary.spec.d.ts +0 -2
- package/lib/services/s3binary.spec.d.ts.map +0 -1
- package/lib/services/s3binary.spec.js +0 -284
- package/lib/services/s3binary.spec.js.map +0 -1
- package/lib/services/secretsmanager.d.ts +0 -81
- package/lib/services/secretsmanager.d.ts.map +0 -1
- package/lib/services/secretsmanager.js.map +0 -1
- package/lib/services/secretsmanager.spec.d.ts +0 -2
- package/lib/services/secretsmanager.spec.d.ts.map +0 -1
- package/lib/services/secretsmanager.spec.js +0 -66
- package/lib/services/secretsmanager.spec.js.map +0 -1
- package/lib/services/sqsqueue.d.ts.map +0 -1
- package/lib/services/sqsqueue.js.map +0 -1
- package/lib/services/sqsqueue.spec.d.ts +0 -2
- package/lib/services/sqsqueue.spec.d.ts.map +0 -1
- package/lib/services/sqsqueue.spec.js +0 -157
- package/lib/services/sqsqueue.spec.js.map +0 -1
- package/lib/shell.d.ts +0 -13
- package/lib/shell.d.ts.map +0 -1
- package/lib/shell.js +0 -42
- package/lib/shell.js.map +0 -1
- package/webda.shell.json +0 -11
|
@@ -0,0 +1,985 @@
|
|
|
1
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
2
|
+
var useValue = arguments.length > 2;
|
|
3
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
4
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
5
|
+
}
|
|
6
|
+
return useValue ? value : void 0;
|
|
7
|
+
};
|
|
8
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
9
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
10
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
11
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
12
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
13
|
+
var _, done = false;
|
|
14
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
15
|
+
var context = {};
|
|
16
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
17
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
18
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
19
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
20
|
+
if (kind === "accessor") {
|
|
21
|
+
if (result === void 0) continue;
|
|
22
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
23
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
24
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
25
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
26
|
+
}
|
|
27
|
+
else if (_ = accept(result)) {
|
|
28
|
+
if (kind === "field") initializers.unshift(_);
|
|
29
|
+
else descriptor[key] = _;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
33
|
+
done = true;
|
|
34
|
+
};
|
|
35
|
+
import { ConditionalCheckFailedException, DynamoDB, DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
36
|
+
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb";
|
|
37
|
+
import { EventRepository, InstanceCache, MemoryRepository, Store, StoreNotFoundError, StoreParameters, UpdateConditionFailError, useModel, useModelMetadata, WebdaError } from "@webda/core";
|
|
38
|
+
import * as WebdaQL from "@webda/ql";
|
|
39
|
+
import { loadAWSParameters } from "./aws-parameters.js";
|
|
40
|
+
/**
|
|
41
|
+
* Name of the DynamoDB hash key attribute
|
|
42
|
+
*/
|
|
43
|
+
const KEY_ATTRIBUTE = "uuid";
|
|
44
|
+
/**
|
|
45
|
+
* Attribute storing the model identifier, used to rebuild subclasses
|
|
46
|
+
*/
|
|
47
|
+
const TYPE_ATTRIBUTE = "__type";
|
|
48
|
+
/**
|
|
49
|
+
* Define DynamoDB parameters
|
|
50
|
+
*/
|
|
51
|
+
export class DynamoStoreParameters extends StoreParameters {
|
|
52
|
+
/**
|
|
53
|
+
* @override
|
|
54
|
+
* @param params - raw parameters
|
|
55
|
+
* @returns this
|
|
56
|
+
*/
|
|
57
|
+
load(params = {}) {
|
|
58
|
+
super.load(params);
|
|
59
|
+
loadAWSParameters(this);
|
|
60
|
+
if (this.table === undefined) {
|
|
61
|
+
throw new WebdaError.CodeError("DYNAMODB_TABLE_PARAMETER_REQUIRED", "Need to define a table at least");
|
|
62
|
+
}
|
|
63
|
+
this.globalIndexes ?? (this.globalIndexes = {});
|
|
64
|
+
this.tables ?? (this.tables = {});
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Serialize a date for DynamoDB
|
|
70
|
+
* @param date - the date to serialize
|
|
71
|
+
* @returns ISO string
|
|
72
|
+
*/
|
|
73
|
+
function serializeDate(date) {
|
|
74
|
+
return date.toISOString();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Clean an object to store in DynamoDB
|
|
78
|
+
*
|
|
79
|
+
* Dates are converted to ISO strings, empty strings and undefined values are removed
|
|
80
|
+
*
|
|
81
|
+
* @param object - the value to clean
|
|
82
|
+
* @returns the cleaned value
|
|
83
|
+
*/
|
|
84
|
+
export function cleanObject(object) {
|
|
85
|
+
if (object === null || typeof object !== "object")
|
|
86
|
+
return object;
|
|
87
|
+
if (object instanceof Date) {
|
|
88
|
+
return serializeDate(object);
|
|
89
|
+
}
|
|
90
|
+
if (typeof object.toJSON === "function" && !Array.isArray(object)) {
|
|
91
|
+
const json = object.toJSON();
|
|
92
|
+
if (json !== object) {
|
|
93
|
+
return cleanObject(json);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const res = Array.isArray(object) ? [] : {};
|
|
97
|
+
for (const i in object) {
|
|
98
|
+
if (object[i] === "" || object[i] === undefined || typeof object[i] === "function") {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
res[i] = cleanObject(object[i]);
|
|
102
|
+
}
|
|
103
|
+
return res;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* DynamoDB-backed repository for a single model class
|
|
107
|
+
*
|
|
108
|
+
* Every object is stored as one item, keyed by the string primary key in the
|
|
109
|
+
* `uuid` hash key attribute.
|
|
110
|
+
*/
|
|
111
|
+
export class DynamoRepository extends MemoryRepository {
|
|
112
|
+
/**
|
|
113
|
+
* Create a new DynamoRepository
|
|
114
|
+
* @param model - the model class
|
|
115
|
+
* @param pks - primary key field names
|
|
116
|
+
* @param client - the DynamoDB document client
|
|
117
|
+
* @param table - the table name
|
|
118
|
+
* @param globalIndexes - global secondary indexes usable by queries
|
|
119
|
+
* @param scanPage - scan page size used by __clean
|
|
120
|
+
* @param storeName - store name used in errors
|
|
121
|
+
*/
|
|
122
|
+
constructor(model, pks, client, table, globalIndexes = {}, scanPage, storeName = table) {
|
|
123
|
+
// Pass an empty Map — we do NOT use in-memory storage
|
|
124
|
+
super(model, pks, undefined, new Map());
|
|
125
|
+
this.client = client;
|
|
126
|
+
this.table = table;
|
|
127
|
+
this.globalIndexes = globalIndexes;
|
|
128
|
+
this.scanPage = scanPage;
|
|
129
|
+
this.storeName = storeName;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The backing table name
|
|
133
|
+
* @returns the table name
|
|
134
|
+
*/
|
|
135
|
+
getTable() {
|
|
136
|
+
return this.table;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Return the DynamoDB key of an object or primary key
|
|
140
|
+
* @param primaryKey - the object or primary key
|
|
141
|
+
* @returns the string key
|
|
142
|
+
*/
|
|
143
|
+
getKey(primaryKey) {
|
|
144
|
+
return this.getPrimaryKey(primaryKey).toString();
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Convert a model or plain object to a DynamoDB item
|
|
148
|
+
* @param data - the object to convert
|
|
149
|
+
* @returns the item
|
|
150
|
+
*/
|
|
151
|
+
toItem(data) {
|
|
152
|
+
const item = cleanObject(data);
|
|
153
|
+
item[KEY_ATTRIBUTE] = this.getKey(data);
|
|
154
|
+
item[TYPE_ATTRIBUTE] = data?.constructor?.Metadata?.Identifier ?? this.model.Metadata?.Identifier;
|
|
155
|
+
if (item[TYPE_ATTRIBUTE] === undefined) {
|
|
156
|
+
delete item[TYPE_ATTRIBUTE];
|
|
157
|
+
}
|
|
158
|
+
return item;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Convert a DynamoDB item to a model instance
|
|
162
|
+
*
|
|
163
|
+
* The `__type` attribute is used to instantiate the right subclass
|
|
164
|
+
* @param item - the DynamoDB item
|
|
165
|
+
* @returns the model instance
|
|
166
|
+
*/
|
|
167
|
+
fromItem(item) {
|
|
168
|
+
const data = { ...item };
|
|
169
|
+
let model = this.model;
|
|
170
|
+
const type = data[TYPE_ATTRIBUTE];
|
|
171
|
+
delete data[TYPE_ATTRIBUTE];
|
|
172
|
+
if (type && type !== this.model.Metadata?.Identifier) {
|
|
173
|
+
try {
|
|
174
|
+
model = useModel(type) ?? this.model;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
model = this.model;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (!this.pks.includes(KEY_ATTRIBUTE)) {
|
|
181
|
+
delete data[KEY_ATTRIBUTE];
|
|
182
|
+
}
|
|
183
|
+
const instance = new model({});
|
|
184
|
+
if (typeof instance.load === "function") {
|
|
185
|
+
instance.load(data);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
Object.assign(instance, data);
|
|
189
|
+
}
|
|
190
|
+
if (type) {
|
|
191
|
+
Object.defineProperty(instance, TYPE_ATTRIBUTE, {
|
|
192
|
+
value: type,
|
|
193
|
+
enumerable: false,
|
|
194
|
+
configurable: true,
|
|
195
|
+
writable: true
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return instance;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Add a write condition to the DynamoDB parameters
|
|
202
|
+
* @param params - the DynamoDB parameters
|
|
203
|
+
* @param writeCondition - the expected value
|
|
204
|
+
* @param field - the field to check
|
|
205
|
+
*/
|
|
206
|
+
setWriteCondition(params, writeCondition, field) {
|
|
207
|
+
params.ExpressionAttributeNames ?? (params.ExpressionAttributeNames = {});
|
|
208
|
+
params.ExpressionAttributeValues ?? (params.ExpressionAttributeValues = {});
|
|
209
|
+
params.ExpressionAttributeNames["#cf"] = field;
|
|
210
|
+
if (writeCondition instanceof Date) {
|
|
211
|
+
writeCondition = serializeDate(writeCondition);
|
|
212
|
+
}
|
|
213
|
+
params.ExpressionAttributeValues[":cf"] = writeCondition;
|
|
214
|
+
params.ConditionExpression = params.ConditionExpression
|
|
215
|
+
? `${params.ConditionExpression} AND #cf = :cf`
|
|
216
|
+
: "#cf = :cf";
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Build a document path expression for an attribute like `team.id`
|
|
220
|
+
* @param attribute - the attribute path
|
|
221
|
+
* @param names - expression attribute names to fill
|
|
222
|
+
* @param prefix - placeholder prefix
|
|
223
|
+
* @returns the path expression
|
|
224
|
+
*/
|
|
225
|
+
attributePath(attribute, names, prefix) {
|
|
226
|
+
return attribute
|
|
227
|
+
.split(".")
|
|
228
|
+
.map((part, i) => {
|
|
229
|
+
names[`#${prefix}${i}`] = part;
|
|
230
|
+
return `#${prefix}${i}`;
|
|
231
|
+
})
|
|
232
|
+
.join(".");
|
|
233
|
+
}
|
|
234
|
+
/** @override */
|
|
235
|
+
async get(primaryKey) {
|
|
236
|
+
const key = this.getKey(primaryKey);
|
|
237
|
+
const item = (await this.client.get({
|
|
238
|
+
TableName: this.table,
|
|
239
|
+
Key: { [KEY_ATTRIBUTE]: key }
|
|
240
|
+
})).Item;
|
|
241
|
+
if (!item) {
|
|
242
|
+
throw new StoreNotFoundError(key, this.storeName);
|
|
243
|
+
}
|
|
244
|
+
return this.fromItem(item);
|
|
245
|
+
}
|
|
246
|
+
/** @override */
|
|
247
|
+
async exists(primaryKey) {
|
|
248
|
+
const item = (await this.client.get({
|
|
249
|
+
TableName: this.table,
|
|
250
|
+
Key: { [KEY_ATTRIBUTE]: this.getKey(primaryKey) },
|
|
251
|
+
ProjectionExpression: "#k",
|
|
252
|
+
ExpressionAttributeNames: { "#k": KEY_ATTRIBUTE }
|
|
253
|
+
})).Item;
|
|
254
|
+
return item !== undefined;
|
|
255
|
+
}
|
|
256
|
+
/** @override */
|
|
257
|
+
async create(data, save = true) {
|
|
258
|
+
// Build once so a generated primary key is kept (and not reset by an undefined one in data)
|
|
259
|
+
const item = this.buildItem(data);
|
|
260
|
+
if (save === false) {
|
|
261
|
+
return item;
|
|
262
|
+
}
|
|
263
|
+
const key = this.getKey(item);
|
|
264
|
+
try {
|
|
265
|
+
await this.client.put({
|
|
266
|
+
TableName: this.table,
|
|
267
|
+
Item: this.toItem(item),
|
|
268
|
+
ConditionExpression: "attribute_not_exists(#k)",
|
|
269
|
+
ExpressionAttributeNames: { "#k": KEY_ATTRIBUTE }
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
if (err instanceof ConditionalCheckFailedException) {
|
|
274
|
+
throw new Error(`Already exists: ${key}`);
|
|
275
|
+
}
|
|
276
|
+
throw err;
|
|
277
|
+
}
|
|
278
|
+
return item;
|
|
279
|
+
}
|
|
280
|
+
/** @override */
|
|
281
|
+
async update(data, conditionField, condition) {
|
|
282
|
+
const key = this.getKey(data);
|
|
283
|
+
const params = {
|
|
284
|
+
TableName: this.table,
|
|
285
|
+
Item: this.toItem(data),
|
|
286
|
+
ConditionExpression: "attribute_exists(#k)",
|
|
287
|
+
ExpressionAttributeNames: { "#k": KEY_ATTRIBUTE }
|
|
288
|
+
};
|
|
289
|
+
if (conditionField) {
|
|
290
|
+
this.setWriteCondition(params, condition, conditionField);
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
await this.client.put(params);
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
if (err instanceof ConditionalCheckFailedException) {
|
|
297
|
+
if (!conditionField) {
|
|
298
|
+
throw new StoreNotFoundError(key, this.storeName);
|
|
299
|
+
}
|
|
300
|
+
throw new UpdateConditionFailError(key, conditionField, condition);
|
|
301
|
+
}
|
|
302
|
+
throw err;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/** @override */
|
|
306
|
+
async patch(primaryKey, data, conditionField, condition) {
|
|
307
|
+
const key = this.getKey(primaryKey);
|
|
308
|
+
const object = cleanObject(data);
|
|
309
|
+
const sets = [];
|
|
310
|
+
const ExpressionAttributeValues = {};
|
|
311
|
+
const ExpressionAttributeNames = { "#k": KEY_ATTRIBUTE };
|
|
312
|
+
let i = 1;
|
|
313
|
+
for (const attr in object) {
|
|
314
|
+
if (attr === KEY_ATTRIBUTE || this.pks.includes(attr) || object[attr] === undefined) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
sets.push(`#a${i} = :v${i}`);
|
|
318
|
+
ExpressionAttributeValues[`:v${i}`] = object[attr];
|
|
319
|
+
ExpressionAttributeNames[`#a${i}`] = attr;
|
|
320
|
+
i++;
|
|
321
|
+
}
|
|
322
|
+
if (!sets.length) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const params = {
|
|
326
|
+
TableName: this.table,
|
|
327
|
+
Key: { [KEY_ATTRIBUTE]: key },
|
|
328
|
+
UpdateExpression: "SET " + sets.join(", "),
|
|
329
|
+
ConditionExpression: "attribute_exists(#k)",
|
|
330
|
+
ExpressionAttributeValues,
|
|
331
|
+
ExpressionAttributeNames
|
|
332
|
+
};
|
|
333
|
+
// The Write Condition checks the value before writing
|
|
334
|
+
if (conditionField) {
|
|
335
|
+
this.setWriteCondition(params, condition, conditionField);
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
await this.client.update(params);
|
|
339
|
+
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
if (err instanceof ConditionalCheckFailedException) {
|
|
342
|
+
if (!conditionField) {
|
|
343
|
+
throw new StoreNotFoundError(key, this.storeName);
|
|
344
|
+
}
|
|
345
|
+
throw new UpdateConditionFailError(key, conditionField, condition);
|
|
346
|
+
}
|
|
347
|
+
throw err;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/** @override */
|
|
351
|
+
async delete(primaryKey, conditionField, condition) {
|
|
352
|
+
const key = this.getKey(primaryKey);
|
|
353
|
+
const params = {
|
|
354
|
+
TableName: this.table,
|
|
355
|
+
Key: { [KEY_ATTRIBUTE]: key }
|
|
356
|
+
};
|
|
357
|
+
if (conditionField) {
|
|
358
|
+
this.setWriteCondition(params, condition, conditionField);
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
await this.client.delete(params);
|
|
362
|
+
}
|
|
363
|
+
catch (err) {
|
|
364
|
+
if (err instanceof ConditionalCheckFailedException) {
|
|
365
|
+
throw new UpdateConditionFailError(key, conditionField, condition);
|
|
366
|
+
}
|
|
367
|
+
throw err;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/** @override */
|
|
371
|
+
async removeAttribute(primaryKey, attribute, conditionField, condition) {
|
|
372
|
+
const key = this.getKey(primaryKey);
|
|
373
|
+
const ExpressionAttributeNames = { "#k": KEY_ATTRIBUTE };
|
|
374
|
+
const params = {
|
|
375
|
+
TableName: this.table,
|
|
376
|
+
Key: { [KEY_ATTRIBUTE]: key },
|
|
377
|
+
UpdateExpression: `REMOVE ${this.attributePath(String(attribute), ExpressionAttributeNames, "r")}`,
|
|
378
|
+
ConditionExpression: "attribute_exists(#k)",
|
|
379
|
+
ExpressionAttributeNames
|
|
380
|
+
};
|
|
381
|
+
if (conditionField) {
|
|
382
|
+
this.setWriteCondition(params, condition, conditionField);
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
await this.client.update(params);
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
if (err instanceof ConditionalCheckFailedException) {
|
|
389
|
+
if (!conditionField) {
|
|
390
|
+
throw new StoreNotFoundError(key, this.storeName);
|
|
391
|
+
}
|
|
392
|
+
throw new UpdateConditionFailError(key, conditionField, condition);
|
|
393
|
+
}
|
|
394
|
+
throw err;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/** @override */
|
|
398
|
+
async incrementAttributes(primaryKey, info, conditionField, condition) {
|
|
399
|
+
const key = this.getKey(primaryKey);
|
|
400
|
+
const entries = Array.isArray(info)
|
|
401
|
+
? info.map((e) => typeof e === "string" ? { property: e, value: 1 } : { property: e.property, value: e.value ?? 1 })
|
|
402
|
+
: Object.entries(info).map(([property, value]) => ({ property: String(property), value: value }));
|
|
403
|
+
if (!entries.length) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const ExpressionAttributeNames = { "#k": KEY_ATTRIBUTE };
|
|
407
|
+
const ExpressionAttributeValues = {};
|
|
408
|
+
const adds = entries.map((p, i) => {
|
|
409
|
+
ExpressionAttributeValues[`:v${i}`] = p.value;
|
|
410
|
+
return `${this.attributePath(p.property, ExpressionAttributeNames, `a${i}_`)} :v${i}`;
|
|
411
|
+
});
|
|
412
|
+
const params = {
|
|
413
|
+
TableName: this.table,
|
|
414
|
+
Key: { [KEY_ATTRIBUTE]: key },
|
|
415
|
+
UpdateExpression: "ADD " + adds.join(", "),
|
|
416
|
+
ConditionExpression: "attribute_exists(#k)",
|
|
417
|
+
ExpressionAttributeValues,
|
|
418
|
+
ExpressionAttributeNames
|
|
419
|
+
};
|
|
420
|
+
if (conditionField) {
|
|
421
|
+
this.setWriteCondition(params, condition, conditionField);
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
await this.client.update(params);
|
|
425
|
+
}
|
|
426
|
+
catch (err) {
|
|
427
|
+
if (err instanceof ConditionalCheckFailedException) {
|
|
428
|
+
if (!conditionField) {
|
|
429
|
+
throw new StoreNotFoundError(key, this.storeName);
|
|
430
|
+
}
|
|
431
|
+
throw new UpdateConditionFailError(key, conditionField, condition);
|
|
432
|
+
}
|
|
433
|
+
throw err;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
/** @override */
|
|
437
|
+
async upsertItemToCollection(primaryKey, collection, item, index, itemWriteConditionField, itemWriteCondition) {
|
|
438
|
+
const key = this.getKey(primaryKey);
|
|
439
|
+
const prop = String(collection);
|
|
440
|
+
const ExpressionAttributeNames = { "#k": KEY_ATTRIBUTE, "#col": prop };
|
|
441
|
+
const ExpressionAttributeValues = {};
|
|
442
|
+
const params = {
|
|
443
|
+
TableName: this.table,
|
|
444
|
+
Key: { [KEY_ATTRIBUTE]: key },
|
|
445
|
+
ConditionExpression: "attribute_exists(#k)",
|
|
446
|
+
ExpressionAttributeNames,
|
|
447
|
+
ExpressionAttributeValues
|
|
448
|
+
};
|
|
449
|
+
if (index === undefined) {
|
|
450
|
+
params.UpdateExpression = "SET #col = list_append(if_not_exists(#col, :empty_list), :item)";
|
|
451
|
+
ExpressionAttributeValues[":item"] = [cleanObject(item)];
|
|
452
|
+
ExpressionAttributeValues[":empty_list"] = [];
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
params.UpdateExpression = `SET #col[${index}] = :item`;
|
|
456
|
+
ExpressionAttributeValues[":item"] = cleanObject(item);
|
|
457
|
+
if (itemWriteCondition !== undefined) {
|
|
458
|
+
ExpressionAttributeValues[":condValue"] = itemWriteCondition;
|
|
459
|
+
ExpressionAttributeNames["#field"] = itemWriteConditionField;
|
|
460
|
+
params.ConditionExpression += ` AND #col[${index}].#field = :condValue`;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
try {
|
|
464
|
+
await this.client.update(params);
|
|
465
|
+
}
|
|
466
|
+
catch (err) {
|
|
467
|
+
if (err instanceof ConditionalCheckFailedException) {
|
|
468
|
+
if (itemWriteCondition === undefined) {
|
|
469
|
+
throw new StoreNotFoundError(key, this.storeName);
|
|
470
|
+
}
|
|
471
|
+
throw new UpdateConditionFailError(key, itemWriteConditionField, itemWriteCondition);
|
|
472
|
+
}
|
|
473
|
+
else if (err.name === "ValidationException") {
|
|
474
|
+
throw new StoreNotFoundError(key, this.storeName);
|
|
475
|
+
}
|
|
476
|
+
throw err;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/** @override */
|
|
480
|
+
async deleteItemFromCollection(primaryKey, collection, index, itemWriteConditionField, itemWriteCondition) {
|
|
481
|
+
const key = this.getKey(primaryKey);
|
|
482
|
+
const ExpressionAttributeNames = { "#k": KEY_ATTRIBUTE, "#col": String(collection) };
|
|
483
|
+
const params = {
|
|
484
|
+
TableName: this.table,
|
|
485
|
+
Key: { [KEY_ATTRIBUTE]: key },
|
|
486
|
+
UpdateExpression: `REMOVE #col[${index}]`,
|
|
487
|
+
ConditionExpression: "attribute_exists(#k)",
|
|
488
|
+
ExpressionAttributeNames
|
|
489
|
+
};
|
|
490
|
+
if (itemWriteCondition !== undefined) {
|
|
491
|
+
params.ExpressionAttributeValues = { ":condValue": itemWriteCondition };
|
|
492
|
+
ExpressionAttributeNames["#field"] = itemWriteConditionField;
|
|
493
|
+
params.ConditionExpression += ` AND #col[${index}].#field = :condValue`;
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
await this.client.update(params);
|
|
497
|
+
}
|
|
498
|
+
catch (err) {
|
|
499
|
+
if (err instanceof ConditionalCheckFailedException) {
|
|
500
|
+
if (itemWriteCondition === undefined) {
|
|
501
|
+
throw new StoreNotFoundError(key, this.storeName);
|
|
502
|
+
}
|
|
503
|
+
throw new UpdateConditionFailError(key, itemWriteConditionField, itemWriteCondition);
|
|
504
|
+
}
|
|
505
|
+
else if (err.name === "ValidationException") {
|
|
506
|
+
throw new StoreNotFoundError(key, this.storeName);
|
|
507
|
+
}
|
|
508
|
+
throw err;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Translate a WebdaQL query into a DynamoDB scan or query
|
|
513
|
+
*
|
|
514
|
+
* A query on the hash key or on a global index hash key with `=` uses a DynamoDB Query,
|
|
515
|
+
* anything else is a Scan. The part of the expression DynamoDB cannot evaluate is
|
|
516
|
+
* returned in `filter` and must be applied on the results.
|
|
517
|
+
*
|
|
518
|
+
* @see https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.html
|
|
519
|
+
* @see https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html
|
|
520
|
+
* @param query - the parsed query
|
|
521
|
+
* @returns the items and the residual filter
|
|
522
|
+
*/
|
|
523
|
+
async find(query) {
|
|
524
|
+
let scan = true;
|
|
525
|
+
let IndexName = undefined;
|
|
526
|
+
let result;
|
|
527
|
+
const limit = query.limit ?? 1000;
|
|
528
|
+
const primaryKeys = { [KEY_ATTRIBUTE]: null };
|
|
529
|
+
Object.keys(this.globalIndexes).forEach(name => {
|
|
530
|
+
primaryKeys[this.globalIndexes[name].key] = name;
|
|
531
|
+
});
|
|
532
|
+
// We could use PartQL but localstack is not compatible
|
|
533
|
+
const filter = new WebdaQL.AndExpression([]);
|
|
534
|
+
let KeyConditionExpression = "";
|
|
535
|
+
let ExpressionAttributeValues = {};
|
|
536
|
+
const FilterExpression = [];
|
|
537
|
+
let ExpressionAttributeNames = {};
|
|
538
|
+
let indexNode;
|
|
539
|
+
let sortOrder = "ASC";
|
|
540
|
+
let processingExpression;
|
|
541
|
+
if (!(query.filter instanceof WebdaQL.AndExpression)) {
|
|
542
|
+
processingExpression = new WebdaQL.AndExpression([query.filter]);
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
processingExpression = query.filter;
|
|
546
|
+
}
|
|
547
|
+
// A FALSE conjunct matches nothing: do not hit DynamoDB (never fail open)
|
|
548
|
+
if (processingExpression.children.some(c => c instanceof WebdaQL.BooleanExpression && !c.value)) {
|
|
549
|
+
return { results: [], filter, continuationToken: undefined };
|
|
550
|
+
}
|
|
551
|
+
// A TRUE conjunct is neutral
|
|
552
|
+
processingExpression = new WebdaQL.AndExpression(processingExpression.children.filter(c => !(c instanceof WebdaQL.BooleanExpression)));
|
|
553
|
+
// Search for the index
|
|
554
|
+
processingExpression.children.some(child => {
|
|
555
|
+
if (child instanceof WebdaQL.ComparisonExpression) {
|
|
556
|
+
// Primary key requires equal operator
|
|
557
|
+
if (child.operator === "=" && child.attribute.length === 1 && primaryKeys[child.attribute[0]] !== undefined) {
|
|
558
|
+
// Query not scan
|
|
559
|
+
scan = false;
|
|
560
|
+
IndexName = primaryKeys[child.attribute[0]] ?? undefined;
|
|
561
|
+
indexNode = child;
|
|
562
|
+
KeyConditionExpression = `#${child.attribute[0]} = :${child.attribute[0]}`;
|
|
563
|
+
ExpressionAttributeNames[`#${child.attribute[0]}`] = child.attribute[0];
|
|
564
|
+
ExpressionAttributeValues[`:${child.attribute[0]}`] = child.value;
|
|
565
|
+
if (IndexName && query.orderBy && this.globalIndexes[IndexName].sort) {
|
|
566
|
+
const sortKey = this.globalIndexes[IndexName].sort;
|
|
567
|
+
// Might update sort order
|
|
568
|
+
query.orderBy.forEach(order => {
|
|
569
|
+
if (order.field === sortKey) {
|
|
570
|
+
sortOrder = order.direction;
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return false;
|
|
578
|
+
});
|
|
579
|
+
let count = 1;
|
|
580
|
+
// Build the Query/Filter
|
|
581
|
+
processingExpression.children.forEach(child => {
|
|
582
|
+
if (child === indexNode) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
// Only work on Comparison Node for now
|
|
586
|
+
if (child instanceof WebdaQL.ComparisonExpression) {
|
|
587
|
+
let operator = child.operator;
|
|
588
|
+
// DynamoDB does not manage LIKE
|
|
589
|
+
if (child.operator === "LIKE") {
|
|
590
|
+
filter.children.push(child);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
// != is <> in Dynamo
|
|
594
|
+
if (operator === "!=") {
|
|
595
|
+
operator = "<>";
|
|
596
|
+
}
|
|
597
|
+
// DynamoDB does not allow more than 100 items in IN
|
|
598
|
+
if (child.operator === "IN" && child.value.length > 100) {
|
|
599
|
+
filter.children.push(child);
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const isSortKey = IndexName && this.globalIndexes[IndexName].sort === child.attribute[0];
|
|
603
|
+
// A sort key cannot be used in a filter expression and IN is not allowed in a key condition
|
|
604
|
+
if (isSortKey && child.operator === "IN") {
|
|
605
|
+
filter.children.push(child);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
// Function conditions (IS NULL / IS NOT NULL) are not allowed on a sort key either
|
|
609
|
+
if (isSortKey && (child.operator === "IS NULL" || child.operator === "IS NOT NULL")) {
|
|
610
|
+
filter.children.push(child);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
// Subfields like team.id needs to be #a1.#a2
|
|
614
|
+
const attr = `a${count++}`;
|
|
615
|
+
let fullAttr = `#${attr}`;
|
|
616
|
+
child.attribute.slice(1).forEach(v => {
|
|
617
|
+
const subAttr = `#a${count++}`;
|
|
618
|
+
fullAttr += `.${subAttr}`;
|
|
619
|
+
ExpressionAttributeNames[subAttr] = v;
|
|
620
|
+
});
|
|
621
|
+
ExpressionAttributeNames[`#${attr}`] = child.attribute[0];
|
|
622
|
+
// IS NULL matches a missing attribute or one stored with the NULL type (JS null).
|
|
623
|
+
// Empty strings are not stored (see cleanObject), so they also match IS NULL here.
|
|
624
|
+
if (child.operator === "IS NULL" || child.operator === "IS NOT NULL") {
|
|
625
|
+
ExpressionAttributeValues[`:${attr}`] = "NULL";
|
|
626
|
+
FilterExpression.push(child.operator === "IS NULL"
|
|
627
|
+
? `(attribute_not_exists(${fullAttr}) OR attribute_type(${fullAttr}, :${attr}))`
|
|
628
|
+
: `(attribute_exists(${fullAttr}) AND NOT attribute_type(${fullAttr}, :${attr}))`);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
let valueExpression = `:${attr}`;
|
|
632
|
+
if (child.operator === "IN") {
|
|
633
|
+
// Need to follow `a IN (b, c, d)
|
|
634
|
+
valueExpression = "(" + valueExpression;
|
|
635
|
+
ExpressionAttributeValues[`:${attr}`] = child.value[0];
|
|
636
|
+
child.value.slice(1).forEach(v => {
|
|
637
|
+
const subAttr = `:a${count++}`;
|
|
638
|
+
valueExpression += `, ${subAttr}`;
|
|
639
|
+
ExpressionAttributeValues[subAttr] = v;
|
|
640
|
+
});
|
|
641
|
+
valueExpression += ")";
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
// For all other just give the value
|
|
645
|
+
ExpressionAttributeValues[`:${attr}`] = child.value;
|
|
646
|
+
}
|
|
647
|
+
// Manage CONTAINS
|
|
648
|
+
if (child.operator === "CONTAINS") {
|
|
649
|
+
FilterExpression.push(`contains(${fullAttr}, :${attr})`);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
// If this is a sort key
|
|
653
|
+
if (isSortKey) {
|
|
654
|
+
// Sort key
|
|
655
|
+
KeyConditionExpression += ` AND ${fullAttr} ${operator} ${valueExpression}`;
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
// Otherwise fallback to normal Filter
|
|
659
|
+
FilterExpression.push(`${fullAttr} ${operator} ${valueExpression}`);
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
filter.children.push(child);
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
if (!Object.keys(ExpressionAttributeNames).length) {
|
|
666
|
+
ExpressionAttributeNames = undefined;
|
|
667
|
+
}
|
|
668
|
+
if (!Object.keys(ExpressionAttributeValues).length) {
|
|
669
|
+
ExpressionAttributeValues = undefined;
|
|
670
|
+
}
|
|
671
|
+
let ExclusiveStartKey = query.continuationToken
|
|
672
|
+
? JSON.parse(Buffer.from(query.continuationToken, "base64").toString())
|
|
673
|
+
: undefined;
|
|
674
|
+
// Scan if not primary key was provided
|
|
675
|
+
if (scan) {
|
|
676
|
+
const Items = [];
|
|
677
|
+
do {
|
|
678
|
+
result = await this.client.scan({
|
|
679
|
+
TableName: this.table,
|
|
680
|
+
FilterExpression: FilterExpression.length ? FilterExpression.join(" AND ") : undefined,
|
|
681
|
+
ExclusiveStartKey,
|
|
682
|
+
ExpressionAttributeNames,
|
|
683
|
+
ExpressionAttributeValues,
|
|
684
|
+
Limit: limit - Items.length
|
|
685
|
+
});
|
|
686
|
+
Items.push(...result.Items);
|
|
687
|
+
result.Items = Items;
|
|
688
|
+
ExclusiveStartKey = result.LastEvaluatedKey;
|
|
689
|
+
} while (Items.length < limit && result.LastEvaluatedKey);
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
result = await this.client.query({
|
|
693
|
+
TableName: this.table,
|
|
694
|
+
IndexName,
|
|
695
|
+
ExclusiveStartKey,
|
|
696
|
+
KeyConditionExpression,
|
|
697
|
+
FilterExpression: FilterExpression.length ? FilterExpression.join(" AND ") : undefined,
|
|
698
|
+
ExpressionAttributeNames,
|
|
699
|
+
ExpressionAttributeValues,
|
|
700
|
+
Limit: limit,
|
|
701
|
+
ScanIndexForward: sortOrder === "ASC"
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
return {
|
|
705
|
+
results: result.Items.map(c => this.fromItem(c)),
|
|
706
|
+
filter,
|
|
707
|
+
continuationToken: result.LastEvaluatedKey
|
|
708
|
+
? Buffer.from(JSON.stringify(result.LastEvaluatedKey)).toString("base64")
|
|
709
|
+
: undefined
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Query the table
|
|
714
|
+
*
|
|
715
|
+
* Pages are retrieved until the limit is reached, applying the part of the
|
|
716
|
+
* filter that DynamoDB cannot evaluate and restricting to the model hierarchy
|
|
717
|
+
* @override
|
|
718
|
+
* @param query - the WebdaQL query
|
|
719
|
+
* @returns the results and the continuation token
|
|
720
|
+
*/
|
|
721
|
+
async query(query) {
|
|
722
|
+
const parsed = typeof query === "string" ? WebdaQL.parse(query) : query;
|
|
723
|
+
const limit = parsed.limit ?? 1000;
|
|
724
|
+
const ids = this.buildClassFilterIdentifiers();
|
|
725
|
+
const results = [];
|
|
726
|
+
let continuationToken = parsed.continuationToken;
|
|
727
|
+
do {
|
|
728
|
+
const page = await this.find({
|
|
729
|
+
...parsed,
|
|
730
|
+
filter: parsed.filter,
|
|
731
|
+
limit: limit - results.length,
|
|
732
|
+
continuationToken
|
|
733
|
+
});
|
|
734
|
+
results.push(...page.results.filter(item => {
|
|
735
|
+
const type = item[TYPE_ATTRIBUTE];
|
|
736
|
+
if (ids && type && !ids.includes(type)) {
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
return page.filter.eval(item);
|
|
740
|
+
}));
|
|
741
|
+
continuationToken = page.continuationToken;
|
|
742
|
+
} while (results.length < limit && continuationToken);
|
|
743
|
+
return {
|
|
744
|
+
results,
|
|
745
|
+
continuationToken: results.length >= limit ? continuationToken : undefined
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Iterate over the query results
|
|
750
|
+
* @override
|
|
751
|
+
* @param query - the WebdaQL query
|
|
752
|
+
*/
|
|
753
|
+
async *iterate(query) {
|
|
754
|
+
const parsed = WebdaQL.parse(query);
|
|
755
|
+
if (!parsed.limit) {
|
|
756
|
+
parsed.limit = 100;
|
|
757
|
+
}
|
|
758
|
+
do {
|
|
759
|
+
const res = await this.query(parsed);
|
|
760
|
+
for (const item of res.results) {
|
|
761
|
+
yield item;
|
|
762
|
+
}
|
|
763
|
+
parsed.continuationToken = res.continuationToken;
|
|
764
|
+
} while (parsed.continuationToken);
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Delete all items from the table (used in tests)
|
|
768
|
+
*/
|
|
769
|
+
async __clean() {
|
|
770
|
+
let ExclusiveStartKey;
|
|
771
|
+
do {
|
|
772
|
+
const result = await this.client.scan({
|
|
773
|
+
TableName: this.table,
|
|
774
|
+
Limit: this.scanPage,
|
|
775
|
+
ExclusiveStartKey
|
|
776
|
+
});
|
|
777
|
+
await Promise.all((result.Items ?? []).map(item => this.client.delete({ TableName: this.table, Key: { [KEY_ATTRIBUTE]: item[KEY_ATTRIBUTE] } })));
|
|
778
|
+
ExclusiveStartKey = result.LastEvaluatedKey;
|
|
779
|
+
} while (ExclusiveStartKey);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* DynamoStore handles the DynamoDB
|
|
784
|
+
*
|
|
785
|
+
* Parameters:
|
|
786
|
+
* accessKeyId: '' // try WEBDA_AWS_KEY env variable if not found
|
|
787
|
+
* secretAccessKey: '' // try WEBDA_AWS_SECRET env variable if not found
|
|
788
|
+
* table: ''
|
|
789
|
+
* region: ''
|
|
790
|
+
*
|
|
791
|
+
* The table must have a `uuid` string hash key
|
|
792
|
+
*
|
|
793
|
+
* @WebdaModda
|
|
794
|
+
*/
|
|
795
|
+
let DynamoStore = (() => {
|
|
796
|
+
var _a;
|
|
797
|
+
let _classSuper = Store;
|
|
798
|
+
let _instanceExtraInitializers = [];
|
|
799
|
+
let _getRepository_decorators;
|
|
800
|
+
return _a = class DynamoStore extends _classSuper {
|
|
801
|
+
constructor() {
|
|
802
|
+
super(...arguments);
|
|
803
|
+
this._client = __runInitializers(this, _instanceExtraInitializers);
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Create the AWS client
|
|
807
|
+
* @returns this
|
|
808
|
+
*/
|
|
809
|
+
resolve() {
|
|
810
|
+
super.resolve();
|
|
811
|
+
this._client = DynamoDBDocument.from(new DynamoDBClient(this.parameters), {
|
|
812
|
+
marshallOptions: {
|
|
813
|
+
removeUndefinedValues: true,
|
|
814
|
+
convertClassInstanceToMap: true
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
return this;
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Copy one DynamoDB table to another
|
|
821
|
+
*
|
|
822
|
+
* @param output - worker output to report progress
|
|
823
|
+
* @param source - source table
|
|
824
|
+
* @param target - target table
|
|
825
|
+
*/
|
|
826
|
+
static async copyTable(output, source, target) {
|
|
827
|
+
const db = new DynamoDB({});
|
|
828
|
+
let ExclusiveStartKey;
|
|
829
|
+
const props = await db.describeTable({
|
|
830
|
+
TableName: source
|
|
831
|
+
});
|
|
832
|
+
output.startProgress("copyTable", props.Table.ItemCount, `Copying ${source} to ${target}`);
|
|
833
|
+
do {
|
|
834
|
+
const info = await db.scan({
|
|
835
|
+
TableName: source,
|
|
836
|
+
ExclusiveStartKey
|
|
837
|
+
});
|
|
838
|
+
do {
|
|
839
|
+
const items = [];
|
|
840
|
+
while (info.Items.length && items.length < 25) {
|
|
841
|
+
items.push(info.Items.shift());
|
|
842
|
+
}
|
|
843
|
+
if (!items.length) {
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
await db.batchWriteItem({
|
|
847
|
+
RequestItems: {
|
|
848
|
+
[target]: items.map(Item => ({
|
|
849
|
+
PutRequest: { Item }
|
|
850
|
+
}))
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
output.incrementProgress(items.length, "copyTable");
|
|
854
|
+
} while (true);
|
|
855
|
+
ExclusiveStartKey = info.LastEvaluatedKey;
|
|
856
|
+
} while (ExclusiveStartKey);
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Resolve the table name for a given model class
|
|
860
|
+
*
|
|
861
|
+
* `tables[identifier]` overrides the default `table`
|
|
862
|
+
* @param model - the model class
|
|
863
|
+
* @returns the table name
|
|
864
|
+
*/
|
|
865
|
+
resolveTable(model) {
|
|
866
|
+
const meta = useModelMetadata(model);
|
|
867
|
+
return (meta && this.parameters.tables?.[meta.Identifier]) || this.parameters.table;
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Build the DynamoRepository for a model
|
|
871
|
+
*
|
|
872
|
+
* The result is cached per model class
|
|
873
|
+
* @param model - the model class
|
|
874
|
+
* @returns a repository backed by this store's DynamoDB table
|
|
875
|
+
*/
|
|
876
|
+
getRepository(model) {
|
|
877
|
+
const meta = useModelMetadata(model);
|
|
878
|
+
const inner = new DynamoRepository(model, meta.PrimaryKey, this._client, this.resolveTable(model), this.parameters.globalIndexes, this.parameters.scanPage, this.getName());
|
|
879
|
+
// Wrap in EventRepository so typed CRUD events fire; consumers reach them
|
|
880
|
+
// via useRepository(model).on(...).
|
|
881
|
+
return new EventRepository(model, meta.PrimaryKey, inner);
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Return the underlying DynamoRepository for each model this store manages
|
|
885
|
+
* @returns the repositories
|
|
886
|
+
*/
|
|
887
|
+
getRepositories() {
|
|
888
|
+
const repos = [];
|
|
889
|
+
for (const modelId of Object.keys(this._modelsHierarchy ?? {})) {
|
|
890
|
+
let model;
|
|
891
|
+
try {
|
|
892
|
+
model = useModel(modelId);
|
|
893
|
+
}
|
|
894
|
+
catch {
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
if (!model) {
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
const repo = this.getRepository(model);
|
|
901
|
+
repos.push((repo.repository ?? repo));
|
|
902
|
+
}
|
|
903
|
+
return repos;
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* IAM policy required by the store
|
|
907
|
+
* @param accountId - AWS account id
|
|
908
|
+
* @returns the policy statement
|
|
909
|
+
*/
|
|
910
|
+
getARNPolicy(accountId) {
|
|
911
|
+
const region = this.parameters.region || "us-east-1";
|
|
912
|
+
const tables = new Set([this.parameters.table, ...Object.values(this.parameters.tables ?? {})]);
|
|
913
|
+
return {
|
|
914
|
+
Sid: this.constructor.name + this.getName(),
|
|
915
|
+
Effect: "Allow",
|
|
916
|
+
Action: [
|
|
917
|
+
"dynamodb:BatchGetItem",
|
|
918
|
+
"dynamodb:BatchWriteItem",
|
|
919
|
+
"dynamodb:DeleteItem",
|
|
920
|
+
"dynamodb:GetItem",
|
|
921
|
+
"dynamodb:GetRecords",
|
|
922
|
+
"dynamodb:GetShardIterator",
|
|
923
|
+
"dynamodb:PutItem",
|
|
924
|
+
"dynamodb:Query",
|
|
925
|
+
"dynamodb:Scan",
|
|
926
|
+
"dynamodb:UpdateItem"
|
|
927
|
+
],
|
|
928
|
+
Resource: [...tables].map(table => "arn:aws:dynamodb:" + region + ":" + accountId + ":table/" + table)
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Delete all items from the managed tables (used in tests)
|
|
933
|
+
*/
|
|
934
|
+
async __clean() {
|
|
935
|
+
const done = new Set();
|
|
936
|
+
for (const repo of this.getRepositories()) {
|
|
937
|
+
if (done.has(repo.getTable())) {
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
done.add(repo.getTable());
|
|
941
|
+
await repo.__clean();
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* CloudFormation resources for the table
|
|
946
|
+
* @param deployer - the deployer requesting the resources
|
|
947
|
+
* @returns the resources
|
|
948
|
+
*/
|
|
949
|
+
getCloudFormation(deployer) {
|
|
950
|
+
if (this.parameters.CloudFormationSkip) {
|
|
951
|
+
return {};
|
|
952
|
+
}
|
|
953
|
+
const resources = {};
|
|
954
|
+
this.parameters.CloudFormation = this.parameters.CloudFormation || {};
|
|
955
|
+
this.parameters.CloudFormation.Table = this.parameters.CloudFormation.Table || {};
|
|
956
|
+
const KeySchema = this.parameters.CloudFormation.KeySchema || [{ KeyType: "HASH", AttributeName: KEY_ATTRIBUTE }];
|
|
957
|
+
const AttributeDefinitions = this.parameters.CloudFormation.AttributeDefinitions || [];
|
|
958
|
+
this.parameters.CloudFormation.Table.BillingMode =
|
|
959
|
+
this.parameters.CloudFormation.Table.BillingMode || "PAY_PER_REQUEST";
|
|
960
|
+
AttributeDefinitions.push({ AttributeName: KEY_ATTRIBUTE, AttributeType: "S" });
|
|
961
|
+
resources[this.getName() + "DynamoTable"] = {
|
|
962
|
+
Type: "AWS::DynamoDB::Table",
|
|
963
|
+
Properties: {
|
|
964
|
+
...this.parameters.CloudFormation.Table,
|
|
965
|
+
TableName: this.parameters.table,
|
|
966
|
+
KeySchema,
|
|
967
|
+
AttributeDefinitions,
|
|
968
|
+
Tags: deployer.getDefaultTags(this.parameters.CloudFormation.Table.Tags)
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
// Add any Other resources with prefix of the service
|
|
972
|
+
return resources;
|
|
973
|
+
}
|
|
974
|
+
},
|
|
975
|
+
(() => {
|
|
976
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
977
|
+
_getRepository_decorators = [InstanceCache()];
|
|
978
|
+
__esDecorate(_a, null, _getRepository_decorators, { kind: "method", name: "getRepository", static: false, private: false, access: { has: obj => "getRepository" in obj, get: obj => obj.getRepository }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
979
|
+
if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
980
|
+
})(),
|
|
981
|
+
_a;
|
|
982
|
+
})();
|
|
983
|
+
export { DynamoStore };
|
|
984
|
+
export default DynamoStore;
|
|
985
|
+
//# sourceMappingURL=dynamodb.service.js.map
|