@coderich/autograph 0.10.3 → 0.11.0

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.10.3",
4
+ "version": "0.11.0",
5
5
  "description": "AutoGraph",
6
6
  "keywords": [
7
7
  "graphql",
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "scripts": {
25
25
  "start": "APP_ROOT_PATH=$(pwd) node ./test/server",
26
- "test": "APP_ROOT_PATH=$(pwd) ratchet test --forceExit",
26
+ "test": "APP_ROOT_PATH=$(pwd) ratchet test",
27
27
  "test:debug": "APP_ROOT_PATH=$(pwd) node --inspect-brk ./node_modules/jest/bin/jest.js --watch --runInBand --logHeapUsage",
28
28
  "lint": "APP_ROOT_PATH=$(pwd) ratchet lint",
29
29
  "inspect": "APP_ROOT_PATH=$(pwd) node --expose-gc --inspect=9222 ./src/server",
@@ -37,10 +37,18 @@ module.exports = class extends Schema {
37
37
  });
38
38
  }
39
39
 
40
+ disconnect() {
41
+ return Promise.all(Object.values(this.drivers).map(({ dao }) => dao.disconnect()));
42
+ }
43
+
40
44
  initialize() {
41
45
  super.initialize();
42
46
  this.models = super.getModels().map(model => new Model(this, model, this.drivers[model.getDriverName()]));
43
- this.models.forEach(model => model.initialize());
47
+ return this;
48
+ }
49
+
50
+ finalize() {
51
+ super.finalize();
44
52
  this.models.forEach(model => model.referentialIntegrity(identifyOnDeletes(this.models, model)));
45
53
  return this;
46
54
  }
@@ -42,8 +42,6 @@ module.exports = class DataLoader extends FBDataLoader {
42
42
  */
43
43
  const whereShape = model.getShape('create', 'where');
44
44
 
45
- // console.log(Object.entries(batchQueries).map(([key, value]) => ({ [key]: value.length })));
46
-
47
45
  return Promise.all(Object.entries(batchQueries).map(([key, values]) => {
48
46
  switch (key) {
49
47
  case defaultBatchName: {
@@ -52,7 +50,7 @@ module.exports = class DataLoader extends FBDataLoader {
52
50
  default: {
53
51
  const keys = Array.from(new Set(values.map(({ where }) => map(where[key], el => `${el}`)).flat()));
54
52
  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
53
+ const batchWhere = model.shapeObject(whereShape, { ...values[0].where, [key]: keys }, batchQuery); // All where's should be the same - this is for idKey on keys etc
56
54
 
57
55
  return driver.resolve(batchQuery.where(batchWhere).toDriver()).then(data => handleData(data, model, batchQuery)).then((results) => {
58
56
  // One-time data transformation on results to make matching back faster (below)
@@ -11,7 +11,7 @@ exports.finalizeResults = (rs, query) => {
11
11
  $remove: { value: (...args) => resolver.match(model).id(doc.id).remove(...args) },
12
12
  $delete: { value: (...args) => resolver.match(model).id(doc.id).delete(...args) },
13
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) },
14
+ // $resolve: { value: (fieldName, args) => model.getFieldByName(fieldName).resolve(resolver, doc, args) },
15
15
  });
16
16
  });
17
17
  };
package/src/data/Model.js CHANGED
@@ -65,7 +65,9 @@ module.exports = class extends Model {
65
65
  }).then(rs => finalizeResults(rs, query));
66
66
  }
67
67
 
68
- getShape(crud = 'read', target = 'doc', paths = []) {
68
+ getShape(crud = 'read', target = 'doc', paths = [], depth = 0) {
69
+ if (depth++ > 10) return {}; // Prevent infinite circular references
70
+
69
71
  // Cache check
70
72
  const cacheKey = `${crud}:${target}`;
71
73
  if (this.shapesCache.has(cacheKey)) return this.shapesCache.get(cacheKey);
@@ -73,6 +75,7 @@ module.exports = class extends Model {
73
75
  const serdes = crud === 'read' ? 'deserialize' : 'serialize';
74
76
  const fields = serdes === 'deserialize' ? this.getSelectFields() : this.getPersistableFields();
75
77
  const crudMap = { create: ['constructs'], update: ['restructs'], delete: ['destructs'], remove: ['destructs'] };
78
+ const sortKeys = ['isIdField', 'isBasicType', 'isEmbedded'];
76
79
  const crudKeys = crudMap[crud] || [];
77
80
 
78
81
  // Define target mapping
@@ -82,10 +85,29 @@ module.exports = class extends Model {
82
85
  // input: ['defaultValue', 'castValue', 'ensureArrayValue'],
83
86
  where: ['castValue', 'instructs', `$${serdes}rs`],
84
87
  };
88
+
85
89
  const structureKeys = targetMap[target] || ['castValue'];
86
90
 
87
- // Create shape, recursive
88
- const shape = fields.map((field) => {
91
+ // Create sorted shape, recursive
92
+ const shape = fields.sort((a, b) => {
93
+ const aObject = a.toObject();
94
+ const bObject = b.toObject();
95
+
96
+ // PK first
97
+ if (aObject.isPrimaryKeyId) return -1;
98
+ if (bObject.isPrimaryKeyId) return 1;
99
+
100
+ // Arrays last
101
+ if (aObject.isArray && !bObject.isArray) return 1;
102
+ if (bObject.isArray && !aObject.isArray) return -1;
103
+
104
+ // Now, follow sort keys
105
+ const aNum = sortKeys.findIndex(key => aObject[key]);
106
+ const bNum = sortKeys.findIndex(key => bObject[key]);
107
+ if (aNum < bNum) return -1;
108
+ if (aNum > bNum) return 1;
109
+ return 0;
110
+ }).map((field) => {
89
111
  let instructed = false;
90
112
  const structures = field.getStructures();
91
113
  const { key, name, type, isArray, isEmbedded, modelRef } = field.toObject();
@@ -93,9 +115,8 @@ module.exports = class extends Model {
93
115
  const actualTo = target === 'input' || target === 'splice' ? from : to;
94
116
  const path = paths.concat(actualTo);
95
117
  const subCrud = crud === 'update' && isArray ? 'create' : crud; // Due to limitation to update embedded array
96
- const subShape = isEmbedded ? modelRef.getShape(subCrud, target, path) : null;
118
+ const subShape = isEmbedded ? modelRef.getShape(subCrud, target, path, depth) : null;
97
119
  const transformers = structureKeys.reduce((prev, struct) => {
98
- if (instructed) return prev;
99
120
  const structs = structures[struct];
100
121
  if (struct === 'instructs' && structs.length) instructed = true;
101
122
  return prev.concat(structs);
@@ -172,7 +193,11 @@ module.exports = class extends Model {
172
193
  return Promise.all(shape.map(({ field, from, path, validators, shape: subShape }) => {
173
194
  const value = parent[from]; // It hasn't been shaped yet
174
195
 
175
- return Promise.all(validators.map(v => v({ model, field, path, docPath, rootPath, parentPath, startValue: value, value, resolver, context }))).then(() => {
196
+ return Promise.all(validators.map((v) => {
197
+ 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);
199
+ });
200
+ })).then(() => {
176
201
  return subShape ? this.validateObject(subShape, value, query, root, true) : Promise.resolve();
177
202
  });
178
203
  }));
@@ -64,15 +64,12 @@ module.exports = class MongoDriver {
64
64
  }
65
65
 
66
66
  createOne({ model, input, options, flags }) {
67
+ // console.log(JSON.stringify(input, null, 2));
67
68
  return this.query(model, 'insertOne', input, options, flags).then(result => Object.assign(input, { id: result.insertedId }));
68
69
  }
69
70
 
70
71
  updateOne({ model, where, $doc, options, flags }) {
71
- const $update = Object.entries($doc).reduce((prev, [key, value]) => {
72
- Object.assign(prev.$set, { [key]: value });
73
- return prev;
74
- }, { $set: {} });
75
-
72
+ const $update = { $set: $doc };
76
73
  return this.query(model, 'updateOne', where, $update, options, flags).then(() => $doc);
77
74
  }
78
75
 
@@ -173,7 +173,7 @@ module.exports = class Field extends Node {
173
173
  return this.getGQLType();
174
174
  }
175
175
 
176
- initialize() {
176
+ finalize() {
177
177
  this.props = {
178
178
  key: this.getKey(),
179
179
  name: this.getName(),
@@ -181,11 +181,13 @@ module.exports = class Field extends Node {
181
181
  model: this.model,
182
182
  datatype: this.getDataType(),
183
183
  defaultValue: this.getDefaultValue(),
184
+ isEnum: this.isEnum(),
184
185
  isArray: this.isArray(),
185
186
  isScalar: this.isScalar(),
186
187
  isVirtual: this.isVirtual(),
187
188
  isRequired: this.isRequired(),
188
189
  isEmbedded: this.isEmbedded(),
190
+ isBasicType: this.isBasicType(),
189
191
  isIdField: this.isIdField(),
190
192
  isPrimaryKeyId: this.isPrimaryKeyId(),
191
193
  isPersistable: this.isPersistable(),
@@ -138,8 +138,8 @@ module.exports = class Model extends Node {
138
138
  });
139
139
  }
140
140
 
141
- initialize() {
142
- this.fields.forEach(field => field.initialize());
141
+ finalize() {
142
+ this.fields.forEach(field => field.finalize());
143
143
  return this;
144
144
  }
145
145
  };
@@ -90,7 +90,6 @@ module.exports = class Schema extends TypeDefApi {
90
90
  */
91
91
  initialize() {
92
92
  super.initialize(this.schema.typeDefs);
93
- this.getModels().forEach(model => model.initialize());
94
93
  return this;
95
94
  }
96
95
 
@@ -119,8 +118,8 @@ module.exports = class Schema extends TypeDefApi {
119
118
  },
120
119
  });
121
120
 
121
+ this.getModels().forEach(model => model.finalize());
122
122
  this.schema.typeDefs = { kind: Kind.DOCUMENT, definitions };
123
- // validateSchema(this.schema);
124
123
  return this;
125
124
  }
126
125
 
@@ -8,6 +8,7 @@ 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(' ')} }
11
12
  enum AutoGraphPipelineEnum { ${Object.keys(Pipeline).join(' ')} }
12
13
  enum AutoGraphAuthzEnum { private protected public }
13
14
  enum AutoGraphOnDeleteEnum { cascade nullify restrict defer }
@@ -16,6 +17,7 @@ module.exports = (schema) => {
16
17
  directive @model(
17
18
  id: String # Specify db key (default "id")
18
19
  key: String # Specify db table/collection name
20
+ driver: AutoGraphDriver # External data driver
19
21
  createdAt: String # Specify db key (default "createdAt")
20
22
  updatedAt: String # Specify db key (default "updatedAt")
21
23
  meta: AutoGraphMixed # Custom input "meta" field for mutations
@@ -24,7 +26,6 @@ module.exports = (schema) => {
24
26
  gqlScope: AutoGraphMixed # Dictate how GraphQL API behaves
25
27
  dalScope: AutoGraphMixed # Dictate how the DAL behaves
26
28
  fieldScope: AutoGraphMixed # Dictate how a FIELD may use me
27
- driver: AutoGraphDriver # External data driver
28
29
  authz: AutoGraphAuthzEnum # Access level used for authorization (default: private)
29
30
  namespace: String # Logical grouping of models that can be globbed (useful for authz)
30
31
  ) on OBJECT | INTERFACE
@@ -45,12 +46,12 @@ module.exports = (schema) => {
45
46
 
46
47
  # Pipeline Structure
47
48
  validate: [AutoGraphPipelineEnum!]
48
- instruct: [AutoGraphPipelineEnum!]
49
+ construct: [AutoGraphPipelineEnum!]
49
50
  restruct: [AutoGraphPipelineEnum!]
50
51
  destruct: [AutoGraphPipelineEnum!]
51
- construct: [AutoGraphPipelineEnum!]
52
- transform: [AutoGraphPipelineEnum!]
52
+ instruct: [AutoGraphPipelineEnum!]
53
53
  normalize: [AutoGraphPipelineEnum!]
54
+ transform: [AutoGraphPipelineEnum!]
54
55
  serialize: [AutoGraphPipelineEnum!]
55
56
  deserialize: [AutoGraphPipelineEnum!]
56
57
  ) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION | SCALAR
@@ -10,6 +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
14
  this.query = new Query({ model, resolver });
14
15
 
15
16
  // Chainable commands
@@ -27,7 +28,7 @@ module.exports = class QueryBuilder {
27
28
  this.after = (cursor) => { this.query.after(cursor); return this; };
28
29
  this.meta = (meta) => { this.query.meta(meta); return this; };
29
30
  this.flags = (flags) => { this.query.flags(flags); return this; };
30
- this.merge = (merge) => { this.query.merge(merge); return this; };
31
+ this.merge = (merge) => { this.query.merge(unravelObject(merge)); return this; };
31
32
  this.batch = (batch) => { this.query.batch(batch); return this; };
32
33
  this.transaction = (txn) => { this.query.transaction(txn); return this; };
33
34
 
@@ -57,6 +58,10 @@ module.exports = class QueryBuilder {
57
58
  }
58
59
 
59
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'));
63
+
64
+ this.terminated = true;
60
65
  let method, crud, input, flags = {};
61
66
  const { id, where } = this.query.toObject();
62
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 { mergeDeep, getGQLReturnType } = require('../service/app.service');
6
+ const { objectIntersectionEqual, mergeDeep, getGQLReturnType } = require('../service/app.service');
7
7
 
8
8
  module.exports = class QueryResolver {
9
9
  constructor(query) {
@@ -81,6 +81,8 @@ module.exports = class QueryResolver {
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
+
84
86
  const merged = mergeDeep(doc, input);
85
87
 
86
88
  return createSystemEvent('Mutation', { query: query.doc(doc).merged(merged) }, async () => {
@@ -130,6 +130,9 @@ exports.castCmp = (type, value) => {
130
130
  }
131
131
  };
132
132
 
133
+ /**
134
+ * Returns true if b is a subset of a
135
+ */
133
136
  exports.objectContaining = (a, b) => {
134
137
  if (a === b) return true;
135
138
 
@@ -137,7 +140,7 @@ exports.objectContaining = (a, b) => {
137
140
  return exports.keyPathLeafs(b).every((leaf) => {
138
141
  const $a = _.get(a, leaf, { a: 'a' });
139
142
  const $b = _.get(b, leaf, { b: 'b' });
140
- if (Array.isArray($b)) return $b.some(bb => exports.ensureArray($a).some(aa => exports.objectContaining(aa, bb)));
143
+ if (Array.isArray($b)) return $b.every(bb => exports.ensureArray($a).some(aa => exports.objectContaining(aa, bb)));
141
144
  if (exports.isScalarValue($a) && exports.isScalarValue($b)) return PicoMatch.isMatch(`${$a}`, `${$b}`, { nocase: true });
142
145
  return exports.hashObject($a) === exports.hashObject($b);
143
146
  });
@@ -146,6 +149,23 @@ exports.objectContaining = (a, b) => {
146
149
  return exports.hashObject(a) === exports.hashObject(b);
147
150
  };
148
151
 
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
+
149
169
  /**
150
170
  * Transform an object with dot.notation keys into an expanded object.
151
171
  * eg. { 'user.name': 'richard' } => { user: { name: 'richard' } }
@@ -180,12 +200,6 @@ exports.keyPaths = (obj = {}, keys = [], path) => {
180
200
  return Object.entries(obj).reduce((prev, [key, value]) => {
181
201
  const keyPath = path ? `${path}.${key}` : key;
182
202
  if (exports.isPlainObject(value) && Object.keys(value).length) return exports.keyPaths(value, prev, keyPath);
183
-
184
- // if (Array.isArray(value)) {
185
- // const arr = value.filter(v => exports.isPlainObject(v));
186
- // if (arr.length) return _.flatten(arr.map(val => exports.keyPaths(val, prev, keyPath)));
187
- // }
188
-
189
203
  return prev.concat(keyPath);
190
204
  }, keys);
191
205
  };
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