@coderich/autograph 0.10.0 → 0.10.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 (42) hide show
  1. package/CHANGELOG.md +21 -3
  2. package/index.js +2 -6
  3. package/package.json +4 -4
  4. package/src/.DS_Store +0 -0
  5. package/src/core/EventEmitter.js +2 -4
  6. package/src/core/Resolver.js +43 -60
  7. package/src/core/Schema.js +3 -36
  8. package/src/data/.DS_Store +0 -0
  9. package/src/data/DataLoader.js +71 -32
  10. package/src/data/DataService.js +59 -58
  11. package/src/data/Field.js +71 -121
  12. package/src/data/Model.js +98 -108
  13. package/src/data/Pipeline.js +174 -0
  14. package/src/data/Type.js +19 -74
  15. package/src/driver/MongoDriver.js +21 -19
  16. package/src/graphql/.DS_Store +0 -0
  17. package/src/graphql/ast/Field.js +43 -24
  18. package/src/graphql/ast/Model.js +5 -16
  19. package/src/graphql/ast/Node.js +0 -25
  20. package/src/graphql/ast/Schema.js +107 -111
  21. package/src/graphql/extension/api.js +20 -18
  22. package/src/graphql/extension/framework.js +25 -33
  23. package/src/graphql/extension/type.js +2 -2
  24. package/src/query/Query.js +72 -14
  25. package/src/query/QueryBuilder.js +38 -30
  26. package/src/query/QueryBuilderTransaction.js +3 -3
  27. package/src/query/QueryResolver.js +92 -42
  28. package/src/query/QueryService.js +31 -34
  29. package/src/service/app.service.js +67 -9
  30. package/src/service/event.service.js +5 -79
  31. package/src/service/schema.service.js +5 -3
  32. package/src/core/Rule.js +0 -107
  33. package/src/core/SchemaDecorator.js +0 -46
  34. package/src/core/Transformer.js +0 -68
  35. package/src/data/Memoizer.js +0 -39
  36. package/src/data/ResultSet.js +0 -205
  37. package/src/data/stream/DataHydrator.js +0 -58
  38. package/src/data/stream/ResultSet.js +0 -34
  39. package/src/data/stream/ResultSetItem.js +0 -158
  40. package/src/data/stream/ResultSetItemProxy.js +0 -161
  41. package/src/graphql/ast/SchemaDecorator.js +0 -141
  42. package/src/graphql/directive/authz.directive.js +0 -84
package/CHANGELOG.md CHANGED
@@ -1,9 +1,27 @@
1
1
  # CHANGELOG
2
2
 
3
3
  ## v0.10.x
4
- - Removed model.tform() method
5
- - Removed embedApi directive and embedded API GQL
6
- - Hydrating fields apriori
4
+ - Replaced ResultSet -> POJOs
5
+ - Removed all $$ magic resolver methods
6
+ - Removed all $ magic field methods
7
+ - Removed .toObject()
8
+ - Removed embedded API completely
9
+ - Removed Directives
10
+ - embedApi -> no replacement
11
+ - enforce -> use pipeline methods
12
+ - resolve -> use graphql resolvers
13
+ - @value -> use @field.instruct directive
14
+ - Removed toId Transform -> use @field(id: '')
15
+ - Removed Model.tform() -> use Model.shapeObject(shape, data)
16
+ - Removed Resolver.toResultSet() -> ? TBD ?
17
+ - Removed Transformer + Rule -> use Pipeline
18
+ - Removed many pre-defined rules + transformers
19
+ - Pre-defined names start with $ (eg. $toLowerCase)
20
+ - Moved "validator" to dev dependency -> isEmail
21
+ - Added QueryBuilder.resolve() terminal command
22
+ - Exported SchemaDecorator -> Schema
23
+ - Removed embedded schema SystemEvents (internal emitter also removed)
24
+ - Removed spread of arguments in QueryBuilder terminal commands (must pass in array)
7
25
 
8
26
  ## v0.9.x
9
27
  - Subscriptions API
package/index.js CHANGED
@@ -1,19 +1,15 @@
1
1
  const Schema = require('./src/core/Schema');
2
- const SchemaDecorator = require('./src/core/SchemaDecorator');
3
2
  const GraphQL = require('./src/core/GraphQL');
4
3
  const Resolver = require('./src/core/Resolver');
5
- const Rule = require('./src/core/Rule');
4
+ const Pipeline = require('./src/data/Pipeline');
6
5
  const Driver = require('./src/driver');
7
- const Transformer = require('./src/core/Transformer');
8
6
  const { eventEmitter: Emitter } = require('./src/service/event.service');
9
7
 
10
8
  module.exports = {
11
9
  Schema,
12
- SchemaDecorator,
13
10
  GraphQL,
14
11
  Resolver,
15
- Rule,
16
12
  Driver,
17
- Transformer,
18
13
  Emitter,
14
+ Pipeline,
19
15
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@coderich/autograph",
3
3
  "author": "Richard Livolsi (coderich)",
4
- "version": "0.10.0",
4
+ "version": "0.10.1",
5
5
  "description": "AutoGraph",
6
6
  "keywords": [
7
7
  "graphql",
@@ -42,8 +42,7 @@
42
42
  "mongodb": "^4.8.0",
43
43
  "object-hash": "^2.0.1",
44
44
  "picomatch": "^2.1.1",
45
- "uuid": "^3.3.3",
46
- "validator": "^12.2.0"
45
+ "uuid": "^3.3.3"
47
46
  },
48
47
  "devDependencies": {
49
48
  "@coderich/ratchet": "^1.5.7",
@@ -52,7 +51,8 @@
52
51
  "neo4j-driver": "^4.0.0",
53
52
  "neodb": "^3.0.0",
54
53
  "redis": "^2.8.0",
55
- "redis-mock": "^0.47.0"
54
+ "redis-mock": "^0.47.0",
55
+ "validator": "^13.7.0"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "graphql": "*"
package/src/.DS_Store CHANGED
Binary file
@@ -8,7 +8,7 @@ const { ensureArray } = require('../service/app.service');
8
8
  * If it expects more than 1 we block and wait for it to finish.
9
9
  */
10
10
  module.exports = class extends EventEmitter {
11
- emit(event, data, prev) {
11
+ emit(event, data) {
12
12
  return Promise.all(this.rawListeners(event).map((wrapper) => {
13
13
  return new Promise((resolve, reject) => {
14
14
  const next = result => resolve(result); // If a result is passed this will bypass middleware thunk()
@@ -17,9 +17,7 @@ module.exports = class extends EventEmitter {
17
17
  if (numArgs < 2) next();
18
18
  });
19
19
  })).then((results) => {
20
- if (event === 'preMutation') return this.emit('validate', data, results);
21
- const target = event === 'validate' ? prev : results;
22
- return target.find(r => r !== undefined); // There can be only one (result)
20
+ return results.find(r => r !== undefined); // There can be only one (result)
23
21
  });
24
22
  }
25
23
 
@@ -1,22 +1,22 @@
1
1
  const Model = require('../data/Model');
2
- const Query = require('../query/Query');
3
- const ResultSet = require('../data/ResultSet');
4
- const DataHydrator = require('../data/stream/DataHydrator');
5
2
  const DataLoader = require('../data/DataLoader');
6
3
  const DataTransaction = require('../data/DataTransaction');
7
4
  const QueryBuilder = require('../query/QueryBuilder');
8
- const { createSystemEvent } = require('../service/event.service');
9
5
 
10
6
  module.exports = class Resolver {
11
7
  constructor(schema, context = {}) {
12
- this.models = schema.getModels();
13
8
  this.schema = schema;
14
9
  this.context = context;
10
+ this.models = schema.getModels();
15
11
  this.loaders = this.models.reduce((prev, model) => prev.set(`${model}`, new DataLoader(this, model)), new Map());
12
+ }
16
13
 
17
- //
18
- this.getSchema = () => this.schema;
19
- this.getContext = () => this.context;
14
+ getSchema() {
15
+ return this.schema;
16
+ }
17
+
18
+ getContext() {
19
+ return this.context;
20
20
  }
21
21
 
22
22
  /**
@@ -38,31 +38,6 @@ module.exports = class Resolver {
38
38
  */
39
39
  raw(model) {
40
40
  return this.toModelEntity(model).raw();
41
- // const entity = this.toModelEntity(model);
42
- // const driver = entity.raw();
43
- // if (!method) return driver;
44
-
45
- // const resolver = this;
46
- // const crud = ['get', 'find', 'count'].indexOf(method) > -1 ? 'read' : method;
47
- // const query = new Query({ model: entity, resolver, crud });
48
-
49
- // return new Proxy(driver, {
50
- // get(target, prop, rec) {
51
- // const value = Reflect.get(target, prop, rec);
52
-
53
- // if (typeof value === 'function') {
54
- // return (...args) => {
55
- // return value.bind(target)(...args).then((result) => {
56
- // const doc = resolver.toResultSet(model, result);
57
- // createSystemEvent('Response', { method, query: query.doc(doc) });
58
- // return result;
59
- // });
60
- // };
61
- // }
62
-
63
- // return value;
64
- // },
65
- // });
66
41
  }
67
42
 
68
43
  /**
@@ -77,31 +52,37 @@ module.exports = class Resolver {
77
52
  }
78
53
 
79
54
  resolve(query) {
80
- const { model, crud } = query.toObject();
55
+ const { model, crud, method } = query.toObject();
81
56
 
82
57
  switch (crud) {
83
58
  case 'create': case 'update': case 'delete': {
84
59
  return model.getDriver().resolve(query.toDriver()).then((data) => {
85
60
  this.clear(model);
86
- data = model.shape(data, 'deserialize');
87
- return new DataHydrator(query, data);
61
+ return model.shapeObject(model.getShape(), data, query);
88
62
  });
89
63
  }
90
64
  default: {
91
- // This is needed in SF tests...
92
65
  const key = model.idKey();
93
- const { where, method } = query.toDriver();
94
- if (Object.prototype.hasOwnProperty.call(where, key) && where[key] == null) return Promise.resolve(method === 'findMany' ? [] : null);
95
-
96
- //
66
+ const { where } = query.toDriver();
67
+ const lookupValue = where[key];
68
+
69
+ // This is a shortcut to prevent making unnecessary query
70
+ if (Object.prototype.hasOwnProperty.call(where, key) && (lookupValue == null || (Array.isArray(lookupValue) && lookupValue.length === 0))) {
71
+ switch (method) {
72
+ case 'count': return Promise.resolve(0);
73
+ case 'findMany': return Promise.resolve([]);
74
+ default: return Promise.resolve(null);
75
+ }
76
+ }
77
+
78
+ // Go through DataLoader to cache results
97
79
  return this.loaders.get(`${model}`).load(query);
98
80
  }
99
81
  }
100
82
  }
101
83
 
102
84
  toModel(model) {
103
- const $model = model instanceof Model ? model : this.schema.getModel(model);
104
- return $model;
85
+ return model instanceof Model ? model : this.schema.getModel(model);
105
86
  }
106
87
 
107
88
  toModelMarked(model) {
@@ -118,23 +99,25 @@ module.exports = class Resolver {
118
99
  return entity;
119
100
  }
120
101
 
121
- toResultSet(model, data, method) {
122
- const crud = ['get', 'find', 'count'].indexOf(method) > -1 ? 'read' : method;
123
- const query = new Query({ model: this.toModel(model), resolver: this, crud });
124
- const result = new ResultSet(query, data);
125
- return createSystemEvent('Response', {
126
- model,
127
- crud,
128
- method,
129
- result,
130
- doc: result,
131
- merged: result,
132
- resolver: this,
133
- key: `${method}${model}`,
134
- context: this.getContext(),
135
- query: query.doc(result).merged(result),
136
- }, () => result);
137
- }
102
+ // toResultSet(model, data, method) {
103
+ // const crud = ['get', 'find', 'count'].indexOf(method) > -1 ? 'read' : method;
104
+ // const doc = model.shape(data);
105
+ // const result = doc;
106
+ // const merged = doc;
107
+
108
+ // return createSystemEvent('Response', {
109
+ // model,
110
+ // crud,
111
+ // method,
112
+ // result,
113
+ // doc,
114
+ // merged,
115
+ // resolver: this,
116
+ // key: `${method}${model}`,
117
+ // context: this.getContext(),
118
+ // query: query.doc(result).merged(result),
119
+ // }, () => result);
120
+ // }
138
121
 
139
122
  // DataLoader Proxy Methods
140
123
  clear(model) {
@@ -1,8 +1,5 @@
1
1
  const Model = require('../data/Model');
2
2
  const Schema = require('../graphql/ast/Schema');
3
- const apiExt = require('../graphql/extension/api');
4
- const typeExt = require('../graphql/extension/type');
5
- const frameworkExt = require('../graphql/extension/framework');
6
3
  const { identifyOnDeletes } = require('../service/schema.service');
7
4
  const { createSystemEvent } = require('../service/event.service');
8
5
 
@@ -23,9 +20,6 @@ module.exports = class extends Schema {
23
20
  },
24
21
  });
25
22
  }, {});
26
-
27
- // Create models
28
- this.createModels();
29
23
  }
30
24
 
31
25
  setup() {
@@ -43,38 +37,11 @@ module.exports = class extends Schema {
43
37
  });
44
38
  }
45
39
 
46
- createModels() {
40
+ initialize() {
41
+ super.initialize();
47
42
  this.models = super.getModels().map(model => new Model(this, model, this.drivers[model.getDriverName()]));
43
+ this.models.forEach(model => model.initialize());
48
44
  this.models.forEach(model => model.referentialIntegrity(identifyOnDeletes(this.models, model)));
49
- this.modelsByName = this.models.reduce((prev, model) => Object.assign(prev, { [model.getName()]: model }), {});
50
- this.modelsByKey = this.models.reduce((prev, model) => Object.assign(prev, { [model.getKey()]: model }), {});
51
- }
52
-
53
- loadDir(dir, options) {
54
- super.loadDir(dir, options);
55
- this.createModels();
56
- return this;
57
- }
58
-
59
- extend(...schemas) {
60
- super.extend(...schemas);
61
- this.createModels();
62
45
  return this;
63
46
  }
64
-
65
- /**
66
- * Called a runtime to get the full server api schema. Done this way because the
67
- * end-user needs a chance to call Transformer.factory() etc (thus cannot be moved to constructor)
68
- */
69
- getServerApiSchema() {
70
- this.extend(frameworkExt(this), typeExt(this));
71
- this.sextend(apiExt(this));
72
- return super.getSchema();
73
- }
74
-
75
- makeServerApiSchema() {
76
- this.extend(frameworkExt(this), typeExt(this));
77
- this.sextend(apiExt(this));
78
- return super.makeExecutableSchema();
79
- }
80
47
  };
Binary file
@@ -1,44 +1,83 @@
1
1
  const FBDataLoader = require('dataloader');
2
- const DataHydrator = require('./stream/DataHydrator');
3
- // const ResultSet = require('./ResultSet');
4
- // const Query = require('../query/Query');
2
+ const { map, ensureArray, hashObject } = require('../service/app.service');
3
+ const Query = require('../query/Query');
5
4
 
6
- const { hashObject } = require('../service/app.service');
5
+ const handleData = (data, model, query) => {
6
+ if (data == null || typeof data !== 'object') return data;
7
+ return model.deserialize(data, query);
8
+ };
7
9
 
8
- // let counter = 0;
9
10
  module.exports = class DataLoader extends FBDataLoader {
10
11
  constructor(resolver, model) {
11
- // const idKey = model.idKey();
12
12
  const driver = model.getDriver();
13
13
 
14
14
  return new FBDataLoader((queries) => {
15
- // // The idea is to group the "findOne by id" queries together to make 1 query instead
16
- // const { findOneByIdQueries, allOtherQueries } = queries.reduce((prev, query, i) => {
17
- // const { id, method } = query.toObject();
18
- // const key = method === 'findOne' && id ? 'findOneByIdQueries' : 'allOtherQueries';
19
- // prev[key].push({ id, query, i });
20
- // return prev;
21
- // }, { findOneByIdQueries: [], allOtherQueries: [] });
22
-
23
- // // Aggregate ids
24
- // const ids = Array.from(new Set(findOneByIdQueries.map(el => `${el.id}`)));
25
- // const batchQuery = new Query({ resolver, model, method: 'findMany', crud: 'read' });
26
- // const batchWhere = model.transform(batchQuery, { id: ids }, 'serialize', true);
27
- // const promises = [Promise.all(allOtherQueries.map(({ query, i }) => driver.resolve(query.toDriver()).then(data => ({ data, query, i }))))];
28
- // if (ids.length) promises.push(driver.resolve(batchQuery.where(batchWhere).toDriver()).then(results => findOneByIdQueries.map(({ query, id, i }) => ({ i, query, data: results.find(r => `${r[idKey]}` === `${id}`) || null }))));
29
-
30
- // return Promise.all(promises).then((results) => {
31
- // const sorted = results.flat().filter(Boolean).sort((a, b) => a.i - b.i);
32
- // return sorted.map(({ query, data }) => (data != null && typeof data === 'object' ? new ResultSet(query, data) : data));
33
- // });
34
-
35
- return Promise.all(queries.map((query) => {
36
- return driver.resolve(query.toDriver()).then((data) => {
37
- return (data != null && typeof data === 'object' ? new DataHydrator(query, data) : data);
38
- });
39
- }));
15
+ let performBatchQuery = false; // If we don't have to batch it's faster to resolve normal
16
+ const defaultBatchName = '__default__'; // Something that won't collide with an actual field name
17
+
18
+ /**
19
+ * Batch queries can save resources and network round-trip latency. However, we have to be careful to
20
+ * preserve the order and adhere to the DataLoader API. This step simply creates a map of batch
21
+ * queries to run; saving the order ("i") along with useful meta information
22
+ */
23
+ const batchQueries = queries.reduce((prev, query, i) => {
24
+ const { batch = defaultBatchName, where, cmd } = query.toObject();
25
+ const key = batch && (cmd === 'one' || cmd === 'many') ? batch : defaultBatchName;
26
+ if (key !== defaultBatchName) performBatchQuery = true;
27
+ prev[key] = prev[key] || [];
28
+ prev[key].push({ query, where, cmd, i });
29
+ return prev;
30
+ }, {});
31
+
32
+ // Don't batch unless it's worth it!
33
+ if (!performBatchQuery) {
34
+ return Promise.all(queries.map((query) => {
35
+ return driver.resolve(query.toDriver()).then(data => handleData(data, model, query));
36
+ }));
37
+ }
38
+
39
+ /**
40
+ * We have reduced the number of queries down to a smaller set of batch queries to run. The dance
41
+ * performed below retreives the data and then expands the results back into the original queries
42
+ */
43
+ const whereShape = model.getShape('create', 'where');
44
+
45
+ // console.log(Object.entries(batchQueries).map(([key, value]) => ({ [key]: value.length })));
46
+
47
+ return Promise.all(Object.entries(batchQueries).map(([key, values]) => {
48
+ switch (key) {
49
+ case defaultBatchName: {
50
+ return values.map(({ query, i }) => driver.resolve(query.toDriver()).then(data => handleData(data, model, query)).then(data => ({ data, i })));
51
+ }
52
+ default: {
53
+ const keys = Array.from(new Set(values.map(({ where }) => map(where[key], el => `${el}`)).flat()));
54
+ const batchQuery = new Query({ resolver, model, method: 'findMany', crud: 'read' });
55
+ const batchWhere = model.shapeObject(whereShape, { [key]: keys }, batchQuery); // This will add back instructs etc
56
+
57
+ return driver.resolve(batchQuery.where(batchWhere).toDriver()).then(data => handleData(data, model, batchQuery)).then((results) => {
58
+ // One-time data transformation on results to make matching back faster (below)
59
+ const resultsByKey = results.reduce((prev, row) => {
60
+ ensureArray(row[key]).forEach((id) => {
61
+ prev[id] = prev[id] || [];
62
+ prev[id].push(row);
63
+ });
64
+ return prev;
65
+ }, {});
66
+
67
+ // Match back
68
+ return values.map(({ where, cmd, i }) => {
69
+ const targets = ensureArray(where[key]).map(t => `${t}`);
70
+ const data = targets.map(t => resultsByKey[t] || null).flat();
71
+ return { i, data: cmd === 'many' ? data.filter(d => d != null) : data[0] };
72
+ });
73
+ });
74
+ }
75
+ }
76
+ }).flat()).then((results) => {
77
+ return results.flat().sort((a, b) => a.i - b.i).map(({ data }) => data);
78
+ });
40
79
  }, {
41
- cache: false,
80
+ cache: true,
42
81
  cacheKeyFn: query => hashObject(query.getCacheKey()),
43
82
  });
44
83
  }
@@ -1,11 +1,19 @@
1
- const { remove } = require('lodash');
2
- const Boom = require('../core/Boom');
3
- const { isPlainObject, objectContaining, mergeDeep, map } = require('../service/app.service');
1
+ const { get, remove } = require('lodash');
2
+ const { map, isPlainObject, objectContaining, mergeDeep, ensureArray, keyPaths } = require('../service/app.service');
4
3
 
5
- exports.paginateResultSet = (rs, first, after, last, before) => {
4
+ exports.paginateResultSet = (rs, query) => {
5
+ const { first, after, last, before, sort } = query.toObject();
6
+ const sortPaths = keyPaths(sort);
7
+ const limiter = first || last;
6
8
  let hasNextPage = false;
7
9
  let hasPreviousPage = false;
8
- const limiter = first || last;
10
+
11
+ // Add $$cursor data
12
+ map(rs, (doc) => {
13
+ const sortValues = sortPaths.reduce((prv, path) => Object.assign(prv, { [path]: get(doc, path) }), {});
14
+ const sortJSON = JSON.stringify(sortValues);
15
+ doc.$$cursor = Buffer.from(sortJSON).toString('base64');
16
+ });
9
17
 
10
18
  // First try to take off the "bookends" ($gte | $lte)
11
19
  if (rs.length && rs[0].$$cursor === after) {
@@ -34,64 +42,57 @@ exports.paginateResultSet = (rs, first, after, last, before) => {
34
42
  }
35
43
  }
36
44
 
37
- return { hasNextPage, hasPreviousPage };
45
+ // Add $$pageInfo data (hidden)
46
+ return Object.defineProperties(rs, {
47
+ $$pageInfo: {
48
+ get() {
49
+ return {
50
+ startCursor: get(rs, '0.$$cursor', ''),
51
+ endCursor: get(rs, `${rs.length - 1}.$$cursor`, ''),
52
+ hasPreviousPage,
53
+ hasNextPage,
54
+ };
55
+ },
56
+ enumerable: false,
57
+ },
58
+ });
38
59
  };
39
60
 
40
- /**
41
- * @param from <Array>
42
- * @param to <Array>
43
- */
44
- exports.spliceEmbeddedArray = (query, doc, key, from, to) => {
45
- const { model } = query.toObject();
46
- const field = model.getField(key);
47
- const modelRef = field.getModelRef();
61
+ exports.spliceEmbeddedArray = (array, from, to) => {
48
62
  const op = from && to ? 'edit' : (from ? 'pull' : 'push'); // eslint-disable-line no-nested-ternary
49
- const promises = [];
50
-
51
- // Can only splice arrays
52
- if (!field || !field.isArray()) return Promise.reject(Boom.badRequest(`Cannot splice field '${key}'`));
53
-
54
- // We have to deserialize because this normalizes the data (casting etc)
55
- let $to = model.deserialize(query, { [key]: to })[key] || to;
56
- const $from = model.deserialize(query, { [key]: from })[key] || from;
57
-
58
- // If it's embedded we need to append default/create fields for insertion
59
- if ($to && field.isEmbedded()) $to = $to.map(el => modelRef.appendDefaultFields(query, modelRef.appendCreateFields(el, true)));
60
63
 
61
64
  // Convenience so the user does not have to explicity type out the same value over and over to replace
62
- if ($from && $from.length > 1 && $to && $to.length === 1) $to = Array.from($from).fill($to[0]);
63
-
64
- // Traverse the document till we find the segment to modify (in place)
65
- return key.split('.').reduce((prev, segment, i, arr) => {
66
- if (prev == null) return prev;
65
+ if (from && from.length > 1 && to && to.length === 1) to = Array.from(from).fill(to[0]);
67
66
 
68
- return map(prev, (data) => {
69
- if (i < (arr.length - 1)) return data[segment]; // We have not found the target segment yet
70
- data[segment] = data[segment] || []; // Ensuring target segment is an array
71
-
72
- switch (op) {
73
- case 'edit': {
74
- data[segment].forEach((el, j) => {
75
- $from.forEach((val, k) => {
76
- if (objectContaining(el, val)) data[segment][j] = isPlainObject(el) ? mergeDeep(el, $to[k]) : $to[k];
77
- });
78
- });
79
- break;
80
- }
81
- case 'push': {
82
- data[segment].push(...$to);
83
- break;
84
- }
85
- case 'pull': {
86
- remove(data[segment], el => $from.find(val => objectContaining(el, val)));
87
- break;
88
- }
89
- default: {
90
- break;
91
- }
92
- }
67
+ switch (op) {
68
+ case 'edit': {
69
+ array.forEach((el, j) => {
70
+ ensureArray(from).forEach((val, k) => {
71
+ if (objectContaining(el, val)) array[j] = isPlainObject(el) ? mergeDeep(el, ensureArray(to)[k]) : ensureArray(to)[k];
72
+ });
73
+ });
74
+ break;
75
+ }
76
+ // case 'edit': {
77
+ // ensureArray(from).forEach((f, i) => {
78
+ // const t = ensureArray(to)[i];
79
+ // const indexes = array.map((el, j) => (el === f ? j : -1)).filter(index => index !== -1);
80
+ // indexes.forEach(index => (array[index] = t));
81
+ // });
82
+ // break;
83
+ // }
84
+ case 'push': {
85
+ array.push(...to);
86
+ break;
87
+ }
88
+ case 'pull': {
89
+ remove(array, el => from.find(val => objectContaining(el, val)));
90
+ break;
91
+ }
92
+ default: {
93
+ break;
94
+ }
95
+ }
93
96
 
94
- return Promise.all(promises);
95
- });
96
- }, doc);
97
+ return array;
97
98
  };