@coderich/autograph 0.10.0 → 0.10.3

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 (44) hide show
  1. package/CHANGELOG.md +20 -3
  2. package/index.js +2 -8
  3. package/package.json +5 -7
  4. package/src/.DS_Store +0 -0
  5. package/src/core/EventEmitter.js +2 -4
  6. package/src/core/Resolver.js +32 -57
  7. package/src/core/Schema.js +5 -38
  8. package/src/data/.DS_Store +0 -0
  9. package/src/data/DataLoader.js +71 -32
  10. package/src/data/DataService.js +82 -59
  11. package/src/data/Field.js +59 -126
  12. package/src/data/Model.js +113 -105
  13. package/src/data/Pipeline.js +184 -0
  14. package/src/data/Type.js +38 -74
  15. package/src/driver/MongoDriver.js +27 -22
  16. package/src/graphql/.DS_Store +0 -0
  17. package/src/graphql/ast/Field.js +46 -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 +105 -112
  21. package/src/graphql/extension/api.js +20 -18
  22. package/src/graphql/extension/framework.js +27 -33
  23. package/src/graphql/extension/type.js +2 -2
  24. package/src/query/Query.js +82 -14
  25. package/src/query/QueryBuilder.js +38 -30
  26. package/src/query/QueryBuilderTransaction.js +3 -3
  27. package/src/query/QueryResolver.js +77 -41
  28. package/src/query/QueryService.js +24 -42
  29. package/src/service/app.service.js +70 -13
  30. package/src/service/event.service.js +30 -73
  31. package/src/service/graphql.service.js +0 -9
  32. package/src/service/schema.service.js +5 -3
  33. package/src/core/GraphQL.js +0 -21
  34. package/src/core/Rule.js +0 -107
  35. package/src/core/SchemaDecorator.js +0 -46
  36. package/src/core/Transformer.js +0 -68
  37. package/src/data/Memoizer.js +0 -39
  38. package/src/data/ResultSet.js +0 -205
  39. package/src/data/stream/DataHydrator.js +0 -58
  40. package/src/data/stream/ResultSet.js +0 -34
  41. package/src/data/stream/ResultSetItem.js +0 -158
  42. package/src/data/stream/ResultSetItemProxy.js +0 -161
  43. package/src/graphql/ast/SchemaDecorator.js +0 -141
  44. package/src/graphql/directive/authz.directive.js +0 -84
package/CHANGELOG.md CHANGED
@@ -1,9 +1,26 @@
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 $field methods (auto populated)
6
+ - Removed .toObject()
7
+ - $model $save remove $delete $lookup $cursor $pageInfo
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 Model.tform() -> use Model.shapeObject(shape, data)
15
+ - Removed Transformer + Rule -> use Pipeline
16
+ - Removed many pre-defined rules + transformers
17
+ - Moved "validator" to dev dependency -> isEmail
18
+ - Added QueryBuilder.resolve() terminal command
19
+ - Exported SchemaDecorator -> Schema
20
+ - Removed embedded schema SystemEvents (internal emitter also removed)
21
+ - Removed spread of arguments in QueryBuilder terminal commands (must pass in array)
22
+ - Mutate "merged" instead of "input"
23
+ - Validate "payload"
7
24
 
8
25
  ## v0.9.x
9
26
  - Subscriptions API
package/index.js CHANGED
@@ -1,19 +1,13 @@
1
1
  const Schema = require('./src/core/Schema');
2
- const SchemaDecorator = require('./src/core/SchemaDecorator');
3
- const GraphQL = require('./src/core/GraphQL');
4
2
  const Resolver = require('./src/core/Resolver');
5
- const Rule = require('./src/core/Rule');
3
+ const Pipeline = require('./src/data/Pipeline');
6
4
  const Driver = require('./src/driver');
7
- const Transformer = require('./src/core/Transformer');
8
5
  const { eventEmitter: Emitter } = require('./src/service/event.service');
9
6
 
10
7
  module.exports = {
11
8
  Schema,
12
- SchemaDecorator,
13
- GraphQL,
14
9
  Resolver,
15
- Rule,
16
10
  Driver,
17
- Transformer,
18
11
  Emitter,
12
+ Pipeline,
19
13
  };
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.3",
5
5
  "description": "AutoGraph",
6
6
  "keywords": [
7
7
  "graphql",
@@ -30,9 +30,7 @@
30
30
  "ratchet": "ratchet"
31
31
  },
32
32
  "dependencies": {
33
- "@graphql-tools/schema": "^8.3.14",
34
33
  "@hapi/boom": "^9.1.0",
35
- "axios": "^0.21.4",
36
34
  "dataloader": "^2.0.0",
37
35
  "deepmerge": "^4.2.2",
38
36
  "fill-range": "^7.0.1",
@@ -41,18 +39,18 @@
41
39
  "lodash": "^4.17.21",
42
40
  "mongodb": "^4.8.0",
43
41
  "object-hash": "^2.0.1",
44
- "picomatch": "^2.1.1",
45
- "uuid": "^3.3.3",
46
- "validator": "^12.2.0"
42
+ "picomatch": "^2.1.1"
47
43
  },
48
44
  "devDependencies": {
49
45
  "@coderich/ratchet": "^1.5.7",
46
+ "@graphql-tools/schema": "^9.0.1",
50
47
  "graphql": "^15.5.0",
51
48
  "mongodb-memory-server": "^8.7.2",
52
49
  "neo4j-driver": "^4.0.0",
53
50
  "neodb": "^3.0.0",
54
51
  "redis": "^2.8.0",
55
- "redis-mock": "^0.47.0"
52
+ "redis-mock": "^0.47.0",
53
+ "validator": "^13.7.0"
56
54
  },
57
55
  "peerDependencies": {
58
56
  "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,25 @@
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');
4
+ const Query = require('../query/Query');
7
5
  const QueryBuilder = require('../query/QueryBuilder');
6
+ const { finalizeResults } = require('../data/DataService');
8
7
  const { createSystemEvent } = require('../service/event.service');
9
8
 
10
9
  module.exports = class Resolver {
11
10
  constructor(schema, context = {}) {
12
- this.models = schema.getModels();
13
11
  this.schema = schema;
14
12
  this.context = context;
13
+ this.models = schema.getModels();
15
14
  this.loaders = this.models.reduce((prev, model) => prev.set(`${model}`, new DataLoader(this, model)), new Map());
15
+ }
16
+
17
+ getSchema() {
18
+ return this.schema;
19
+ }
16
20
 
17
- //
18
- this.getSchema = () => this.schema;
19
- this.getContext = () => this.context;
21
+ getContext() {
22
+ return this.context;
20
23
  }
21
24
 
22
25
  /**
@@ -38,31 +41,6 @@ module.exports = class Resolver {
38
41
  */
39
42
  raw(model) {
40
43
  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
44
  }
67
45
 
68
46
  /**
@@ -77,31 +55,38 @@ module.exports = class Resolver {
77
55
  }
78
56
 
79
57
  resolve(query) {
80
- const { model, crud } = query.toObject();
58
+ const { model, crud, method } = query.toObject();
81
59
 
82
60
  switch (crud) {
83
61
  case 'create': case 'update': case 'delete': {
84
62
  return model.getDriver().resolve(query.toDriver()).then((data) => {
85
63
  this.clear(model);
86
- data = model.shape(data, 'deserialize');
87
- return new DataHydrator(query, data);
64
+ const rs = model.shapeObject(model.getShape(), data, query);
65
+ return finalizeResults(rs, query);
88
66
  });
89
67
  }
90
68
  default: {
91
- // This is needed in SF tests...
92
69
  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
- //
70
+ const { where } = query.toDriver();
71
+ const lookupValue = where[key];
72
+
73
+ // This is a shortcut to prevent making unnecessary query
74
+ if (Object.prototype.hasOwnProperty.call(where, key) && (lookupValue == null || (Array.isArray(lookupValue) && lookupValue.length === 0))) {
75
+ switch (method) {
76
+ case 'count': return Promise.resolve(0);
77
+ case 'findMany': return Promise.resolve([]);
78
+ default: return Promise.resolve(null);
79
+ }
80
+ }
81
+
82
+ // Go through DataLoader to cache results
97
83
  return this.loaders.get(`${model}`).load(query);
98
84
  }
99
85
  }
100
86
  }
101
87
 
102
88
  toModel(model) {
103
- const $model = model instanceof Model ? model : this.schema.getModel(model);
104
- return $model;
89
+ return model instanceof Model ? model : this.schema.getModel(model);
105
90
  }
106
91
 
107
92
  toModelMarked(model) {
@@ -119,21 +104,11 @@ module.exports = class Resolver {
119
104
  }
120
105
 
121
106
  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);
107
+ model = this.toModel(model);
108
+ const query = new Query({ model, resolver: this, context: this.context, method });
109
+ const result = model.deserialize(data, query);
110
+ const event = { result, query, ...query.doc(result).merged(result).toObject() };
111
+ return createSystemEvent('Response', event, () => result);
137
112
  }
138
113
 
139
114
  // DataLoader Proxy Methods
@@ -1,10 +1,7 @@
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
- const { createSystemEvent } = require('../service/event.service');
4
+ const { eventEmitter } = require('../service/event.service');
8
5
 
9
6
  // Export class
10
7
  module.exports = class extends Schema {
@@ -23,13 +20,10 @@ 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() {
32
- return createSystemEvent('Setup', this, () => {
26
+ return eventEmitter.emit('setup', this).then(() => {
33
27
  const entities = this.models.filter(m => m.isEntity());
34
28
 
35
29
  // Create model indexes
@@ -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,19 +1,50 @@
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');
3
+
4
+ exports.finalizeResults = (rs, query) => {
5
+ const { model, resolver } = query.toObject();
6
+
7
+ return map(exports.paginateResults(rs, query), (doc) => {
8
+ return Object.defineProperties(doc, {
9
+ $model: { value: model },
10
+ $save: { value: input => resolver.match(model).id(doc.id).save({ ...doc, ...input }) },
11
+ $remove: { value: (...args) => resolver.match(model).id(doc.id).remove(...args) },
12
+ $delete: { value: (...args) => resolver.match(model).id(doc.id).delete(...args) },
13
+ $lookup: { value: (fieldName, args) => model.getFieldByName(fieldName).resolve(resolver, doc, args) },
14
+ // $resolve: { value: (fieldName, args) => model.getFieldByName(fieldName).resolve(resolver, doc, args, true) },
15
+ });
16
+ });
17
+ };
18
+
19
+ /**
20
+ * This is cursor-style pagination only
21
+ * You add 2 extra records to the result in order to determine previous/next
22
+ */
23
+ exports.paginateResults = (rs, query) => {
24
+ const { first, after, last, before, sort } = query.toObject();
25
+ const isPaginating = Boolean(first || last || after || before);
26
+
27
+ // Return right away if not paginating
28
+ if (!isPaginating) return rs;
4
29
 
5
- exports.paginateResultSet = (rs, first, after, last, before) => {
6
30
  let hasNextPage = false;
7
31
  let hasPreviousPage = false;
8
32
  const limiter = first || last;
33
+ const sortPaths = keyPaths(sort);
34
+
35
+ // Add $cursor data
36
+ map(rs, (doc) => {
37
+ const sortValues = sortPaths.reduce((prv, path) => Object.assign(prv, { [path]: get(doc, path) }), {});
38
+ Object.defineProperty(doc, '$cursor', { get() { return Buffer.from(JSON.stringify(sortValues)).toString('base64'); } });
39
+ });
9
40
 
10
41
  // First try to take off the "bookends" ($gte | $lte)
11
- if (rs.length && rs[0].$$cursor === after) {
42
+ if (rs.length && rs[0].$cursor === after) {
12
43
  rs.shift();
13
44
  hasPreviousPage = true;
14
45
  }
15
46
 
16
- if (rs.length && rs[rs.length - 1].$$cursor === before) {
47
+ if (rs.length && rs[rs.length - 1].$cursor === before) {
17
48
  rs.pop();
18
49
  hasNextPage = true;
19
50
  }
@@ -34,64 +65,56 @@ exports.paginateResultSet = (rs, first, after, last, before) => {
34
65
  }
35
66
  }
36
67
 
37
- return { hasNextPage, hasPreviousPage };
68
+ // Add $pageInfo
69
+ return Object.defineProperties(rs, {
70
+ $pageInfo: {
71
+ get() {
72
+ return {
73
+ startCursor: get(rs, '0.$cursor', ''),
74
+ endCursor: get(rs, `${rs.length - 1}.$cursor`, ''),
75
+ hasPreviousPage,
76
+ hasNextPage,
77
+ };
78
+ },
79
+ },
80
+ });
38
81
  };
39
82
 
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();
83
+ exports.spliceEmbeddedArray = (array, from, to) => {
48
84
  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
85
 
61
86
  // 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]);
87
+ if (from && from.length > 1 && to && to.length === 1) to = Array.from(from).fill(to[0]);
63
88
 
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;
67
-
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
- }
89
+ switch (op) {
90
+ case 'edit': {
91
+ array.forEach((el, j) => {
92
+ ensureArray(from).forEach((val, k) => {
93
+ if (objectContaining(el, val)) array[j] = isPlainObject(el) ? mergeDeep(el, ensureArray(to)[k]) : ensureArray(to)[k];
94
+ });
95
+ });
96
+ break;
97
+ }
98
+ // case 'edit': {
99
+ // ensureArray(from).forEach((f, i) => {
100
+ // const t = ensureArray(to)[i];
101
+ // const indexes = array.map((el, j) => (el === f ? j : -1)).filter(index => index !== -1);
102
+ // indexes.forEach(index => (array[index] = t));
103
+ // });
104
+ // break;
105
+ // }
106
+ case 'push': {
107
+ array.push(...to);
108
+ break;
109
+ }
110
+ case 'pull': {
111
+ remove(array, el => from.find(val => objectContaining(el, val)));
112
+ break;
113
+ }
114
+ default: {
115
+ break;
116
+ }
117
+ }
93
118
 
94
- return Promise.all(promises);
95
- });
96
- }, doc);
119
+ return array;
97
120
  };