objection 3.3.0 → 3.4.1

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.
Files changed (29) hide show
  1. package/lib/model/AjvValidator.js +87 -10
  2. package/lib/model/Model.js +3 -1
  3. package/lib/model/modelId.js +10 -2
  4. package/lib/model/modelQueryProps.js +7 -0
  5. package/lib/queryBuilder/QueryBuilder.js +5 -3
  6. package/lib/queryBuilder/QueryBuilderBase.js +44 -9
  7. package/lib/queryBuilder/ReferenceBuilder.js +8 -1
  8. package/lib/queryBuilder/graph/GraphFetcher.js +1 -3
  9. package/lib/queryBuilder/graph/GraphOptions.js +30 -1
  10. package/lib/queryBuilder/graph/GraphUpsert.js +16 -1
  11. package/lib/queryBuilder/graph/patch/GraphPatchAction.js +8 -2
  12. package/lib/queryBuilder/join/RelationJoiner.js +62 -9
  13. package/lib/queryBuilder/join/utils.js +20 -2
  14. package/lib/queryBuilder/operations/InsertOperation.js +11 -4
  15. package/lib/queryBuilder/operations/UpdateOperation.js +2 -0
  16. package/lib/queryBuilder/operations/jsonApi/WhereJsonHasPostgresOperation.js +1 -0
  17. package/lib/queryBuilder/operations/jsonApi/WhereJsonNotObjectPostgresOperation.js +1 -0
  18. package/lib/queryBuilder/operations/jsonApi/WhereJsonPostgresOperation.js +33 -0
  19. package/lib/queryBuilder/operations/jsonApi/postgresJsonApi.js +16 -1
  20. package/lib/queryBuilder/operations/select/SelectOperation.js +39 -1
  21. package/lib/relations/RelationFindOperation.js +66 -2
  22. package/lib/relations/RelationProperty.js +3 -2
  23. package/lib/relations/manyToMany/ManyToManyRelation.js +34 -3
  24. package/lib/relations/manyToMany/find/ManyToManyFindOperation.js +34 -4
  25. package/lib/relations/manyToMany/update/ManyToManyUpdateOperationBase.js +4 -0
  26. package/lib/utils/assert.js +9 -0
  27. package/lib/utils/wrapDbError.js +36 -0
  28. package/package.json +2 -5
  29. package/typings/objection/index.d.ts +1 -1
@@ -6,6 +6,7 @@ const { Validator } = require('./Validator');
6
6
  const { ValidationErrorType } = require('../model/ValidationError');
7
7
  const { isObject, once, cloneDeep: lodashCloneDeep, omit } = require('../utils/objectUtils');
8
8
  const { parseFieldExpression } = require('../utils/parseFieldExpression');
9
+ const { getQueryPropKeys } = require('./modelQueryProps');
9
10
 
10
11
  class AjvValidator extends Validator {
11
12
  static init(self, conf) {
@@ -51,6 +52,10 @@ class AjvValidator extends Validator {
51
52
 
52
53
  setupAjv(self.ajv);
53
54
  setupAjv(self.ajvNoDefaults);
55
+
56
+ // Created when needed by `compileQueryPropValidator()`.
57
+ self.ajvQueryProps = null;
58
+ self.setupAjv = setupAjv;
54
59
  }
55
60
 
56
61
  beforeValidate({ json, model, options, ctx }) {
@@ -75,7 +80,12 @@ class AjvValidator extends Validator {
75
80
  }
76
81
 
77
82
  const modelClass = model.constructor;
78
- const validator = this.getValidator(modelClass, ctx.jsonSchema, !!options.patch);
83
+ const validator = this.getValidator(
84
+ modelClass,
85
+ ctx.jsonSchema,
86
+ !!options.patch,
87
+ options.patch ? null : getMissingQueryPropKeys(model, json, ctx.jsonSchema),
88
+ );
79
89
 
80
90
  // We need to clone the input json if we are about to set default values.
81
91
  if (!options.mutable && !options.patch && setsDefaultValues(ctx.jsonSchema)) {
@@ -103,7 +113,7 @@ class AjvValidator extends Validator {
103
113
  return json;
104
114
  }
105
115
 
106
- getValidator(modelClass, jsonSchema, isPatchObject) {
116
+ getValidator(modelClass, jsonSchema, isPatchObject, queryPropKeys = null) {
107
117
  // Use the Ajv custom serializer if provided.
108
118
  const createCacheKey = this.ajvOptions.serialize || JSON.stringify;
109
119
 
@@ -136,6 +146,10 @@ class AjvValidator extends Validator {
136
146
 
137
147
  // Validator created for the unmodified schema.
138
148
  normalValidator: null,
149
+
150
+ // Validators created for the schema object without the `required`
151
+ // properties that are given as query properties, keyed by their names.
152
+ queryPropValidators: null,
139
153
  };
140
154
 
141
155
  this.cache.set(cacheKey, validators);
@@ -148,6 +162,17 @@ class AjvValidator extends Validator {
148
162
  validator = this.compilePatchValidator(jsonSchema);
149
163
  validators.patchValidator = validator;
150
164
  }
165
+ } else if (queryPropKeys) {
166
+ const key = JSON.stringify(queryPropKeys);
167
+ validators.queryPropValidators = validators.queryPropValidators || new Map();
168
+ validator = validators.queryPropValidators.get(key);
169
+
170
+ if (!validator) {
171
+ validator = this.compileQueryPropValidator(
172
+ jsonSchemaWithoutRequiredKeys(jsonSchema, queryPropKeys),
173
+ );
174
+ validators.queryPropValidators.set(key, validator);
175
+ }
151
176
  } else {
152
177
  validator = validators.normalValidator;
153
178
 
@@ -169,6 +194,29 @@ class AjvValidator extends Validator {
169
194
  compileNormalValidator(jsonSchema) {
170
195
  return this.ajv.compile(jsonSchema);
171
196
  }
197
+
198
+ compileQueryPropValidator(jsonSchema) {
199
+ // The schema is a modified copy that keeps the `$id`s of the original, so
200
+ // it's compiled on an instance that doesn't add compiled schemas by their
201
+ // `$id`. Otherwise ajv would throw because the ids already exist.
202
+ if (!this.ajvQueryProps) {
203
+ this.ajvQueryProps = new Ajv(
204
+ Object.assign(
205
+ {
206
+ useDefaults: true,
207
+ },
208
+ this.ajvOptions,
209
+ {
210
+ addUsedSchema: false,
211
+ },
212
+ ),
213
+ );
214
+
215
+ this.setupAjv(this.ajvQueryProps);
216
+ }
217
+
218
+ return this.ajvQueryProps.compile(jsonSchema);
219
+ }
172
220
  }
173
221
 
174
222
  function parseValidationError(errors, modelClass, options, ajvOptions) {
@@ -397,6 +445,30 @@ function objectHasDefaults(obj) {
397
445
  return false;
398
446
  }
399
447
 
448
+ // Returns the keys of the query properties of `model` (`raw()`, `val()`,
449
+ // `ref()`, subqueries...) that are required by the schema but missing from
450
+ // `json`, sorted, or `null` if there are none. Query properties are split from
451
+ // the json before validation, but they do provide a value for the property.
452
+ function getMissingQueryPropKeys(model, json, jsonSchema) {
453
+ const required = jsonSchema.required;
454
+
455
+ if (!Array.isArray(required) || required.length === 0) {
456
+ return null;
457
+ }
458
+
459
+ const keys = getQueryPropKeys(model).filter(
460
+ (key) => required.includes(key) && !Object.prototype.hasOwnProperty.call(json, key),
461
+ );
462
+
463
+ return keys.length > 0 ? keys.sort() : null;
464
+ }
465
+
466
+ function jsonSchemaWithoutRequiredKeys(jsonSchema, keys) {
467
+ return Object.assign({}, jsonSchema, {
468
+ required: jsonSchema.required.filter((key) => !keys.includes(key)),
469
+ });
470
+ }
471
+
400
472
  function jsonSchemaWithoutRequired(jsonSchema) {
401
473
  // `not` is intentionally left untouched: Stripping `required` inside it
402
474
  // would turn it into a schema that rejects everything (e.g.
@@ -436,11 +508,20 @@ function definitionsWithoutRequired(jsonSchema, prop) {
436
508
  function subSchemaWithoutRequired(jsonSchema, prop) {
437
509
  if (jsonSchema[prop]) {
438
510
  if (Array.isArray(jsonSchema[prop])) {
439
- const schemaArray = jsonSchemaArrayWithoutRequired(jsonSchema[prop]);
440
-
441
- if (schemaArray.length !== 0) {
511
+ const schemaArray = jsonSchema[prop].map(jsonSchemaWithoutRequired);
512
+ const nonEmptySchemaArray = schemaArray.filter(isNotEmptyObject);
513
+
514
+ // An `anyOf` or `oneOf` option that only consisted of `required` is
515
+ // empty once `required` is stripped, and an empty schema matches any
516
+ // patch, just like the stripped `required` keywords elsewhere. Drop the
517
+ // whole keyword then: requiring the patch to match one of the remaining
518
+ // options would reject valid patches, and with `oneOf` keeping the empty
519
+ // option would fail every patch that also matches another option.
520
+ if (prop !== 'allOf' && nonEmptySchemaArray.length !== schemaArray.length) {
521
+ return {};
522
+ } else if (nonEmptySchemaArray.length !== 0) {
442
523
  return {
443
- [prop]: schemaArray,
524
+ [prop]: nonEmptySchemaArray,
444
525
  };
445
526
  } else {
446
527
  return {};
@@ -467,10 +548,6 @@ function subSchemaWithoutRequired(jsonSchema, prop) {
467
548
  }
468
549
  }
469
550
 
470
- function jsonSchemaArrayWithoutRequired(jsonSchemaArray) {
471
- return jsonSchemaArray.map(jsonSchemaWithoutRequired).filter(isNotEmptyObject);
472
- }
473
-
474
551
  function isNotEmptyObject(obj) {
475
552
  return Object.keys(obj).length !== 0;
476
553
  }
@@ -839,11 +839,13 @@ function getColumnNameMappers(modelClass) {
839
839
  }
840
840
 
841
841
  function getIdRelationProperty(modelClass) {
842
- const idColumn = asArray(modelClass.getIdColumn());
842
+ // `idColumn = null` means that the model has no primary key.
843
+ const idColumn = modelClass.getIdColumn() === null ? [] : asArray(modelClass.getIdColumn());
843
844
 
844
845
  return new RelationProperty(
845
846
  idColumn.map((idCol) => `${modelClass.getTableName()}.${idCol}`),
846
847
  () => modelClass,
848
+ { allowEmpty: true },
847
849
  );
848
850
  }
849
851
 
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { assertHasIdColumn } = require('../utils/assert');
4
+
3
5
  function getSetId(model, maybeId) {
4
6
  if (maybeId !== undefined) {
5
7
  return setId(model, maybeId);
@@ -9,10 +11,13 @@ function getSetId(model, maybeId) {
9
11
  }
10
12
 
11
13
  function hasId(model) {
12
- return model.$hasProps(model.constructor.getIdPropertyArray());
14
+ const idProps = model.constructor.getIdPropertyArray();
15
+ // Models without an `idColumn` never have an id.
16
+ return idProps.length > 0 && model.$hasProps(idProps);
13
17
  }
14
18
 
15
19
  function setId(model, id) {
20
+ assertHasIdColumn(model.constructor);
16
21
  const idProp = model.constructor.getIdProperty();
17
22
  const isCompositeId = Array.isArray(idProp);
18
23
 
@@ -49,7 +54,10 @@ function getId(model) {
49
54
  const idProp = model.constructor.getIdProperty();
50
55
  const isCompositeId = Array.isArray(idProp);
51
56
 
52
- if (isCompositeId) {
57
+ if (isCompositeId && idProp.length === 0) {
58
+ // Models without an `idColumn` have no id.
59
+ return undefined;
60
+ } else if (isCompositeId) {
53
61
  return model.$values(idProp);
54
62
  } else {
55
63
  return model[idProp];
@@ -108,6 +108,12 @@ function convertAndMergeHiddenQueryProps(model, json, omitProps, builder) {
108
108
  return json;
109
109
  }
110
110
 
111
+ // Returns the keys of the query properties that were split from `model`.
112
+ function getQueryPropKeys(model) {
113
+ const queryProps = model[QUERY_PROPS_PROPERTY];
114
+ return queryProps ? Object.keys(queryProps) : [];
115
+ }
116
+
111
117
  // Converts a query property into a knex `raw` instance.
112
118
  function queryPropToKnexRaw(queryProp, builder) {
113
119
  if (!queryProp) {
@@ -144,4 +150,5 @@ function isKnexRawConvertable(queryProp) {
144
150
  module.exports = {
145
151
  splitQueryProps,
146
152
  mergeQueryProps,
153
+ getQueryPropKeys,
147
154
  };
@@ -1,13 +1,13 @@
1
1
  'use strict';
2
2
 
3
- const { wrapError } = require('db-errors');
3
+ const { wrapDbError } = require('../utils/wrapDbError');
4
4
 
5
5
  const { raw } = require('./RawBuilder');
6
6
  const { createModifier } = require('../utils/createModifier');
7
7
  const { ValidationErrorType } = require('../model/ValidationError');
8
8
  const { isObject, isString, isFunction, last, flatten } = require('../utils/objectUtils');
9
9
  const { RelationExpression, DuplicateRelationError } = require('./RelationExpression');
10
- const { assertIdNotUndefined } = require('../utils/assert');
10
+ const { assertIdNotUndefined, assertHasIdColumn } = require('../utils/assert');
11
11
 
12
12
  const { Selection } = require('./operations/select/Selection');
13
13
 
@@ -125,6 +125,8 @@ class QueryBuilder extends QueryBuilderBase {
125
125
  }
126
126
 
127
127
  fullIdColumnFor(modelClass) {
128
+ assertHasIdColumn(modelClass);
129
+
128
130
  const tableName = this.tableRefFor(modelClass);
129
131
  const idColumn = modelClass.getIdColumn();
130
132
 
@@ -1408,7 +1410,7 @@ function handleExecuteError(builder, err) {
1408
1410
  return Promise.resolve(err.value);
1409
1411
  }
1410
1412
 
1411
- let promise = Promise.reject(wrapError(err));
1413
+ let promise = Promise.reject(wrapDbError(err));
1412
1414
 
1413
1415
  builder.forEachOperation(true, (op) => {
1414
1416
  if (op.hasOnError()) {
@@ -616,9 +616,14 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
616
616
  // whereJson(Not)SupersetOf / whereJson(Not)SubsetOf are now supported by knex >= 1.0, but for now
617
617
  // objection handles them differently and only for postgres.
618
618
  // Changing them to utilize knex methods directly may require a major version bump and upgrade guide.
619
+ // On MySQL, the simple `(column, object | array)` form is passed on to knex (ditojs#113).
619
620
  whereJsonSupersetOf(...args) {
620
621
  return this.addOperation(
621
- new WhereJsonPostgresOperation('whereJsonSupersetOf', { operator: '@>', bool: 'and' }),
622
+ new WhereJsonPostgresOperation('whereJsonSupersetOf', {
623
+ operator: '@>',
624
+ bool: 'and',
625
+ knexMethod: 'whereJsonSupersetOf',
626
+ }),
622
627
  args,
623
628
  );
624
629
  }
@@ -629,7 +634,11 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
629
634
 
630
635
  orWhereJsonSupersetOf(...args) {
631
636
  return this.addOperation(
632
- new WhereJsonPostgresOperation('orWhereJsonSupersetOf', { operator: '@>', bool: 'or' }),
637
+ new WhereJsonPostgresOperation('orWhereJsonSupersetOf', {
638
+ operator: '@>',
639
+ bool: 'or',
640
+ knexMethod: 'whereJsonSupersetOf',
641
+ }),
633
642
  args,
634
643
  );
635
644
  }
@@ -640,13 +649,14 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
640
649
  operator: '@>',
641
650
  bool: 'and',
642
651
  prefix: 'not',
652
+ knexMethod: 'whereJsonNotSupersetOf',
643
653
  }),
644
654
  args,
645
655
  );
646
656
  }
647
657
 
648
658
  andWhereJsonNotSupersetOf(...args) {
649
- return this.andWhereJsonNotSupersetOf(...args);
659
+ return this.whereJsonNotSupersetOf(...args);
650
660
  }
651
661
 
652
662
  orWhereJsonNotSupersetOf(...args) {
@@ -655,6 +665,7 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
655
665
  operator: '@>',
656
666
  bool: 'or',
657
667
  prefix: 'not',
668
+ knexMethod: 'whereJsonNotSupersetOf',
658
669
  }),
659
670
  args,
660
671
  );
@@ -662,7 +673,11 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
662
673
 
663
674
  whereJsonSubsetOf(...args) {
664
675
  return this.addOperation(
665
- new WhereJsonPostgresOperation('whereJsonSubsetOf', { operator: '<@', bool: 'and' }),
676
+ new WhereJsonPostgresOperation('whereJsonSubsetOf', {
677
+ operator: '<@',
678
+ bool: 'and',
679
+ knexMethod: 'whereJsonSubsetOf',
680
+ }),
666
681
  args,
667
682
  );
668
683
  }
@@ -673,7 +688,11 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
673
688
 
674
689
  orWhereJsonSubsetOf(...args) {
675
690
  return this.addOperation(
676
- new WhereJsonPostgresOperation('orWhereJsonSubsetOf', { operator: '<@', bool: 'or' }),
691
+ new WhereJsonPostgresOperation('orWhereJsonSubsetOf', {
692
+ operator: '<@',
693
+ bool: 'or',
694
+ knexMethod: 'whereJsonSubsetOf',
695
+ }),
677
696
  args,
678
697
  );
679
698
  }
@@ -684,6 +703,7 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
684
703
  operator: '<@',
685
704
  bool: 'and',
686
705
  prefix: 'not',
706
+ knexMethod: 'whereJsonNotSubsetOf',
687
707
  }),
688
708
  args,
689
709
  );
@@ -699,6 +719,7 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
699
719
  operator: '<@',
700
720
  bool: 'or',
701
721
  prefix: 'not',
722
+ knexMethod: 'whereJsonNotSubsetOf',
702
723
  }),
703
724
  args,
704
725
  );
@@ -772,20 +793,34 @@ class QueryBuilderBase extends QueryBuilderOperationSupport {
772
793
  );
773
794
  }
774
795
 
796
+ // Not passed on to knex like `whereJsonSupersetOf()`, as MySQL's `json_contains()`
797
+ // also matches `{}` in arrays of objects.
775
798
  whereJsonIsArray(fieldExpression) {
776
- return this.whereJsonSupersetOf(fieldExpression, []);
799
+ return this.addOperation(
800
+ new WhereJsonPostgresOperation('whereJsonSupersetOf', { operator: '@>', bool: 'and' }),
801
+ [fieldExpression, []],
802
+ );
777
803
  }
778
804
 
779
805
  orWhereJsonIsArray(fieldExpression) {
780
- return this.orWhereJsonSupersetOf(fieldExpression, []);
806
+ return this.addOperation(
807
+ new WhereJsonPostgresOperation('orWhereJsonSupersetOf', { operator: '@>', bool: 'or' }),
808
+ [fieldExpression, []],
809
+ );
781
810
  }
782
811
 
783
812
  whereJsonIsObject(fieldExpression) {
784
- return this.whereJsonSupersetOf(fieldExpression, {});
813
+ return this.addOperation(
814
+ new WhereJsonPostgresOperation('whereJsonSupersetOf', { operator: '@>', bool: 'and' }),
815
+ [fieldExpression, {}],
816
+ );
785
817
  }
786
818
 
787
819
  orWhereJsonIsObject(fieldExpression) {
788
- return this.orWhereJsonSupersetOf(fieldExpression, {});
820
+ return this.addOperation(
821
+ new WhereJsonPostgresOperation('orWhereJsonSupersetOf', { operator: '@>', bool: 'or' }),
822
+ [fieldExpression, {}],
823
+ );
789
824
  }
790
825
 
791
826
  whereColumn(...args) {
@@ -5,6 +5,7 @@ const {
5
5
  toPostgresJsonPathLiteral,
6
6
  } = require('../utils/parseFieldExpression');
7
7
  const { isObject } = require('../utils/objectUtils');
8
+ const { warnIfNotPostgres } = require('./operations/jsonApi/postgresJsonApi');
8
9
 
9
10
  class ReferenceBuilder {
10
11
  constructor(expr) {
@@ -163,7 +164,13 @@ class ReferenceBuilder {
163
164
  }
164
165
 
165
166
  toKnexRaw(builder) {
166
- return builder.knex().raw(...this._createRawArgs(builder));
167
+ const knex = builder.knex();
168
+
169
+ if (this._parsedExpr.access.length > 0) {
170
+ warnIfNotPostgres(knex);
171
+ }
172
+
173
+ return knex.raw(...this._createRawArgs(builder));
167
174
  }
168
175
 
169
176
  _parseExpression(expr) {
@@ -153,9 +153,7 @@ function createManyToManyExtraSelectionIfNeeded(builder, column, node) {
153
153
  const extra = relation.joinTableExtras.find((extra) => extra.aliasCol === column);
154
154
 
155
155
  if (extra) {
156
- return `${builder.tableRefFor(relation.joinModelClass)}.${extra.joinTableCol} as ${
157
- extra.aliasCol
158
- }`;
156
+ return `${relation.joinTableAliasFor(builder)}.${extra.joinTableCol} as ${extra.aliasCol}`;
159
157
  }
160
158
  }
161
159
 
@@ -13,6 +13,20 @@ const INSERT_MISSING = 'insertMissing';
13
13
  const FETCH_STRATEGY = 'fetchStrategy';
14
14
  const ALLOW_REFS = 'allowRefs';
15
15
 
16
+ const OPTION_NAMES = [
17
+ NO_RELATE,
18
+ NO_UNRELATE,
19
+ NO_INSERT,
20
+ NO_UPDATE,
21
+ NO_DELETE,
22
+ UPDATE,
23
+ RELATE,
24
+ UNRELATE,
25
+ INSERT_MISSING,
26
+ FETCH_STRATEGY,
27
+ ALLOW_REFS,
28
+ ];
29
+
16
30
  const FetchStrategy = {
17
31
  OnlyIdentifiers: 'OnlyIdentifiers',
18
32
  Everything: 'Everything',
@@ -64,7 +78,7 @@ class GraphOptions {
64
78
  !getCurrentNode(node, graphData) &&
65
79
  !!node.parentEdge &&
66
80
  !!node.parentEdge.relation &&
67
- node.parentEdge.relation.hasRelateProp(node.obj) &&
81
+ hasRelateProp(node) &&
68
82
  graphData.nodeDbExistence.doesNodeExistInDb(node)
69
83
  );
70
84
  }
@@ -212,6 +226,20 @@ function getCurrentNode(node, graphData) {
212
226
  return graphData.currentGraph.nodeForNode(node);
213
227
  }
214
228
 
229
+ // For `HasMany` and `HasOne` relations, the relate prop is the id of the
230
+ // related model. Use the id state of the input graph, since the parent's
231
+ // insert copies the foreign key into the related model, which can complete
232
+ // a composite id that contains it and would turn an insert into a relate (#2544).
233
+ function hasRelateProp(node) {
234
+ const relation = node.parentEdge.relation;
235
+
236
+ if (relation.isObjectionHasManyRelation) {
237
+ return node.hasId;
238
+ } else {
239
+ return relation.hasRelateProp(node.obj);
240
+ }
241
+ }
242
+
215
243
  function getNode(currentNode, graph) {
216
244
  if (!graph || !currentNode) {
217
245
  return null;
@@ -222,5 +250,6 @@ function getNode(currentNode, graph) {
222
250
 
223
251
  module.exports = {
224
252
  GraphOptions,
253
+ OPTION_NAMES,
225
254
  FetchStrategy,
226
255
  };
@@ -8,7 +8,7 @@ const { GraphInsert } = require('./insert/GraphInsert');
8
8
  const { GraphPatch } = require('./patch/GraphPatch');
9
9
  const { GraphDelete } = require('./delete/GraphDelete');
10
10
  const { GraphRecursiveUpsert } = require('./recursiveUpsert/GraphRecursiveUpsert');
11
- const { GraphOptions } = require('./GraphOptions');
11
+ const { GraphOptions, OPTION_NAMES } = require('./GraphOptions');
12
12
  const { ValidationErrorType } = require('../../model/ValidationError');
13
13
  const { RelationExpression } = require('../RelationExpression');
14
14
  const { GraphNodeDbExistence } = require('./GraphNodeDbExistence');
@@ -19,6 +19,7 @@ const { deprecate } = require('../../utils/deprecate');
19
19
  class GraphUpsert {
20
20
  constructor({ rootModelClass, objects, upsertOptions }) {
21
21
  checkCanBeConvertedToModels(rootModelClass, objects);
22
+ warnOnUnknownOptions(upsertOptions);
22
23
 
23
24
  this.objects = rootModelClass.ensureModelArray(objects, GraphUpsert.modelOptions);
24
25
  this.isArray = Array.isArray(objects);
@@ -61,6 +62,20 @@ class GraphUpsert {
61
62
  }
62
63
  }
63
64
 
65
+ // Misspelled options are silently ignored, so the operation runs with the
66
+ // default behaviour instead (#84). A warning is logged once per option.
67
+ // TODO: Throw an error instead in objection 4.0.
68
+ function warnOnUnknownOptions(upsertOptions) {
69
+ for (const name of Object.keys(upsertOptions)) {
70
+ if (!OPTION_NAMES.includes(name)) {
71
+ deprecate(
72
+ `Unknown graph option "${name}" is ignored. ` +
73
+ `Valid options are: ${OPTION_NAMES.join(', ')}. This will throw in objection 4.0.`,
74
+ );
75
+ }
76
+ }
77
+ }
78
+
64
79
  function checkCanBeConvertedToModels(modelClass, objects) {
65
80
  asArray(objects).forEach((obj) => {
66
81
  if (!isObject(obj)) {
@@ -3,7 +3,7 @@
3
3
  const { getModel } = require('../../../model/getModel');
4
4
  const { GraphAction } = require('../GraphAction');
5
5
  const { isInternalProp } = require('../../../utils/internalPropUtils');
6
- const { difference, isObject, jsonEquals } = require('../../../utils/objectUtils');
6
+ const { difference, isObject, isPlainObject, jsonEquals } = require('../../../utils/objectUtils');
7
7
  const promiseUtils = require('../../../utils/promiseUtils');
8
8
 
9
9
  class GraphPatchAction extends GraphAction {
@@ -251,13 +251,19 @@ function childQueryOptions() {
251
251
  }
252
252
 
253
253
  function nonStrictEquals(val1, val2) {
254
- if (val1 == val2) {
254
+ // Only use loose equality for non-JSON values, so that for example `false`
255
+ // and `[]` are not considered equal. See issue #1874.
256
+ if (!isJsonValue(val1) && !isJsonValue(val2) && val1 == val2) {
255
257
  return true;
256
258
  }
257
259
 
258
260
  return jsonEquals(val1, val2);
259
261
  }
260
262
 
263
+ function isJsonValue(value) {
264
+ return Array.isArray(value) || isPlainObject(value);
265
+ }
266
+
261
267
  function isToBeUnrelatedOrDeleted(node) {
262
268
  return !!node && node.toBeUnrelatedOrDeleted;
263
269
  }