@aws-amplify/datastore 3.12.6-next.13 → 3.12.6-next.32
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/CHANGELOG.md +58 -0
- package/lib/authModeStrategies/multiAuthStrategy.js +17 -64
- package/lib/authModeStrategies/multiAuthStrategy.js.map +1 -1
- package/lib/datastore/datastore.js +682 -469
- package/lib/datastore/datastore.js.map +1 -1
- package/lib/index.js +2 -4
- package/lib/index.js.map +1 -1
- package/lib/predicates/index.js +12 -2
- package/lib/predicates/index.js.map +1 -1
- package/lib/storage/adapter/AsyncStorageAdapter.js +393 -298
- package/lib/storage/adapter/AsyncStorageAdapter.js.map +1 -1
- package/lib/storage/adapter/AsyncStorageDatabase.js +97 -122
- package/lib/storage/adapter/AsyncStorageDatabase.js.map +1 -1
- package/lib/storage/adapter/InMemoryStore.js +16 -67
- package/lib/storage/adapter/InMemoryStore.js.map +1 -1
- package/lib/storage/adapter/InMemoryStore.native.js +2 -4
- package/lib/storage/adapter/InMemoryStore.native.js.map +1 -1
- package/lib/storage/adapter/IndexedDBAdapter.js +497 -404
- package/lib/storage/adapter/IndexedDBAdapter.js.map +1 -1
- package/lib/storage/adapter/getDefaultAdapter/index.js +3 -5
- package/lib/storage/adapter/getDefaultAdapter/index.js.map +1 -1
- package/lib/storage/adapter/getDefaultAdapter/index.native.js +2 -4
- package/lib/storage/adapter/getDefaultAdapter/index.native.js.map +1 -1
- package/lib/storage/storage.js +129 -151
- package/lib/storage/storage.js.map +1 -1
- package/lib/sync/datastoreConnectivity.js +13 -17
- package/lib/sync/datastoreConnectivity.js.map +1 -1
- package/lib/sync/datastoreReachability/index.native.js +2 -4
- package/lib/sync/datastoreReachability/index.native.js.map +1 -1
- package/lib/sync/index.js +544 -488
- package/lib/sync/index.js.map +1 -1
- package/lib/sync/merger.js +21 -80
- package/lib/sync/merger.js.map +1 -1
- package/lib/sync/outbox.js +95 -162
- package/lib/sync/outbox.js.map +1 -1
- package/lib/sync/processors/errorMaps.js +4 -34
- package/lib/sync/processors/errorMaps.js.map +1 -1
- package/lib/sync/processors/mutation.js +285 -312
- package/lib/sync/processors/mutation.js.map +1 -1
- package/lib/sync/processors/subscription.js +218 -259
- package/lib/sync/processors/subscription.js.map +1 -1
- package/lib/sync/processors/sync.js +141 -212
- package/lib/sync/processors/sync.js.map +1 -1
- package/lib/sync/utils.js +50 -61
- package/lib/sync/utils.js.map +1 -1
- package/lib/types.js +13 -39
- package/lib/types.js.map +1 -1
- package/lib/util.js +429 -242
- package/lib/util.js.map +1 -1
- package/lib-esm/authModeStrategies/multiAuthStrategy.d.ts +11 -0
- package/lib-esm/authModeStrategies/multiAuthStrategy.js +13 -57
- package/lib-esm/authModeStrategies/multiAuthStrategy.js.map +1 -1
- package/lib-esm/datastore/datastore.d.ts +107 -17
- package/lib-esm/datastore/datastore.js +649 -433
- package/lib-esm/datastore/datastore.js.map +1 -1
- package/lib-esm/index.d.ts +3 -19
- package/lib-esm/predicates/index.d.ts +3 -2
- package/lib-esm/predicates/index.js +13 -3
- package/lib-esm/predicates/index.js.map +1 -1
- package/lib-esm/storage/adapter/AsyncStorageAdapter.d.ts +4 -3
- package/lib-esm/storage/adapter/AsyncStorageAdapter.js +356 -258
- package/lib-esm/storage/adapter/AsyncStorageAdapter.js.map +1 -1
- package/lib-esm/storage/adapter/AsyncStorageDatabase.d.ts +14 -4
- package/lib-esm/storage/adapter/AsyncStorageDatabase.js +67 -92
- package/lib-esm/storage/adapter/AsyncStorageDatabase.js.map +1 -1
- package/lib-esm/storage/adapter/InMemoryStore.js +1 -52
- package/lib-esm/storage/adapter/InMemoryStore.js.map +1 -1
- package/lib-esm/storage/adapter/IndexedDBAdapter.d.ts +26 -4
- package/lib-esm/storage/adapter/IndexedDBAdapter.js +446 -346
- package/lib-esm/storage/adapter/IndexedDBAdapter.js.map +1 -1
- package/lib-esm/storage/adapter/index.d.ts +1 -1
- package/lib-esm/storage/storage.d.ts +1 -1
- package/lib-esm/storage/storage.js +94 -113
- package/lib-esm/storage/storage.js.map +1 -1
- package/lib-esm/sync/datastoreConnectivity.d.ts +1 -0
- package/lib-esm/sync/datastoreConnectivity.js +10 -11
- package/lib-esm/sync/datastoreConnectivity.js.map +1 -1
- package/lib-esm/sync/index.d.ts +31 -5
- package/lib-esm/sync/index.js +525 -466
- package/lib-esm/sync/index.js.map +1 -1
- package/lib-esm/sync/merger.d.ts +9 -3
- package/lib-esm/sync/merger.js +14 -73
- package/lib-esm/sync/merger.js.map +1 -1
- package/lib-esm/sync/outbox.d.ts +2 -2
- package/lib-esm/sync/outbox.js +79 -146
- package/lib-esm/sync/outbox.js.map +1 -1
- package/lib-esm/sync/processors/errorMaps.js +1 -31
- package/lib-esm/sync/processors/errorMaps.js.map +1 -1
- package/lib-esm/sync/processors/mutation.d.ts +2 -0
- package/lib-esm/sync/processors/mutation.js +271 -295
- package/lib-esm/sync/processors/mutation.js.map +1 -1
- package/lib-esm/sync/processors/subscription.d.ts +2 -0
- package/lib-esm/sync/processors/subscription.js +214 -245
- package/lib-esm/sync/processors/subscription.js.map +1 -1
- package/lib-esm/sync/processors/sync.d.ts +2 -1
- package/lib-esm/sync/processors/sync.js +127 -195
- package/lib-esm/sync/processors/sync.js.map +1 -1
- package/lib-esm/sync/utils.d.ts +3 -2
- package/lib-esm/sync/utils.js +45 -57
- package/lib-esm/sync/utils.js.map +1 -1
- package/lib-esm/types.d.ts +65 -26
- package/lib-esm/types.js +10 -38
- package/lib-esm/types.js.map +1 -1
- package/lib-esm/util.d.ts +67 -24
- package/lib-esm/util.js +420 -233
- package/lib-esm/util.js.map +1 -1
- package/package.json +14 -7
- package/src/authModeStrategies/multiAuthStrategy.ts +12 -1
- package/src/datastore/datastore.ts +798 -397
- package/src/predicates/index.ts +32 -10
- package/src/storage/adapter/AsyncStorageAdapter.ts +309 -93
- package/src/storage/adapter/AsyncStorageDatabase.ts +74 -26
- package/src/storage/adapter/IndexedDBAdapter.ts +358 -134
- package/src/storage/adapter/index.ts +1 -1
- package/src/storage/storage.ts +69 -22
- package/src/sync/datastoreConnectivity.ts +6 -0
- package/src/sync/index.ts +521 -412
- package/src/sync/merger.ts +20 -4
- package/src/sync/outbox.ts +22 -9
- package/src/sync/processors/mutation.ts +188 -150
- package/src/sync/processors/subscription.ts +289 -253
- package/src/sync/processors/sync.ts +151 -138
- package/src/sync/utils.ts +67 -12
- package/src/types.ts +182 -30
- package/src/util.ts +505 -176
- package/build.js +0 -5
- package/dist/aws-amplify-datastore.js +0 -98255
- package/dist/aws-amplify-datastore.js.map +0 -1
- package/dist/aws-amplify-datastore.min.js +0 -66
- package/dist/aws-amplify-datastore.min.js.map +0 -1
- package/index.js +0 -7
- package/lib/authModeStrategies/defaultAuthStrategy.d.ts +0 -2
- package/lib/authModeStrategies/index.d.ts +0 -2
- package/lib/authModeStrategies/multiAuthStrategy.d.ts +0 -2
- package/lib/datastore/datastore.d.ts +0 -66
- package/lib/index.d.ts +0 -31
- package/lib/predicates/index.d.ts +0 -15
- package/lib/predicates/sort.d.ts +0 -8
- package/lib/ssr/index.d.ts +0 -3
- package/lib/storage/adapter/AsyncStorageAdapter.d.ts +0 -40
- package/lib/storage/adapter/AsyncStorageDatabase.d.ts +0 -29
- package/lib/storage/adapter/InMemoryStore.d.ts +0 -11
- package/lib/storage/adapter/InMemoryStore.native.d.ts +0 -1
- package/lib/storage/adapter/IndexedDBAdapter.d.ts +0 -37
- package/lib/storage/adapter/getDefaultAdapter/index.d.ts +0 -3
- package/lib/storage/adapter/getDefaultAdapter/index.native.d.ts +0 -3
- package/lib/storage/adapter/index.d.ts +0 -9
- package/lib/storage/storage.d.ts +0 -49
- package/lib/sync/datastoreConnectivity.d.ts +0 -15
- package/lib/sync/datastoreReachability/index.d.ts +0 -3
- package/lib/sync/datastoreReachability/index.native.d.ts +0 -3
- package/lib/sync/index.d.ts +0 -63
- package/lib/sync/merger.d.ts +0 -11
- package/lib/sync/outbox.d.ts +0 -27
- package/lib/sync/processors/errorMaps.d.ts +0 -17
- package/lib/sync/processors/mutation.d.ts +0 -56
- package/lib/sync/processors/subscription.d.ts +0 -31
- package/lib/sync/processors/sync.d.ts +0 -27
- package/lib/sync/utils.d.ts +0 -41
- package/lib/types.d.ts +0 -462
- package/lib/util.d.ts +0 -113
- package/webpack.config.dev.js +0 -6
package/src/util.ts
CHANGED
|
@@ -26,9 +26,41 @@ import {
|
|
|
26
26
|
NonModelTypeConstructor,
|
|
27
27
|
DeferredCallbackResolverOptions,
|
|
28
28
|
LimitTimerRaceResolvedValues,
|
|
29
|
+
SchemaModel,
|
|
30
|
+
ModelAttribute,
|
|
31
|
+
IndexesType,
|
|
32
|
+
ModelAssociation,
|
|
29
33
|
} from './types';
|
|
30
34
|
import { WordArray } from 'amazon-cognito-identity-js';
|
|
31
35
|
|
|
36
|
+
export const ID = 'id';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Used by the Async Storage Adapter to concatenate key values
|
|
40
|
+
* for a record. For instance, if a model has the following keys:
|
|
41
|
+
* `customId: ID! @primaryKey(sortKeyFields: ["createdAt"])`,
|
|
42
|
+
* we concatenate the `customId` and `createdAt` as:
|
|
43
|
+
* `12-234-5#2022-09-28T00:00:00.000Z`
|
|
44
|
+
*/
|
|
45
|
+
export const DEFAULT_PRIMARY_KEY_VALUE_SEPARATOR = '#';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Used for generating spinal-cased index name from an array of
|
|
49
|
+
* key field names.
|
|
50
|
+
* E.g. for keys `[id, title]` => 'id-title'
|
|
51
|
+
*/
|
|
52
|
+
export const IDENTIFIER_KEY_SEPARATOR = '-';
|
|
53
|
+
|
|
54
|
+
export const errorMessages = {
|
|
55
|
+
idEmptyString: 'An index field cannot contain an empty string value',
|
|
56
|
+
queryByPkWithCompositeKeyPresent:
|
|
57
|
+
'Models with composite primary keys cannot be queried by a single key value. Use object literal syntax for composite keys instead: https://docs.amplify.aws/lib/datastore/advanced-workflows/q/platform/js/#querying-records-with-custom-primary-keys',
|
|
58
|
+
deleteByPkWithCompositeKeyPresent:
|
|
59
|
+
'Models with composite primary keys cannot be deleted by a single key value, unless using a predicate. Use object literal syntax for composite keys instead: https://docs.amplify.aws/lib/datastore/advanced-workflows/q/platform/js/#querying-records-with-custom-primary-keys',
|
|
60
|
+
observeWithObjectLiteral:
|
|
61
|
+
'Object literal syntax cannot be used with observe. Use a predicate instead: https://docs.amplify.aws/lib/datastore/data-access/q/platform/js/#predicates',
|
|
62
|
+
};
|
|
63
|
+
|
|
32
64
|
export enum NAMESPACES {
|
|
33
65
|
DATASTORE = 'datastore',
|
|
34
66
|
USER = 'user',
|
|
@@ -162,154 +194,6 @@ export const isNonModelConstructor = (
|
|
|
162
194
|
return nonModelClasses.has(obj);
|
|
163
195
|
};
|
|
164
196
|
|
|
165
|
-
/*
|
|
166
|
-
When we have GSI(s) with composite sort keys defined on a model
|
|
167
|
-
There are some very particular rules regarding which fields must be included in the update mutation input
|
|
168
|
-
The field selection becomes more complex as the number of GSIs with composite sort keys grows
|
|
169
|
-
|
|
170
|
-
To summarize: any time we update a field that is part of the composite sort key of a GSI, we must include:
|
|
171
|
-
1. all of the other fields in that composite sort key
|
|
172
|
-
2. all of the fields from any other composite sort key that intersect with the fields from 1.
|
|
173
|
-
|
|
174
|
-
E.g.,
|
|
175
|
-
Model @model
|
|
176
|
-
@key(name: 'key1' fields: ['hk', 'a', 'b', 'c'])
|
|
177
|
-
@key(name: 'key2' fields: ['hk', 'a', 'b', 'd'])
|
|
178
|
-
@key(name: 'key3' fields: ['hk', 'x', 'y', 'z'])
|
|
179
|
-
|
|
180
|
-
Model.a is updated => include ['a', 'b', 'c', 'd']
|
|
181
|
-
Model.c is updated => include ['a', 'b', 'c', 'd']
|
|
182
|
-
Model.d is updated => include ['a', 'b', 'c', 'd']
|
|
183
|
-
Model.x is updated => include ['x', 'y', 'z']
|
|
184
|
-
|
|
185
|
-
This function accepts a model's attributes and returns grouped sets of composite key fields
|
|
186
|
-
Using our example Model above, the function will return:
|
|
187
|
-
[
|
|
188
|
-
Set('a', 'b', 'c', 'd'),
|
|
189
|
-
Set('x', 'y', 'z'),
|
|
190
|
-
]
|
|
191
|
-
|
|
192
|
-
This gives us the opportunity to correctly include the required fields for composite keys
|
|
193
|
-
When crafting the mutation input in Storage.getUpdateMutationInput
|
|
194
|
-
|
|
195
|
-
See 'processCompositeKeys' test in util.test.ts for more examples
|
|
196
|
-
*/
|
|
197
|
-
export const processCompositeKeys = (
|
|
198
|
-
attributes: ModelAttributes
|
|
199
|
-
): Set<string>[] => {
|
|
200
|
-
const extractCompositeSortKey = ({
|
|
201
|
-
properties: {
|
|
202
|
-
// ignore the HK (fields[0]) we only need to include the composite sort key fields[1...n]
|
|
203
|
-
fields: [, ...sortKeyFields],
|
|
204
|
-
},
|
|
205
|
-
}) => sortKeyFields;
|
|
206
|
-
|
|
207
|
-
const compositeKeyFields = attributes
|
|
208
|
-
.filter(isModelAttributeCompositeKey)
|
|
209
|
-
.map(extractCompositeSortKey);
|
|
210
|
-
|
|
211
|
-
/*
|
|
212
|
-
if 2 sets of fields have any intersecting fields => combine them into 1 union set
|
|
213
|
-
e.g., ['a', 'b', 'c'] and ['a', 'b', 'd'] => ['a', 'b', 'c', 'd']
|
|
214
|
-
*/
|
|
215
|
-
const combineIntersecting = (fields): Set<string>[] =>
|
|
216
|
-
fields.reduce((combined, sortKeyFields) => {
|
|
217
|
-
const sortKeyFieldsSet = new Set(sortKeyFields);
|
|
218
|
-
|
|
219
|
-
if (combined.length === 0) {
|
|
220
|
-
combined.push(sortKeyFieldsSet);
|
|
221
|
-
return combined;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// does the current set share values with another set we've already added to `combined`?
|
|
225
|
-
const intersectingSetIdx = combined.findIndex(existingSet => {
|
|
226
|
-
return [...existingSet].some(f => sortKeyFieldsSet.has(f));
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
if (intersectingSetIdx > -1) {
|
|
230
|
-
const union = new Set([
|
|
231
|
-
...combined[intersectingSetIdx],
|
|
232
|
-
...sortKeyFieldsSet,
|
|
233
|
-
]);
|
|
234
|
-
// combine the current set with the intersecting set we found above
|
|
235
|
-
combined[intersectingSetIdx] = union;
|
|
236
|
-
} else {
|
|
237
|
-
// none of the sets in `combined` have intersecting values with the current set
|
|
238
|
-
combined.push(sortKeyFieldsSet);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return combined;
|
|
242
|
-
}, []);
|
|
243
|
-
|
|
244
|
-
const initial = combineIntersecting(compositeKeyFields);
|
|
245
|
-
// a single pass pay not be enough to correctly combine all the fields
|
|
246
|
-
// call the function once more to get a final merged list of sets
|
|
247
|
-
const combined = combineIntersecting(initial);
|
|
248
|
-
|
|
249
|
-
return combined;
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
export const establishRelationAndKeys = (
|
|
253
|
-
namespace: SchemaNamespace
|
|
254
|
-
): [RelationshipType, ModelKeys] => {
|
|
255
|
-
const relationship: RelationshipType = {};
|
|
256
|
-
const keys: ModelKeys = {};
|
|
257
|
-
|
|
258
|
-
Object.keys(namespace.models).forEach((mKey: string) => {
|
|
259
|
-
relationship[mKey] = { indexes: [], relationTypes: [] };
|
|
260
|
-
keys[mKey] = {};
|
|
261
|
-
|
|
262
|
-
const model = namespace.models[mKey];
|
|
263
|
-
Object.keys(model.fields).forEach((attr: string) => {
|
|
264
|
-
const fieldAttribute = model.fields[attr];
|
|
265
|
-
if (
|
|
266
|
-
typeof fieldAttribute.type === 'object' &&
|
|
267
|
-
'model' in fieldAttribute.type
|
|
268
|
-
) {
|
|
269
|
-
const connectionType = fieldAttribute.association.connectionType;
|
|
270
|
-
relationship[mKey].relationTypes.push({
|
|
271
|
-
fieldName: fieldAttribute.name,
|
|
272
|
-
modelName: fieldAttribute.type.model,
|
|
273
|
-
relationType: connectionType,
|
|
274
|
-
targetName: fieldAttribute.association['targetName'],
|
|
275
|
-
associatedWith: fieldAttribute.association['associatedWith'],
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
if (connectionType === 'BELONGS_TO') {
|
|
279
|
-
relationship[mKey].indexes.push(
|
|
280
|
-
fieldAttribute.association['targetName']
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
if (model.attributes) {
|
|
287
|
-
keys[mKey].compositeKeys = processCompositeKeys(model.attributes);
|
|
288
|
-
|
|
289
|
-
for (const attribute of model.attributes) {
|
|
290
|
-
if (!isModelAttributeKey(attribute)) {
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (isModelAttributePrimaryKey(attribute)) {
|
|
295
|
-
keys[mKey].primaryKey = attribute.properties.fields;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const { fields } = attribute.properties;
|
|
299
|
-
for (const field of fields) {
|
|
300
|
-
// only add index if it hasn't already been added
|
|
301
|
-
const exists = relationship[mKey].indexes.includes(field);
|
|
302
|
-
if (!exists) {
|
|
303
|
-
relationship[mKey].indexes.push(field);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
return [relationship, keys];
|
|
311
|
-
};
|
|
312
|
-
|
|
313
197
|
const topologicallySortedModels = new WeakMap<SchemaNamespace, string[]>();
|
|
314
198
|
|
|
315
199
|
export const traverseModel = <T extends PersistentModel>(
|
|
@@ -323,12 +207,14 @@ export const traverseModel = <T extends PersistentModel>(
|
|
|
323
207
|
) => PersistentModelConstructor<any>
|
|
324
208
|
) => {
|
|
325
209
|
const relationships = namespace.relationships;
|
|
210
|
+
|
|
326
211
|
const modelConstructor = getModelConstructorByModelName(
|
|
327
212
|
namespace.name,
|
|
328
213
|
srcModelName
|
|
329
214
|
);
|
|
330
215
|
|
|
331
216
|
const relation = relationships[srcModelName];
|
|
217
|
+
|
|
332
218
|
const result: {
|
|
333
219
|
modelName: string;
|
|
334
220
|
item: T;
|
|
@@ -362,15 +248,36 @@ export const traverseModel = <T extends PersistentModel>(
|
|
|
362
248
|
instance: modelInstance,
|
|
363
249
|
});
|
|
364
250
|
|
|
365
|
-
|
|
251
|
+
const targetNames: string[] | undefined =
|
|
252
|
+
extractTargetNamesFromSrc(rItem);
|
|
253
|
+
|
|
254
|
+
// `targetName` will be defined for Has One if feature flag
|
|
366
255
|
// https://docs.amplify.aws/cli/reference/feature-flags/#useAppsyncModelgenPlugin
|
|
367
256
|
// is true (default as of 5/7/21)
|
|
368
257
|
// Making this conditional for backward-compatibility
|
|
369
|
-
if (
|
|
370
|
-
(
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
258
|
+
if (targetNames) {
|
|
259
|
+
targetNames.forEach((targetName, idx) => {
|
|
260
|
+
// Get the connected record
|
|
261
|
+
const relatedRecordInProxy = <PersistentModel>(
|
|
262
|
+
draftInstance[rItem.fieldName]
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
// Previously, we used the hardcoded 'id' as they key,
|
|
266
|
+
// now we need the value of the key to get the PK (and SK)
|
|
267
|
+
// values from the related record
|
|
268
|
+
|
|
269
|
+
const { primaryKey } = namespace.keys[modelConstructor.name];
|
|
270
|
+
const keyField = primaryKey && primaryKey[idx];
|
|
271
|
+
|
|
272
|
+
// Get the value
|
|
273
|
+
const relatedRecordInProxyPkValue =
|
|
274
|
+
relatedRecordInProxy[keyField];
|
|
275
|
+
|
|
276
|
+
// Set the targetName value
|
|
277
|
+
(<any>draftInstance)[targetName] = relatedRecordInProxyPkValue;
|
|
278
|
+
});
|
|
279
|
+
// Delete the instance from the proxy
|
|
280
|
+
delete (<any>draftInstance)[rItem.fieldName];
|
|
374
281
|
} else {
|
|
375
282
|
(<any>draftInstance)[rItem.fieldName] = (<PersistentModel>(
|
|
376
283
|
draftInstance[rItem.fieldName]
|
|
@@ -405,10 +312,33 @@ export const traverseModel = <T extends PersistentModel>(
|
|
|
405
312
|
}
|
|
406
313
|
|
|
407
314
|
if (draftInstance[rItem.fieldName]) {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
315
|
+
const targetNames: string[] | undefined =
|
|
316
|
+
extractTargetNamesFromSrc(rItem);
|
|
317
|
+
|
|
318
|
+
if (targetNames) {
|
|
319
|
+
targetNames.forEach((targetName, idx) => {
|
|
320
|
+
// Get the connected record
|
|
321
|
+
const relatedRecordInProxy = <PersistentModel>(
|
|
322
|
+
draftInstance[rItem.fieldName]
|
|
323
|
+
);
|
|
324
|
+
// Previously, we used the hardcoded `id` for the key.
|
|
325
|
+
// Now, we need the value of the key to get the PK (and SK)
|
|
326
|
+
// values from the related record
|
|
327
|
+
const { primaryKey } = namespace.keys[modelConstructor.name];
|
|
328
|
+
|
|
329
|
+
// fall back to ID if
|
|
330
|
+
const keyField = primaryKey && primaryKey[idx];
|
|
331
|
+
|
|
332
|
+
// Get the value
|
|
333
|
+
const relatedRecordInProxyPkValue =
|
|
334
|
+
relatedRecordInProxy[keyField];
|
|
335
|
+
|
|
336
|
+
// Set the targetName value
|
|
337
|
+
(<any>draftInstance)[targetName] = relatedRecordInProxyPkValue;
|
|
338
|
+
});
|
|
339
|
+
// Delete the instance from the proxy
|
|
340
|
+
delete (<any>draftInstance)[rItem.fieldName];
|
|
341
|
+
}
|
|
412
342
|
}
|
|
413
343
|
|
|
414
344
|
break;
|
|
@@ -446,24 +376,6 @@ export const traverseModel = <T extends PersistentModel>(
|
|
|
446
376
|
return result;
|
|
447
377
|
};
|
|
448
378
|
|
|
449
|
-
export const getIndex = (rel: RelationType[], src: string): string => {
|
|
450
|
-
let index = '';
|
|
451
|
-
rel.some((relItem: RelationType) => {
|
|
452
|
-
if (relItem.modelName === src) {
|
|
453
|
-
index = relItem.targetName;
|
|
454
|
-
}
|
|
455
|
-
});
|
|
456
|
-
return index;
|
|
457
|
-
};
|
|
458
|
-
|
|
459
|
-
export const getIndexFromAssociation = (
|
|
460
|
-
indexes: string[],
|
|
461
|
-
src: string
|
|
462
|
-
): string => {
|
|
463
|
-
const index = indexes.find(idx => idx === src);
|
|
464
|
-
return index;
|
|
465
|
-
};
|
|
466
|
-
|
|
467
379
|
let privateModeCheckResult;
|
|
468
380
|
|
|
469
381
|
export const isPrivateMode = () => {
|
|
@@ -505,6 +417,96 @@ export const isPrivateMode = () => {
|
|
|
505
417
|
});
|
|
506
418
|
};
|
|
507
419
|
|
|
420
|
+
let safariCompatabilityModeResult;
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Whether the browser's implementation of IndexedDB breaks on array lookups
|
|
424
|
+
* against composite indexes whose keypath contains a single column.
|
|
425
|
+
*
|
|
426
|
+
* E.g., Whether `store.createIndex(indexName, ['id'])` followed by
|
|
427
|
+
* `store.index(indexName).get([1])` will *ever* return records.
|
|
428
|
+
*
|
|
429
|
+
* In all known, modern Safari browsers as of Q4 2022, the query against an index like
|
|
430
|
+
* this will *always* return `undefined`. So, the index needs to be created as a scalar.
|
|
431
|
+
*/
|
|
432
|
+
export const isSafariCompatabilityMode: () => Promise<boolean> = async () => {
|
|
433
|
+
try {
|
|
434
|
+
const dbName = uuid();
|
|
435
|
+
const storeName = 'indexedDBFeatureProbeStore';
|
|
436
|
+
const indexName = 'idx';
|
|
437
|
+
|
|
438
|
+
if (indexedDB === null) return false;
|
|
439
|
+
|
|
440
|
+
if (safariCompatabilityModeResult !== undefined) {
|
|
441
|
+
return safariCompatabilityModeResult;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const db: IDBDatabase | false = await new Promise(resolve => {
|
|
445
|
+
const dbOpenRequest = indexedDB.open(dbName);
|
|
446
|
+
dbOpenRequest.onerror = () => resolve(false);
|
|
447
|
+
|
|
448
|
+
dbOpenRequest.onsuccess = () => {
|
|
449
|
+
const db = dbOpenRequest.result;
|
|
450
|
+
resolve(db);
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
dbOpenRequest.onupgradeneeded = (event: any) => {
|
|
454
|
+
const db = event?.target?.result;
|
|
455
|
+
|
|
456
|
+
db.onerror = () => resolve(false);
|
|
457
|
+
|
|
458
|
+
const store = db.createObjectStore(storeName, {
|
|
459
|
+
autoIncrement: true,
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
store.createIndex(indexName, ['id']);
|
|
463
|
+
};
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
if (!db) {
|
|
467
|
+
throw new Error('Could not open probe DB');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const rwTx = db.transaction(storeName, 'readwrite');
|
|
471
|
+
const rwStore = rwTx.objectStore(storeName);
|
|
472
|
+
rwStore.add({
|
|
473
|
+
id: 1,
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
(rwTx as any).commit();
|
|
477
|
+
|
|
478
|
+
const result = await new Promise(resolve => {
|
|
479
|
+
const tx = db.transaction(storeName, 'readonly');
|
|
480
|
+
const store = tx.objectStore(storeName);
|
|
481
|
+
const index = store.index(indexName);
|
|
482
|
+
|
|
483
|
+
const getRequest = index.get([1]);
|
|
484
|
+
|
|
485
|
+
getRequest.onerror = () => resolve(false);
|
|
486
|
+
|
|
487
|
+
getRequest.onsuccess = (event: any) => {
|
|
488
|
+
resolve(event?.target?.result);
|
|
489
|
+
};
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
if (db && typeof db.close === 'function') {
|
|
493
|
+
await db.close();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
await indexedDB.deleteDatabase(dbName);
|
|
497
|
+
|
|
498
|
+
if (result === undefined) {
|
|
499
|
+
safariCompatabilityModeResult = true;
|
|
500
|
+
} else {
|
|
501
|
+
safariCompatabilityModeResult = false;
|
|
502
|
+
}
|
|
503
|
+
} catch (error) {
|
|
504
|
+
safariCompatabilityModeResult = false;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return safariCompatabilityModeResult;
|
|
508
|
+
};
|
|
509
|
+
|
|
508
510
|
const randomBytes = (nBytes: number): Buffer => {
|
|
509
511
|
return Buffer.from(new WordArray().random(nBytes).toString(), 'hex');
|
|
510
512
|
};
|
|
@@ -811,3 +813,330 @@ export function mergePatches<T>(
|
|
|
811
813
|
);
|
|
812
814
|
return patches;
|
|
813
815
|
}
|
|
816
|
+
|
|
817
|
+
export const getStorename = (namespace: string, modelName: string) => {
|
|
818
|
+
const storeName = `${namespace}_${modelName}`;
|
|
819
|
+
|
|
820
|
+
return storeName;
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
//#region Key Utils
|
|
824
|
+
|
|
825
|
+
/*
|
|
826
|
+
When we have GSI(s) with composite sort keys defined on a model
|
|
827
|
+
There are some very particular rules regarding which fields must be included in the update mutation input
|
|
828
|
+
The field selection becomes more complex as the number of GSIs with composite sort keys grows
|
|
829
|
+
|
|
830
|
+
To summarize: any time we update a field that is part of the composite sort key of a GSI, we must include:
|
|
831
|
+
1. all of the other fields in that composite sort key
|
|
832
|
+
2. all of the fields from any other composite sort key that intersect with the fields from 1.
|
|
833
|
+
|
|
834
|
+
E.g.,
|
|
835
|
+
Model @model
|
|
836
|
+
@key(name: 'key1' fields: ['hk', 'a', 'b', 'c'])
|
|
837
|
+
@key(name: 'key2' fields: ['hk', 'a', 'b', 'd'])
|
|
838
|
+
@key(name: 'key3' fields: ['hk', 'x', 'y', 'z'])
|
|
839
|
+
|
|
840
|
+
Model.a is updated => include ['a', 'b', 'c', 'd']
|
|
841
|
+
Model.c is updated => include ['a', 'b', 'c', 'd']
|
|
842
|
+
Model.d is updated => include ['a', 'b', 'c', 'd']
|
|
843
|
+
Model.x is updated => include ['x', 'y', 'z']
|
|
844
|
+
|
|
845
|
+
This function accepts a model's attributes and returns grouped sets of composite key fields
|
|
846
|
+
Using our example Model above, the function will return:
|
|
847
|
+
[
|
|
848
|
+
Set('a', 'b', 'c', 'd'),
|
|
849
|
+
Set('x', 'y', 'z'),
|
|
850
|
+
]
|
|
851
|
+
|
|
852
|
+
This gives us the opportunity to correctly include the required fields for composite keys
|
|
853
|
+
When crafting the mutation input in Storage.getUpdateMutationInput
|
|
854
|
+
|
|
855
|
+
See 'processCompositeKeys' test in util.test.ts for more examples
|
|
856
|
+
*/
|
|
857
|
+
export const processCompositeKeys = (
|
|
858
|
+
attributes: ModelAttributes
|
|
859
|
+
): Set<string>[] => {
|
|
860
|
+
const extractCompositeSortKey = ({
|
|
861
|
+
properties: {
|
|
862
|
+
// ignore the HK (fields[0]) we only need to include the composite sort key fields[1...n]
|
|
863
|
+
fields: [, ...sortKeyFields],
|
|
864
|
+
},
|
|
865
|
+
}) => sortKeyFields;
|
|
866
|
+
|
|
867
|
+
const compositeKeyFields = attributes
|
|
868
|
+
.filter(isModelAttributeCompositeKey)
|
|
869
|
+
.map(extractCompositeSortKey);
|
|
870
|
+
|
|
871
|
+
/*
|
|
872
|
+
if 2 sets of fields have any intersecting fields => combine them into 1 union set
|
|
873
|
+
e.g., ['a', 'b', 'c'] and ['a', 'b', 'd'] => ['a', 'b', 'c', 'd']
|
|
874
|
+
*/
|
|
875
|
+
const combineIntersecting = (fields): Set<string>[] =>
|
|
876
|
+
fields.reduce((combined, sortKeyFields) => {
|
|
877
|
+
const sortKeyFieldsSet = new Set(sortKeyFields);
|
|
878
|
+
|
|
879
|
+
if (combined.length === 0) {
|
|
880
|
+
combined.push(sortKeyFieldsSet);
|
|
881
|
+
return combined;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// does the current set share values with another set we've already added to `combined`?
|
|
885
|
+
const intersectingSetIdx = combined.findIndex(existingSet => {
|
|
886
|
+
return [...existingSet].some(f => sortKeyFieldsSet.has(f));
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
if (intersectingSetIdx > -1) {
|
|
890
|
+
const union = new Set([
|
|
891
|
+
...combined[intersectingSetIdx],
|
|
892
|
+
...sortKeyFieldsSet,
|
|
893
|
+
]);
|
|
894
|
+
// combine the current set with the intersecting set we found above
|
|
895
|
+
combined[intersectingSetIdx] = union;
|
|
896
|
+
} else {
|
|
897
|
+
// none of the sets in `combined` have intersecting values with the current set
|
|
898
|
+
combined.push(sortKeyFieldsSet);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return combined;
|
|
902
|
+
}, []);
|
|
903
|
+
|
|
904
|
+
const initial = combineIntersecting(compositeKeyFields);
|
|
905
|
+
// a single pass pay not be enough to correctly combine all the fields
|
|
906
|
+
// call the function once more to get a final merged list of sets
|
|
907
|
+
const combined = combineIntersecting(initial);
|
|
908
|
+
|
|
909
|
+
return combined;
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
export const extractKeyIfExists = (
|
|
913
|
+
modelDefinition: SchemaModel
|
|
914
|
+
): ModelAttribute | undefined => {
|
|
915
|
+
const keyAttribute = modelDefinition?.attributes?.find(isModelAttributeKey);
|
|
916
|
+
|
|
917
|
+
return keyAttribute;
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
export const extractPrimaryKeyFieldNames = (
|
|
921
|
+
modelDefinition: SchemaModel
|
|
922
|
+
): string[] => {
|
|
923
|
+
const keyAttribute = extractKeyIfExists(modelDefinition);
|
|
924
|
+
if (keyAttribute && isModelAttributePrimaryKey(keyAttribute)) {
|
|
925
|
+
return keyAttribute.properties.fields;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
return [ID];
|
|
929
|
+
};
|
|
930
|
+
|
|
931
|
+
export const extractPrimaryKeyValues = <T extends PersistentModel>(
|
|
932
|
+
model: T,
|
|
933
|
+
keyFields: string[]
|
|
934
|
+
): string[] => {
|
|
935
|
+
return keyFields.map(key => model[key]);
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
export const extractPrimaryKeysAndValues = <T extends PersistentModel>(
|
|
939
|
+
model: T,
|
|
940
|
+
keyFields: string[]
|
|
941
|
+
): any => {
|
|
942
|
+
const primaryKeysAndValues = {};
|
|
943
|
+
keyFields.forEach(key => (primaryKeysAndValues[key] = model[key]));
|
|
944
|
+
return primaryKeysAndValues;
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
// IdentifierFields<ManagedIdentifier>
|
|
948
|
+
// Default behavior without explicit @primaryKey defined
|
|
949
|
+
export const isIdManaged = (modelDefinition: SchemaModel): boolean => {
|
|
950
|
+
const keyAttribute = extractKeyIfExists(modelDefinition);
|
|
951
|
+
|
|
952
|
+
if (keyAttribute && isModelAttributePrimaryKey(keyAttribute)) {
|
|
953
|
+
return false;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
return true;
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
// IdentifierFields<OptionallyManagedIdentifier>
|
|
960
|
+
// @primaryKey with explicit `id` in the PK. Single key or composite
|
|
961
|
+
export const isIdOptionallyManaged = (
|
|
962
|
+
modelDefinition: SchemaModel
|
|
963
|
+
): boolean => {
|
|
964
|
+
const keyAttribute = extractKeyIfExists(modelDefinition);
|
|
965
|
+
|
|
966
|
+
if (keyAttribute && isModelAttributePrimaryKey(keyAttribute)) {
|
|
967
|
+
return keyAttribute.properties.fields[0] === ID;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
return false;
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
export const establishRelationAndKeys = (
|
|
974
|
+
namespace: SchemaNamespace
|
|
975
|
+
): [RelationshipType, ModelKeys] => {
|
|
976
|
+
const relationship: RelationshipType = {};
|
|
977
|
+
const keys: ModelKeys = {};
|
|
978
|
+
|
|
979
|
+
Object.keys(namespace.models).forEach((mKey: string) => {
|
|
980
|
+
relationship[mKey] = { indexes: [], relationTypes: [] };
|
|
981
|
+
keys[mKey] = {};
|
|
982
|
+
|
|
983
|
+
const model = namespace.models[mKey];
|
|
984
|
+
Object.keys(model.fields).forEach((attr: string) => {
|
|
985
|
+
const fieldAttribute = model.fields[attr];
|
|
986
|
+
if (
|
|
987
|
+
typeof fieldAttribute.type === 'object' &&
|
|
988
|
+
'model' in fieldAttribute.type
|
|
989
|
+
) {
|
|
990
|
+
const connectionType = fieldAttribute.association.connectionType;
|
|
991
|
+
relationship[mKey].relationTypes.push({
|
|
992
|
+
fieldName: fieldAttribute.name,
|
|
993
|
+
modelName: fieldAttribute.type.model,
|
|
994
|
+
relationType: connectionType,
|
|
995
|
+
targetName: fieldAttribute.association['targetName'],
|
|
996
|
+
targetNames: fieldAttribute.association['targetNames'],
|
|
997
|
+
associatedWith: fieldAttribute.association['associatedWith'],
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
if (connectionType === 'BELONGS_TO') {
|
|
1001
|
+
const targetNames = extractTargetNamesFromSrc(
|
|
1002
|
+
fieldAttribute.association
|
|
1003
|
+
);
|
|
1004
|
+
|
|
1005
|
+
if (targetNames) {
|
|
1006
|
+
const idxName = indexNameFromKeys(targetNames);
|
|
1007
|
+
relationship[mKey].indexes.push([idxName, targetNames]);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
if (model.attributes) {
|
|
1014
|
+
keys[mKey].compositeKeys = processCompositeKeys(model.attributes);
|
|
1015
|
+
|
|
1016
|
+
for (const attribute of model.attributes) {
|
|
1017
|
+
if (!isModelAttributeKey(attribute)) {
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const { fields } = attribute.properties;
|
|
1022
|
+
|
|
1023
|
+
if (isModelAttributePrimaryKey(attribute)) {
|
|
1024
|
+
keys[mKey].primaryKey = fields;
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// create indexes for all other keys
|
|
1029
|
+
const idxName = indexNameFromKeys(fields);
|
|
1030
|
+
const idxExists = relationship[mKey].indexes.find(
|
|
1031
|
+
([index]) => index === idxName
|
|
1032
|
+
);
|
|
1033
|
+
|
|
1034
|
+
if (!idxExists) {
|
|
1035
|
+
relationship[mKey].indexes.push([idxName, fields]);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// set 'id' as the PK for models without a custom PK explicitly defined
|
|
1041
|
+
if (!keys[mKey].primaryKey) {
|
|
1042
|
+
keys[mKey].primaryKey = [ID];
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// create primary index
|
|
1046
|
+
relationship[mKey].indexes.push([
|
|
1047
|
+
'byPk',
|
|
1048
|
+
keys[mKey].primaryKey as string[],
|
|
1049
|
+
{ unique: true },
|
|
1050
|
+
]);
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
return [relationship, keys];
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
export const getIndex = (
|
|
1057
|
+
rel: RelationType[],
|
|
1058
|
+
src: string
|
|
1059
|
+
): string | undefined => {
|
|
1060
|
+
let indexName;
|
|
1061
|
+
rel.some((relItem: RelationType) => {
|
|
1062
|
+
if (relItem.modelName === src) {
|
|
1063
|
+
const targetNames = extractTargetNamesFromSrc(relItem);
|
|
1064
|
+
indexName = targetNames && indexNameFromKeys(targetNames);
|
|
1065
|
+
return true;
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
return indexName;
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
export const getIndexFromAssociation = (
|
|
1072
|
+
indexes: IndexesType,
|
|
1073
|
+
src: string | string[]
|
|
1074
|
+
): string | undefined => {
|
|
1075
|
+
let indexName: string;
|
|
1076
|
+
|
|
1077
|
+
if (Array.isArray(src)) {
|
|
1078
|
+
indexName = indexNameFromKeys(src);
|
|
1079
|
+
} else {
|
|
1080
|
+
indexName = src;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const associationIndex = indexes.find(([idxName]) => idxName === indexName);
|
|
1084
|
+
return associationIndex && associationIndex[0];
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Backwards-compatability for schema generated prior to custom primary key support:
|
|
1089
|
+
the single field `targetName` has been replaced with an array of `targetNames`.
|
|
1090
|
+
`targetName` and `targetNames` are exclusive (will never exist on the same schema)
|
|
1091
|
+
* @param src {RelationType | ModelAssociation | undefined}
|
|
1092
|
+
* @returns array of targetNames, or `undefined`
|
|
1093
|
+
*/
|
|
1094
|
+
export const extractTargetNamesFromSrc = (
|
|
1095
|
+
src: RelationType | ModelAssociation | undefined
|
|
1096
|
+
): string[] | undefined => {
|
|
1097
|
+
const targetName = src?.targetName;
|
|
1098
|
+
const targetNames = src?.targetNames;
|
|
1099
|
+
|
|
1100
|
+
if (Array.isArray(targetNames)) {
|
|
1101
|
+
return targetNames;
|
|
1102
|
+
} else if (typeof targetName === 'string') {
|
|
1103
|
+
return [targetName];
|
|
1104
|
+
} else {
|
|
1105
|
+
return undefined;
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
// Generates spinal-cased index name from an array of key field names
|
|
1110
|
+
// E.g. for keys `[id, title]` => 'id-title'
|
|
1111
|
+
export const indexNameFromKeys = (keys: string[]): string => {
|
|
1112
|
+
return keys.reduce((prev: string, cur: string, idx: number) => {
|
|
1113
|
+
if (idx === 0) {
|
|
1114
|
+
return cur;
|
|
1115
|
+
}
|
|
1116
|
+
return `${prev}${IDENTIFIER_KEY_SEPARATOR}${cur}`;
|
|
1117
|
+
}, '');
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
export const keysEqual = (keysA, keysB): boolean => {
|
|
1121
|
+
if (keysA.length !== keysB.length) {
|
|
1122
|
+
return false;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
return keysA.every((key, idx) => key === keysB[idx]);
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
// Returns primary keys for a model
|
|
1129
|
+
export const getIndexKeys = (
|
|
1130
|
+
namespace: SchemaNamespace,
|
|
1131
|
+
modelName: string
|
|
1132
|
+
): string[] => {
|
|
1133
|
+
const keyPath = namespace?.keys[modelName]?.primaryKey;
|
|
1134
|
+
|
|
1135
|
+
if (keyPath) {
|
|
1136
|
+
return keyPath;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
return [ID];
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
//#endregion
|