@coderich/autograph 0.9.11 → 0.9.14

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/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const Schema = require('./src/core/Schema');
2
+ const SchemaDecorator = require('./src/core/SchemaDecorator');
2
3
  const GraphQL = require('./src/core/GraphQL');
3
4
  const Resolver = require('./src/core/Resolver');
4
5
  const Rule = require('./src/core/Rule');
@@ -8,6 +9,7 @@ const { eventEmitter: Emitter } = require('./src/service/event.service');
8
9
 
9
10
  module.exports = {
10
11
  Schema,
12
+ SchemaDecorator,
11
13
  GraphQL,
12
14
  Resolver,
13
15
  Rule,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@coderich/autograph",
3
3
  "author": "Richard Livolsi (coderich)",
4
- "version": "0.9.11",
4
+ "version": "0.9.14",
5
5
  "description": "AutoGraph",
6
6
  "keywords": [
7
7
  "graphql",
@@ -30,6 +30,7 @@
30
30
  "ratchet": "ratchet"
31
31
  },
32
32
  "dependencies": {
33
+ "@graphql-tools/schema": "^8.3.14",
33
34
  "@hapi/boom": "^9.1.0",
34
35
  "axios": "^0.21.4",
35
36
  "dataloader": "^2.0.0",
@@ -54,6 +55,9 @@
54
55
  "redis": "^2.8.0",
55
56
  "redis-mock": "^0.47.0"
56
57
  },
58
+ "peerDependencies": {
59
+ "graphql": ">15"
60
+ },
57
61
  "repository": {
58
62
  "type": "git",
59
63
  "url": "git@github.com:coderich/autograph.git"
package/src/.DS_Store CHANGED
Binary file
@@ -1,3 +1,4 @@
1
+ const { isEmpty } = require('lodash');
1
2
  const Model = require('../data/Model');
2
3
  const Query = require('../query/Query');
3
4
  const ResultSet = require('../data/ResultSet');
@@ -89,7 +90,7 @@ module.exports = class Resolver {
89
90
  // This is needed in SF tests...
90
91
  const key = model.idKey();
91
92
  const { where, method } = query.toDriver();
92
- if (Object.prototype.hasOwnProperty.call(where, key) && where[key] == null) return Promise.resolve(method === 'findMany' ? [] : null);
93
+ if (Object.prototype.hasOwnProperty.call(where, key) && isEmpty(where[key])) return Promise.resolve(method === 'findMany' ? [] : null);
93
94
 
94
95
  //
95
96
  return this.loaders.get(`${model}`).load(query);
@@ -0,0 +1,46 @@
1
+ const Model = require('../data/Model');
2
+ const SchemaDecorator = require('../graphql/ast/SchemaDecorator');
3
+ const { identifyOnDeletes } = require('../service/schema.service');
4
+ const { createSystemEvent } = require('../service/event.service');
5
+
6
+ // Export class
7
+ module.exports = class extends SchemaDecorator {
8
+ constructor(schema, stores) {
9
+ super(schema);
10
+
11
+ // Create drivers
12
+ this.drivers = Object.entries(stores).reduce((prev, [key, value]) => {
13
+ const { Driver } = value;
14
+
15
+ return Object.assign(prev, {
16
+ [key]: {
17
+ dao: new Driver(value, this),
18
+ idKey: Driver.idKey,
19
+ idValue: Driver.idValue,
20
+ },
21
+ });
22
+ }, {});
23
+ }
24
+
25
+ setup() {
26
+ return createSystemEvent('Setup', this, () => {
27
+ const entities = this.models.filter(m => m.isEntity());
28
+
29
+ // Create model indexes
30
+ return Promise.all(entities.map(async (model) => {
31
+ const key = model.getKey();
32
+ const indexes = model.getIndexes();
33
+ const driver = model.getDriver();
34
+ if (driver.createCollection) await driver.createCollection(key);
35
+ return driver.createIndexes(key, indexes);
36
+ }));
37
+ });
38
+ }
39
+
40
+ initialize() {
41
+ super.initialize();
42
+ this.models = super.getModels().map(model => new Model(this, model, this.drivers[model.getDriverName()]));
43
+ this.models.forEach(model => model.referentialIntegrity(identifyOnDeletes(this.models, model)));
44
+ return this;
45
+ }
46
+ };
package/src/data/Field.js CHANGED
@@ -2,7 +2,7 @@ const Type = require('./Type');
2
2
  const Field = require('../graphql/ast/Field');
3
3
  const Rule = require('../core/Rule');
4
4
  const Transformer = require('../core/Transformer');
5
- const { map, uvl, isPlainObject, ensureArray, promiseChain } = require('../service/app.service');
5
+ const { map, uvl, isPlainObject, ensureArray } = require('../service/app.service');
6
6
 
7
7
  module.exports = class extends Field {
8
8
  constructor(model, field) {
@@ -63,17 +63,6 @@ module.exports = class extends Field {
63
63
  return transformers.concat(this.type.getDeserializers());
64
64
  }
65
65
 
66
- getResolvers() {
67
- const resolvers = [];
68
-
69
- Object.entries(this.getDirectiveArgs('field', {})).forEach(([key, value]) => {
70
- if (!Array.isArray(value)) value = [value];
71
- if (key === 'resolve') resolvers.push(...value.map(t => Transformer.getInstances()[t]));
72
- });
73
-
74
- return resolvers.concat(this.type.getResolvers());
75
- }
76
-
77
66
  validate(query, value) {
78
67
  const modelRef = this.getModelRef();
79
68
  const rules = [...this.getRules()];
@@ -124,20 +113,6 @@ module.exports = class extends Field {
124
113
  return this.transform(query, value, 'deserialize');
125
114
  }
126
115
 
127
- /**
128
- * Applies any user-defined @field(resolve: [...methods]) in series
129
- * This is ONLY run when resolving a value via the $<name> attribute
130
- */
131
- resolve(query, value) {
132
- const resolvers = [...this.getResolvers()];
133
-
134
- return promiseChain(resolvers.map(fn => (chain) => {
135
- return Promise.resolve(fn(this, uvl(this.cast(chain.pop()), value), query));
136
- })).then((results) => {
137
- return uvl(this.cast(results.pop()), value);
138
- });
139
- }
140
-
141
116
  tform(query, value) {
142
117
  // Determine transformers
143
118
  const transformers = this.getTransformers();
package/src/data/Model.js CHANGED
@@ -160,4 +160,24 @@ module.exports = class extends Model {
160
160
  }, {});
161
161
  });
162
162
  }
163
+
164
+ getShape(serdes = 'deserialize', recursive = true) {
165
+ return this.getSelectFields().map((field) => {
166
+ const [from, to] = serdes === 'serialize' ? [field.getName(), field.getKey()] : [field.getKey(), field.getName()];
167
+ const shape = recursive && field.isEmbedded() ? field.getModelRef().getShape(serdes, recursive) : null;
168
+ return { from, to, type: field.getDataType(), isArray: field.isArray(), shape };
169
+ });
170
+ }
171
+
172
+ shape(data, serdes = (() => { throw new Error('No Sir Sir SerDes!'); }), shape) {
173
+ shape = shape || this.getShape(serdes);
174
+
175
+ return map(data, (doc) => {
176
+ return shape.reduce((prev, { from, to, shape: subShape }) => {
177
+ const value = doc[from];
178
+ if (value === undefined) return prev;
179
+ return Object.assign(prev, { [to]: subShape ? this.shape(value, serdes, subShape) : value });
180
+ }, {});
181
+ });
182
+ }
163
183
  };
@@ -1,170 +1,51 @@
1
1
  const { get } = require('lodash');
2
2
  const DataService = require('./DataService');
3
- const { map, ensureArray, keyPaths, mapPromise, toGUID, hashObject } = require('../service/app.service');
3
+ const { map, ensureArray, keyPaths, toGUID } = require('../service/app.service');
4
+
5
+ const modelCache = new WeakMap();
4
6
 
5
7
  module.exports = class ResultSet {
6
8
  constructor(query, data, adjustForPagination = true) {
7
9
  if (data == null) return data;
10
+
8
11
  const { resolver, model, sort, first, after, last, before } = query.toObject();
9
- const fields = model.getFields().filter(f => f.getName() !== 'id');
10
12
 
11
- const rs = map(data, (doc) => {
12
- if (doc == null || typeof doc !== 'object') return doc;
13
+ ResultSet.ensureModelCache(model);
13
14
 
14
- //
15
- const cache = new Map();
16
-
17
- const definition = fields.reduce((prev, field) => {
18
- const key = field.getKey();
19
- const name = field.getName();
20
- const $name = `$${name}`;
21
- const value = doc[key];
22
-
23
- // Field attributes
24
- prev[name] = {
25
- get() {
26
- if (cache.has(name)) return cache.get(name);
27
- let $value = field.deserialize(query, value);
28
- $value = $value != null && field.isEmbedded() ? new ResultSet(query.model(field.getModelRef()), $value, false) : $value;
29
- cache.set(name, $value);
30
- return $value;
31
- },
32
- set($value) {
33
- cache.set(name, $value);
34
- },
35
- enumerable: true,
36
- configurable: true, // Allows things like delete
37
- };
38
-
39
- // Hydrated field attributes
40
- prev[`$${name}`] = {
41
- get() {
42
- return (args = {}) => {
43
- // Ensure where clause
44
- args.where = args.where || {};
45
-
46
- // Cache
47
- const cacheKey = `${$name}-${hashObject(args)}`;
48
- if (cache.has(cacheKey)) return cache.get(cacheKey);
49
-
50
- const promise = new Promise((resolve, reject) => {
51
- (() => {
52
- const $value = this[name];
53
-
54
- if (field.isScalar() || field.isEmbedded()) return Promise.resolve($value);
55
-
56
- const modelRef = field.getModelRef();
57
-
58
- if (field.isArray()) {
59
- if (field.isVirtual()) {
60
- args.where[[field.getVirtualField()]] = this.id; // Is where[[field.getVirtualField()]] correct?
61
- return resolver.match(modelRef).merge(args).many();
62
- }
63
-
64
- // Not a "required" query + strip out nulls
65
- args.where.id = $value;
66
- return resolver.match(modelRef).merge(args).many();
67
- }
68
-
69
- if (field.isVirtual()) {
70
- args.where[[field.getVirtualField()]] = this.id;
71
- return resolver.match(modelRef).merge(args).one();
72
- }
73
-
74
- return resolver.match(modelRef).id($value).one({ required: field.isRequired() });
75
- })().then((results) => {
76
- if (results == null) return field.resolve(query, results); // Allow field to determine
77
- return mapPromise(results, result => field.resolve(query, result)).then(() => results); // Resolve the inside fields but still return "results"!!!!
78
- }).then((resolved) => {
79
- resolve(resolved);
80
- }).catch((e) => {
81
- reject(e);
82
- });
83
- });
84
-
85
- cache.set(cacheKey, promise);
86
- return promise;
87
- };
88
- },
89
- enumerable: false,
90
- };
91
-
92
- // Field count (let's assume it's a Connection Type - meaning dont try with anything else)
93
- prev[`$${name}:count`] = {
94
- get() {
95
- return (q = {}) => {
96
- q.where = q.where || {};
97
- if (field.isVirtual()) q.where[field.getVirtualField()] = this.id;
98
- else q.where.id = this[name];
99
- return resolver.match(field.getModelRef()).merge(q).count();
100
- };
101
- },
102
- enumerable: false,
103
- };
104
-
105
- return prev;
106
- }, {
107
- id: {
108
- get() { return doc.id || doc[model.idKey()]; },
109
- set(id) { doc.id = id; }, // Embedded array of documents need to set id
110
- enumerable: true,
111
- },
15
+ const { template, fieldDefs } = modelCache.get(model);
112
16
 
113
- $id: {
114
- get() { return toGUID(model.getName(), this.id); },
115
- enumerable: false,
116
- },
17
+ const rs = map(data, (doc) => {
18
+ if (doc == null || typeof doc !== 'object') return doc;
117
19
 
118
- $$cursor: {
119
- get() {
120
- const sortPaths = keyPaths(sort);
121
- const sortValues = sortPaths.reduce((prv, path) => Object.assign(prv, { [path]: get(this, path) }), {});
122
- const sortJSON = JSON.stringify(sortValues);
123
- return Buffer.from(sortJSON).toString('base64');
20
+ const instance = Object.create(template, {
21
+ $$services: {
22
+ value: {
23
+ cache: new Map(),
24
+ data: doc,
25
+ resolver,
26
+ query,
27
+ sort,
124
28
  },
125
29
  enumerable: false,
126
30
  },
31
+ });
127
32
 
128
- $$model: {
129
- value: model,
130
- enumerable: false,
33
+ return new Proxy(instance, {
34
+ ownKeys(target) {
35
+ return Reflect.ownKeys(target).concat('id', fieldDefs.map(d => d.name));
131
36
  },
132
-
133
- $$isResultSetItem: {
134
- value: true,
135
- enumerable: false,
37
+ getOwnPropertyDescriptor(target, prop) {
38
+ return (prop === 'id' || fieldDefs.find(el => el.name === prop)) ? { enumerable: true, configurable: true } : Reflect.getOwnPropertyDescriptor(target, prop);
136
39
  },
137
-
138
- $$save: {
139
- get() { return input => resolver.match(model).id(this.id).save({ ...this, ...input }); },
140
- enumerable: false,
40
+ getPrototypeOf() {
41
+ return { $$services: instance.$$services };
141
42
  },
142
-
143
- $$remove: {
144
- get() { return () => resolver.match(model).id(this.id).remove(); },
145
- enumerable: false,
146
- },
147
-
148
- $$delete: {
149
- get() { return () => resolver.match(model).id(this.id).delete(); },
150
- enumerable: false,
151
- },
152
-
153
- toObject: {
154
- get() {
155
- return () => map(this, obj => Object.entries(obj).reduce((prev, [key, value]) => {
156
- if (value === undefined) return prev;
157
- prev[key] = get(value, '$$isResultSet') ? value.toObject() : value;
158
- return prev;
159
- }, {}));
160
- },
161
- enumerable: false,
162
- configurable: true,
43
+ deleteProperty(target, prop) {
44
+ const { key = prop } = fieldDefs.find(d => d.name === prop);
45
+ delete instance[prop];
46
+ delete instance.$$services.data[key];
163
47
  },
164
48
  });
165
-
166
- // Create and return ResultSetItem
167
- return Object.defineProperties({}, definition);
168
49
  });
169
50
 
170
51
  let hasNextPage = false;
@@ -202,4 +83,164 @@ module.exports = class ResultSet {
202
83
  },
203
84
  });
204
85
  }
86
+
87
+ static ensureModelCache(model) {
88
+ if (!modelCache.has(model)) {
89
+ const fields = model.getFields().filter(f => f.getName() !== 'id');
90
+
91
+ const fieldDefs = fields.map(field => ({
92
+ field,
93
+ key: field.getKey(),
94
+ name: field.getName(),
95
+ isArray: field.isArray(),
96
+ isScalar: field.isScalar(),
97
+ isVirtual: field.isVirtual(),
98
+ isRequired: field.isRequired(),
99
+ isEmbedded: field.isEmbedded(),
100
+ modelRef: field.getModelRef(),
101
+ virtualField: field.getVirtualField(),
102
+ // deserialize: field.deserialize.bind(field),
103
+ // fieldResolve: field.resolve.bind(field),
104
+ get useDefaultResolver() { return Boolean(this.isScalar || this.isEmbedded); },
105
+ }));
106
+
107
+ const template = ResultSet.makeModelTemplate(model, fieldDefs);
108
+
109
+ modelCache.set(model, { template, fieldDefs });
110
+ }
111
+ }
112
+
113
+ static makeModelTemplate(model, fieldDefs) {
114
+ const definition = fieldDefs.reduce((prev, fieldDef) => {
115
+ const { field, key, name, isArray, isVirtual, isRequired, isEmbedded, modelRef, virtualField, useDefaultResolver } = fieldDef;
116
+ const $name = `$${name}`;
117
+
118
+ // Deserialized field attributes
119
+ prev[name] = {
120
+ get() {
121
+ if (this.$$services.cache.has(name)) return this.$$services.cache.get(name);
122
+ let $value = field.deserialize(this.$$services.query, this.$$services.data[key]);
123
+ if ($value != null && isEmbedded) $value = new ResultSet(this.$$services.query.model(modelRef), $value, false);
124
+ this.$$services.cache.set(name, $value);
125
+ return $value;
126
+ },
127
+ set($value) {
128
+ this.$$services.cache.set(name, $value);
129
+ },
130
+ enumerable: true,
131
+ configurable: true, // Allows things like delete
132
+ };
133
+
134
+ // Fully deserialized, hydrated, and resolved field attributes
135
+ prev[$name] = {
136
+ get() {
137
+ return (args = {}) => {
138
+ // Grab deserialized value
139
+ const $value = this[name];
140
+
141
+ // Default resolver return immediately!
142
+ if (useDefaultResolver) return $value;
143
+
144
+ // Ensure where clause for DB lookup
145
+ args.where = args.where || {};
146
+
147
+ if (isArray) {
148
+ if (isVirtual) {
149
+ args.where[[virtualField]] = this.id; // Is where[[virtualField]] correct?
150
+ return this.$$services.resolver.match(modelRef).merge(args).many();
151
+ }
152
+
153
+ // Not a "required" query + strip out nulls
154
+ args.where.id = $value;
155
+ return this.$$services.resolver.match(modelRef).merge(args).many();
156
+ }
157
+
158
+ if (isVirtual) {
159
+ args.where[[virtualField]] = this.id;
160
+ return this.$$services.resolver.match(modelRef).merge(args).one();
161
+ }
162
+
163
+ return this.$$services.resolver.match(modelRef).id($value).one({ required: isRequired });
164
+ };
165
+ },
166
+ enumerable: false,
167
+ };
168
+
169
+ // Field count (let's assume it's a Connection Type - meaning dont try with anything else)
170
+ prev[`${$name}:count`] = {
171
+ get() {
172
+ return (q = {}) => {
173
+ q.where = q.where || {};
174
+ if (isVirtual) q.where[virtualField] = this.id;
175
+ else q.where.id = this[name];
176
+ return this.$$services.resolver.match(modelRef).merge(q).count();
177
+ };
178
+ },
179
+ enumerable: false,
180
+ };
181
+
182
+ return prev;
183
+ }, {
184
+ id: {
185
+ get() { return this.$$services.data.id || this.$$services.data[model.idKey()]; },
186
+ set(id) { this.$$services.data.id = id; }, // Embedded array of documents need to set id
187
+ enumerable: true,
188
+ },
189
+
190
+ $id: {
191
+ get() { return toGUID(model.getName(), this.id); },
192
+ enumerable: false,
193
+ },
194
+
195
+ $$cursor: {
196
+ get() {
197
+ const sortPaths = keyPaths(this.$$services.sort);
198
+ const sortValues = sortPaths.reduce((prv, path) => Object.assign(prv, { [path]: get(this, path) }), {});
199
+ const sortJSON = JSON.stringify(sortValues);
200
+ return Buffer.from(sortJSON).toString('base64');
201
+ },
202
+ enumerable: false,
203
+ },
204
+
205
+ $$model: {
206
+ value: model,
207
+ enumerable: false,
208
+ },
209
+
210
+ $$isResultSetItem: {
211
+ value: true,
212
+ enumerable: false,
213
+ },
214
+
215
+ $$save: {
216
+ get() { return input => this.$$services.resolver.match(model).id(this.id).save({ ...this, ...input }); },
217
+ enumerable: false,
218
+ },
219
+
220
+ $$remove: {
221
+ get() { return () => this.$$services.resolver.match(model).id(this.id).remove(); },
222
+ enumerable: false,
223
+ },
224
+
225
+ $$delete: {
226
+ get() { return () => this.$$services.resolver.match(model).id(this.id).delete(); },
227
+ enumerable: false,
228
+ },
229
+
230
+ toObject: {
231
+ get() {
232
+ return () => map(this, obj => Object.entries(obj).reduce((prev, [key, value]) => {
233
+ if (value === undefined) return prev;
234
+ prev[key] = get(value, '$$isResultSet') ? value.toObject() : value;
235
+ return prev;
236
+ }, {}));
237
+ },
238
+ enumerable: false,
239
+ configurable: true,
240
+ },
241
+ });
242
+
243
+ // return Object.defineProperties({}, definition);
244
+ return Object.create(null, definition);
245
+ }
205
246
  };
package/src/data/Type.js CHANGED
@@ -69,18 +69,4 @@ module.exports = class extends Type {
69
69
 
70
70
  return transformers;
71
71
  }
72
-
73
- getResolvers() {
74
- const resolvers = [];
75
- const scalarType = this.field.getScalarRef();
76
-
77
- if (scalarType) {
78
- Object.entries(scalarType.getDirectiveArgs('field', {})).forEach(([key, value]) => {
79
- if (!Array.isArray(value)) value = [value];
80
- if (key === 'resolve') resolvers.push(...value.map(t => Transformer.getInstances()[t]));
81
- });
82
- }
83
-
84
- return resolvers;
85
- }
86
72
  };
@@ -1,3 +1,4 @@
1
+ const Util = require('util');
1
2
  const { get, has } = require('lodash');
2
3
  const { MongoClient, ObjectID } = require('mongodb');
3
4
  const { proxyDeep, toKeyObj, globToRegex, proxyPromise, isScalarDataType, promiseRetry } = require('../service/app.service');
@@ -25,7 +26,7 @@ module.exports = class MongoDriver {
25
26
  }
26
27
 
27
28
  query(collection, method, ...args) {
28
- if (has(args[args.length - 1], 'debug')) console.log(collection, method, JSON.stringify(args));
29
+ if (has(args[args.length - 1], 'debug')) console.log(collection, method, Util.inspect(args, { depth: null, showHidden: false, colors: true }));
29
30
  return this.raw(collection)[method](...args);
30
31
  }
31
32
 
@@ -148,15 +149,15 @@ module.exports = class MongoDriver {
148
149
  }
149
150
 
150
151
  static getAddFields(query) {
151
- const { schema, where } = query;
152
+ const { shape, where } = query;
152
153
 
153
- return Object.entries(schema).reduce((prev, [key, { type }]) => {
154
- const value = where[key];
154
+ return shape.reduce((prev, { from, type }) => {
155
+ const value = where[from];
155
156
  if (value === undefined) return prev;
156
157
  if (!isScalarDataType(type)) return prev;
157
158
  const stype = String((type === 'Float' || type === 'Int' ? 'Number' : type)).toLowerCase();
158
159
  if (String(typeof value) === `${stype}`) return prev;
159
- return Object.assign(prev, { [key]: { $toString: `$${key}` } });
160
+ return Object.assign(prev, { [from]: { $toString: `$${from}` } });
160
161
  }, {});
161
162
  }
162
163
 
Binary file
Binary file
@@ -12,6 +12,10 @@ module.exports = class Model extends Node {
12
12
  this.key = uvl(this.getDirectiveArg('model', 'key'), this.getName());
13
13
  }
14
14
 
15
+ idKey() {
16
+ return this.getDirectiveArg('model', 'id', '_id');
17
+ }
18
+
15
19
  getSchema() {
16
20
  return this.schema;
17
21
  }
@@ -1,6 +1,5 @@
1
1
  const FS = require('fs');
2
2
  const Glob = require('glob');
3
- const Path = require('path');
4
3
  const Merge = require('deepmerge');
5
4
  const { nvl, uvl } = require('../../service/app.service');
6
5
  const { validateSchema, makeExecutableSchema, mergeASTSchema, mergeASTArray } = require('../../service/graphql.service');