@coderich/autograph 0.12.0 → 0.12.2

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@coderich/autograph",
3
3
  "author": "Richard Livolsi (coderich)",
4
- "version": "0.12.0",
4
+ "version": "0.12.2",
5
5
  "description": "AutoGraph",
6
6
  "keywords": [
7
7
  "graphql",
@@ -19,34 +19,32 @@
19
19
  "index.js"
20
20
  ],
21
21
  "engines": {
22
- "node": ">=14.17.0"
22
+ "node": ">=22.0.0"
23
23
  },
24
24
  "scripts": {
25
25
  "start": "APP_ROOT_PATH=$(pwd) node ./test/server",
26
- "test": "APP_ROOT_PATH=$(pwd) ratchet test",
27
- "test:debug": "APP_ROOT_PATH=$(pwd) node --inspect-brk ./node_modules/jest/bin/jest.js --watch --runInBand --logHeapUsage",
28
- "lint": "APP_ROOT_PATH=$(pwd) ratchet lint",
29
- "inspect": "APP_ROOT_PATH=$(pwd) node --expose-gc --inspect=9222 ./src/server",
30
- "ratchet": "ratchet"
26
+ "test": "jest --config=jest.config.js",
27
+ "dev": "coderich-dev",
28
+ "lint": "eslint ./"
31
29
  },
32
30
  "dependencies": {
31
+ "@coderich/util": "2.0.1",
33
32
  "@hapi/boom": "^9.1.0",
34
33
  "dataloader": "^2.0.0",
35
34
  "deepmerge": "^4.2.2",
36
35
  "fill-range": "^7.0.1",
37
- "flat": "^5.0.2",
38
- "glob": "^7.1.6",
36
+ "glob": "10.4.5",
39
37
  "graphql-fields": "^2.0.3",
40
38
  "lodash": "^4.17.21",
41
- "mongodb": "4.8.1",
42
- "object-hash": "^2.0.1",
39
+ "mongodb": "6.9.0",
40
+ "object-hash": "^3.0.0",
43
41
  "picomatch": "^2.1.1"
44
42
  },
45
43
  "devDependencies": {
46
- "@coderich/ratchet": "^1.5.8",
44
+ "@coderich/dev": "0.5.1",
47
45
  "@graphql-tools/schema": "^9.0.1",
48
46
  "graphql": "^15.5.0",
49
- "mongodb-memory-server": "^8.7.2",
47
+ "mongodb-memory-server": "10.0.0",
50
48
  "validator": "^13.7.0"
51
49
  },
52
50
  "peerDependencies": {
@@ -56,4 +54,4 @@
56
54
  "type": "git",
57
55
  "url": "git@github.com:coderich/autograph.git"
58
56
  }
59
- }
57
+ }
@@ -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
  }
@@ -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');
@@ -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, base) {
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;
@@ -170,7 +173,13 @@ module.exports = class extends Model {
170
173
  if (!instructed && subShape && typeof transformedValue !== 'object') return prev;
171
174
 
172
175
  // Rename key & assign value
173
- prev[to] = (!subShape || transformedValue == null) ? transformedValue : this.shapeObject(subShape, transformedValue, query, root, base);
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
+ }
174
183
 
175
184
  return prev;
176
185
  }, {});
@@ -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]) => {
@@ -1,12 +1,13 @@
1
1
  const Util = require('util');
2
- const Flat = require('flat');
3
2
  const { get } = require('lodash');
3
+ const { unflatten } = require('@coderich/util');
4
4
  const { MongoClient, ObjectId } = require('mongodb');
5
5
  const { map, ensureArray, proxyDeep, toKeyObj, globToRegex, proxyPromise, isScalarDataType, promiseRetry } = require('../service/app.service');
6
6
 
7
7
  module.exports = class MongoDriver {
8
8
  constructor(config) {
9
9
  this.config = config;
10
+ this.config.query = this.config.query || {};
10
11
  this.connection = this.connect();
11
12
  this.getDirectives = () => get(config, 'directives', {});
12
13
  }
@@ -28,7 +29,7 @@ module.exports = class MongoDriver {
28
29
  query(collection, method, ...args) {
29
30
  if (get(args[args.length - 1], 'debug') === true) console.log(collection, method, Util.inspect(args, { depth: null, showHidden: false, colors: true }));
30
31
  if (method === 'aggregate') args.splice(2);
31
- return this.raw(collection)[method](...args);
32
+ return this.raw(collection)[method](args[0], args[1]);
32
33
  }
33
34
 
34
35
  resolve(query) {
@@ -49,15 +50,15 @@ module.exports = class MongoDriver {
49
50
 
50
51
  findMany(query) {
51
52
  const { model, options = {}, flags } = query;
52
- Object.assign(options, this.config.query || {});
53
- 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());
54
55
  }
55
56
 
56
57
  count(query) {
57
58
  const { model, options = {}, flags } = query;
58
- Object.assign(options, this.config.query || {});
59
+ const $options = { ...this.config.query, ...options };
59
60
 
60
- 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) => {
61
62
  return cursor.next().then((doc) => {
62
63
  return doc ? doc.count : 0;
63
64
  });
@@ -69,8 +70,8 @@ module.exports = class MongoDriver {
69
70
  }
70
71
 
71
72
  updateOne({ model, where, $doc, options, flags }) {
72
- const $update = { $set: Flat.flatten($doc, { safe: true }) };
73
- return this.query(model, 'updateOne', where, $update, options, flags).then(() => $doc);
73
+ const $update = { $set: $doc };
74
+ return this.query(model, 'updateOne', where, $update, options, flags).then(() => unflatten($doc, { safe: true }));
74
75
  }
75
76
 
76
77
  deleteOne({ model, where, options, flags }) {
@@ -97,25 +98,23 @@ module.exports = class MongoDriver {
97
98
  }
98
99
 
99
100
  transaction(ops) {
100
- const promise = async () => {
101
+ return promiseRetry(() => {
101
102
  // 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;
103
+ return this.connection.then(client => client.startSession({ readPreference: { mode: 'primary' } })).then((session) => {
104
+ session.startTransaction({ readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' } });
105
+ const close = () => { session.endSession(); };
106
+
107
+ // Execute each operation with session
108
+ return Promise.all(ops.map(op => op.exec({ session }))).then((results) => {
109
+ results.$commit = () => session.commitTransaction().then(close);
110
+ results.$rollback = () => session.abortTransaction().then(close);
111
+ return results;
112
+ }).catch((e) => {
113
+ close();
114
+ throw e;
115
+ });
114
116
  });
115
- };
116
-
117
- // Retry promise conditionally
118
- return promiseRetry(promise, 200, 5, e => e.errorLabels && e.errorLabels.indexOf('TransientTransactionError') > -1);
117
+ }, 200, 5, e => e.errorLabels && e.errorLabels.indexOf('TransientTransactionError') > -1);
119
118
  }
120
119
 
121
120
  static idKey() {
@@ -126,9 +125,9 @@ module.exports = class MongoDriver {
126
125
  if (value instanceof ObjectId) return value;
127
126
 
128
127
  try {
129
- const id = ObjectId(value);
128
+ const id = new ObjectId(value);
130
129
  return id;
131
- } catch (e) {
130
+ } catch {
132
131
  return value;
133
132
  }
134
133
  }
@@ -140,7 +139,6 @@ module.exports = class MongoDriver {
140
139
  if (typeof value === 'function') return value.bind(target);
141
140
  const $value = map(value, v => (typeof v === 'string' ? globToRegex(v, { nocase: true, regex: true }) : v));
142
141
  if (Array.isArray($value)) {
143
- // console.log(Util.inspect({ value, $value }, { depth: null, showHidden: false, colors: true }));
144
142
  return { $in: $value };
145
143
  }
146
144
  return $value;
@@ -1,4 +1,3 @@
1
- /* eslint-disable global-require */
2
1
  module.exports = class Driver {
3
2
  constructor(name) {
4
3
  switch (name) {
@@ -117,7 +117,7 @@ module.exports = class Node {
117
117
  }
118
118
 
119
119
  getVirtualRef() {
120
- return this.getDirectiveArg('join', 'by');
120
+ return this.getDirectiveArg('link', 'by');
121
121
  }
122
122
 
123
123
  getAuthz() {
@@ -153,7 +153,7 @@ module.exports = class Node {
153
153
  * Is the field virtual; does it's value come from another model
154
154
  */
155
155
  isVirtual() {
156
- return Boolean(this.getDirectiveArg('join', 'by'));
156
+ return Boolean(this.getDirectiveArg('link', 'by'));
157
157
  }
158
158
 
159
159
  /**
@@ -268,7 +268,6 @@ module.exports = class Node {
268
268
  return Boolean(this.getDirectiveArg('field', 'resolve'));
269
269
  }
270
270
 
271
-
272
271
  // Create
273
272
  isCreatable() {
274
273
  return Boolean(this.getDALScope().toLowerCase().indexOf('c') > -1);
@@ -50,7 +50,7 @@ module.exports = class Schema extends TypeDefApi {
50
50
  try {
51
51
  const ast = typeof td === 'object' ? td : parse(td);
52
52
  return ast.definitions;
53
- } catch (e) {
53
+ } catch {
54
54
  return null;
55
55
  }
56
56
  }), ['loc']).filter(Boolean).flat();
@@ -70,18 +70,14 @@ module.exports = class Schema extends TypeDefApi {
70
70
  * Asynchronously load files from a given glob pattern and merge each schema
71
71
  */
72
72
  mergeSchemaFromFiles(globPattern, options) {
73
- return new Promise((resolve, reject) => {
74
- Glob(globPattern, options, (err, files) => {
75
- if (err) return reject(err);
76
-
77
- return Promise.all(files.map((file) => {
78
- return new Promise((res) => {
79
- if (file.endsWith('.js')) res(require(file)); // eslint-disable-line global-require,import/no-dynamic-require
80
- else res(FS.readFileSync(file, 'utf8'));
81
- }).then(schema => this.mergeSchema(schema, options));
82
- })).then(() => resolve(this)).catch(e => reject(e));
83
- });
84
- });
73
+ return Glob.glob(globPattern, options).then((files) => {
74
+ return Promise.all(files.sort().map((file) => {
75
+ return new Promise((res) => {
76
+ if (file.endsWith('.js')) res(require(file));
77
+ else res(FS.readFileSync(file, 'utf8'));
78
+ }).then(schema => this.mergeSchema(schema, options));
79
+ }));
80
+ }).then(() => this);
85
81
  }
86
82
 
87
83
  /**
@@ -9,7 +9,7 @@ const interfaceKinds = [Kind.INTERFACE_TYPE_DEFINITION, Kind.INTERFACE_TYPE_EXTE
9
9
 
10
10
  const getGQLWhereFields = (model) => {
11
11
  return model.getFields().filter((field) => {
12
- if (!field.hasGQLScope('r')) return false;
12
+ if (!field.isPersistable() || !field.hasGQLScope('r')) return false;
13
13
  const modelRef = field.getModelRef();
14
14
  if (modelRef && !modelRef.isEmbedded() && !modelRef.isEntity()) return false;
15
15
  return true;
@@ -150,7 +150,7 @@ module.exports = (schema) => {
150
150
 
151
151
  if (model.isEntity() && model.hasGQLScope('s')) {
152
152
  prev[`${model.getName()}SubscriptionQuery`] = {
153
- __resolveType: root => root.__typename, // eslint-disable-line no-underscore-dangle
153
+ __resolveType: root => root.__typename,
154
154
  ...fieldResolvers,
155
155
  };
156
156
  prev[`${model.getName()}Create`] = fieldResolvers;
@@ -167,7 +167,7 @@ module.exports = (schema) => {
167
167
  });
168
168
  }, {
169
169
  Node: {
170
- __resolveType: (doc, args, context, info) => doc.__typename, // eslint-disable-line no-underscore-dangle
170
+ __resolveType: (doc, args, context, info) => doc.__typename,
171
171
  },
172
172
 
173
173
  Query: entityModels.reduce((prev, model) => {
@@ -179,7 +179,7 @@ module.exports = (schema) => {
179
179
  const model = schema.getModel(modelName);
180
180
  return resolver.get(context, model, args, false, info).then((result) => {
181
181
  if (result == null) return result;
182
- result.__typename = modelName; // eslint-disable-line no-underscore-dangle
182
+ result.__typename = modelName;
183
183
  return result;
184
184
  });
185
185
  },
@@ -27,6 +27,12 @@ module.exports = (schema) => {
27
27
  fieldScope: AutoGraphMixed # Dictate how a FIELD may use me
28
28
  authz: AutoGraphAuthzEnum # Access level used for authorization (default: private)
29
29
  namespace: String # Logical grouping of models that can be globbed (useful for authz)
30
+
31
+ # FOR TRANSITION TO NEW VERSION
32
+ crud: AutoGraphMixed # CRUD API
33
+ scope: AutoGraphMixed #
34
+ source: AutoGraphMixed # Data source (default: "default")
35
+ decorate: AutoGraphMixed # Decorator (default: "default")
30
36
  ) on OBJECT | INTERFACE
31
37
 
32
38
  directive @field(
@@ -53,12 +59,16 @@ module.exports = (schema) => {
53
59
  transform: [AutoGraphPipelineEnum!]
54
60
  serialize: [AutoGraphPipelineEnum!]
55
61
  deserialize: [AutoGraphPipelineEnum!]
62
+
63
+ # FOR TRANSITION TO NEW VERSION
64
+ crud: AutoGraphMixed # CRUD API
65
+ finalize: [AutoGraphPipelineEnum!]
56
66
  ) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION | SCALAR
57
67
 
58
- directive @join(
59
- to: AutoGraphMixed # The MODEL to join to (default's to modelRef)
68
+ directive @link(
69
+ to: AutoGraphMixed # The MODEL to link to (default's to modelRef)
60
70
  by: AutoGraphMixed! # The FIELD to match yourself by
61
- use: AutoGraphMixed # The VALUE to use (default's to @join'd value); useful for many-to-many relationships
71
+ use: AutoGraphMixed # The VALUE to use (default's to @link'd value); useful for many-to-many relationships
62
72
  ) on FIELD_DEFINITION
63
73
 
64
74
  directive @index(
@@ -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;
@@ -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 { 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,17 +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
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;
85
90
 
86
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
87
96
  const payload = model.shapeObject(inputShape, merged, query);
88
97
  await model.validateObject(inputShape, payload, query.payload(payload));
89
- 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));
90
100
  });
91
101
  });
92
102
  }
@@ -185,7 +195,8 @@ module.exports = class QueryResolver {
185
195
  // Can only splice arrays
186
196
  const field = model.getField(key);
187
197
  const isArray = field.isArray();
188
- 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 });
189
200
 
190
201
  return this.resolver.match(model).match(match).one({ required: true }).then(async (doc) => {
191
202
  const array = get(doc, key) || [];
@@ -44,6 +44,7 @@ exports.resolveSortBy = (query) => {
44
44
  const { model, sort = {} } = query.toObject();
45
45
  const shape = model.getShape('create', 'sortBy');
46
46
  const $sort = model.shapeObject(shape, sort, query);
47
+ const deletions = [];
47
48
 
48
49
  // Because normalize casts the value (sometimes to an array) need special handling
49
50
  keyPaths($sort).forEach((path) => {
@@ -55,7 +56,7 @@ exports.resolveSortBy = (query) => {
55
56
 
56
57
  // If you need to sort by something that's in another FK document
57
58
  if (join) {
58
- delete $sort[attr];
59
+ deletions.push(attr); // Keep track of what to delete
59
60
  query.joins(Object.assign(join, { as: `_.${field}`, left: true }));
60
61
  path = `_.${path}`;
61
62
  }
@@ -63,6 +64,9 @@ exports.resolveSortBy = (query) => {
63
64
  set($sort, path, val.toLowerCase() === 'asc' ? 1 : -1);
64
65
  });
65
66
 
67
+ // Delete the sorts on the "base" collection because you're sorting by _.path.to.it (above)
68
+ deletions.forEach(attr => delete $sort[attr]);
69
+
66
70
  return $sort;
67
71
  };
68
72
 
@@ -40,7 +40,7 @@ exports.isIdValue = value => exports.isScalarValue(value) || value instanceof Ob
40
40
  exports.mergeDeep = (...args) => DeepMerge.all(args, { isMergeableObject: obj => (exports.isPlainObject(obj) || Array.isArray(obj)), arrayMerge: smartMerge });
41
41
  exports.uniq = arr => [...new Set(arr.map(a => `${a}`))];
42
42
  exports.timeout = ms => new Promise(res => setTimeout(res, ms));
43
- exports.hashObject = obj => ObjectHash(obj, { respectType: false, respectFunctionNames: false, respectFunctionProperties: false, unorderedArrays: true, ignoreUnknown: true, replacer: r => (r instanceof ObjectId ? `${r}` : r) });
43
+ exports.hashObject = obj => ObjectHash(obj, { respectType: false, respectFunctionNames: false, respectFunctionProperties: false, unorderedArrays: true, ignoreUnknown: true, replacer: r => (ObjectId.isValid(r) ? `${r}` : r) });
44
44
  exports.globToRegex = (glob, options = {}) => PicoMatch.makeRe(glob, { ...options, expandRange: (a, b) => `(${FillRange(a, b, { toRegex: true })})` });
45
45
  exports.globToRegexp = (glob, options = {}) => PicoMatch.toRegex(exports.globToRegex(glob, options));
46
46
  exports.toGUID = (model, id) => Buffer.from(`${model},${`${id}`}`).toString('base64');
@@ -3,53 +3,6 @@
3
3
  // https://graphql.org/graphql-js/utilities/
4
4
 
5
5
  const { uniqWith } = require('lodash');
6
- const { GraphQLScalarType, GraphQLObjectType, GraphQLInterfaceType, GraphQLUnionType, GraphQLEnumType, GraphQLInputObjectType } = require('graphql');
7
-
8
- exports.getSchemaData = (schema) => {
9
- const operations = ['Query', 'Mutation', 'Subscription'];
10
-
11
- return Object.entries(schema.getTypeMap()).reduce((prev, [key, value]) => {
12
- let type;
13
-
14
- if (value instanceof GraphQLScalarType) {
15
- type = 'scalars';
16
- } else if (value instanceof GraphQLEnumType) {
17
- type = 'enums';
18
- } else if (value instanceof GraphQLUnionType) {
19
- type = 'unions';
20
- } else if (value instanceof GraphQLInterfaceType) {
21
- type = 'interfaces';
22
- } else if (value instanceof GraphQLInputObjectType) {
23
- type = 'inputs';
24
- } else if (value instanceof GraphQLObjectType) {
25
- if (operations.includes(key)) {
26
- type = 'operations';
27
- } else {
28
- type = 'models';
29
- }
30
- }
31
-
32
- if (type) {
33
- if (!key.startsWith('__')) {
34
- prev[type][key] = value;
35
- }
36
- } else {
37
- console.log(`Unknown schema type { ${key}: ${value} }`);
38
- }
39
-
40
- return prev;
41
- }, {
42
- enums: {},
43
- models: {},
44
- inputs: {},
45
- unions: {},
46
- scalars: {},
47
- operations: {},
48
- directives: {},
49
- interfaces: {},
50
- enumerations: {},
51
- });
52
- };
53
6
 
54
7
  exports.identifyOnDeletes = (models, parentModel) => {
55
8
  return models.reduce((prev, model) => {
package/CHANGELOG.md DELETED
@@ -1,41 +0,0 @@
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/src/.DS_Store DELETED
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file