Browse Source

fix: replaceOrCreate method should check document existence before create

e22m4u 1 year ago
parent
commit
23fd7a3d34
97 changed files with 853 additions and 6 deletions
  1. 0 0
      docs/assets/search.js
  2. 0 0
      docs/classes/Adapter.html
  3. 0 0
      docs/classes/AdapterLoader.html
  4. 0 0
      docs/classes/AdapterRegistry.html
  5. 0 0
      docs/classes/BelongsToResolver.html
  6. 0 0
      docs/classes/DatasourceDefinitionValidator.html
  7. 0 0
      docs/classes/DefinitionRegistry.html
  8. 0 0
      docs/classes/FieldsClauseTool.html
  9. 0 0
      docs/classes/HasManyResolver.html
  10. 0 0
      docs/classes/HasOneResolver.html
  11. 0 0
      docs/classes/IncludeClauseTool.html
  12. 0 0
      docs/classes/InvalidArgumentError.html
  13. 0 0
      docs/classes/InvalidOperatorValueError.html
  14. 0 0
      docs/classes/ModelDataSanitizer.html
  15. 0 0
      docs/classes/ModelDataValidator.html
  16. 0 0
      docs/classes/ModelDefinitionUtils.html
  17. 0 0
      docs/classes/ModelDefinitionValidator.html
  18. 0 0
      docs/classes/NotImplementedError.html
  19. 0 0
      docs/classes/OperatorClauseTool.html
  20. 0 0
      docs/classes/OrderClauseTool.html
  21. 0 0
      docs/classes/PrimaryKeysDefinitionValidator.html
  22. 0 0
      docs/classes/PropertiesDefinitionValidator.html
  23. 0 0
      docs/classes/ReferencesManyResolver.html
  24. 0 0
      docs/classes/RelationsDefinitionValidator.html
  25. 0 0
      docs/classes/Repository.html
  26. 0 0
      docs/classes/RepositoryRegistry.html
  27. 0 0
      docs/classes/Schema.html
  28. 0 0
      docs/classes/SliceClauseTool.html
  29. 0 0
      docs/classes/WhereClauseTool.html
  30. 0 0
      docs/enums/DataType.html
  31. 0 0
      docs/enums/RelationType.html
  32. 0 0
      docs/functions/capitalize.html
  33. 0 0
      docs/functions/cloneDeep.html
  34. 0 0
      docs/functions/excludeObjectKeys.html
  35. 0 0
      docs/functions/getCtorName.html
  36. 0 0
      docs/functions/getValueByPath.html
  37. 0 0
      docs/functions/isCtor.html
  38. 0 0
      docs/functions/isPureObject.html
  39. 0 0
      docs/functions/selectObjectKeys.html
  40. 0 0
      docs/functions/singularize.html
  41. 0 0
      docs/functions/stringToRegexp.html
  42. 0 0
      docs/interfaces/AndClause.html
  43. 0 0
      docs/interfaces/OrClause.html
  44. 0 0
      docs/types/AnyObject.html
  45. 0 0
      docs/types/BelongsToDefinition.html
  46. 0 0
      docs/types/DEFAULT_PRIMARY_KEY_PROPERTY_NAME.html
  47. 0 0
      docs/types/DatasourceDefinition.html
  48. 0 0
      docs/types/FieldsClause.html
  49. 0 0
      docs/types/FilterClause.html
  50. 0 0
      docs/types/Flatten.html
  51. 0 0
      docs/types/FullPropertyDefinition.html
  52. 0 0
      docs/types/HasManyDefinition.html
  53. 0 0
      docs/types/HasOneDefinition.html
  54. 0 0
      docs/types/Identity.html
  55. 0 0
      docs/types/IncludeClause.html
  56. 0 0
      docs/types/ItemFilterClause.html
  57. 0 0
      docs/types/ModelData.html
  58. 0 0
      docs/types/ModelDefinition.html
  59. 0 0
      docs/types/ModelId.html
  60. 0 0
      docs/types/NestedIncludeClause.html
  61. 0 0
      docs/types/NormalizedFieldsClause.html
  62. 0 0
      docs/types/NormalizedIncludeClause.html
  63. 0 0
      docs/types/OperatorClause.html
  64. 0 0
      docs/types/OptionalUnlessRequiredId.html
  65. 0 0
      docs/types/OrderClause.html
  66. 0 0
      docs/types/PartialBy.html
  67. 0 0
      docs/types/PartialWithoutId.html
  68. 0 0
      docs/types/PolyBelongsToDefinition.html
  69. 0 0
      docs/types/PolyHasManyDefinitionWithTargetKeys.html
  70. 0 0
      docs/types/PolyHasManyDefinitionWithTargetRelationName.html
  71. 0 0
      docs/types/PolyHasOneDefinitionWithTargetKeys.html
  72. 0 0
      docs/types/PolyHasOneDefinitionWithTargetRelationName.html
  73. 0 0
      docs/types/PropertiesClause.html
  74. 0 0
      docs/types/PropertyDefinition.html
  75. 0 0
      docs/types/PropertyDefinitionMap.html
  76. 0 0
      docs/types/ReferencesManyDefinition.html
  77. 0 0
      docs/types/RelationDefinition.html
  78. 0 0
      docs/types/RelationDefinitionMap.html
  79. 0 0
      docs/types/WhereClause.html
  80. 0 0
      docs/types/WithoutId.html
  81. 13 0
      src/adapter/adapter.d.ts
  82. 15 0
      src/adapter/adapter.js
  83. 8 0
      src/adapter/adapter.spec.js
  84. 13 0
      src/adapter/builtin/memory-adapter.d.ts
  85. 30 0
      src/adapter/builtin/memory-adapter.js
  86. 652 0
      src/adapter/builtin/memory-adapter.spec.js
  87. 6 0
      src/adapter/decorator/data-sanitizing-decorator.js
  88. 13 0
      src/adapter/decorator/data-sanitizing-decorator.spec.js
  89. 6 0
      src/adapter/decorator/data-validation-decorator.js
  90. 13 0
      src/adapter/decorator/data-validation-decorator.spec.js
  91. 6 0
      src/adapter/decorator/default-values-decorator.js
  92. 16 0
      src/adapter/decorator/default-values-decorator.spec.js
  93. 13 0
      src/adapter/decorator/fields-filtering-decorator.js
  94. 17 0
      src/adapter/decorator/fields-filtering-decorator.spec.js
  95. 13 0
      src/adapter/decorator/inclusion-decorator.js
  96. 17 0
      src/adapter/decorator/inclusion-decorator.spec.js
  97. 2 6
      src/repository/repository.js

File diff suppressed because it is too large
+ 0 - 0
docs/assets/search.js


File diff suppressed because it is too large
+ 0 - 0
docs/classes/Adapter.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/AdapterLoader.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/AdapterRegistry.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/BelongsToResolver.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/DatasourceDefinitionValidator.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/DefinitionRegistry.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/FieldsClauseTool.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/HasManyResolver.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/HasOneResolver.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/IncludeClauseTool.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/InvalidArgumentError.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/InvalidOperatorValueError.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/ModelDataSanitizer.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/ModelDataValidator.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/ModelDefinitionUtils.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/ModelDefinitionValidator.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/NotImplementedError.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/OperatorClauseTool.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/OrderClauseTool.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/PrimaryKeysDefinitionValidator.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/PropertiesDefinitionValidator.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/ReferencesManyResolver.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/RelationsDefinitionValidator.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/Repository.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/RepositoryRegistry.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/Schema.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/SliceClauseTool.html


File diff suppressed because it is too large
+ 0 - 0
docs/classes/WhereClauseTool.html


File diff suppressed because it is too large
+ 0 - 0
docs/enums/DataType.html


File diff suppressed because it is too large
+ 0 - 0
docs/enums/RelationType.html


File diff suppressed because it is too large
+ 0 - 0
docs/functions/capitalize.html


File diff suppressed because it is too large
+ 0 - 0
docs/functions/cloneDeep.html


File diff suppressed because it is too large
+ 0 - 0
docs/functions/excludeObjectKeys.html


File diff suppressed because it is too large
+ 0 - 0
docs/functions/getCtorName.html


File diff suppressed because it is too large
+ 0 - 0
docs/functions/getValueByPath.html


File diff suppressed because it is too large
+ 0 - 0
docs/functions/isCtor.html


File diff suppressed because it is too large
+ 0 - 0
docs/functions/isPureObject.html


File diff suppressed because it is too large
+ 0 - 0
docs/functions/selectObjectKeys.html


File diff suppressed because it is too large
+ 0 - 0
docs/functions/singularize.html


File diff suppressed because it is too large
+ 0 - 0
docs/functions/stringToRegexp.html


File diff suppressed because it is too large
+ 0 - 0
docs/interfaces/AndClause.html


File diff suppressed because it is too large
+ 0 - 0
docs/interfaces/OrClause.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/AnyObject.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/BelongsToDefinition.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/DEFAULT_PRIMARY_KEY_PROPERTY_NAME.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/DatasourceDefinition.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/FieldsClause.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/FilterClause.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/Flatten.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/FullPropertyDefinition.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/HasManyDefinition.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/HasOneDefinition.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/Identity.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/IncludeClause.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/ItemFilterClause.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/ModelData.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/ModelDefinition.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/ModelId.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/NestedIncludeClause.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/NormalizedFieldsClause.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/NormalizedIncludeClause.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/OperatorClause.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/OptionalUnlessRequiredId.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/OrderClause.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/PartialBy.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/PartialWithoutId.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/PolyBelongsToDefinition.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/PolyHasManyDefinitionWithTargetKeys.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/PolyHasManyDefinitionWithTargetRelationName.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/PolyHasOneDefinitionWithTargetKeys.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/PolyHasOneDefinitionWithTargetRelationName.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/PropertiesClause.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/PropertyDefinition.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/PropertyDefinitionMap.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/ReferencesManyDefinition.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/RelationDefinition.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/RelationDefinitionMap.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/WhereClause.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/WithoutId.html


+ 13 - 0
src/adapter/adapter.d.ts

@@ -52,6 +52,19 @@ export declare class Adapter extends Service {
     filter?: ItemFilterClause,
   ): Promise<ModelData>;
 
+  /**
+   * Replace or create.
+   *
+   * @param modelName
+   * @param modelData
+   * @param filter
+   */
+  replaceOrCreate(
+    modelName: string,
+    modelData: ModelData,
+    filter?: ItemFilterClause,
+  ): Promise<ModelData>;
+
   /**
    * Patch.
    *

+ 15 - 0
src/adapter/adapter.js

@@ -78,6 +78,21 @@ export class Adapter extends Service {
     );
   }
 
+  /**
+   * Replace or create.
+   *
+   * @param {string} modelName
+   * @param {object} modelData
+   * @param {object|undefined} filter
+   * @returns {Promise<object>}
+   */
+  replaceOrCreate(modelName, modelData, filter = undefined) {
+    throw new NotImplementedError(
+      '%s.replaceOrCreate is not implemented.',
+      this.constructor.name,
+    );
+  }
+
   /**
    * Patch.
    *

+ 8 - 0
src/adapter/adapter.spec.js

@@ -86,6 +86,14 @@ describe('Adapter', function () {
     });
   });
 
+  describe('replaceOrCreate', function () {
+    it('throws the "Not implemented"', function () {
+      const adapter = new Adapter();
+      const throwable = () => adapter.replaceOrCreate();
+      expect(throwable).to.throw('Adapter.replaceOrCreate is not implemented.');
+    });
+  });
+
   describe('patchById', function () {
     it('throws the "Not implemented"', function () {
       const adapter = new Adapter();

+ 13 - 0
src/adapter/builtin/memory-adapter.d.ts

@@ -52,6 +52,19 @@ export declare class MemoryAdapter extends Adapter {
     filter?: ItemFilterClause,
   ): Promise<ModelData>;
 
+  /**
+   * Replace or create.
+   *
+   * @param modelName
+   * @param modelData
+   * @param filter
+   */
+  replaceOrCreate(
+    modelName: string,
+    modelData: ModelData,
+    filter?: ItemFilterClause,
+  ): Promise<ModelData>;
+
   /**
    * Patch.
    *

+ 30 - 0
src/adapter/builtin/memory-adapter.js

@@ -154,6 +154,36 @@ export class MemoryAdapter extends Adapter {
     ).convertColumnNamesToPropertyNames(modelName, tableData);
   }
 
+  /**
+   * Replace or create.
+   *
+   * @param {string} modelName
+   * @param {object} modelData
+   * @param {object|undefined} filter
+   * @returns {Promise<object>}
+   */
+  async replaceOrCreate(modelName, modelData, filter = undefined) {
+    const pkPropName =
+      this.getService(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(
+        modelName,
+      );
+    let idValue = modelData[pkPropName];
+    if (idValue == null) idValue = this._genNextIdValue(modelName, pkPropName);
+
+    const table = this._getTableOrCreate(modelName);
+    modelData = cloneDeep(modelData);
+    modelData[pkPropName] = idValue;
+
+    const tableData = this.getService(
+      ModelDefinitionUtils,
+    ).convertPropertyNamesToColumnNames(modelName, modelData);
+    table.set(idValue, tableData);
+
+    return this.getService(
+      ModelDefinitionUtils,
+    ).convertColumnNamesToPropertyNames(modelName, tableData);
+  }
+
   /**
    * Patch.
    *

+ 652 - 0
src/adapter/builtin/memory-adapter.spec.js

@@ -1133,6 +1133,658 @@ describe('MemoryAdapter', function () {
     });
   });
 
+  describe('replaceOrCreate', function () {
+    it('generates a new identifier when a value of a primary key has not provided', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const input = {foo: 'string', bar: 10};
+      const created = await adapter.replaceOrCreate('model', input);
+      const idValue = created[DEF_PK];
+      expect(created).to.be.eql({...input, [DEF_PK]: idValue});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({...input, [DEF_PK]: idValue});
+    });
+
+    it('generates a new identifier when a value of a primary key is undefined', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const input = {
+        [DEF_PK]: undefined,
+        foo: 'string',
+        bar: 10,
+      };
+      const created = await adapter.replaceOrCreate('model', input);
+      const idValue = created[DEF_PK];
+      expect(created).to.be.eql({...input, [DEF_PK]: idValue});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({...input, [DEF_PK]: idValue});
+    });
+
+    it('generates a new identifier when a value of a primary key is null', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const input = {
+        [DEF_PK]: null,
+        foo: 'string',
+        bar: 10,
+      };
+      const created = await adapter.replaceOrCreate('model', input);
+      const idValue = created[DEF_PK];
+      expect(created).to.be.eql({...input, [DEF_PK]: idValue});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({...input, [DEF_PK]: idValue});
+    });
+
+    it('generates a new identifier for a primary key of a "number" type', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          myId: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const result1 = await adapter.replaceOrCreate('model', {});
+      const result2 = await adapter.replaceOrCreate('model', {});
+      const result3 = await adapter.replaceOrCreate('model', {});
+      expect(result1).to.be.eql({myId: 1});
+      expect(result2).to.be.eql({myId: 2});
+      expect(result3).to.be.eql({myId: 3});
+    });
+
+    it('generates a new identifier for a primary key of an "any" type', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          myId: {
+            type: DataType.ANY,
+            primaryKey: true,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const result1 = await adapter.replaceOrCreate('model', {});
+      const result2 = await adapter.replaceOrCreate('model', {});
+      const result3 = await adapter.replaceOrCreate('model', {});
+      expect(result1).to.be.eql({myId: 1});
+      expect(result2).to.be.eql({myId: 2});
+      expect(result3).to.be.eql({myId: 3});
+    });
+
+    it('throws an error when generating a new value for a primary key of a "string" type', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          myId: {
+            type: DataType.STRING,
+            primaryKey: true,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const promise = adapter.replaceOrCreate('model', {
+        foo: 'string',
+        bar: 10,
+      });
+      await expect(promise).to.be.rejectedWith(
+        'The memory adapter able to generate only Number identifiers, ' +
+          'but the primary key "myId" of the model "model" is defined as String. ' +
+          'Do provide your own value for the "myId" property, or change the type ' +
+          'in the primary key definition to a Number that will be ' +
+          'generated automatically.',
+      );
+    });
+
+    it('throws an error when generating a new value for a primary key of a "boolean" type', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          myId: {
+            type: DataType.BOOLEAN,
+            primaryKey: true,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const promise = adapter.replaceOrCreate('model', {
+        foo: 'string',
+        bar: 10,
+      });
+      await expect(promise).to.be.rejectedWith(
+        'The memory adapter able to generate only Number identifiers, ' +
+          'but the primary key "myId" of the model "model" is defined as Boolean. ' +
+          'Do provide your own value for the "myId" property, or change the type ' +
+          'in the primary key definition to a Number that will be ' +
+          'generated automatically.',
+      );
+    });
+
+    it('throws an error when generating a new value for a primary key of an "array" type', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          myId: {
+            type: DataType.ARRAY,
+            itemType: DataType.NUMBER,
+            primaryKey: true,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const promise = adapter.replaceOrCreate('model', {});
+      await expect(promise).to.be.rejectedWith(
+        'The memory adapter able to generate only Number identifiers, ' +
+          'but the primary key "myId" of the model "model" is defined as Array. ' +
+          'Do provide your own value for the "myId" property, or change the type ' +
+          'in the primary key definition to a Number that will be ' +
+          'generated automatically.',
+      );
+    });
+
+    it('throws an error when generating a new value for a primary key of an "object" type', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          myId: {
+            type: DataType.OBJECT,
+            primaryKey: true,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const promise = adapter.replaceOrCreate('model', {});
+      await expect(promise).to.be.rejectedWith(
+        'The memory adapter able to generate only Number identifiers, ' +
+          'but the primary key "myId" of the model "model" is defined as Object. ' +
+          'Do provide your own value for the "myId" property, or change the type ' +
+          'in the primary key definition to a Number that will be ' +
+          'generated automatically.',
+      );
+    });
+
+    it('allows to specify an identifier value for a new item', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const idValue = 5;
+      const input = {foo: 'string', bar: 10};
+      const created = await adapter.replaceOrCreate('model', {
+        [DEF_PK]: idValue,
+        ...input,
+      });
+      expect(created).to.be.eql({[DEF_PK]: idValue, ...input});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({[DEF_PK]: idValue, ...input});
+    });
+
+    it('sets default values if they are not provided for a new item', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            default: 10,
+          },
+          bar: {
+            type: DataType.STRING,
+            default: 'string',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const created = await adapter.replaceOrCreate('model', {});
+      const idValue = created[DEF_PK];
+      const defaults = {foo: 10, bar: 'string'};
+      expect(created).to.be.eql({[DEF_PK]: idValue, ...defaults});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({[DEF_PK]: idValue, ...defaults});
+    });
+
+    it('sets default values for properties provided with an undefined value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            default: 1,
+          },
+          bar: {
+            type: DataType.NUMBER,
+            default: 2,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const created = await adapter.replaceOrCreate('model', {foo: undefined});
+      const idValue = created[DEF_PK];
+      const defaults = {foo: 1, bar: 2};
+      expect(created).to.be.eql({[DEF_PK]: idValue, ...defaults});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({[DEF_PK]: idValue, ...defaults});
+    });
+
+    it('sets default values for properties provided with a null value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            default: 1,
+          },
+          bar: {
+            type: DataType.NUMBER,
+            default: 2,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const created = await adapter.replaceOrCreate('model', {foo: null});
+      const idValue = created[DEF_PK];
+      const defaults = {foo: 1, bar: 2};
+      expect(created).to.be.eql({[DEF_PK]: idValue, ...defaults});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({[DEF_PK]: idValue, ...defaults});
+    });
+
+    it('uses a specified column name for a primary key', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            primaryKey: true,
+            columnName: 'bar',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const created = await adapter.replaceOrCreate('model', {});
+      expect(created).to.be.eql({foo: created.foo});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(created.foo);
+      expect(tableData).to.be.eql({bar: created.foo});
+    });
+
+    it('uses a specified column name for a regular property', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            columnName: 'bar',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const created = await adapter.replaceOrCreate('model', {foo: 10});
+      const idValue = created[DEF_PK];
+      expect(created).to.be.eql({[DEF_PK]: idValue, foo: 10});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({[DEF_PK]: idValue, bar: 10});
+    });
+
+    it('uses a specified column name for a regular property with a default value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            columnName: 'bar',
+            default: 10,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const created = await adapter.replaceOrCreate('model', {});
+      const idValue = created[DEF_PK];
+      expect(created).to.be.eql({[DEF_PK]: idValue, foo: 10});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({[DEF_PK]: idValue, bar: 10});
+    });
+
+    it('uses a short form of a fields clause to filter a return value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const input = {foo: 'string', bar: 10};
+      const filter = {fields: 'foo'};
+      const result = await adapter.replaceOrCreate('model', input, filter);
+      expect(result).to.be.eql({[DEF_PK]: result[DEF_PK], foo: input.foo});
+    });
+
+    it('uses a full form of a fields clause to filter a return value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.NUMBER,
+          baz: DataType.BOOLEAN,
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const input = {foo: 'string', bar: 10, baz: true};
+      const filter = {fields: ['foo', 'bar']};
+      const result = await adapter.replaceOrCreate('model', input, filter);
+      expect(result).to.be.eql({
+        [DEF_PK]: result[DEF_PK],
+        foo: input.foo,
+        bar: input.bar,
+      });
+    });
+
+    it('a fields clause uses property names instead of column names', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.STRING,
+            columnName: 'fooCol',
+          },
+          bar: {
+            type: DataType.NUMBER,
+            columnName: 'barCol',
+          },
+          baz: {
+            type: DataType.BOOLEAN,
+            columnName: 'bazCol',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const input = {foo: 'string', bar: 10, baz: true};
+      const filter = {fields: ['foo', 'bar']};
+      const result = await adapter.replaceOrCreate('model', input, filter);
+      expect(result).to.be.eql({
+        [DEF_PK]: result[DEF_PK],
+        foo: input.foo,
+        bar: input.bar,
+      });
+    });
+
+    it('removes properties when replacing an item by a given identifier', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.NUMBER,
+          bar: DataType.NUMBER,
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const input = {foo: 1, bar: 2};
+      const created = await adapter.create('model', input);
+      const idValue = created[DEF_PK];
+      expect(created).to.be.eql({[DEF_PK]: idValue, ...input});
+      const replacement = {[DEF_PK]: idValue, foo: 2};
+      const replaced = await adapter.replaceOrCreate('model', replacement);
+      expect(replaced).to.be.eql(replacement);
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql(replacement);
+    });
+
+    it('sets a default values for removed properties when replacing an item', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            default: 1,
+          },
+          bar: {
+            type: DataType.NUMBER,
+            default: 2,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const created = await adapter.create('model', {});
+      const idValue = created[DEF_PK];
+      const defaults = {foo: 1, bar: 2};
+      expect(created).to.be.eql({[DEF_PK]: idValue, ...defaults});
+      const replacement = {[DEF_PK]: idValue, foo: 2};
+      const replaced = await adapter.replaceOrCreate('model', replacement);
+      expect(replaced).to.be.eql({...defaults, ...replacement});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({...defaults, ...replacement});
+    });
+
+    it('sets a default values for replaced properties with an undefined value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            default: 1,
+          },
+          bar: {
+            type: DataType.NUMBER,
+            default: 2,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const created = await adapter.create('model', {});
+      const idValue = created[DEF_PK];
+      const defaults = {foo: 1, bar: 2};
+      expect(created).to.be.eql({[DEF_PK]: idValue, ...defaults});
+      const replaced = await adapter.replaceOrCreate('model', {
+        [DEF_PK]: idValue,
+        foo: undefined,
+      });
+      expect(replaced).to.be.eql({[DEF_PK]: idValue, ...defaults});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({[DEF_PK]: idValue, ...defaults});
+    });
+
+    it('sets a default values for replaced properties with a null value', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: {
+            type: DataType.NUMBER,
+            default: 1,
+          },
+          bar: {
+            type: DataType.NUMBER,
+            default: 2,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const created = await adapter.create('model', {});
+      const idValue = created[DEF_PK];
+      const defaults = {foo: 1, bar: 2};
+      expect(created).to.be.eql({[DEF_PK]: idValue, ...defaults});
+      const replaced = await adapter.replaceOrCreate('model', {
+        [DEF_PK]: idValue,
+        foo: null,
+      });
+      expect(replaced).to.be.eql({[DEF_PK]: idValue, ...defaults});
+      const table = adapter._getTableOrCreate('model');
+      const tableData = table.get(idValue);
+      expect(tableData).to.be.eql({[DEF_PK]: idValue, ...defaults});
+    });
+  });
+
   describe('patch', function () {
     it('updates only provided properties for all items and returns their number', async function () {
       const schema = new Schema();

+ 6 - 0
src/adapter/decorator/data-sanitizing-decorator.js

@@ -35,6 +35,12 @@ export class DataSanitizingDecorator extends Service {
       return replaceById.call(this, modelName, id, modelData, filter);
     };
 
+    const replaceOrCreate = adapter.replaceOrCreate;
+    adapter.replaceOrCreate = async function (modelName, modelData, filter) {
+      modelData = sanitize(modelName, modelData);
+      return replaceOrCreate.call(this, modelName, modelData, filter);
+    };
+
     const patch = adapter.patch;
     adapter.patch = async function (modelName, modelData, where) {
       modelData = sanitize(modelName, modelData);

+ 13 - 0
src/adapter/decorator/data-sanitizing-decorator.spec.js

@@ -18,6 +18,11 @@ class TestAdapter extends Adapter {
     return Promise.resolve({});
   }
 
+  // eslint-disable-next-line no-unused-vars
+  replaceOrCreate(modelName, modelData, filter = undefined) {
+    return Promise.resolve({});
+  }
+
   // eslint-disable-next-line no-unused-vars
   patch(modelName, modelData, where = undefined) {
     return Promise.resolve(1);
@@ -54,6 +59,14 @@ describe('DataSanitizingDecorator', function () {
     expect(V.sanitize).to.be.called.with.exactly('model', data);
   });
 
+  it('overrides the "replaceOrCreate" method and sanitizes a given data', async function () {
+    sandbox.on(V, 'sanitize');
+    const data = {};
+    await A.replaceOrCreate('model', data);
+    expect(V.sanitize).to.be.called.once;
+    expect(V.sanitize).to.be.called.with.exactly('model', data);
+  });
+
   it('overrides the "patch" method and sanitizes a given data', async function () {
     sandbox.on(V, 'sanitize');
     const data = {};

+ 6 - 0
src/adapter/decorator/data-validation-decorator.js

@@ -32,6 +32,12 @@ export class DataValidationDecorator extends Service {
       return replaceById.call(this, modelName, id, modelData, filter);
     };
 
+    const replaceOrCreate = adapter.replaceOrCreate;
+    adapter.replaceOrCreate = function (modelName, modelData, filter) {
+      this.getService(ModelDataValidator).validate(modelName, modelData);
+      return replaceOrCreate.call(this, modelName, modelData, filter);
+    };
+
     const patch = adapter.patch;
     adapter.patch = function (modelName, modelData, where) {
       this.getService(ModelDataValidator).validate(modelName, modelData, true);

+ 13 - 0
src/adapter/decorator/data-validation-decorator.spec.js

@@ -18,6 +18,11 @@ class TestAdapter extends Adapter {
     return Promise.resolve({});
   }
 
+  // eslint-disable-next-line no-unused-vars
+  replaceOrCreate(modelName, modelData, filter = undefined) {
+    return Promise.resolve({});
+  }
+
   // eslint-disable-next-line no-unused-vars
   patch(modelName, modelData, where = undefined) {
     return Promise.resolve(1);
@@ -54,6 +59,14 @@ describe('DataValidationDecorator', function () {
     expect(V.validate).to.be.called.with.exactly('model', data);
   });
 
+  it('overrides the "replaceOrCreate" method and validates a given data', async function () {
+    sandbox.on(V, 'validate');
+    const data = {};
+    await A.replaceOrCreate('model', data);
+    expect(V.validate).to.be.called.once;
+    expect(V.validate).to.be.called.with.exactly('model', data);
+  });
+
   it('overrides the "patchById" method and validates a given data', async function () {
     sandbox.on(V, 'validate');
     const data = {};

+ 6 - 0
src/adapter/decorator/default-values-decorator.js

@@ -36,6 +36,12 @@ export class DefaultValuesDecorator extends Service {
       return replaceById.call(this, modelName, id, modelData, filter);
     };
 
+    const replaceOrCreate = adapter.replaceOrCreate;
+    adapter.replaceOrCreate = function (modelName, modelData, filter) {
+      modelData = setDefaults(modelName, modelData);
+      return replaceOrCreate.call(this, modelName, modelData, filter);
+    };
+
     const patch = adapter.patch;
     adapter.patch = function (modelName, modelData, where) {
       modelData = setDefaults(modelName, modelData, true);

+ 16 - 0
src/adapter/decorator/default-values-decorator.spec.js

@@ -29,6 +29,11 @@ class TestAdapter extends Adapter {
     return modelData;
   }
 
+  // eslint-disable-next-line no-unused-vars
+  async replaceOrCreate(modelName, modelData, filter = undefined) {
+    return modelData;
+  }
+
   // eslint-disable-next-line no-unused-vars
   patch(modelName, modelData, where = undefined) {
     return Promise.resolve(modelData);
@@ -81,6 +86,17 @@ describe('DefaultValuesDecorator', function () {
     );
   });
 
+  it('overrides the "replaceOrCreate" method and sets default values to input data', async function () {
+    sandbox.on(U, 'setDefaultValuesToEmptyProperties');
+    const retval = await A.replaceOrCreate('model', INPUT_DATA);
+    expect(retval).to.be.eql({prop: 'value'});
+    expect(U.setDefaultValuesToEmptyProperties).to.be.called.once;
+    expect(U.setDefaultValuesToEmptyProperties).to.be.called.with.exactly(
+      'model',
+      INPUT_DATA,
+    );
+  });
+
   describe('overrides the "patch" method and sets default values to input data', function () {
     it('does not set default values to not existing properties of input data', async function () {
       sandbox.on(U, 'setDefaultValuesToEmptyProperties');

+ 13 - 0
src/adapter/decorator/fields-filtering-decorator.js

@@ -45,6 +45,19 @@ export class FieldsFilteringDecorator extends Service {
       return result;
     };
 
+    const replaceOrCreate = adapter.replaceOrCreate;
+    adapter.replaceOrCreate = async function (modelName, modelData, filter) {
+      let result = await replaceOrCreate.call(
+        this,
+        modelName,
+        modelData,
+        filter,
+      );
+      if (filter && typeof filter === 'object' && filter.fields)
+        result = selectFields(result, modelName, filter.fields);
+      return result;
+    };
+
     const patchById = adapter.patchById;
     adapter.patchById = async function (modelName, id, modelData, filter) {
       let result = await patchById.call(this, modelName, id, modelData, filter);

+ 17 - 0
src/adapter/decorator/fields-filtering-decorator.spec.js

@@ -32,6 +32,11 @@ class TestAdapter extends Adapter {
     return MODEL_DATA;
   }
 
+  // eslint-disable-next-line no-unused-vars
+  async replaceOrCreate(modelName, modelData, filter = undefined) {
+    return MODEL_DATA;
+  }
+
   // eslint-disable-next-line no-unused-vars
   async patchById(modelName, id, modelData, filter = undefined) {
     return MODEL_DATA;
@@ -81,6 +86,18 @@ describe('FieldsFilteringDecorator', function () {
     );
   });
 
+  it('overrides the "replaceOrCreate" method and filtering output fields', async function () {
+    sandbox.on(T, 'filter');
+    const retval = await A.replaceOrCreate(MODEL_NAME, {}, FILTER);
+    expect(retval).to.be.eql(RETVAL_DATA);
+    expect(T.filter).to.be.called.once;
+    expect(T.filter).to.be.called.with.exactly(
+      MODEL_DATA,
+      MODEL_NAME,
+      FILTER.fields,
+    );
+  });
+
   it('overrides the "patchById" method and filtering output fields', async function () {
     sandbox.on(T, 'filter');
     const retval = await A.patchById(MODEL_NAME, 1, {}, FILTER);

+ 13 - 0
src/adapter/decorator/inclusion-decorator.js

@@ -45,6 +45,19 @@ export class InclusionDecorator extends Service {
       return retvalData;
     };
 
+    const replaceOrCreate = adapter.replaceOrCreate;
+    adapter.replaceOrCreate = async function (modelName, modelData, filter) {
+      const retvalData = await replaceOrCreate.call(
+        this,
+        modelName,
+        modelData,
+        filter,
+      );
+      if (filter && typeof filter === 'object' && filter.include)
+        await includeTo([retvalData], modelName, filter.include);
+      return retvalData;
+    };
+
     const patchById = adapter.patchById;
     adapter.patchById = async function (modelName, id, modelData, filter) {
       const retvalData = await patchById.call(

+ 17 - 0
src/adapter/decorator/inclusion-decorator.spec.js

@@ -30,6 +30,11 @@ class TestAdapter extends Adapter {
     return Object.assign({}, MODEL_DATA);
   }
 
+  // eslint-disable-next-line no-unused-vars
+  async replaceOrCreate(modelName, modelData, filter = undefined) {
+    return Object.assign({}, MODEL_DATA);
+  }
+
   // eslint-disable-next-line no-unused-vars
   async patchById(modelName, id, modelData, filter = undefined) {
     return Object.assign({}, MODEL_DATA);
@@ -79,6 +84,18 @@ describe('InclusionDecorator', function () {
     expect(T.includeTo).to.be.called.once;
   });
 
+  it('overrides the "replaceOrCreate" method and applies clause inclusion', async function () {
+    sandbox.on(T, 'includeTo', function (entities, modelName, clause) {
+      expect(entities).to.be.eql([MODEL_DATA]);
+      expect(modelName).to.be.eql('model');
+      expect(clause).to.be.eql(FILTER.include);
+      Object.assign(entities[0], RETVAL_DATA);
+    });
+    const retval = await A.replaceOrCreate('model', {}, FILTER);
+    expect(retval).to.be.eql(RETVAL_DATA);
+    expect(T.includeTo).to.be.called.once;
+  });
+
   it('overrides the "patchById" method and applies clause inclusion', async function () {
     sandbox.on(T, 'includeTo', function (entities, modelName, clause) {
       expect(entities).to.be.eql([MODEL_DATA]);

+ 2 - 6
src/repository/repository.js

@@ -103,12 +103,8 @@ export class Repository extends Service {
    * @returns {Promise<object>}
    */
   async replaceOrCreate(data, filter = undefined) {
-    const pkPropName = this.getService(
-      ModelDefinitionUtils,
-    ).getPrimaryKeyAsPropertyName(this.modelName);
-    const pkValue = data[pkPropName];
-    if (pkValue == null) return this.create(data, filter);
-    return this.replaceById(pkValue, data, filter);
+    const adapter = await this.getAdapter();
+    return adapter.replaceOrCreate(this.modelName, data, filter);
   }
 
   /**

Some files were not shown because too many files changed in this diff