Browse Source

fix: do not match undefined with null

e22m4u 2 years ago
parent
commit
0da22c72ce

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

@@ -52,6 +52,19 @@ export declare class Adapter extends Service {
     filter?: ItemFilterClause,
   ): Promise<ModelData>;
 
+  /**
+   * Patch.
+   *
+   * @param modelName
+   * @param modelData
+   * @param where
+   */
+  patch(
+    modelName: string,
+    modelData: ModelData,
+    where?: WhereClause,
+  ): Promise<number>;
+
   /**
    * Patch by id.
    *

+ 15 - 0
src/adapter/adapter.js

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

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

@@ -154,6 +154,47 @@ export class MemoryAdapter extends Adapter {
     ).convertColumnNamesToPropertyNames(modelName, tableData);
   }
 
+  /**
+   * Patch.
+   *
+   * @param {string} modelName
+   * @param {object} modelData
+   * @param {object|undefined} where
+   * @returns {Promise<number>}
+   */
+  async patch(modelName, modelData, where = undefined) {
+    const table = this._getTableOrCreate(modelName);
+    const tableItems = Array.from(table.values());
+    if (!tableItems.length) return 0;
+    let modelItems = tableItems.map(tableItem =>
+      this.getService(ModelDefinitionUtils).convertColumnNamesToPropertyNames(
+        modelName,
+        tableItem,
+      ),
+    );
+
+    if (where && typeof where === 'object')
+      modelItems = this.getService(WhereClauseTool).filter(modelItems, where);
+    const size = modelItems.length;
+
+    const pkPropName =
+      this.getService(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(
+        modelName,
+      );
+    modelData = cloneDeep(modelData);
+    delete modelData[pkPropName];
+
+    modelItems.forEach(existingModelData => {
+      const mergedModelData = Object.assign({}, existingModelData, modelData);
+      const mergedTableData = this.getService(
+        ModelDefinitionUtils,
+      ).convertPropertyNamesToColumnNames(modelName, mergedModelData);
+      const idValue = existingModelData[pkPropName];
+      table.set(idValue, mergedTableData);
+    });
+    return size;
+  }
+
   /**
    * Patch by id.
    *

+ 575 - 7
src/adapter/builtin/memory-adapter.spec.js

@@ -1133,6 +1133,574 @@ describe('MemoryAdapter', function () {
     });
   });
 
+  describe('patch', function () {
+    it('updates only provided properties for all items and returns their number', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.STRING,
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const input1 = {foo: 'a1', bar: 'a2'};
+      const input2 = {foo: 'b1', bar: 'b2'};
+      const input3 = {foo: 'c1', bar: 'c2'};
+      const created1 = await adapter.create('model', input1);
+      const created2 = await adapter.create('model', input2);
+      const created3 = await adapter.create('model', input3);
+      const id1 = created1[DEF_PK];
+      const id2 = created2[DEF_PK];
+      const id3 = created3[DEF_PK];
+      const table = adapter._getTableOrCreate('model');
+      const createdItems = Array.from(table.values());
+      expect(createdItems).to.be.eql([
+        {[DEF_PK]: id1, ...input1},
+        {[DEF_PK]: id2, ...input2},
+        {[DEF_PK]: id3, ...input3},
+      ]);
+      const result = await adapter.patch('model', {foo: 'd1'});
+      expect(result).to.be.eq(3);
+      const patchedItems = Array.from(table.values());
+      expect(patchedItems).to.be.eql([
+        {[DEF_PK]: id1, foo: 'd1', bar: 'a2'},
+        {[DEF_PK]: id2, foo: 'd1', bar: 'b2'},
+        {[DEF_PK]: id3, foo: 'd1', bar: 'c2'},
+      ]);
+    });
+
+    it('does not throw an error if a partial data does not have required property', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: {
+            type: DataType.STRING,
+            required: true,
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const input1 = {foo: 'a1', bar: 'a2'};
+      const input2 = {foo: 'b1', bar: 'b2'};
+      const input3 = {foo: 'c1', bar: 'c2'};
+      const created1 = await adapter.create('model', input1);
+      const created2 = await adapter.create('model', input2);
+      const created3 = await adapter.create('model', input3);
+      const id1 = created1[DEF_PK];
+      const id2 = created2[DEF_PK];
+      const id3 = created3[DEF_PK];
+      const table = adapter._getTableOrCreate('model');
+      const createdItems = Array.from(table.values());
+      expect(createdItems).to.be.eql([
+        {[DEF_PK]: id1, ...input1},
+        {[DEF_PK]: id2, ...input2},
+        {[DEF_PK]: id3, ...input3},
+      ]);
+      const result = await adapter.patch('model', {foo: 'd1'});
+      expect(result).to.be.eq(3);
+      const patchedItems = Array.from(table.values());
+      expect(patchedItems).to.be.eql([
+        {[DEF_PK]: id1, foo: 'd1', bar: 'a2'},
+        {[DEF_PK]: id2, foo: 'd1', bar: 'b2'},
+        {[DEF_PK]: id3, foo: 'd1', bar: 'c2'},
+      ]);
+    });
+
+    it('ignores identifier value in a given data in case of a default primary key', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.STRING,
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const input1 = {foo: 'a1', bar: 'a2'};
+      const input2 = {foo: 'b1', bar: 'b2'};
+      const input3 = {foo: 'c1', bar: 'c2'};
+      const created1 = await adapter.create('model', input1);
+      const created2 = await adapter.create('model', input2);
+      const created3 = await adapter.create('model', input3);
+      const id1 = created1[DEF_PK];
+      const id2 = created2[DEF_PK];
+      const id3 = created3[DEF_PK];
+      const table = adapter._getTableOrCreate('model');
+      const createdItems = Array.from(table.values());
+      expect(createdItems).to.be.eql([
+        {[DEF_PK]: id1, ...input1},
+        {[DEF_PK]: id2, ...input2},
+        {[DEF_PK]: id3, ...input3},
+      ]);
+      const result = await adapter.patch('model', {[DEF_PK]: 100, foo: 'd1'});
+      expect(result).to.be.eq(3);
+      const patchedItems = Array.from(table.values());
+      expect(patchedItems).to.be.eql([
+        {[DEF_PK]: id1, foo: 'd1', bar: 'a2'},
+        {[DEF_PK]: id2, foo: 'd1', bar: 'b2'},
+        {[DEF_PK]: id3, foo: 'd1', bar: 'c2'},
+      ]);
+    });
+
+    it('ignores identifier value in a given data in case of a specified primary key', 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,
+          },
+          foo: DataType.STRING,
+          bar: DataType.STRING,
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const input1 = {foo: 'a1', bar: 'a2'};
+      const input2 = {foo: 'b1', bar: 'b2'};
+      const input3 = {foo: 'c1', bar: 'c2'};
+      const created1 = await adapter.create('model', input1);
+      const created2 = await adapter.create('model', input2);
+      const created3 = await adapter.create('model', input3);
+      const id1 = created1.myId;
+      const id2 = created2.myId;
+      const id3 = created3.myId;
+      const table = adapter._getTableOrCreate('model');
+      const createdItems = Array.from(table.values());
+      expect(createdItems).to.be.eql([
+        {myId: id1, ...input1},
+        {myId: id2, ...input2},
+        {myId: id3, ...input3},
+      ]);
+      const result = await adapter.patch('model', {myId: 100, foo: 'd1'});
+      expect(result).to.be.eq(3);
+      const patchedItems = Array.from(table.values());
+      expect(patchedItems).to.be.eql([
+        {myId: id1, foo: 'd1', bar: 'a2'},
+        {myId: id2, foo: 'd1', bar: 'b2'},
+        {myId: id3, foo: 'd1', bar: 'c2'},
+      ]);
+    });
+
+    it('sets a default values for patched 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.STRING,
+            default: 'fooVal',
+          },
+          bar: {
+            type: DataType.STRING,
+            default: 'barVal',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const input1 = {foo: 'a1', bar: 'a2'};
+      const input2 = {foo: 'b1', bar: 'b2'};
+      const input3 = {foo: 'c1', bar: 'c2'};
+      const created1 = await adapter.create('model', input1);
+      const created2 = await adapter.create('model', input2);
+      const created3 = await adapter.create('model', input3);
+      const id1 = created1[DEF_PK];
+      const id2 = created2[DEF_PK];
+      const id3 = created3[DEF_PK];
+      const table = adapter._getTableOrCreate('model');
+      const createdItems = Array.from(table.values());
+      expect(createdItems).to.be.eql([
+        {[DEF_PK]: id1, ...input1},
+        {[DEF_PK]: id2, ...input2},
+        {[DEF_PK]: id3, ...input3},
+      ]);
+      const result = await adapter.patch('model', {foo: undefined});
+      expect(result).to.be.eq(3);
+      const patchedItems = Array.from(table.values());
+      expect(patchedItems).to.be.eql([
+        {[DEF_PK]: id1, foo: 'fooVal', bar: 'a2'},
+        {[DEF_PK]: id2, foo: 'fooVal', bar: 'b2'},
+        {[DEF_PK]: id3, foo: 'fooVal', bar: 'c2'},
+      ]);
+    });
+
+    it('sets a default values for patched 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.STRING,
+            default: 'fooVal',
+          },
+          bar: {
+            type: DataType.STRING,
+            default: 'barVal',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const input1 = {foo: 'a1', bar: 'a2'};
+      const input2 = {foo: 'b1', bar: 'b2'};
+      const input3 = {foo: 'c1', bar: 'c2'};
+      const created1 = await adapter.create('model', input1);
+      const created2 = await adapter.create('model', input2);
+      const created3 = await adapter.create('model', input3);
+      const id1 = created1[DEF_PK];
+      const id2 = created2[DEF_PK];
+      const id3 = created3[DEF_PK];
+      const table = adapter._getTableOrCreate('model');
+      const createdItems = Array.from(table.values());
+      expect(createdItems).to.be.eql([
+        {[DEF_PK]: id1, ...input1},
+        {[DEF_PK]: id2, ...input2},
+        {[DEF_PK]: id3, ...input3},
+      ]);
+      const result = await adapter.patch('model', {foo: null});
+      expect(result).to.be.eq(3);
+      const patchedItems = Array.from(table.values());
+      expect(patchedItems).to.be.eql([
+        {[DEF_PK]: id1, foo: 'fooVal', bar: 'a2'},
+        {[DEF_PK]: id2, foo: 'fooVal', bar: 'b2'},
+        {[DEF_PK]: id3, foo: 'fooVal', bar: 'c2'},
+      ]);
+    });
+
+    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.STRING,
+            columnName: 'fooCol',
+          },
+          bar: {
+            type: DataType.STRING,
+            columnName: 'barCol',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const input1 = {foo: 'a1', bar: 'a2'};
+      const input2 = {foo: 'b1', bar: 'b2'};
+      const input3 = {foo: 'c1', bar: 'c2'};
+      const created1 = await adapter.create('model', input1);
+      const created2 = await adapter.create('model', input2);
+      const created3 = await adapter.create('model', input3);
+      const id1 = created1[DEF_PK];
+      const id2 = created2[DEF_PK];
+      const id3 = created3[DEF_PK];
+      const table = adapter._getTableOrCreate('model');
+      const createdItems = Array.from(table.values());
+      expect(createdItems).to.be.eql([
+        {[DEF_PK]: id1, fooCol: 'a1', barCol: 'a2'},
+        {[DEF_PK]: id2, fooCol: 'b1', barCol: 'b2'},
+        {[DEF_PK]: id3, fooCol: 'c1', barCol: 'c2'},
+      ]);
+      const result = await adapter.patch('model', {foo: 'd1'});
+      expect(result).to.be.eq(3);
+      const patchedItems = Array.from(table.values());
+      expect(patchedItems).to.be.eql([
+        {[DEF_PK]: id1, fooCol: 'd1', barCol: 'a2'},
+        {[DEF_PK]: id2, fooCol: 'd1', barCol: 'b2'},
+        {[DEF_PK]: id3, fooCol: 'd1', barCol: 'c2'},
+      ]);
+    });
+
+    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.STRING,
+            columnName: 'fooCol',
+            default: 'fooVal',
+          },
+          bar: {
+            type: DataType.STRING,
+            columnName: 'barCol',
+            default: 'barVal',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const input1 = {foo: 'a1', bar: 'a2'};
+      const input2 = {foo: 'b1', bar: 'b2'};
+      const input3 = {foo: 'c1', bar: 'c2'};
+      const created1 = await adapter.create('model', input1);
+      const created2 = await adapter.create('model', input2);
+      const created3 = await adapter.create('model', input3);
+      const id1 = created1[DEF_PK];
+      const id2 = created2[DEF_PK];
+      const id3 = created3[DEF_PK];
+      const table = adapter._getTableOrCreate('model');
+      const createdItems = Array.from(table.values());
+      expect(createdItems).to.be.eql([
+        {[DEF_PK]: id1, fooCol: 'a1', barCol: 'a2'},
+        {[DEF_PK]: id2, fooCol: 'b1', barCol: 'b2'},
+        {[DEF_PK]: id3, fooCol: 'c1', barCol: 'c2'},
+      ]);
+      const result = await adapter.patch('model', {foo: undefined});
+      expect(result).to.be.eq(3);
+      const patchedItems = Array.from(table.values());
+      expect(patchedItems).to.be.eql([
+        {[DEF_PK]: id1, fooCol: 'fooVal', barCol: 'a2'},
+        {[DEF_PK]: id2, fooCol: 'fooVal', barCol: 'b2'},
+        {[DEF_PK]: id3, fooCol: 'fooVal', barCol: 'c2'},
+      ]);
+    });
+
+    it('returns zero if nothing matched by the "where" clause', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.STRING,
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const input1 = {foo: 'a1', bar: 'a2'};
+      const input2 = {foo: 'b1', bar: 'b2'};
+      const input3 = {foo: 'c1', bar: 'c2'};
+      const created1 = await adapter.create('model', input1);
+      const created2 = await adapter.create('model', input2);
+      const created3 = await adapter.create('model', input3);
+      const id1 = created1[DEF_PK];
+      const id2 = created2[DEF_PK];
+      const id3 = created3[DEF_PK];
+      const table = adapter._getTableOrCreate('model');
+      const createdItems = Array.from(table.values());
+      expect(createdItems).to.be.eql([
+        {[DEF_PK]: id1, ...input1},
+        {[DEF_PK]: id2, ...input2},
+        {[DEF_PK]: id3, ...input3},
+      ]);
+      const result = await adapter.patch('model', {foo: 'test'}, {baz: 'd3'});
+      expect(result).to.be.eq(0);
+      const patchedItems = Array.from(table.values());
+      expect(patchedItems).to.be.eql([
+        {[DEF_PK]: id1, foo: 'a1', bar: 'a2'},
+        {[DEF_PK]: id2, foo: 'b1', bar: 'b2'},
+        {[DEF_PK]: id3, foo: 'c1', bar: 'c2'},
+      ]);
+    });
+
+    it('uses the "where" clause to patch specific items', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: DataType.STRING,
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const input1 = {foo: 'a', bar: '1'};
+      const input2 = {foo: 'b', bar: '2'};
+      const input3 = {foo: 'c', bar: '2'};
+      const created1 = await adapter.create('model', input1);
+      const created2 = await adapter.create('model', input2);
+      const created3 = await adapter.create('model', input3);
+      const id1 = created1[DEF_PK];
+      const id2 = created2[DEF_PK];
+      const id3 = created3[DEF_PK];
+      const table = adapter._getTableOrCreate('model');
+      const createdItems = Array.from(table.values());
+      expect(createdItems).to.be.eql([
+        {[DEF_PK]: id1, ...input1},
+        {[DEF_PK]: id2, ...input2},
+        {[DEF_PK]: id3, ...input3},
+      ]);
+      const result = await adapter.patch('model', {foo: 'd'}, {bar: '2'});
+      expect(result).to.be.eq(2);
+      const patchedItems = Array.from(table.values());
+      expect(patchedItems).to.be.eql([
+        {[DEF_PK]: id1, foo: 'a', bar: '1'},
+        {[DEF_PK]: id2, foo: 'd', bar: '2'},
+        {[DEF_PK]: id3, foo: 'd', bar: '2'},
+      ]);
+    });
+
+    it('the "where" 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: 'fooVal',
+          },
+          bar: {
+            type: DataType.STRING,
+            columnName: 'barVal',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const input1 = {foo: 'a', bar: '1'};
+      const input2 = {foo: 'b', bar: '2'};
+      const input3 = {foo: 'c', bar: '2'};
+      const created1 = await adapter.create('model', input1);
+      const created2 = await adapter.create('model', input2);
+      const created3 = await adapter.create('model', input3);
+      const id1 = created1[DEF_PK];
+      const id2 = created2[DEF_PK];
+      const id3 = created3[DEF_PK];
+      const table = adapter._getTableOrCreate('model');
+      const createdItems = Array.from(table.values());
+      expect(createdItems).to.be.eql([
+        {[DEF_PK]: id1, fooVal: 'a', barVal: '1'},
+        {[DEF_PK]: id2, fooVal: 'b', barVal: '2'},
+        {[DEF_PK]: id3, fooVal: 'c', barVal: '2'},
+      ]);
+      const result = await adapter.patch('model', {foo: 'd'}, {bar: '2'});
+      expect(result).to.be.eq(2);
+      const patchedItems = Array.from(table.values());
+      expect(patchedItems).to.be.eql([
+        {[DEF_PK]: id1, fooVal: 'a', barVal: '1'},
+        {[DEF_PK]: id2, fooVal: 'd', barVal: '2'},
+        {[DEF_PK]: id3, fooVal: 'd', barVal: '2'},
+      ]);
+    });
+
+    it('the "where" clause uses a persisted data instead of default values in case of undefined', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: {
+            type: DataType.STRING,
+            default: 'barVal',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const input1 = {[DEF_PK]: 1, foo: 'a', bar: undefined};
+      const input2 = {[DEF_PK]: 2, foo: 'b', bar: undefined};
+      const input3 = {[DEF_PK]: 3, foo: 'c', bar: 10};
+      const input4 = {[DEF_PK]: 4, foo: 'd', bar: null};
+      const table = adapter._getTableOrCreate('model');
+      table.set(input1[DEF_PK], input1);
+      table.set(input2[DEF_PK], input2);
+      table.set(input3[DEF_PK], input3);
+      table.set(input4[DEF_PK], input4);
+      const result = await adapter.patch('model', {foo: 'e'}, {bar: undefined});
+      expect(result).to.be.eq(2);
+      const patchedItems = Array.from(table.values());
+      expect(patchedItems).to.be.eql([
+        {[DEF_PK]: 1, foo: 'e', bar: undefined},
+        {[DEF_PK]: 2, foo: 'e', bar: undefined},
+        {[DEF_PK]: 3, foo: 'c', bar: 10},
+        {[DEF_PK]: 4, foo: 'd', bar: null},
+      ]);
+    });
+
+    it('the "where" clause uses a persisted data instead of default values in case of null', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({
+        name: 'memory',
+        adapter: 'memory',
+      });
+      schema.defineModel({
+        name: 'model',
+        datasource: 'memory',
+        properties: {
+          foo: DataType.STRING,
+          bar: {
+            type: DataType.STRING,
+            default: 'barVal',
+          },
+        },
+      });
+      const adapter = new MemoryAdapter(schema.container, {});
+      const input1 = {[DEF_PK]: 1, foo: 'a', bar: undefined};
+      const input2 = {[DEF_PK]: 2, foo: 'b', bar: undefined};
+      const input3 = {[DEF_PK]: 3, foo: 'c', bar: 10};
+      const input4 = {[DEF_PK]: 4, foo: 'd', bar: null};
+      const table = adapter._getTableOrCreate('model');
+      table.set(input1[DEF_PK], input1);
+      table.set(input2[DEF_PK], input2);
+      table.set(input3[DEF_PK], input3);
+      table.set(input4[DEF_PK], input4);
+      const result = await adapter.patch('model', {foo: 'e'}, {bar: null});
+      expect(result).to.be.eq(1);
+      const patchedItems = Array.from(table.values());
+      expect(patchedItems).to.be.eql([
+        {[DEF_PK]: 1, foo: 'a', bar: undefined},
+        {[DEF_PK]: 2, foo: 'b', bar: undefined},
+        {[DEF_PK]: 3, foo: 'c', bar: 10},
+        {[DEF_PK]: 4, foo: 'e', bar: null},
+      ]);
+    });
+  });
+
   describe('patchById', function () {
     it('updates only provided properties by a given identifier', async function () {
       const schema = new Schema();
@@ -2050,7 +2618,7 @@ describe('MemoryAdapter', function () {
       expect(result4[2]).to.be.eql({[DEF_PK]: 2, foo: 2, bar: 10});
     });
 
-    it('allows to specify a where clause to filter a return value', async function () {
+    it('allows to specify the "where" clause to filter a return value', async function () {
       const schema = new Schema();
       schema.defineDatasource({
         name: 'memory',
@@ -2087,7 +2655,7 @@ describe('MemoryAdapter', function () {
       expect(result3[1]).to.be.eql({[DEF_PK]: 2, ...input2});
     });
 
-    it('a where clause uses property names instead of column names', async function () {
+    it('the "where" clause uses property names instead of column names', async function () {
       const schema = new Schema();
       schema.defineDatasource({
         name: 'memory',
@@ -2144,7 +2712,7 @@ describe('MemoryAdapter', function () {
       expect(result3[1]).to.be.eql({[DEF_PK]: 2, ...input2});
     });
 
-    it('a where clause uses a persisted data instead of default values', async function () {
+    it('the "where" clause uses a persisted data instead of default values', async function () {
       const schema = new Schema();
       schema.defineDatasource({
         name: 'memory',
@@ -2585,7 +3153,7 @@ describe('MemoryAdapter', function () {
       expect(table.get(2)).to.be.eql({[DEF_PK]: 2, foo: 10});
     });
 
-    it('a where clause uses property names instead of column names', async function () {
+    it('the "where" clause uses property names instead of column names', async function () {
       const schema = new Schema();
       schema.defineDatasource({
         name: 'memory',
@@ -2612,7 +3180,7 @@ describe('MemoryAdapter', function () {
       expect(table.get(2)).to.be.eql({[DEF_PK]: 2, fooCol: 10});
     });
 
-    it('a where clause uses a persisted data instead of default values', async function () {
+    it('the "where" clause uses a persisted data instead of default values', async function () {
       const schema = new Schema();
       schema.defineDatasource({
         name: 'memory',
@@ -2863,7 +3431,7 @@ describe('MemoryAdapter', function () {
       expect(table.get(3)).to.be.eql({[DEF_PK]: 3, foo: 15});
     });
 
-    it('a where clause uses property names instead of column names', async function () {
+    it('the "where" clause uses property names instead of column names', async function () {
       const schema = new Schema();
       schema.defineDatasource({
         name: 'memory',
@@ -2893,7 +3461,7 @@ describe('MemoryAdapter', function () {
       expect(table.get(3)).to.be.eql({[DEF_PK]: 3, fooCol: 15});
     });
 
-    it('a where clause uses a persisted data instead of default values', async function () {
+    it('the "where" clause uses a persisted data instead of default values', async function () {
       const schema = new Schema();
       schema.defineDatasource({
         name: 'memory',

+ 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 patch = adapter.patch;
+    adapter.patch = async function (modelName, modelData, where) {
+      modelData = sanitize(modelName, modelData);
+      return patch.call(this, modelName, modelData, where);
+    };
+
     const patchById = adapter.patchById;
     adapter.patchById = async function (modelName, id, modelData, filter) {
       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
+  patch(modelName, modelData, where = undefined) {
+    return Promise.resolve(1);
+  }
+
   // eslint-disable-next-line no-unused-vars
   patchById(modelName, id, modelData, filter = undefined) {
     return Promise.resolve({});
@@ -49,6 +54,14 @@ describe('DataSanitizingDecorator', function () {
     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 = {};
+    await A.patch('model', data);
+    expect(V.sanitize).to.be.called.once;
+    expect(V.sanitize).to.be.called.with.exactly('model', data);
+  });
+
   it('overrides the "patchById" 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 patch = adapter.patch;
+    adapter.patch = function (modelName, modelData, where) {
+      this.getService(ModelDataValidator).validate(modelName, modelData, true);
+      return patch.call(this, modelName, modelData, where);
+    };
+
     const patchById = adapter.patchById;
     adapter.patchById = function (modelName, id, modelData, filter) {
       this.getService(ModelDataValidator).validate(modelName, modelData, true);

+ 5 - 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
+  patch(modelName, modelData, where = undefined) {
+    return Promise.resolve(1);
+  }
+
   // eslint-disable-next-line no-unused-vars
   patchById(modelName, id, modelData, filter = undefined) {
     return Promise.resolve({});

+ 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 patch = adapter.patch;
+    adapter.patch = function (modelName, modelData, where) {
+      modelData = setDefaults(modelName, modelData, true);
+      return patch.call(this, modelName, modelData, where);
+    };
+
     const patchById = adapter.patchById;
     adapter.patchById = function (modelName, id, modelData, filter) {
       modelData = setDefaults(modelName, modelData, true);

+ 46 - 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
+  patch(modelName, modelData, where = undefined) {
+    return Promise.resolve(modelData);
+  }
+
   // eslint-disable-next-line no-unused-vars
   async patchById(modelName, id, modelData, filter = undefined) {
     return modelData;
@@ -76,6 +81,47 @@ describe('DefaultValuesDecorator', function () {
     );
   });
 
+  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');
+      const data = {};
+      const retval = await A.patch('model', data);
+      expect(retval).to.be.eql({});
+      expect(U.setDefaultValuesToEmptyProperties).to.be.called.once;
+      expect(U.setDefaultValuesToEmptyProperties).to.be.called.with.exactly(
+        'model',
+        data,
+        true,
+      );
+    });
+
+    it('does set default values to input properties of null', async function () {
+      sandbox.on(U, 'setDefaultValuesToEmptyProperties');
+      const data = {prop: null};
+      const retval = await A.patch('model', data);
+      expect(retval).to.be.eql({prop: 'value'});
+      expect(U.setDefaultValuesToEmptyProperties).to.be.called.once;
+      expect(U.setDefaultValuesToEmptyProperties).to.be.called.with.exactly(
+        'model',
+        data,
+        true,
+      );
+    });
+
+    it('does set default values to input properties of undefined', async function () {
+      sandbox.on(U, 'setDefaultValuesToEmptyProperties');
+      const data = {prop: undefined};
+      const retval = await A.patch('model', data);
+      expect(retval).to.be.eql({prop: 'value'});
+      expect(U.setDefaultValuesToEmptyProperties).to.be.called.once;
+      expect(U.setDefaultValuesToEmptyProperties).to.be.called.with.exactly(
+        'model',
+        data,
+        true,
+      );
+    });
+  });
+
   describe('overrides the "patchById" 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');

+ 6 - 2
src/filter/where-clause-tool.js

@@ -127,8 +127,12 @@ export class WhereClauseTool extends Service {
    */
   _test(example, value) {
     // Test null.
-    if (example == null) {
-      return value == null;
+    if (example === null) {
+      return value === null;
+    }
+    // Test undefined.
+    if (example === undefined) {
+      return value === undefined;
     }
     // Test RegExp.
     // noinspection ALL

+ 8 - 3
src/filter/where-clause-tool.spec.js

@@ -266,11 +266,16 @@ describe('WhereClauseTool', function () {
       expect(result[0]).to.be.eql(OBJECTS[2]);
     });
 
-    it('uses null to match an undefined and null value', function () {
+    it('does not use null to match an undefined value', function () {
       const result = S.filter(OBJECTS, {nickname: null});
-      expect(result).to.have.length(2);
+      expect(result).to.have.length(1);
       expect(result[0]).to.be.eql(OBJECTS[2]);
-      expect(result[1]).to.be.eql(OBJECTS[3]);
+    });
+
+    it('does not use undefined to match a null value', function () {
+      const result = S.filter(OBJECTS, {nickname: undefined});
+      expect(result).to.have.length(1);
+      expect(result[0]).to.be.eql(OBJECTS[3]);
     });
   });
 

+ 11 - 0
src/repository/repository.d.ts

@@ -77,6 +77,17 @@ export declare class Repository<
     filter?: ItemFilterClause,
   ): Promise<FlatData>;
 
+  /**
+   * Patch.
+   *
+   * @param data
+   * @param where
+   */
+  patch(
+    data: PartialWithoutId<IdName, Data>,
+    where?: WhereClause,
+  ): Promise<number>;
+
   /**
    * Patch by id.
    *

+ 12 - 0
src/repository/repository.js

@@ -111,6 +111,18 @@ export class Repository extends Service {
     return this.replaceById(pkValue, data, filter);
   }
 
+  /**
+   * Patch.
+   *
+   * @param {object} data
+   * @param {object|undefined} where
+   * @returns {Promise<number>}
+   */
+  async patch(data, where = undefined) {
+    const adapter = await this.getAdapter();
+    return adapter.patch(this.modelName, data, where);
+  }
+
   /**
    * Patch by id.
    *

+ 26 - 0
src/repository/repository.spec.js

@@ -50,6 +50,32 @@ describe('Repository', function () {
     });
   });
 
+  describe('patch', function () {
+    it('patches all items', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({name: 'datasource', adapter: 'memory'});
+      schema.defineModel({name: 'model', datasource: 'datasource'});
+      const rep = schema.getRepository('model');
+      await rep.create({foo: 'a1', bar: 'b1'});
+      await rep.create({foo: 'a2', bar: 'b2'});
+      await rep.create({foo: 'a3', bar: 'b3'});
+      const result = await rep.patch({foo: 'test'});
+      expect(result).to.be.eq(3);
+    });
+
+    it('patches found items by the "where" clause', async function () {
+      const schema = new Schema();
+      schema.defineDatasource({name: 'datasource', adapter: 'memory'});
+      schema.defineModel({name: 'model', datasource: 'datasource'});
+      const rep = schema.getRepository('model');
+      await rep.create({foo: 'a', bar: '1'});
+      await rep.create({foo: 'b', bar: '2'});
+      await rep.create({foo: 'c', bar: '2'});
+      const result = await rep.patch({foo: 'test'}, {bar: '2'});
+      expect(result).to.be.eq(2);
+    });
+  });
+
   describe('patchById', function () {
     it('patches an item by the given id', async function () {
       const schema = new Schema();