@coderich/autograph 0.12.0 → 0.13.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.
Files changed (56) hide show
  1. package/index.js +4 -6
  2. package/package.json +30 -44
  3. package/src/data/DataLoader.js +77 -70
  4. package/src/data/Emitter.js +89 -0
  5. package/src/data/Loader.js +33 -0
  6. package/src/data/Pipeline.js +84 -101
  7. package/src/data/Resolver.js +304 -0
  8. package/src/data/Transaction.js +49 -0
  9. package/src/query/Query.js +159 -335
  10. package/src/query/QueryBuilder.js +228 -114
  11. package/src/query/QueryResolver.js +110 -205
  12. package/src/query/QueryResolverTransaction.js +16 -0
  13. package/src/schema/Schema.js +602 -0
  14. package/src/service/AppService.js +38 -0
  15. package/src/service/ErrorService.js +7 -0
  16. package/CHANGELOG.md +0 -41
  17. package/LICENSE +0 -21
  18. package/README.md +0 -76
  19. package/src/.DS_Store +0 -0
  20. package/src/core/.DS_Store +0 -0
  21. package/src/core/Boom.js +0 -9
  22. package/src/core/EventEmitter.js +0 -95
  23. package/src/core/Resolver.js +0 -124
  24. package/src/core/Schema.js +0 -55
  25. package/src/core/ServerResolver.js +0 -15
  26. package/src/data/.DS_Store +0 -0
  27. package/src/data/DataService.js +0 -120
  28. package/src/data/DataTransaction.js +0 -161
  29. package/src/data/Field.js +0 -83
  30. package/src/data/Model.js +0 -214
  31. package/src/data/TreeMap.js +0 -78
  32. package/src/data/Type.js +0 -50
  33. package/src/driver/.DS_Store +0 -0
  34. package/src/driver/MongoDriver.js +0 -227
  35. package/src/driver/index.js +0 -11
  36. package/src/graphql/.DS_Store +0 -0
  37. package/src/graphql/ast/.DS_Store +0 -0
  38. package/src/graphql/ast/Field.js +0 -206
  39. package/src/graphql/ast/Model.js +0 -145
  40. package/src/graphql/ast/Node.js +0 -291
  41. package/src/graphql/ast/Schema.js +0 -133
  42. package/src/graphql/ast/Type.js +0 -26
  43. package/src/graphql/ast/TypeDefApi.js +0 -93
  44. package/src/graphql/extension/.DS_Store +0 -0
  45. package/src/graphql/extension/api.js +0 -193
  46. package/src/graphql/extension/framework.js +0 -71
  47. package/src/graphql/extension/type.js +0 -34
  48. package/src/query/.DS_Store +0 -0
  49. package/src/query/QueryBuilderTransaction.js +0 -26
  50. package/src/query/QueryService.js +0 -111
  51. package/src/service/.DS_Store +0 -0
  52. package/src/service/app.service.js +0 -319
  53. package/src/service/decorator.service.js +0 -114
  54. package/src/service/event.service.js +0 -66
  55. package/src/service/graphql.service.js +0 -92
  56. package/src/service/schema.service.js +0 -95
@@ -1,363 +1,187 @@
1
- const Boom = require('../core/Boom');
1
+ const Util = require('@coderich/util');
2
+ const Pipeline = require('../data/Pipeline');
3
+ const { isGlob, globToRegex, mergeDeep, finalizeWhereClause } = require('../service/AppService');
2
4
 
3
5
  module.exports = class Query {
4
- constructor(props = {}) {
5
- this.props = {};
6
- this.timers = {};
7
- this.isCursorPaging = false;
8
- this.isClassicPaging = false;
9
- this.props.joins = this.props.joins || [];
10
- this.props.match = this.props.match || {};
11
- this.props.options = this.props.options || {};
12
- this.props.flags = this.props.flags || {
13
- debug: false,
14
- silent: false,
15
- validate: true, // { externals }
16
- pipeline: true, // { instruct, construct, destruct, restruct, serialize, deserialize, transform, rekey }
17
- required: false,
18
- };
19
- this.merge(props);
20
- }
21
-
22
- propCheck(prop, ...checks) {
23
- checks.forEach((check) => {
24
- if (this.props[check]) throw Boom.badRequest(`Cannot use "${prop}" while using "${check}"`);
25
- });
26
- }
27
-
28
- id(id) {
29
- this.propCheck('id', 'where', 'native', 'sort', 'skip', 'limit', 'before', 'after', 'first', 'last');
30
- this.props.id = id;
31
- this.props.batch = 'id';
32
- this.props.where = { id };
33
- return this.match({ id });
34
- }
35
-
36
- batch(batch) {
37
- this.props.batch = batch;
38
- return this;
39
- }
40
-
41
- where(where) {
42
- this.propCheck('where', 'id', 'native');
43
- this.props.where = where;
44
- return this.match(where);
45
- }
46
-
47
- search(search) {
48
- this.propCheck('search', 'id');
49
- this.props.search = search;
50
- return this;
51
- }
6
+ #config;
7
+ #resolver;
8
+ #context;
9
+ #schema;
10
+ #model;
11
+ #query;
52
12
 
53
- native(native) {
54
- this.propCheck('native', 'id', 'where');
55
- this.props.native = native;
56
- return this.match(native);
13
+ constructor(config) {
14
+ const { schema, context, resolver, query } = config;
15
+ this.#config = config;
16
+ this.#resolver = resolver;
17
+ this.#context = context;
18
+ this.#schema = schema;
19
+ this.#model = schema.models[query.model];
20
+ this.#query = query;
57
21
  }
58
22
 
59
- match(match) {
60
- this.props.match = match;
61
- return this;
23
+ clone(query) {
24
+ query = { ...this.#query, ...query }; // NO deepMerge here; must replace fields entirely
25
+ return new Query({ ...this.#config, query });
62
26
  }
63
27
 
64
- select(select) {
65
- this.props.select = select;
66
- return this;
67
- }
68
-
69
- $select(select) {
70
- this.props.$select = select;
71
- return this;
72
- }
73
-
74
- fields(fields) {
75
- this.props.select = fields;
76
- return this;
77
- }
78
-
79
- joins(...joins) {
80
- this.props.joins.push(...joins);
81
- return this;
82
- }
83
-
84
- sort(sort) {
85
- this.propCheck('sort', 'id');
86
- this.props.sort = sort;
87
- return this;
88
- }
89
-
90
- $sort(sort) {
91
- this.props.$sort = sort;
92
- return this;
93
- }
94
-
95
- sortBy(sort) {
96
- return this.sort(sort);
97
- }
98
-
99
- skip(skip) {
100
- this.propCheck('skip', 'id');
101
- if (this.isCursorPaging) throw Boom.badRequest('Cannot use "skip" while using Cursor-Style Pagination');
102
- this.isClassicPaging = true;
103
- this.props.skip = skip;
104
- return this;
105
- }
106
-
107
- limit(limit) {
108
- this.propCheck('limit', 'id');
109
- if (this.isCursorPaging) throw Boom.badRequest('Cannot use "limit" while using Cursor-Style Pagination');
110
- this.isClassicPaging = true;
111
- this.props.limit = limit;
112
- return this;
113
- }
114
-
115
- first(first) {
116
- this.propCheck('first', 'id', 'last');
117
- if (this.isClassicPaging) throw Boom.badRequest('Cannot use "first" while using Classic-Style Pagination');
118
- this.isCursorPaging = true;
119
- this.props.first = first + 2; // Adding 2 for pagination meta info (hasNext hasPrev)
120
- return this;
121
- }
122
-
123
- last(last) {
124
- this.propCheck('last', 'id', 'first');
125
- if (this.isClassicPaging) throw Boom.badRequest('Cannot use "last" while using Classic-Style Pagination');
126
- this.isCursorPaging = true;
127
- this.props.last = last + 2; // Adding 2 for pagination meta info (hasNext hasPrev)
128
- return this;
129
- }
130
-
131
- before(before) {
132
- this.propCheck('before', 'id');
133
- if (this.isClassicPaging) throw Boom.badRequest('Cannot use "before" while using Classic-Style Pagination');
134
- this.isCursorPaging = true;
135
- this.props.before = before;
136
- return this;
137
- }
138
-
139
- after(after) {
140
- this.propCheck('after', 'id');
141
- if (this.isClassicPaging) throw Boom.badRequest('Cannot use "after" while using Classic-Style Pagination');
142
- this.isCursorPaging = true;
143
- this.props.after = after;
144
- return this;
145
- }
146
-
147
- options(options) {
148
- this.props.options = options;
149
- return this;
150
- }
151
-
152
- meta(meta) {
153
- this.props.meta = meta;
154
- return this;
155
- }
156
-
157
- flags(flags) {
158
- Object.assign(this.props.flags, flags);
159
- return this;
28
+ toObject() {
29
+ return this.#query;
160
30
  }
161
31
 
162
- root(root) {
163
- this.props.root = root;
164
- return this;
32
+ toCacheKey() {
33
+ return {
34
+ op: this.#query.op,
35
+ where: this.#query.where,
36
+ sort: this.#query.sort,
37
+ joins: this.#query.joins,
38
+ skip: this.#query.skip,
39
+ limit: this.#query.limit,
40
+ before: this.#query.before,
41
+ after: this.#query.after,
42
+ first: this.#query.first,
43
+ last: this.#query.last,
44
+ };
165
45
  }
166
46
 
167
47
  /**
168
- * Merge unknown attributes into props; hence the check to do a noop
48
+ * Run a portion of the pipeline against a data set
169
49
  */
170
- merge(query) {
171
- Object.entries(query).forEach(([key, value]) => { if (this[key]) this[key](value); }); // Call method only if exists
172
- return this;
50
+ pipeline(target, data, transformers) {
51
+ data = Util.unflatten(data);
52
+ const crudMap = { create: ['$construct', '$serialize'], update: ['$restruct', '$serialize'] };
53
+ const crudLines = crudMap[this.#query.crud] || [];
54
+ const transformerMap = { where: ['$cast', '$instruct', '$serialize'], sort: [], input: [] };
55
+ if (this.#query.isMutation) transformerMap.input = ['$default', '$cast', '$normalize', '$instruct', ...crudLines];
56
+ transformers = transformers || transformerMap[target];
57
+ return this.#pipeline(this.#query, target, this.#model, data, transformers.map(el => Pipeline[el]));
173
58
  }
174
59
 
175
- resolver(resolver) {
176
- this.props.resolver = resolver;
177
- this.props.context = resolver.getContext();
178
- return this;
60
+ /**
61
+ * Transform entire query via pipeline. At minimum, pipeline is needed to unflatten the data...
62
+ */
63
+ transform() {
64
+ return Promise.all([
65
+ this.pipeline('input', this.#query.input),
66
+ this.#query.isNative ? this.#query.where : this.pipeline('where', this.#query.where),
67
+ this.pipeline('sort', this.#query.sort),
68
+ ]).then(([input, where, sort]) => this.clone({ input, where, sort }));
179
69
  }
180
70
 
181
- context(context) {
182
- this.props.context = context;
183
- return this;
184
- }
71
+ /**
72
+ * Transform entire query for driver
73
+ */
74
+ toDriver() {
75
+ const { input, where, sort, before, after, isNative, isCursorPaging } = this.#query;
76
+
77
+ const query = this.clone({
78
+ model: this.#model.key,
79
+ select: Object.values(this.#model.fields).map(field => field.key),
80
+ input: this.#model.walk(input, node => node.value !== undefined && Object.assign(node, { key: node.field.key })),
81
+ where: isNative ? where : this.#model.walk(where, node => Object.assign(node, { key: node.field.key })),
82
+ sort: this.#model.walk(sort, node => Object.assign(node, { key: node.field.key })),
83
+ before: (!isCursorPaging || !before) ? undefined : JSON.parse(Buffer.from(before, 'base64').toString('ascii')),
84
+ after: (!isCursorPaging || !after) ? undefined : JSON.parse(Buffer.from(after, 'base64').toString('ascii')),
85
+ $schema: this.#schema.resolvePath,
86
+ });
185
87
 
186
- model(model) {
187
- this.props.model = model;
188
- return this;
189
- }
88
+ if (!isNative) this.#finalize(query.toObject());
190
89
 
191
- transaction(transaction) {
192
- this.props.transaction = transaction;
193
- return this;
90
+ return query;
194
91
  }
195
92
 
196
- cmd(cmd) {
197
- this.props.cmd = cmd; // Terminal cmd from QueryBuilder
198
- return this;
93
+ /**
94
+ * Recursive pipeline function
95
+ */
96
+ #pipeline(query, target, model, data, transformers = [], paths = []) {
97
+ return Util.mapPromise(data, (doc, index) => {
98
+ const path = [...paths];
99
+ if (Array.isArray(data)) path.push(index);
100
+ if (target === 'input') doc = mergeDeep(model.pipelineFields.input, doc);
101
+ else if (target === 'where') doc = mergeDeep(model.pipelineFields.where, doc);
102
+
103
+ return Util.pipeline(Object.entries(doc).map(([key, startValue]) => async (prev) => {
104
+ const field = model.fields[key];
105
+ if (!field) return prev;
106
+
107
+ // Transform value
108
+ let $value = await Util.pipeline(transformers.map(t => async (value) => {
109
+ const v = await t({ query, model, field, value, path: path.concat(key), startValue, resolver: this.#resolver, context: this.#context, schema: this.#schema });
110
+ return v === undefined ? value : v;
111
+ }), startValue);
112
+
113
+ // If it's embedded - delegate
114
+ if (field.isEmbedded) $value = await this.#pipeline(query, target, field.model, $value, transformers, path.concat(key));
115
+
116
+ // Assign it back
117
+ if (target === 'input' && $value === undefined) return prev;
118
+ return Object.assign(prev, { [field.name]: $value });
119
+ }), {});
120
+ });
199
121
  }
200
122
 
201
- method(method) {
202
- this.props.method = method;
203
-
204
- switch (method) {
205
- case 'createOne': case 'createMany': {
206
- this.props.crud = 'create';
207
- this.props.key = `create${this.props.model}`;
208
- break;
209
- }
210
- case 'updateOne': case 'updateMany': {
211
- this.props.crud = 'update';
212
- this.props.key = `update${this.props.model}`;
213
- break;
214
- }
215
- case 'deleteOne': case 'deleteMany': case 'removeOne': case 'removeMany': {
216
- this.props.crud = 'delete';
217
- this.props.key = `delete${this.props.model}`;
218
- break;
219
- }
220
- case 'count': {
221
- this.props.crud = 'read';
222
- this.props.key = `count${this.props.model}`;
223
- break;
224
- }
225
- case 'findOne': {
226
- this.props.crud = 'read';
227
- this.props.key = `get${this.props.model}`;
228
- break;
229
- }
230
- case 'findMany': {
231
- this.props.crud = 'read';
232
- this.props.key = `find${this.props.model}`;
233
- break;
234
- }
235
- default: {
236
- this.props.crud = 'read';
237
- break;
123
+ /**
124
+ * Finalize the query for the driver
125
+ */
126
+ #finalize(query) {
127
+ const { where = {}, sort = {} } = query;
128
+ const flatSort = Util.flatten(sort, { safe: true });
129
+ const flatWhere = Util.flatten(where, { safe: true });
130
+ const $sort = Util.unflatten(Object.keys(flatSort).reduce((prev, key) => Object.assign(prev, { [key]: {} }), {}));
131
+
132
+ //
133
+ query.sort = this.#model.walk(sort, (node) => {
134
+ if (node.field.isVirtual || node.field.isFKReference) node.key = `join_${node.field.model.key}`;
135
+ return node;
136
+ }, { key: 'key' });
137
+
138
+ // Reconstruct the where clause by pulling out anything that requires a join
139
+ query.where = finalizeWhereClause(Util.unflatten(Object.entries(flatWhere).reduce((prev, [key, value]) => {
140
+ if (this.#model.isJoinPath(key, 'key')) return prev;
141
+ value = Util.map(value, el => (isGlob(el) ? globToRegex(el) : el));
142
+ return Object.assign(prev, { [key]: value });
143
+ }, {}), { safe: true }));
144
+
145
+ // Determine what join data is needed (derived from where + sort)
146
+ const joinData = mergeDeep($sort, Util.unflatten(Object.entries(flatWhere).reduce((prev, [key, value]) => {
147
+ if (this.#model.isJoinPath(key, 'key')) return Object.assign(prev, { [key]: value });
148
+ return prev;
149
+ }, {}), { safe: true }));
150
+
151
+ // Construct joins
152
+ query.joins = [];
153
+
154
+ this.#model.walk(joinData, (node) => {
155
+ const { model, field, key, value, isLeaf, path, run } = node;
156
+
157
+ if (field.join) {
158
+ let isArray;
159
+ const join = { ...field.join, where: {} };
160
+
161
+ if (run.length > 1) {
162
+ join.from = path.reduce((prev, curr, i) => {
163
+ const $field = this.#model.resolvePath(path.slice(0, i + 1).join('.'), 'key');
164
+ if ($field.isArray) isArray = true;
165
+ return prev.concat($field.linkField.key);
166
+ }, []).join('.');
167
+ }
168
+
169
+ join.isArray = isArray || model.resolvePath(join.from).isArray;
170
+
171
+ query.joins.push(join);
238
172
  }
239
- }
240
-
241
- return this;
242
- }
243
-
244
- crud(crud) {
245
- this.props.crud = crud;
246
- return this;
247
- }
248
-
249
- key(key) {
250
- this.props.key = key;
251
- return this;
252
- }
253
-
254
- input(input = {}) { // Allows .save(/* empty */);
255
- // delete input.id; // We do not want to allow changing id via input
256
- this.props.input = input;
257
- return this;
258
- }
259
-
260
- $input(input) {
261
- this.props.$input = input;
262
- return this;
263
- }
264
173
 
265
- doc(doc) {
266
- this.props.doc = doc;
267
- return this;
268
- }
269
-
270
- merged(merged) {
271
- this.props.merged = merged;
272
- return this;
273
- }
274
-
275
- payload(payload) {
276
- this.props.payload = payload;
277
- return this;
278
- }
279
-
280
- result(result) {
281
- this.props.result = result;
282
- return this;
283
- }
284
-
285
- $doc($doc) {
286
- this.props.$doc = $doc;
287
- return this;
288
- }
289
-
290
- args(args) {
291
- this.props.args = args;
292
- return this;
293
- }
294
-
295
- clone() {
296
- const clone = new Query();
297
- clone.props = { ...this.props };
298
- return clone;
299
- }
300
-
301
- toDriver() {
302
- const self = this;
303
- const { model } = this.props;
304
- const isSorted = Boolean(Object.keys(this.props.$sort || {}).length);
305
-
306
- return {
307
- isNative: Boolean(this.props.native),
308
- model: model.getKey(),
309
- shape: model.getShape(),
310
- method: this.props.method,
311
- select: this.props.$select,
312
- joins: this.props.joins,
313
- where: this.props.match,
314
- search: this.props.search,
315
- sort: this.props.$sort,
316
- skip: this.props.skip,
317
- limit: this.props.limit,
318
- first: isSorted ? this.props.first : undefined,
319
- last: isSorted ? this.props.last : undefined,
320
- // before: isSorted && this.props.before ? model.normalize(this, JSON.parse(Buffer.from(this.props.before, 'base64').toString('ascii')), 'serialize') : undefined,
321
- get before() {
322
- if (!isSorted || !self.props.before) return undefined;
323
- const shape = model.getShape('create', 'sort');
324
- const before = JSON.parse(Buffer.from(self.props.before, 'base64').toString('ascii'));
325
- const $before = model.shapeObject(shape, before, self);
326
- return $before;
327
- },
328
- get after() {
329
- if (!isSorted || !self.props.after) return undefined;
330
- const shape = model.getShape('create', 'sort');
331
- const after = JSON.parse(Buffer.from(self.props.after, 'base64').toString('ascii'));
332
- const $after = model.shapeObject(shape, after, self);
333
- return $after;
334
- },
335
- options: this.props.options,
336
- input: this.props.$input,
337
- flags: this.props.flags,
338
- $doc: this.props.$doc,
339
- doc: this.props.doc,
340
- };
341
- }
342
-
343
- toObject() {
344
- return this.props;
345
- }
174
+ if (isLeaf) {
175
+ const $model = field.model || model;
176
+ const join = query.joins.find(j => j.to === $model.key);
177
+ const $value = Util.map(value, el => (isGlob(el) ? globToRegex(el) : el));
178
+ const $$value = Array.isArray($value) ? { $in: $value } : $value;
179
+ const from = field.model ? join.from : key;
180
+ join.where[from] = $$value;
181
+ return false;
182
+ }
346
183
 
347
- getCacheKey() {
348
- return {
349
- cmd: this.props.cmd,
350
- method: this.props.method,
351
- where: this.props.match,
352
- search: this.props.search,
353
- sort: this.props.sort,
354
- skip: this.props.skip,
355
- limit: this.props.limit,
356
- before: this.props.before,
357
- after: this.props.after,
358
- first: this.props.first,
359
- last: this.props.last,
360
- options: this.props.options,
361
- };
184
+ return node;
185
+ }, { key: 'key' });
362
186
  }
363
187
  };