@coderich/autograph 0.11.0 → 0.11.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,41 @@
1
+ # CHANGELOG
2
+
3
+ ## v0.10.x
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"
24
+
25
+ ## v0.9.x
26
+ - Subscriptions API
27
+ - postMutation no longer mutates "doc" and adds "result"
28
+ - Added onDelete defer option
29
+
30
+ ## v0.8.x
31
+ - Engine 14+
32
+
33
+ ## v0.7.x
34
+ - Complete overhaul of Query to Mongo Driver (pagination, sorting, counts, etc)
35
+ - Removed countModel Queries from the API (now available as `count` property on `Connetion` types)
36
+ - Dropped Neo4J (temporarily)
37
+
38
+ ## v0.6.x
39
+ - Mongo driver no longer checks for `version` directive
40
+ - Models no longer share a Connection type; removing the need to use `... on Model` for GraphQL queries
41
+ - Added `@field(connection: Boolean)` parameter to specifically indicate fields that should return a Connection type
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@coderich/autograph",
3
3
  "author": "Richard Livolsi (coderich)",
4
- "version": "0.11.0",
4
+ "version": "0.11.1",
5
5
  "description": "AutoGraph",
6
6
  "keywords": [
7
7
  "graphql",
@@ -30,6 +30,7 @@
30
30
  "ratchet": "ratchet"
31
31
  },
32
32
  "dependencies": {
33
+ "@coderich/util": "^0.1.1",
33
34
  "@hapi/boom": "^9.1.0",
34
35
  "dataloader": "^2.0.0",
35
36
  "deepmerge": "^4.2.2",
@@ -37,19 +38,15 @@
37
38
  "glob": "^7.1.6",
38
39
  "graphql-fields": "^2.0.3",
39
40
  "lodash": "^4.17.21",
40
- "mongodb": "^4.8.0",
41
- "object-hash": "^2.0.1",
41
+ "mongodb": "4.8.1",
42
+ "object-hash": "^3.0.0",
42
43
  "picomatch": "^2.1.1"
43
44
  },
44
45
  "devDependencies": {
45
- "@coderich/ratchet": "^1.5.7",
46
+ "@coderich/ratchet": "^1.5.8",
46
47
  "@graphql-tools/schema": "^9.0.1",
47
48
  "graphql": "^15.5.0",
48
49
  "mongodb-memory-server": "^8.7.2",
49
- "neo4j-driver": "^4.0.0",
50
- "neodb": "^3.0.0",
51
- "redis": "^2.8.0",
52
- "redis-mock": "^0.47.0",
53
50
  "validator": "^13.7.0"
54
51
  },
55
52
  "peerDependencies": {
package/src/.DS_Store ADDED
Binary file
Binary file
@@ -31,8 +31,8 @@ module.exports = class extends EventEmitter {
31
31
  const { key } = event;
32
32
 
33
33
  if (ensureArray(keys).indexOf(key) > -1) {
34
- await fn(event, next);
35
34
  if (numArgs < 2) next();
35
+ await fn(event, next);
36
36
  } else {
37
37
  next();
38
38
  }
@@ -49,8 +49,8 @@ module.exports = class extends EventEmitter {
49
49
  const { key } = event;
50
50
 
51
51
  if (ensureArray(keys).indexOf(key) > -1) {
52
- await fn(event, next);
53
52
  if (numArgs < 2) next();
53
+ await fn(event, next);
54
54
  } else {
55
55
  next();
56
56
  }
@@ -67,8 +67,8 @@ module.exports = class extends EventEmitter {
67
67
  const { model } = event;
68
68
 
69
69
  if (ensureArray(models).indexOf(`${model}`) > -1) {
70
- await fn(event, next);
71
70
  if (numArgs < 2) next();
71
+ await fn(event, next);
72
72
  } else {
73
73
  next();
74
74
  }
@@ -85,8 +85,8 @@ module.exports = class extends EventEmitter {
85
85
  const { model } = event;
86
86
 
87
87
  if (ensureArray(models).indexOf(`${model}`) > -1) {
88
- await fn(event, next);
89
88
  if (numArgs < 2) next();
89
+ await fn(event, next);
90
90
  } else {
91
91
  next();
92
92
  }
Binary file
@@ -1,161 +1,96 @@
1
- const { flatten } = require('lodash');
1
+ const { get } = require('lodash');
2
2
  const TreeMap = require('./TreeMap');
3
3
  const QueryBuilderTransaction = require('../query/QueryBuilderTransaction');
4
4
 
5
+ const makeMap = (resolver) => {
6
+ let resolve, reject;
7
+ const map = new TreeMap();
8
+ map.promise = new Promise((good, bad) => { resolve = good; reject = bad; });
9
+ map.resolve = resolve;
10
+ map.reject = reject;
11
+
12
+ map.ready = () => {
13
+ const elements = map.elements();
14
+ const notReady = elements.filter(el => !el.marker);
15
+ if (notReady.length) return [undefined, undefined];
16
+ let rollbackIndex = elements.findIndex(el => el.marker === 'rollback');
17
+ if (rollbackIndex === -1) rollbackIndex = Infinity;
18
+ return [elements.slice(0, rollbackIndex), elements.slice(rollbackIndex)];
19
+ };
20
+
21
+ map.perform = () => {
22
+ const [commits, rollbacks] = map.ready();
23
+
24
+ if (commits && rollbacks) {
25
+ const rollbackData = rollbacks.map(tnx => tnx.data).flat();
26
+ const commitData = commits.map(tnx => tnx.data).flat();
27
+
28
+ Promise.all(rollbackData.map(rbd => rbd.$rollback())).then(() => {
29
+ if (commits.length) resolver.clearAll();
30
+ Promise.all(commitData.map(cd => cd.$commit())).then(d => map.resolve(d));
31
+ }).catch(e => map.reject(e));
32
+ }
33
+
34
+ return map.promise;
35
+ };
36
+
37
+ return map;
38
+ };
39
+
5
40
  module.exports = class DataTransaction {
6
41
  constructor(resolver, parentTxn) {
7
- const txnMap = (parentTxn || {}).txnMap || (() => {
8
- let resolve, reject;
9
- const map = new TreeMap();
10
- map.promise = new Promise((good, bad) => { resolve = good; reject = bad; });
11
- map.resolve = resolve;
12
- map.reject = reject;
13
-
14
- map.ready = () => {
15
- const elements = map.elements();
16
- const notReady = elements.filter(el => !el.marker);
17
- if (notReady.length) return [undefined, undefined];
18
- let rollbackIndex = elements.findIndex(el => el.marker === 'rollback');
19
- if (rollbackIndex === -1) rollbackIndex = Infinity;
20
- return [elements.slice(0, rollbackIndex), elements.slice(rollbackIndex)];
21
- };
22
-
23
- map.perform = () => {
24
- const [commits, rollbacks] = map.ready();
25
-
26
- if (commits && rollbacks) {
27
- const rollbackData = flatten(rollbacks.map(tnx => tnx.data));
28
- const commitData = flatten(commits.map(tnx => tnx.data));
29
-
30
- Promise.all(rollbackData.map(rbd => rbd.$rollback())).then(() => {
31
- if (commits.length) resolver.clearAll();
32
- Promise.all(commitData.map(cd => cd.$commit())).then(d => map.resolve(d));
33
- }).catch(e => map.reject(e));
34
- }
35
-
36
- return map.promise;
37
- };
38
-
39
- return map;
40
- })();
41
-
42
- // Create txn
43
- const txn = ((data, driverMap, txMap) => {
44
- return {
45
- get match() {
46
- return (modelName) => {
47
- const model = resolver.toModelMarked(modelName);
48
- const driver = model.getDriver();
49
- if (!driverMap.has(driver)) driverMap.set(driver, []);
50
- const op = new QueryBuilderTransaction(resolver, model, this);
51
- driverMap.get(driver).push(op);
52
- return op;
53
- };
54
- },
55
- get exec() {
56
- return () => {
57
- return Promise.all(Array.from(driverMap.entries()).map(([driver, ops]) => {
58
- if (driver.getDirectives().transactions === false) {
59
- return Promise.all(ops.map(op => op.exec())).then((results) => {
60
- results.$commit = () => resolver.clearAll();
61
- results.$rollback = () => resolver.clearAll();
62
- return results;
63
- });
64
- }
65
-
66
- return driver.transaction(ops);
67
- })).then((results) => {
68
- data = results;
69
- return flatten(results);
70
- });
71
- };
72
- },
73
- get run() {
74
- return () => {
75
- return this.exec().then((results) => {
76
- if (txMap.root(this) === this) return this.commit().then(() => results);
77
- this.commit();
78
- return results;
79
- }).catch((e) => {
80
- if (txMap.root(this) === this) return this.rollback().then(() => Promise.reject(e));
81
- this.rollback();
82
- throw e;
83
- });
84
- };
85
- },
86
- get commit() {
87
- return () => {
88
- if (this.marker !== 'rollback') this.marker = 'commit';
89
- return txMap.perform();
90
- };
91
- },
92
- get rollback() {
93
- return () => {
94
- this.marker = 'rollback';
95
- return txMap.perform();
96
- };
97
- },
98
- get data() {
99
- return data;
100
- },
101
- get txnMap() {
102
- return txMap;
103
- },
104
- };
105
- })([], new Map(), txnMap);
106
-
107
- // Save txn to map
108
- txnMap.add(parentTxn, txn);
109
-
110
- // Return to caller
111
- return txn;
42
+ this.data = [];
43
+ this.resolver = resolver;
44
+ this.driverMap = new Map();
45
+ this.txnMap = get(parentTxn, 'txnMap') || makeMap(resolver);
46
+ this.txnMap.add(parentTxn, this);
112
47
  }
113
48
 
114
- // match(modelish) {
115
- // const model = this.resolver.toModelMarked(modelish);
116
- // const driver = model.getDriver();
117
- // if (!this.driverMap.has(driver)) this.driverMap.set(driver, []);
118
- // const op = new QueryBuilderTransaction(model, this.resolver, this);
119
- // this.driverMap.get(driver).push(op);
120
- // return op;
121
- // }
122
-
123
- // exec() {
124
- // return Promise.all(Array.from(this.driverMap.entries()).map(([driver, ops]) => {
125
- // if (driver.getDirectives().transactions === false) {
126
- // return Promise.all(ops.map(op => op.exec())).then((results) => {
127
- // results.$commit = () => this.resolver.clearAll();
128
- // results.$rollback = () => this.resolver.clearAll();
129
- // return results;
130
- // });
131
- // }
49
+ match(modelish) {
50
+ const model = this.resolver.toModelMarked(modelish);
51
+ const driver = model.getDriver();
52
+ if (!this.driverMap.has(driver)) this.driverMap.set(driver, []);
53
+ const op = new QueryBuilderTransaction(this.resolver, model, this);
54
+ this.driverMap.get(driver).push(op);
55
+ return op;
56
+ }
132
57
 
133
- // return driver.transaction(ops);
134
- // })).then((results) => {
135
- // this.data = results;
136
- // return flatten(results);
137
- // });
138
- // }
58
+ exec() {
59
+ return Promise.all(Array.from(this.driverMap.entries()).map(([driver, ops]) => {
60
+ if (driver.getDirectives().transactions === false) {
61
+ return Promise.all(ops.map(op => op.exec())).then((results) => {
62
+ results.$commit = () => this.resolver.clearAll();
63
+ results.$rollback = () => this.resolver.clearAll();
64
+ return results;
65
+ });
66
+ }
67
+
68
+ return driver.transaction(ops);
69
+ })).then((results) => {
70
+ this.data = results;
71
+ return results.flat();
72
+ });
73
+ }
139
74
 
140
- // run() {
141
- // return this.exec().then((results) => {
142
- // if (this.txMap.root(this) === this) return this.commit().then(() => results);
143
- // this.commit();
144
- // return results;
145
- // }).catch((e) => {
146
- // if (this.txMap.root(this) === this) return this.rollback().then(() => Promise.reject(e));
147
- // this.rollback();
148
- // throw e;
149
- // });
150
- // }
75
+ run() {
76
+ return this.exec().then((results) => {
77
+ if (this.txnMap.root(this) === this) return this.commit().then(() => results);
78
+ this.commit();
79
+ return results;
80
+ }).catch((e) => {
81
+ if (this.txnMap.root(this) === this) return this.rollback().then(() => Promise.reject(e));
82
+ this.rollback();
83
+ throw e;
84
+ });
85
+ }
151
86
 
152
- // commit() {
153
- // if (this.marker !== 'rollback') this.marker = 'commit';
154
- // return this.txMap.perform();
155
- // }
87
+ commit() {
88
+ if (this.marker !== 'rollback') this.marker = 'commit';
89
+ return this.txnMap.perform();
90
+ }
156
91
 
157
- // rollback() {
158
- // this.marker = 'rollback';
159
- // return this.txMap.perform();
160
- // }
92
+ rollback() {
93
+ this.marker = 'rollback';
94
+ return this.txnMap.perform();
95
+ }
161
96
  };
package/src/data/Model.js CHANGED
@@ -1,4 +1,6 @@
1
1
  const Stream = require('stream');
2
+ const { get } = require('lodash');
3
+ const { flatten } = require('@coderich/util');
2
4
  const Field = require('./Field');
3
5
  const Model = require('../graphql/ast/Model');
4
6
  const { eventEmitter } = require('../service/event.service');
@@ -80,9 +82,9 @@ module.exports = class extends Model {
80
82
 
81
83
  // Define target mapping
82
84
  const targetMap = {
83
- doc: ['defaultValue', 'castValue', 'ensureArrayValue', 'normalizers', 'instructs', ...crudKeys, `$${serdes}rs`, `${serdes}rs`, 'transforms'],
85
+ doc: [], // Do nothing...
86
+ // doc: ['defaultValue', 'castValue', 'ensureArrayValue', 'normalizers', 'instructs', ...crudKeys, `$${serdes}rs`, `${serdes}rs`, 'transforms'],
84
87
  input: ['defaultValue', 'castValue', 'ensureArrayValue', 'normalizers', 'instructs', ...crudKeys, `$${serdes}rs`, `${serdes}rs`, 'transforms'],
85
- // input: ['defaultValue', 'castValue', 'ensureArrayValue'],
86
88
  where: ['castValue', 'instructs', `$${serdes}rs`],
87
89
  };
88
90
 
@@ -129,13 +131,14 @@ module.exports = class extends Model {
129
131
  shape.model = this;
130
132
  shape.serdes = serdes;
131
133
  shape.target = target;
134
+ // console.log(shape.modelRef);
132
135
 
133
136
  // Cache and return
134
137
  this.shapesCache.set(cacheKey, shape);
135
138
  return shape;
136
139
  }
137
140
 
138
- shapeObject(shape, obj, query, root) {
141
+ shapeObject(shape, obj, query, root, base, toFlat = false) {
139
142
  const { serdes, model } = shape;
140
143
  const { context, resolver, doc = {}, flags = {} } = query.toObject();
141
144
  const { pipeline } = flags;
@@ -143,8 +146,11 @@ module.exports = class extends Model {
143
146
  if (!pipeline) return obj;
144
147
  // const filters = pipeline === true ? [] : Object.entries(pipeline).map(([k, v]) => (v === false ? k : null)).filter(Boolean);
145
148
 
149
+ // base is the base model
150
+ base = base || model;
151
+
146
152
  return map(obj, (parent) => {
147
- // "root" is the base of the object
153
+ // root is the base data object
148
154
  root = root || parent;
149
155
 
150
156
  // Lookup helper functions
@@ -158,7 +164,7 @@ module.exports = class extends Model {
158
164
 
159
165
  // Transform value
160
166
  const transformedValue = transformers.reduce((value, t) => {
161
- const v = t({ model, field, path, docPath, rootPath, parentPath, startValue, value, resolver, context });
167
+ const v = t({ base, model, field, path, docPath, rootPath, parentPath, startValue, value, resolver, context });
162
168
  return v === undefined ? value : v;
163
169
  }, startValue);
164
170
 
@@ -167,22 +173,31 @@ module.exports = class extends Model {
167
173
  if (!instructed && subShape && typeof transformedValue !== 'object') return prev;
168
174
 
169
175
  // Rename key & assign value
170
- prev[to] = (!subShape || transformedValue == null) ? transformedValue : this.shapeObject(subShape, transformedValue, query, root);
176
+ prev[to] = (!subShape || transformedValue == null) ? transformedValue : this.shapeObject(subShape, transformedValue, query, root, base, toFlat);
177
+
178
+ if (toFlat && get(doc, to) && field.getModelRef()) {
179
+ const val = prev[to];
180
+ delete prev[to];
181
+ Object.assign(prev, flatten({ [to]: val }, { safe: true, depth: 1 }));
182
+ }
171
183
 
172
184
  return prev;
173
185
  }, {});
174
186
  });
175
187
  }
176
188
 
177
- validateObject(shape, obj, query, root, silent = false) {
189
+ validateObject(shape, obj, query, root, base, silent = false) {
178
190
  const { model } = shape;
179
191
  const { context, resolver, doc = {}, flags = {} } = query.toObject();
180
192
  const { validate = true } = flags;
181
193
 
182
194
  if (!validate) return Promise.resolve();
183
195
 
196
+ // base is the base model
197
+ base = base || model;
198
+
184
199
  return mapPromise(obj, (parent) => {
185
- // "root" is the base of the object
200
+ // root is the base data object
186
201
  root = root || parent;
187
202
 
188
203
  // Lookup helper functions
@@ -195,10 +210,10 @@ module.exports = class extends Model {
195
210
 
196
211
  return Promise.all(validators.map((v) => {
197
212
  return new Promise((resolve, reject) => {
198
- return Promise.resolve(v({ model, field, path, docPath, rootPath, parentPath, startValue: value, value, resolver, context })).then(resolve).catch(reject);
213
+ return Promise.resolve(v({ base, model, field, path, docPath, rootPath, parentPath, startValue: value, value, resolver, context })).then(resolve).catch(reject);
199
214
  });
200
215
  })).then(() => {
201
- return subShape ? this.validateObject(subShape, value, query, root, true) : Promise.resolve();
216
+ return subShape ? this.validateObject(subShape, value, query, root, base, true) : Promise.resolve();
202
217
  });
203
218
  }));
204
219
  }).then(() => {
@@ -42,22 +42,6 @@ module.exports = class Pipeline {
42
42
  return Object.defineProperty(Pipeline, name, { value: (...args) => Object.defineProperty(thunk(...args), 'options', { value: options }) })[name];
43
43
  }
44
44
 
45
- // static wrapper(name, factory, { ignoreNull, itemize }) {
46
- // return Object.defineProperty((args) => {
47
- // if (ignoreNull && args.value == null) return args.value;
48
-
49
- // if (ignoreNull && itemize) {
50
- // return map(args.value, (val, index) => {
51
- // const v = factory({ ...args, value: val, index });
52
- // return v === undefined ? val : v;
53
- // });
54
- // }
55
-
56
- // const val = factory(args);
57
- // return val === undefined ? args.value : val;
58
- // }, 'name', { value: name });
59
- // }
60
-
61
45
  static createPresets() {
62
46
  // Built-In Javascript String Transformers
63
47
  const jsStringTransformers = ['toLowerCase', 'toUpperCase', 'toString', 'trim', 'trimEnd', 'trimStart'];
@@ -76,12 +60,13 @@ module.exports = class Pipeline {
76
60
  Pipeline.define('idField', ({ model, field, value }) => field.getIdModel().idValue(value.id || value));
77
61
  Pipeline.define('ensureArrayValue', ({ field, value }) => (field.toObject().isArray && !Array.isArray(value) ? [value] : value), { itemize: false });
78
62
 
79
- Pipeline.define('ensureId', ({ resolver, field, value }) => {
63
+ Pipeline.define('ensureId', ({ resolver, model, field, value }) => {
64
+ const path = `${model}.${field}`;
80
65
  const { type } = field.toObject();
81
66
  const ids = Array.from(new Set(ensureArray(value).map(v => `${v}`)));
82
67
 
83
68
  return resolver.match(type).where({ id: ids }).count().then((count) => {
84
- if (count !== ids.length) throw Boom.notFound(`${type} Not Found`);
69
+ if (count !== ids.length) throw Boom.notFound(`${type} Not Found`, { path });
85
70
  });
86
71
  }, { itemize: false });
87
72
 
@@ -124,29 +109,34 @@ module.exports = class Pipeline {
124
109
 
125
110
  // Required fields
126
111
  Pipeline.define('required', ({ model, field, value }) => {
127
- if (value == null) throw Boom.badRequest(`${model}.${field} is required`);
112
+ const path = `${model}.${field}`;
113
+ if (value == null) throw Boom.badRequest(`${path} is required`, { path });
128
114
  }, { ignoreNull: false });
129
115
 
130
116
  // A field cannot hold a reference to itself
131
117
  Pipeline.define('selfless', ({ model, field, parent, parentPath, value }) => {
132
- if (`${value}` === `${parentPath('id')}`) throw Boom.badRequest(`${model}.${field} cannot hold a reference to itself`);
118
+ const path = `${model}.${field}`;
119
+ if (`${value}` === `${parentPath('id')}`) throw Boom.badData(`${path} cannot hold a reference to itself`, { path });
133
120
  });
134
121
 
135
122
  // Once set it cannot be changed
136
123
  Pipeline.define('immutable', ({ model, field, docPath, parentPath, path, value }) => {
124
+ const $path = `${model}.${field}`;
137
125
  const hint = { id: parentPath('id') };
138
126
  const oldVal = docPath(path, hint);
139
- if (oldVal !== undefined && value !== undefined && `${hashObject(oldVal)}` !== `${hashObject(value)}`) throw Boom.badRequest(`${model}.${field} is immutable; cannot be changed once set ${oldVal} -> ${value}`);
127
+ if (oldVal !== undefined && value !== undefined && `${hashObject(oldVal)}` !== `${hashObject(value)}`) throw Boom.badData(`${$path} is immutable; cannot be changed once set ${oldVal} -> ${value}`, { path: $path });
140
128
  });
141
129
 
142
130
  // List of allowed values
143
131
  Pipeline.factory('Allow', (...args) => function allow({ model, field, value }) {
144
- if (args.indexOf(value) === -1) throw Boom.badRequest(`${model}.${field} allows ${args}; found '${value}'`);
132
+ const path = `${model}.${field}`;
133
+ if (args.indexOf(value) === -1) throw Boom.badData(`${path} allows ${args}; found '${value}'`, { path });
145
134
  });
146
135
 
147
136
  // List of disallowed values
148
137
  Pipeline.factory('Deny', (...args) => function deny({ model, field, value }) {
149
- if (args.indexOf(value) > -1) throw Boom.badRequest(`${model}.${field} denys ${args}; found '${value}'`);
138
+ const path = `${model}.${field}`;
139
+ if (args.indexOf(value) > -1) throw Boom.badData(`${path} denys ${args}; found '${value}'`, { path });
150
140
  });
151
141
 
152
142
  // Min/Max range
@@ -155,9 +145,10 @@ module.exports = class Pipeline {
155
145
  if (max == null) max = undefined;
156
146
 
157
147
  return function range({ model, field, value }) {
148
+ const path = `${model}.${field}`;
158
149
  const num = +value; // Coerce to number if possible
159
150
  const test = Number.isNaN(num) ? value.length : num;
160
- if (test < min || test > max) throw Boom.badRequest(`${model}.${field} must satisfy range ${min}:${max}; found '${value}'`);
151
+ if (test < min || test > max) throw Boom.badData(`${path} must satisfy range ${min}:${max}; found '${value}'`, { path });
161
152
  };
162
153
  }, { itemize: false });
163
154
  }
package/src/data/Type.js CHANGED
@@ -30,7 +30,7 @@ module.exports = class extends Type {
30
30
  structures.defaultValue = Pipeline.defaultValue;
31
31
  structures.ensureArrayValue = Pipeline.ensureArrayValue;
32
32
 
33
- if (enumType) structures.validators.push(Pipeline.define(`allow:${type}`, Pipeline.Allow(...enumType.getValue()), { configurable: true }));
33
+ if (enumType) structures.validators.push(Pipeline.define(`allow${type}`, Pipeline.Allow(...enumType.getValue()), { configurable: true }));
34
34
  if (!scalarType) return structures;
35
35
 
36
36
  return Object.entries(scalarType.getDirectiveArgs('field', {})).reduce((prev, [key, value]) => {
Binary file
@@ -1,11 +1,13 @@
1
1
  const Util = require('util');
2
2
  const { get } = require('lodash');
3
+ const { unflatten } = require('@coderich/util');
3
4
  const { MongoClient, ObjectId } = require('mongodb');
4
5
  const { map, ensureArray, proxyDeep, toKeyObj, globToRegex, proxyPromise, isScalarDataType, promiseRetry } = require('../service/app.service');
5
6
 
6
7
  module.exports = class MongoDriver {
7
8
  constructor(config) {
8
9
  this.config = config;
10
+ this.config.query = this.config.query || {};
9
11
  this.connection = this.connect();
10
12
  this.getDirectives = () => get(config, 'directives', {});
11
13
  }
@@ -48,15 +50,15 @@ module.exports = class MongoDriver {
48
50
 
49
51
  findMany(query) {
50
52
  const { model, options = {}, flags } = query;
51
- Object.assign(options, this.config.query || {});
52
- return this.query(model, 'aggregate', MongoDriver.aggregateQuery(query), options, flags).then(cursor => cursor.stream());
53
+ const $options = { ...this.config.query, ...options };
54
+ return this.query(model, 'aggregate', MongoDriver.aggregateQuery(query), $options, flags).then(cursor => cursor.stream());
53
55
  }
54
56
 
55
57
  count(query) {
56
58
  const { model, options = {}, flags } = query;
57
- Object.assign(options, this.config.query || {});
59
+ const $options = { ...this.config.query, ...options };
58
60
 
59
- return this.query(model, 'aggregate', MongoDriver.aggregateQuery(query, true), options, flags).then((cursor) => {
61
+ return this.query(model, 'aggregate', MongoDriver.aggregateQuery(query, true), $options, flags).then((cursor) => {
60
62
  return cursor.next().then((doc) => {
61
63
  return doc ? doc.count : 0;
62
64
  });
@@ -70,7 +72,7 @@ module.exports = class MongoDriver {
70
72
 
71
73
  updateOne({ model, where, $doc, options, flags }) {
72
74
  const $update = { $set: $doc };
73
- return this.query(model, 'updateOne', where, $update, options, flags).then(() => $doc);
75
+ return this.query(model, 'updateOne', where, $update, options, flags).then(() => unflatten($doc, { safe: true }));
74
76
  }
75
77
 
76
78
  deleteOne({ model, where, options, flags }) {
@@ -97,25 +99,23 @@ module.exports = class MongoDriver {
97
99
  }
98
100
 
99
101
  transaction(ops) {
100
- const promise = async () => {
102
+ return promiseRetry(() => {
101
103
  // Create session and start transaction
102
- const session = await this.connection.then(client => client.startSession({ readPreference: { mode: 'primary' } }));
103
- session.startTransaction({ readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' } });
104
- const close = () => { session.endSession(); };
105
-
106
- // Execute each operation with session
107
- return Promise.all(ops.map(op => op.exec({ session }))).then((results) => {
108
- results.$commit = () => session.commitTransaction().then(close);
109
- results.$rollback = () => session.abortTransaction().then(close);
110
- return results;
111
- }).catch((e) => {
112
- close();
113
- throw e;
104
+ return this.connection.then(client => client.startSession({ readPreference: { mode: 'primary' } })).then((session) => {
105
+ session.startTransaction({ readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' } });
106
+ const close = () => { session.endSession(); };
107
+
108
+ // Execute each operation with session
109
+ return Promise.all(ops.map(op => op.exec({ session }))).then((results) => {
110
+ results.$commit = () => session.commitTransaction().then(close);
111
+ results.$rollback = () => session.abortTransaction().then(close);
112
+ return results;
113
+ }).catch((e) => {
114
+ close();
115
+ throw e;
116
+ });
114
117
  });
115
- };
116
-
117
- // Retry promise conditionally
118
- return promiseRetry(promise, 200, 5, e => e.errorLabels && e.errorLabels.indexOf('TransientTransactionError') > -1);
118
+ }, 200, 5, e => e.errorLabels && e.errorLabels.indexOf('TransientTransactionError') > -1);
119
119
  }
120
120
 
121
121
  static idKey() {
Binary file
Binary file
@@ -128,6 +128,6 @@ module.exports = class Schema extends TypeDefApi {
128
128
  }
129
129
 
130
130
  toString() {
131
- return print(this.typeDefs);
131
+ return print(this.schema.typeDefs);
132
132
  }
133
133
  };
Binary file
@@ -107,8 +107,6 @@ module.exports = (schema) => {
107
107
  `type Query {
108
108
  node(id: ID!): Node
109
109
  ${entityModels.map(model => makeReadAPI(model.getName(), model))}
110
- ${entityModels.map(model => makeReadAPI(`${model.getName()}Create`, model))}
111
- ${entityModels.map(model => makeReadAPI(`${model.getName()}Update`, model))}
112
110
  }`,
113
111
 
114
112
  `type Mutation {
@@ -8,7 +8,6 @@ module.exports = (schema) => {
8
8
  scalar AutoGraphMixed
9
9
  scalar AutoGraphDriver
10
10
  scalar AutoGraphDateTime @field(transform: toDate)
11
- enum AutoGraphSchemaEnum { ${schema.getModels().map(m => m.getName()).join(' ')} }
12
11
  enum AutoGraphPipelineEnum { ${Object.keys(Pipeline).join(' ')} }
13
12
  enum AutoGraphAuthzEnum { private protected public }
14
13
  enum AutoGraphOnDeleteEnum { cascade nullify restrict defer }
Binary file
@@ -1,5 +1,3 @@
1
- const Boom = require('../core/Boom');
2
-
3
1
  module.exports = class Query {
4
2
  constructor(props = {}) {
5
3
  this.props = {};
@@ -21,7 +19,7 @@ module.exports = class Query {
21
19
 
22
20
  propCheck(prop, ...checks) {
23
21
  checks.forEach((check) => {
24
- if (this.props[check]) throw Boom.badRequest(`Cannot use "${prop}" while using "${check}"`);
22
+ if (this.props[check]) throw new Error(`Cannot use "${prop}" while using "${check}"`);
25
23
  });
26
24
  }
27
25
 
@@ -98,7 +96,7 @@ module.exports = class Query {
98
96
 
99
97
  skip(skip) {
100
98
  this.propCheck('skip', 'id');
101
- if (this.isCursorPaging) throw Boom.badRequest('Cannot use "skip" while using Cursor-Style Pagination');
99
+ if (this.isCursorPaging) throw new Error('Cannot use "skip" while using Cursor-Style Pagination');
102
100
  this.isClassicPaging = true;
103
101
  this.props.skip = skip;
104
102
  return this;
@@ -106,7 +104,7 @@ module.exports = class Query {
106
104
 
107
105
  limit(limit) {
108
106
  this.propCheck('limit', 'id');
109
- if (this.isCursorPaging) throw Boom.badRequest('Cannot use "limit" while using Cursor-Style Pagination');
107
+ if (this.isCursorPaging) throw new Error('Cannot use "limit" while using Cursor-Style Pagination');
110
108
  this.isClassicPaging = true;
111
109
  this.props.limit = limit;
112
110
  return this;
@@ -114,7 +112,7 @@ module.exports = class Query {
114
112
 
115
113
  first(first) {
116
114
  this.propCheck('first', 'id', 'last');
117
- if (this.isClassicPaging) throw Boom.badRequest('Cannot use "first" while using Classic-Style Pagination');
115
+ if (this.isClassicPaging) throw new Error('Cannot use "first" while using Classic-Style Pagination');
118
116
  this.isCursorPaging = true;
119
117
  this.props.first = first + 2; // Adding 2 for pagination meta info (hasNext hasPrev)
120
118
  return this;
@@ -122,7 +120,7 @@ module.exports = class Query {
122
120
 
123
121
  last(last) {
124
122
  this.propCheck('last', 'id', 'first');
125
- if (this.isClassicPaging) throw Boom.badRequest('Cannot use "last" while using Classic-Style Pagination');
123
+ if (this.isClassicPaging) throw new Error('Cannot use "last" while using Classic-Style Pagination');
126
124
  this.isCursorPaging = true;
127
125
  this.props.last = last + 2; // Adding 2 for pagination meta info (hasNext hasPrev)
128
126
  return this;
@@ -130,7 +128,7 @@ module.exports = class Query {
130
128
 
131
129
  before(before) {
132
130
  this.propCheck('before', 'id');
133
- if (this.isClassicPaging) throw Boom.badRequest('Cannot use "before" while using Classic-Style Pagination');
131
+ if (this.isClassicPaging) throw new Error('Cannot use "before" while using Classic-Style Pagination');
134
132
  this.isCursorPaging = true;
135
133
  this.props.before = before;
136
134
  return this;
@@ -138,7 +136,7 @@ module.exports = class Query {
138
136
 
139
137
  after(after) {
140
138
  this.propCheck('after', 'id');
141
- if (this.isClassicPaging) throw Boom.badRequest('Cannot use "after" while using Classic-Style Pagination');
139
+ if (this.isClassicPaging) throw new Error('Cannot use "after" while using Classic-Style Pagination');
142
140
  this.isCursorPaging = true;
143
141
  this.props.after = after;
144
142
  return this;
@@ -10,7 +10,7 @@ const { unravelObject } = require('../service/app.service');
10
10
  */
11
11
  module.exports = class QueryBuilder {
12
12
  constructor(resolver, model) {
13
- this.terminated = false; // Prevent accidental re-use of the QueryBuilder
13
+ // this.terminated = false; // Prevent accidental re-use of the QueryBuilder
14
14
  this.query = new Query({ model, resolver });
15
15
 
16
16
  // Chainable commands
@@ -58,10 +58,10 @@ module.exports = class QueryBuilder {
58
58
  }
59
59
 
60
60
  execute(cmd, args) {
61
- // Do not allow re-use
62
- if (this.terminated) return Promise.reject(new Error('This query has already been executed'));
61
+ // // Do not allow re-use
62
+ // if (this.terminated) return Promise.reject(new Error('This query has already been executed'));
63
+ // this.terminated = true;
63
64
 
64
- this.terminated = true;
65
65
  let method, crud, input, flags = {};
66
66
  const { id, where } = this.query.toObject();
67
67
 
@@ -3,7 +3,7 @@ const Boom = require('../core/Boom');
3
3
  const QueryService = require('./QueryService');
4
4
  const DataService = require('../data/DataService');
5
5
  const { createSystemEvent } = require('../service/event.service');
6
- const { objectIntersectionEqual, mergeDeep, getGQLReturnType } = require('../service/app.service');
6
+ const { mergeDeep, hashObject, getGQLReturnType } = require('../service/app.service');
7
7
 
8
8
  module.exports = class QueryResolver {
9
9
  constructor(query) {
@@ -76,19 +76,27 @@ module.exports = class QueryResolver {
76
76
  }
77
77
 
78
78
  async updateOne(query) {
79
- const { model, match, input } = query.toObject();
79
+ const { model, match, input, flags } = query.toObject();
80
80
  const inputShape = model.getShape('update', 'input');
81
81
  const docShape = model.getShape('update', 'doc');
82
82
 
83
83
  return this.resolver.match(model).match(match).one({ required: true }).then((doc) => {
84
- if (objectIntersectionEqual(doc, input)) return doc; // If no changes do not perform query
85
-
86
84
  const merged = mergeDeep(doc, input);
85
+ const docHash = hashObject(doc);
86
+ const skipUnchanged = get(flags, 'skipUnchanged');
87
+
88
+ // Prevent udpates when no data has changed
89
+ if (skipUnchanged && docHash === hashObject(merged)) return doc;
87
90
 
88
91
  return createSystemEvent('Mutation', { query: query.doc(doc).merged(merged) }, async () => {
92
+ // Prevent udpates when no data has changed (because merged can be mutated)
93
+ if (skipUnchanged && docHash === hashObject(merged)) return doc;
94
+
95
+ // Process
89
96
  const payload = model.shapeObject(inputShape, merged, query);
90
97
  await model.validateObject(inputShape, payload, query.payload(payload));
91
- return this.resolver.resolve(query.$doc(model.shapeObject(docShape, payload, query)));
98
+ const $doc = model.shapeObject(docShape, payload, query, undefined, undefined, true);
99
+ return this.resolver.resolve(query.$doc($doc));
92
100
  });
93
101
  });
94
102
  }
@@ -187,7 +195,8 @@ module.exports = class QueryResolver {
187
195
  // Can only splice arrays
188
196
  const field = model.getField(key);
189
197
  const isArray = field.isArray();
190
- if (!isArray) throw Boom.badRequest(`Cannot splice field '${model}.${field}'`);
198
+ const path = `${model}.${field}`;
199
+ if (!isArray) throw Boom.badRequest(`Cannot splice field '${path}'`, { path });
191
200
 
192
201
  return this.resolver.match(model).match(match).one({ required: true }).then(async (doc) => {
193
202
  const array = get(doc, key) || [];
@@ -58,9 +58,9 @@ exports.resolveSortBy = (query) => {
58
58
  delete $sort[attr];
59
59
  query.joins(Object.assign(join, { as: `_.${field}`, left: true }));
60
60
  path = `_.${path}`;
61
+ } else {
62
+ set($sort, path, val.toLowerCase() === 'asc' ? 1 : -1);
61
63
  }
62
-
63
- set($sort, path, val.toLowerCase() === 'asc' ? 1 : -1);
64
64
  });
65
65
 
66
66
  return $sort;
Binary file
@@ -130,9 +130,6 @@ exports.castCmp = (type, value) => {
130
130
  }
131
131
  };
132
132
 
133
- /**
134
- * Returns true if b is a subset of a
135
- */
136
133
  exports.objectContaining = (a, b) => {
137
134
  if (a === b) return true;
138
135
 
@@ -140,7 +137,7 @@ exports.objectContaining = (a, b) => {
140
137
  return exports.keyPathLeafs(b).every((leaf) => {
141
138
  const $a = _.get(a, leaf, { a: 'a' });
142
139
  const $b = _.get(b, leaf, { b: 'b' });
143
- if (Array.isArray($b)) return $b.every(bb => exports.ensureArray($a).some(aa => exports.objectContaining(aa, bb)));
140
+ if (Array.isArray($b)) return $b.some(bb => exports.ensureArray($a).some(aa => exports.objectContaining(aa, bb)));
144
141
  if (exports.isScalarValue($a) && exports.isScalarValue($b)) return PicoMatch.isMatch(`${$a}`, `${$b}`, { nocase: true });
145
142
  return exports.hashObject($a) === exports.hashObject($b);
146
143
  });
@@ -149,23 +146,6 @@ exports.objectContaining = (a, b) => {
149
146
  return exports.hashObject(a) === exports.hashObject(b);
150
147
  };
151
148
 
152
- /**
153
- * Returns true if (b intersection a) are equal
154
- */
155
- exports.objectIntersectionEqual = (a, b) => {
156
- if (a === b) return true;
157
-
158
- if (exports.isPlainObject(b)) {
159
- return exports.keyPathLeafs(b).every((leaf) => {
160
- const $a = _.get(a, leaf, { a: 'a' });
161
- const $b = _.get(b, leaf, { b: 'b' });
162
- return exports.hashObject($a) === exports.hashObject($b);
163
- });
164
- }
165
-
166
- return exports.hashObject(a) === exports.hashObject(b);
167
- };
168
-
169
149
  /**
170
150
  * Transform an object with dot.notation keys into an expanded object.
171
151
  * eg. { 'user.name': 'richard' } => { user: { name: 'richard' } }
@@ -29,7 +29,7 @@ const makeMiddleware = () => {
29
29
  query.match($$where);
30
30
  }
31
31
 
32
- if (sort) {
32
+ if (sort && Object.keys(sort).length) {
33
33
  query.$sort(QueryService.resolveSortBy(query));
34
34
  }
35
35