electrodb 2.9.3 → 2.10.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/src/clauses.js CHANGED
@@ -1,4 +1,6 @@
1
- const { QueryTypes, MethodTypes, ItemOperations, ExpressionTypes, TransactionCommitSymbol, TransactionOperations, TerminalOperation, KeyTypes, IndexTypes } = require("./types");
1
+ const { QueryTypes, MethodTypes, ItemOperations, ExpressionTypes, TransactionCommitSymbol, TransactionOperations, TerminalOperation, KeyTypes, IndexTypes,
2
+ UpsertOperations
3
+ } = require("./types");
2
4
  const {AttributeOperationProxy, UpdateOperations, FilterOperationNames, UpdateOperationNames} = require("./operations");
3
5
  const {UpdateExpression} = require("./update");
4
6
  const {FilterExpression} = require("./where");
@@ -6,6 +8,13 @@ const v = require("./validations");
6
8
  const e = require("./errors");
7
9
  const u = require("./util");
8
10
 
11
+ const methodChildren = {
12
+ upsert: ["upsertSet", "upsertAppend", "upsertAdd", "go", "params", "upsertSubtract", "commit", "upsertIfNotExists", "where"],
13
+ update: ["data", "set", "append", "add", "updateRemove", "updateDelete", "go", "params", "subtract", "commit", "composite", "ifNotExists", "where"],
14
+ put: ["where", "params", "go", "commit"],
15
+ del: ["where", "params", "go", "commit"],
16
+ }
17
+
9
18
  function batchAction(action, type, entity, state, payload) {
10
19
  if (state.getError() !== null) {
11
20
  return state;
@@ -236,7 +245,7 @@ let clauses = {
236
245
  return state;
237
246
  }
238
247
  },
239
- children: ["where", "params", "go", "commit"],
248
+ children: methodChildren.del,
240
249
  },
241
250
  upsert: {
242
251
  name: 'upsert',
@@ -245,33 +254,115 @@ let clauses = {
245
254
  return state;
246
255
  }
247
256
  try {
248
- let record = entity.model.schema.checkCreate({...payload});
249
- const attributes = state.getCompositeAttributes();
250
- const pkComposite = entity._expectFacets(record, attributes.pk);
251
- state.addOption('_includeOnResponseItem', pkComposite);
257
+
252
258
  return state
253
259
  .setMethod(MethodTypes.upsert)
254
260
  .setType(QueryTypes.eq)
255
- .applyUpsert(record)
256
- .setPK(pkComposite)
257
- .ifSK(() => {
258
- entity._expectFacets(record, attributes.sk);
259
- const skComposite = entity._buildQueryFacets(record, attributes.sk);
260
- state.setSK(skComposite);
261
- state.addOption('_includeOnResponseItem', {...skComposite, ...pkComposite});
262
- })
263
- .whenOptions(({ state, options }) => {
264
- if (!state.getParams()) {
265
- state.query.update.set(entity.identifiers.entity, entity.getName());
266
- state.query.update.set(entity.identifiers.version, entity.getVersion());
261
+ .applyUpsert(UpsertOperations.set, payload)
262
+ .beforeBuildParams(({ state }) => {
263
+ const { upsert, update, updateProxy } = state.query;
264
+
265
+ state.query.update.set(entity.identifiers.entity, entity.getName());
266
+ state.query.update.set(entity.identifiers.version, entity.getVersion());
267
+
268
+ // only "set" data is used to make keys
269
+ const setData = {};
270
+ const nonSetData = {};
271
+ let allData = {};
272
+
273
+ for (const name in upsert.data) {
274
+ const { operation, value } = upsert.data[name];
275
+ allData[name] = value;
276
+ if (operation === UpsertOperations.set) {
277
+ setData[name] = value;
278
+ } else {
279
+ nonSetData[name] = value;
280
+ }
267
281
  }
282
+
283
+ const upsertData = entity.model.schema.checkCreate({ ...allData });
284
+ const attributes = state.getCompositeAttributes();
285
+ const pkComposite = entity._expectFacets(upsertData, attributes.pk);
286
+
287
+ state
288
+ .addOption('_includeOnResponseItem', pkComposite)
289
+ .setPK(pkComposite)
290
+ .ifSK(() => {
291
+ entity._expectFacets(upsertData, attributes.sk);
292
+ const skComposite = entity._buildQueryFacets(upsertData, attributes.sk);
293
+ state.setSK(skComposite);
294
+ state.addOption('_includeOnResponseItem', {...skComposite, ...pkComposite});
295
+ });
296
+
297
+ const appliedData = entity.model.schema.applyAttributeSetters({...upsertData});
298
+
299
+ const onlySetAppliedData = {};
300
+ const nonSetAppliedData = {};
301
+ for (const name in appliedData) {
302
+ const value = appliedData[name];
303
+ const isSetOperation = setData[name] !== undefined;
304
+ const cameFromApplyingSetters = allData[name] === undefined;
305
+ const isNotUndefined = appliedData[name] !== undefined;
306
+ const applyAsSet = isSetOperation || cameFromApplyingSetters;
307
+ if (applyAsSet && isNotUndefined) {
308
+ onlySetAppliedData[name] = value;
309
+ } else {
310
+ nonSetAppliedData[name] = value;
311
+ }
312
+ }
313
+
314
+ // we build this above, and set them to state, but use it here, not ideal but
315
+ // the way it worked out so that this could be wrapped in beforeBuildParams
316
+ const { pk } = state.query.keys;
317
+ const sk = state.query.keys.sk[0];
318
+
319
+ const { updatedKeys, setAttributes, indexKey } = entity._getPutKeys(pk, sk && sk.facets, onlySetAppliedData);
320
+
321
+ // calculated here but needs to be used when building the params
322
+ upsert.indexKey = indexKey;
323
+
324
+ // only "set" data is used to make keys
325
+ const setFields = Object.entries(entity.model.schema.translateToFields(setAttributes));
326
+
327
+ // add the keys impacted except for the table index keys; they are upserted
328
+ // automatically by dynamo
329
+ for (const key in updatedKeys) {
330
+ const value = updatedKeys[key];
331
+ if (indexKey[key] === undefined) {
332
+ setFields.push([key, value]);
333
+ }
334
+ }
335
+
336
+ entity._maybeApplyUpsertUpdate({
337
+ fields: setFields,
338
+ operation: UpsertOperations.set,
339
+ updateProxy,
340
+ update,
341
+ });
342
+
343
+ for (const name in nonSetData) {
344
+ const value = appliedData[name];
345
+ if (value === undefined || upsert.data[name] === undefined) {
346
+ continue;
347
+ }
348
+
349
+ const { operation } = upsert.data[name];
350
+ const fields = entity.model.schema.translateToFields({ [name]: value });
351
+ entity._maybeApplyUpsertUpdate({
352
+ fields: Object.entries(fields),
353
+ updateProxy,
354
+ operation,
355
+ update,
356
+ });
357
+ }
358
+
268
359
  });
269
360
  } catch(err) {
270
361
  state.setError(err);
271
362
  return state;
272
363
  }
273
364
  },
274
- children: ["params", "go", "where", "commit"],
365
+ children: methodChildren.upsert,
275
366
  },
276
367
  put: {
277
368
  name: "put",
@@ -297,7 +388,7 @@ let clauses = {
297
388
  return state;
298
389
  }
299
390
  },
300
- children: ["params", "go", "commit"],
391
+ children: methodChildren.put,
301
392
  },
302
393
  batchPut: {
303
394
  name: "batchPut",
@@ -333,7 +424,7 @@ let clauses = {
333
424
  return state;
334
425
  }
335
426
  },
336
- children: ["params", "go", "commit"],
427
+ children: methodChildren.put,
337
428
  },
338
429
  patch: {
339
430
  name: "patch",
@@ -366,7 +457,7 @@ let clauses = {
366
457
  return state;
367
458
  }
368
459
  },
369
- children: ["set", "append","updateRemove", "updateDelete", "add", "subtract", "data", "composite", "commit"],
460
+ children: methodChildren.update,
370
461
  },
371
462
  update: {
372
463
  name: "update",
@@ -393,7 +484,7 @@ let clauses = {
393
484
  return state;
394
485
  }
395
486
  },
396
- children: ["data", "set", "append", "add", "updateRemove", "updateDelete", "go", "params", "subtract", "commit", "composite"],
487
+ children: methodChildren.update,
397
488
  },
398
489
  data: {
399
490
  name: "data",
@@ -423,7 +514,7 @@ let clauses = {
423
514
  return state;
424
515
  }
425
516
  },
426
- children: ["data", "set", "append", "add", "updateRemove", "updateDelete", "go", "params", "subtract", "commit", "composite"],
517
+ children: methodChildren.update,
427
518
  },
428
519
  set: {
429
520
  name: "set",
@@ -440,7 +531,24 @@ let clauses = {
440
531
  return state;
441
532
  }
442
533
  },
443
- children: ["data", "set", "append", "add", "updateRemove", "updateDelete", "go", "params", "subtract", "commit", "composite"],
534
+ children: methodChildren.update,
535
+ },
536
+ upsertSet: {
537
+ name: "set",
538
+ action(entity, state, data) {
539
+ if (state.getError() !== null) {
540
+ return state;
541
+ }
542
+ try {
543
+ entity.model.schema.checkUpdate(data, { allowReadOnly: true });
544
+ state.query.upsert.addData(UpsertOperations.set, data);
545
+ return state;
546
+ } catch(err) {
547
+ state.setError(err);
548
+ return state;
549
+ }
550
+ },
551
+ children: methodChildren.upsert,
444
552
  },
445
553
  composite: {
446
554
  name: "composite",
@@ -465,7 +573,7 @@ let clauses = {
465
573
  return state;
466
574
  }
467
575
  },
468
- children: ["data", "set", "append", "add", "updateRemove", "updateDelete", "go", "params", "subtract", "commit", "composite"],
576
+ children: methodChildren.update,
469
577
  },
470
578
  append: {
471
579
  name: "append",
@@ -482,7 +590,50 @@ let clauses = {
482
590
  return state;
483
591
  }
484
592
  },
485
- children: ["data", "set", "append", "add", "updateRemove", "updateDelete", "go", "params", "subtract", "commit", "composite"],
593
+ children: methodChildren.update,
594
+ },
595
+ ifNotExists: {
596
+ name: 'ifNotExists',
597
+ action(entity, state, data = {}) {
598
+ entity.model.schema.checkUpdate(data);
599
+ state.query.updateProxy.fromObject(ItemOperations.ifNotExists, data);
600
+ return state;
601
+ },
602
+ children: methodChildren.update,
603
+ },
604
+ upsertIfNotExists: {
605
+ name: 'ifNotExists',
606
+ action(entity, state, data = {}) {
607
+ if (state.getError() !== null) {
608
+ return state;
609
+ }
610
+ try {
611
+ entity.model.schema.checkUpdate(data, { allowReadOnly: true });
612
+ state.query.upsert.addData(UpsertOperations.ifNotExists, data);
613
+ return state;
614
+ } catch(err) {
615
+ state.setError(err);
616
+ return state;
617
+ }
618
+ },
619
+ children: methodChildren.upsert,
620
+ },
621
+ upsertAppend: {
622
+ name: "append",
623
+ action(entity, state, data = {}) {
624
+ if (state.getError() !== null) {
625
+ return state;
626
+ }
627
+ try {
628
+ entity.model.schema.checkUpdate(data, { allowReadOnly: true });
629
+ state.query.upsert.addData(UpsertOperations.append, data);
630
+ return state;
631
+ } catch(err) {
632
+ state.setError(err);
633
+ return state;
634
+ }
635
+ },
636
+ children: methodChildren.upsert,
486
637
  },
487
638
  updateRemove: {
488
639
  name: "remove",
@@ -502,7 +653,7 @@ let clauses = {
502
653
  return state;
503
654
  }
504
655
  },
505
- children: ["data", "set", "append", "add", "updateRemove", "updateDelete", "go", "params", "subtract", "commit", "composite"],
656
+ children: methodChildren.update
506
657
  },
507
658
  updateDelete: {
508
659
  name: "delete",
@@ -519,7 +670,7 @@ let clauses = {
519
670
  return state;
520
671
  }
521
672
  },
522
- children: ["data", "set", "append", "add", "updateRemove", "updateDelete", "go", "params", "subtract", "commit", "composite"],
673
+ children: methodChildren.update
523
674
  },
524
675
  add: {
525
676
  name: "add",
@@ -536,7 +687,41 @@ let clauses = {
536
687
  return state;
537
688
  }
538
689
  },
539
- children: ["data", "set", "append", "add", "updateRemove", "updateDelete", "go", "params", "subtract", "commit", "composite"],
690
+ children: methodChildren.update
691
+ },
692
+ upsertAdd: {
693
+ name: "add",
694
+ action(entity, state, data) {
695
+ if (state.getError() !== null) {
696
+ return state;
697
+ }
698
+ try {
699
+ entity.model.schema.checkUpdate(data, { allowReadOnly: true });
700
+ state.query.upsert.addData(UpsertOperations.add, data);
701
+ return state;
702
+ } catch(err) {
703
+ state.setError(err);
704
+ return state;
705
+ }
706
+ },
707
+ children: methodChildren.upsert
708
+ },
709
+ upsertSubtract: {
710
+ name: "subtract",
711
+ action(entity, state, data) {
712
+ if (state.getError() !== null) {
713
+ return state;
714
+ }
715
+ try {
716
+ entity.model.schema.checkUpdate(data, { allowReadOnly: true });
717
+ state.query.upsert.addData(UpsertOperations.subtract, data);
718
+ return state;
719
+ } catch(err) {
720
+ state.setError(err);
721
+ return state;
722
+ }
723
+ },
724
+ children: methodChildren.upsert
540
725
  },
541
726
  subtract: {
542
727
  name: "subtract",
@@ -553,7 +738,7 @@ let clauses = {
553
738
  return state;
554
739
  }
555
740
  },
556
- children: ["data", "set", "append", "add", "updateRemove", "updateDelete", "go", "params", "subtract", "commit", "composite"],
741
+ children: methodChildren.update
557
742
  },
558
743
  query: {
559
744
  name: "query",
@@ -767,6 +952,7 @@ let clauses = {
767
952
  });
768
953
 
769
954
  state.applyWithOptions(normalizedOptions);
955
+ state.applyBeforeBuildParams(normalizedOptions);
770
956
 
771
957
  let results;
772
958
  switch (method) {
@@ -794,15 +980,15 @@ let clauses = {
794
980
  delete results.ExpressionAttributeValues;
795
981
  }
796
982
 
797
- state.setParams(results);
798
-
799
983
  if (options._returnOptions) {
800
- return {
984
+ results = {
801
985
  params: results,
802
986
  options: normalizedOptions,
803
987
  }
804
988
  }
805
989
 
990
+ state.setParams(results);
991
+
806
992
  return results;
807
993
  } catch(err) {
808
994
  throw err;
@@ -854,7 +1040,27 @@ class ChainState {
854
1040
  },
855
1041
  upsert: {
856
1042
  data: {},
857
- ifNotExists: {},
1043
+ indexKey: null,
1044
+ addData(operation = UpsertOperations.set, data = {}) {
1045
+ for (const name of Object.keys(data)) {
1046
+ const value = data[name];
1047
+ this.data[name] = {
1048
+ operation,
1049
+ value,
1050
+ }
1051
+ }
1052
+ },
1053
+ getData(operationFilter) {
1054
+ const results = {};
1055
+ for (const name in this.data) {
1056
+ const { operation, value } = this.data[name];
1057
+ if (!operationFilter || operationFilter === operation) {
1058
+ results[name] = value;
1059
+ }
1060
+ }
1061
+
1062
+ return results;
1063
+ }
858
1064
  },
859
1065
  keys: {
860
1066
  provided: [],
@@ -868,11 +1074,13 @@ class ChainState {
868
1074
  options,
869
1075
  };
870
1076
  this.subStates = [];
871
- this.applyAfterOptions = [];
872
1077
  this.hasSortKey = hasSortKey;
873
1078
  this.prev = null;
874
1079
  this.self = null;
875
1080
  this.params = null;
1081
+ this.applyAfterOptions = [];
1082
+ this.beforeBuildParamsOperations = [];
1083
+ this.beforeBuildParamsHasRan = false;
876
1084
  }
877
1085
 
878
1086
  getParams() {
@@ -913,6 +1121,7 @@ class ChainState {
913
1121
 
914
1122
  addOption(key, value) {
915
1123
  this.query.options[key] = value;
1124
+ return this;
916
1125
  }
917
1126
 
918
1127
  _appendProvided(type, attributes) {
@@ -1075,12 +1284,8 @@ class ChainState {
1075
1284
  }
1076
1285
  }
1077
1286
 
1078
- applyUpsert(data = {}, { ifNotExists } = {}) {
1079
- if (ifNotExists) {
1080
- this.query.upsert.ifNotExists = {...this.query.upsert.ifNotExists, ...data};
1081
- } else {
1082
- this.query.upsert.data = {...this.query.upsert.data, ...data};
1083
- }
1287
+ applyUpsert(operation = UpsertOperations.set, data = {}) {
1288
+ this.query.upsert.addData(operation, data);
1084
1289
  return this;
1085
1290
  }
1086
1291
 
@@ -1100,6 +1305,21 @@ class ChainState {
1100
1305
  applyWithOptions(options = {}) {
1101
1306
  this.applyAfterOptions.forEach((fn) => fn(options));
1102
1307
  }
1308
+
1309
+ beforeBuildParams(fn) {
1310
+ if (v.isFunction(fn)) {
1311
+ this.beforeBuildParamsOperations.push((options) => {
1312
+ fn({ options, state: this });
1313
+ });
1314
+ }
1315
+ }
1316
+
1317
+ applyBeforeBuildParams(options = {}) {
1318
+ if (!this.beforeBuildParamsHasRan) {
1319
+ this.beforeBuildParamsHasRan = true;
1320
+ this.beforeBuildParamsOperations.forEach((fn) => fn(options));
1321
+ }
1322
+ }
1103
1323
  }
1104
1324
 
1105
1325
  module.exports = {
package/src/entity.js CHANGED
@@ -27,6 +27,7 @@ const {
27
27
  MethodTypeTranslation,
28
28
  TransactionCommitSymbol,
29
29
  CastKeyOptions,
30
+ UpsertOperations,
30
31
  } = require("./types");
31
32
  const { FilterFactory } = require("./filters");
32
33
  const { FilterOperations } = require("./operations");
@@ -1581,7 +1582,7 @@ class Entity {
1581
1582
  }
1582
1583
  /* istanbul ignore next */
1583
1584
  _params(state, config = {}) {
1584
- const { keys = {}, method = "", put = {}, update = {}, filter = {}, upsert } = state.query;
1585
+ const { keys = {}, method = "", put = {}, update = {}, filter = {}, upsert, updateProxy } = state.query;
1585
1586
  let consolidatedQueryFacets = this._consolidateQueryFacets(keys.sk);
1586
1587
  let params = {};
1587
1588
  switch (method) {
@@ -1592,7 +1593,7 @@ class Entity {
1592
1593
  params = this._makeSimpleIndexParams(keys.pk, ...consolidatedQueryFacets);
1593
1594
  break;
1594
1595
  case MethodTypes.upsert:
1595
- params = this._makeUpsertParams({update, upsert}, keys.pk, ...keys.sk)
1596
+ params = this._makeUpsertParams({update, upsert, updateProxy}, keys.pk, ...keys.sk)
1596
1597
  break;
1597
1598
  case MethodTypes.put:
1598
1599
  case MethodTypes.create:
@@ -1957,7 +1958,8 @@ class Entity {
1957
1958
 
1958
1959
  /* istanbul ignore next */
1959
1960
  _makePutParams({ data } = {}, pk, sk) {
1960
- let { updatedKeys, setAttributes } = this._getPutKeys(pk, sk && sk.facets, data);
1961
+ let appliedData = this.model.schema.applyAttributeSetters(data);
1962
+ let { updatedKeys, setAttributes } = this._getPutKeys(pk, sk && sk.facets, appliedData);
1961
1963
  let translatedFields = this.model.schema.translateToFields(setAttributes);
1962
1964
  return {
1963
1965
  Item: {
@@ -1970,31 +1972,52 @@ class Entity {
1970
1972
  };
1971
1973
  }
1972
1974
 
1973
- _makeUpsertParams({ update, upsert } = {}, pk, sk) {
1974
- const { updatedKeys, setAttributes, indexKey } = this._getPutKeys(pk, sk && sk.facets, upsert.data);
1975
- const upsertAttributes = this.model.schema.translateToFields(setAttributes);
1976
- const keyNames = Object.keys(indexKey);
1977
- for (const field of [...Object.keys(upsertAttributes), ...Object.keys(updatedKeys)]) {
1978
- const value = u.getFirstDefined(upsertAttributes[field], updatedKeys[field]);
1979
- if (!keyNames.includes(field)) {
1980
- let operation = ItemOperations.set;
1981
- const name = this.model.schema.translationForRetrieval[field];
1982
- if (name) {
1983
- const attribute = this.model.schema.attributes[name];
1984
- if (this.model.schema.readOnlyAttributes.has(name) && (!attribute || !attribute.indexes || attribute.indexes.length === 0)) {
1985
- operation = ItemOperations.ifNotExists;
1986
- }
1975
+ _maybeApplyUpsertUpdate({fields = [], operation, updateProxy, update}) {
1976
+ for (let [field, value] of fields) {
1977
+ const name = this.model.schema.translationForRetrieval[field];
1978
+ if (name) {
1979
+ const attribute = this.model.schema.attributes[name];
1980
+ if (this.model.schema.readOnlyAttributes.has(name) && (!attribute || !attribute.indexes || attribute.indexes.length === 0)) {
1981
+ /*
1982
+ // this should be considered but is likely overkill at best and unexpected at worst.
1983
+ // It also is likely symbolic of a deeper issue. That said maybe it could be helpful
1984
+ // in the future? It is unclear, if this were added, whether this should get the
1985
+ // default value and then call the setter on the defaultValue. That would at least
1986
+ // make parity between upsert and a create (without including the attribute) and then
1987
+ // an "update"
1988
+
1989
+ const defaultValue = attribute.default();
1990
+ const valueIsNumber = typeof value === 'number';
1991
+ const resolvedDefaultValue = typeof defaultValue === 'number' ? defaultValue : 0;
1992
+ if (operation === UpsertOperations.subtract && valueIsNumber) {
1993
+ value = resolvedDefaultValue - value;
1994
+ } else if (operation === UpsertOperations.add && valueIsNumber) {
1995
+ value = resolvedDefaultValue + value;
1996
+ // }
1997
+ */
1998
+ update.set(field, value, ItemOperations.ifNotExists);
1999
+ } else {
2000
+ updateProxy.performOperation({
2001
+ value,
2002
+ operation,
2003
+ path: name,
2004
+ force: true
2005
+ });
1987
2006
  }
2007
+ } else {
2008
+ // I think this is for keys
1988
2009
  update.set(field, value, operation);
1989
2010
  }
1990
2011
  }
2012
+ }
1991
2013
 
2014
+ _makeUpsertParams({ update, upsert } = {}) {
1992
2015
  return {
1993
2016
  TableName: this.getTableName(),
1994
2017
  UpdateExpression: update.build(),
1995
2018
  ExpressionAttributeNames: update.getNames(),
1996
2019
  ExpressionAttributeValues: update.getValues(),
1997
- Key: indexKey,
2020
+ Key: upsert.indexKey,
1998
2021
  };
1999
2022
  }
2000
2023
 
@@ -2391,13 +2414,13 @@ class Entity {
2391
2414
  return indexKeys;
2392
2415
  }
2393
2416
 
2394
- _getPutKeys(pk, sk, set) {
2395
- let setAttributes = this.model.schema.applyAttributeSetters(set);
2417
+ _getPutKeys(pk, sk, set, validationAssistance) {
2418
+ let setAttributes = set;
2396
2419
  let updateIndex = TableIndex;
2397
2420
  let keyTranslations = this.model.translations.keys;
2398
2421
  let keyAttributes = { ...sk, ...pk };
2399
2422
  let completeFacets = this._expectIndexFacets(
2400
- { ...setAttributes },
2423
+ { ...setAttributes, ...validationAssistance },
2401
2424
  { ...keyAttributes },
2402
2425
  );
2403
2426