@coderich/autograph 0.9.12 → 0.10.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.
@@ -0,0 +1,161 @@
1
+ const { get } = require('lodash');
2
+ const ResultSet = require('./ResultSet');
3
+ const { map, keyPaths, mapPromise, toGUID, hashObject } = require('../../service/app.service');
4
+
5
+ module.exports = class ResultSetItem {
6
+ constructor(query, doc) {
7
+ if (doc == null) return doc;
8
+
9
+ const cache = new Map();
10
+ const { resolver, model, sort } = query.toObject();
11
+ const fields = model.getFields().filter(f => f.getName() !== 'id');
12
+
13
+ const proxy = new Proxy(doc, {
14
+ get(target, prop, rec) {
15
+ const value = Reflect.get(target, prop, rec);
16
+ if (typeof value === 'function') return value.bind(target);
17
+
18
+ if (cache.has(prop)) return cache.get(prop);
19
+
20
+ const field = fields.find(f => `${f}` === prop);
21
+
22
+ if (field) {
23
+ let $value = field.deserialize(query, value);
24
+
25
+ if ($value != null && field.isEmbedded()) {
26
+ const newQuery = query.model(field.getModelRef());
27
+ $value = new ResultSet(newQuery, map($value, v => new ResultSetItem(newQuery, v)), false);
28
+ }
29
+
30
+ cache.set(prop, $value);
31
+ return $value;
32
+ }
33
+
34
+ return value;
35
+ },
36
+ set(target, prop, value, receiver) {
37
+ cache.set(prop, value);
38
+ return Reflect.set(target, prop, value);
39
+ },
40
+ deleteProperty(target, prop) {
41
+ cache.delete(prop);
42
+ return Reflect.delete(target, prop);
43
+ },
44
+ });
45
+
46
+ const definition = fields.reduce((prev, field) => {
47
+ const name = field.getName();
48
+ const $name = `$${name}`;
49
+
50
+ // Hydrated field attributes
51
+ prev[`$${name}`] = {
52
+ get() {
53
+ return (args = {}) => {
54
+ // Ensure where clause
55
+ args.where = args.where || {};
56
+
57
+ // Cache
58
+ const cacheKey = `${$name}-${hashObject(args)}`;
59
+ if (cache.has(cacheKey)) return cache.get(cacheKey);
60
+
61
+ const promise = new Promise((resolve, reject) => {
62
+ (() => {
63
+ const $value = this[name];
64
+
65
+ if (field.isScalar() || field.isEmbedded()) return Promise.resolve($value);
66
+
67
+ const modelRef = field.getModelRef();
68
+
69
+ if (field.isArray()) {
70
+ if (field.isVirtual()) {
71
+ args.where[[field.getVirtualField()]] = this.id; // Is where[[field.getVirtualField()]] correct?
72
+ return resolver.match(modelRef).merge(args).many();
73
+ }
74
+
75
+ // Not a "required" query + strip out nulls
76
+ args.where.id = $value;
77
+ return resolver.match(modelRef).merge(args).many();
78
+ }
79
+
80
+ if (field.isVirtual()) {
81
+ args.where[[field.getVirtualField()]] = this.id;
82
+ return resolver.match(modelRef).merge(args).one();
83
+ }
84
+
85
+ return resolver.match(modelRef).id($value).one({ required: field.isRequired() });
86
+ })().then((results) => {
87
+ if (results == null) return field.resolve(query, results); // Allow field to determine
88
+ return mapPromise(results, result => field.resolve(query, result)).then(() => results); // Resolve the inside fields but still return "results"!!!!
89
+ }).then((resolved) => {
90
+ resolve(resolved);
91
+ }).catch((e) => {
92
+ reject(e);
93
+ });
94
+ });
95
+
96
+ cache.set(cacheKey, promise);
97
+ return promise;
98
+ };
99
+ },
100
+ enumerable: false,
101
+ };
102
+
103
+ // Field count (let's assume it's a Connection Type - meaning dont try with anything else)
104
+ prev[`$${name}:count`] = {
105
+ get() {
106
+ return (q = {}) => {
107
+ q.where = q.where || {};
108
+ if (field.isVirtual()) q.where[field.getVirtualField()] = this.id;
109
+ else q.where.id = this[name];
110
+ return resolver.match(field.getModelRef()).merge(q).count();
111
+ };
112
+ },
113
+ enumerable: false,
114
+ };
115
+
116
+ return prev;
117
+ }, {
118
+ $id: {
119
+ get() { return toGUID(model.getName(), this.id); },
120
+ enumerable: false,
121
+ },
122
+
123
+ $$cursor: {
124
+ get() {
125
+ const sortPaths = keyPaths(sort);
126
+ const sortValues = sortPaths.reduce((prv, path) => Object.assign(prv, { [path]: get(this, path) }), {});
127
+ const sortJSON = JSON.stringify(sortValues);
128
+ return Buffer.from(sortJSON).toString('base64');
129
+ },
130
+ enumerable: false,
131
+ },
132
+
133
+ $$model: {
134
+ value: model,
135
+ enumerable: false,
136
+ },
137
+
138
+ $$isResultSetItem: {
139
+ value: true,
140
+ enumerable: false,
141
+ },
142
+
143
+ $$save: {
144
+ get() { return input => resolver.match(model).id(this.id).save({ ...this, ...input }); },
145
+ enumerable: false,
146
+ },
147
+
148
+ $$remove: {
149
+ get() { return () => resolver.match(model).id(this.id).remove(); },
150
+ enumerable: false,
151
+ },
152
+
153
+ $$delete: {
154
+ get() { return () => resolver.match(model).id(this.id).delete(); },
155
+ enumerable: false,
156
+ },
157
+ });
158
+
159
+ return Object.defineProperties(proxy, definition);
160
+ }
161
+ };
@@ -3,16 +3,15 @@ const { MongoClient, ObjectID } = require('mongodb');
3
3
  const { proxyDeep, toKeyObj, globToRegex, proxyPromise, isScalarDataType, promiseRetry } = require('../service/app.service');
4
4
 
5
5
  module.exports = class MongoDriver {
6
- constructor(config, schema) {
6
+ constructor(config) {
7
7
  this.config = config;
8
- this.schema = schema;
9
8
  this.connection = this.connect();
10
9
  this.getDirectives = () => get(config, 'directives', {});
11
10
  }
12
11
 
13
12
  connect() {
14
13
  const { uri, options = {} } = this.config;
15
- options.ignoreUndefined = true;
14
+ options.ignoreUndefined = false;
16
15
  return MongoClient.connect(uri, options);
17
16
  }
18
17
 
@@ -25,7 +24,8 @@ module.exports = class MongoDriver {
25
24
  }
26
25
 
27
26
  query(collection, method, ...args) {
28
- if (has(args[args.length - 1], 'debug')) console.log(collection, method, JSON.stringify(args));
27
+ if (has(args[args.length - 1], 'debug')) console.log(collection, method, JSON.stringify(args, null, 2));
28
+ if (method === 'aggregate') args.splice(2);
29
29
  return this.raw(collection)[method](...args);
30
30
  }
31
31
 
@@ -36,18 +36,21 @@ module.exports = class MongoDriver {
36
36
  }
37
37
 
38
38
  findOne(query) {
39
- return this.findMany(Object.assign(query, { first: 1 })).then(docs => docs[0]);
39
+ return this.findMany(Object.assign(query, { first: 1 })).then((stream) => {
40
+ return new Promise((resolve, reject) => {
41
+ stream.on('data', resolve);
42
+ stream.on('error', reject);
43
+ stream.on('end', resolve);
44
+ });
45
+ });
40
46
  }
41
47
 
42
48
  findMany(query) {
43
- const { model, options = {}, last, flags } = query;
49
+ const { model, options = {}, flags } = query;
44
50
  Object.assign(options, this.config.query || {});
45
51
 
46
52
  return this.query(model, 'aggregate', MongoDriver.aggregateQuery(query), options, flags).then((cursor) => {
47
- return cursor.toArray().then((results) => {
48
- if (last) return results.splice(-last);
49
- return results;
50
- });
53
+ return cursor.stream();
51
54
  });
52
55
  }
53
56
 
@@ -63,7 +66,7 @@ module.exports = class MongoDriver {
63
66
  }
64
67
 
65
68
  createOne({ model, input, options, flags }) {
66
- return this.query(model, 'insertOne', input, options, flags).then(result => Object.assign(input, { _id: result.insertedId }));
69
+ return this.query(model, 'insertOne', input, options, flags).then(result => Object.assign(input, { id: result.insertedId }));
67
70
  }
68
71
 
69
72
  updateOne({ model, where, $doc, options, flags }) {
@@ -76,7 +79,7 @@ module.exports = class MongoDriver {
76
79
  }
77
80
 
78
81
  deleteOne({ model, where, options, flags }) {
79
- return this.query(model, 'deleteOne', where, options, flags);
82
+ return this.query(model, 'deleteOne', where, options, flags).then(() => true);
80
83
  }
81
84
 
82
85
  dropModel(model) {
@@ -148,20 +151,38 @@ module.exports = class MongoDriver {
148
151
  }
149
152
 
150
153
  static getAddFields(query) {
151
- const { schema, where } = query;
154
+ const { shape, where } = query;
152
155
 
153
- return Object.entries(schema).reduce((prev, [key, { type }]) => {
154
- const value = where[key];
156
+ return shape.reduce((prev, { from, type }) => {
157
+ const value = where[from];
155
158
  if (value === undefined) return prev;
156
159
  if (!isScalarDataType(type)) return prev;
157
160
  const stype = String((type === 'Float' || type === 'Int' ? 'Number' : type)).toLowerCase();
158
161
  if (String(typeof value) === `${stype}`) return prev;
159
- return Object.assign(prev, { [key]: { $toString: `$${key}` } });
162
+ return Object.assign(prev, { [from]: { $toString: `$${from}` } });
160
163
  }, {});
161
164
  }
162
165
 
166
+ static getProjectFields(parentShape, currentShape = { _id: 0, id: '$_id' }, isEmbedded, isEmbeddedArray, path = []) {
167
+ return parentShape.reduce((project, value) => {
168
+ const { from, to, shape: subShape, isArray } = value;
169
+ const $key = isEmbedded && isEmbeddedArray ? `$$embedded.${from}` : `$${path.concat(from).join('.')}`;
170
+
171
+ if (subShape) {
172
+ const $project = MongoDriver.getProjectFields(subShape, {}, true, isArray, path.concat(from));
173
+ Object.assign(project, { [to]: isArray ? { $map: { input: $key, as: 'embedded', in: $project } } : $project });
174
+ } else if (isEmbedded) {
175
+ Object.assign(project, { [to]: $key });
176
+ } else {
177
+ Object.assign(project, { [to]: from === to ? 1 : $key });
178
+ }
179
+
180
+ return project;
181
+ }, currentShape);
182
+ }
183
+
163
184
  static aggregateQuery(query, count = false) {
164
- const { where: $match, sort, skip, limit, joins } = query;
185
+ const { where: $match, sort = {}, skip, limit, joins, shape, after, before, first } = query;
165
186
  const $aggregate = [{ $match }];
166
187
 
167
188
  // Used for $regex matching
@@ -190,10 +211,13 @@ module.exports = class MongoDriver {
190
211
  if (limit) $aggregate.push({ $limit: limit });
191
212
 
192
213
  // Pagination
193
- const { after, before, first } = query;
194
214
  if (after) $aggregate.push({ $match: { $or: Object.entries(after).reduce((prev, [key, value]) => prev.concat({ [key]: { [sort[key] === 1 ? '$gte' : '$lte']: value } }), []) } });
195
215
  if (before) $aggregate.push({ $match: { $or: Object.entries(before).reduce((prev, [key, value]) => prev.concat({ [key]: { [sort[key] === 1 ? '$lte' : '$gte']: value } }), []) } });
196
216
  if (first) $aggregate.push({ $limit: first });
217
+
218
+ // Projection
219
+ const $project = MongoDriver.getProjectFields(shape);
220
+ $aggregate.push({ $project });
197
221
  }
198
222
 
199
223
  return $aggregate;
@@ -145,12 +145,11 @@ module.exports = class Field extends Node {
145
145
  // GQL Schema Methods
146
146
  getGQLType(suffix, options = {}) {
147
147
  let type = this.getType();
148
- // if (suffix === 'InputUpdate' && this.isSpliceable()) suffix = 'InputSplice';
149
148
  const modelType = `${type}${suffix}`;
150
149
  if (suffix && !this.isScalar()) type = this.isEmbedded() ? modelType : 'ID';
151
150
  type = this.isArray() ? `[${type}${this.isArrayElementRequired() ? '!' : ''}]` : type;
152
151
  if (!suffix && this.isRequired()) type += '!';
153
- if (!options.splice && suffix === 'InputCreate' && this.isRequired() && !this.isDefaulted()) type += '!';
152
+ if (suffix === 'InputCreate' && this.isRequired() && !this.isDefaulted()) type += '!';
154
153
  return type;
155
154
  }
156
155
 
@@ -208,13 +208,6 @@ module.exports = class Node {
208
208
  }
209
209
  }
210
210
 
211
- /**
212
- * Is this API embedded in another document
213
- */
214
- isEmbeddedApi() {
215
- return this.isEmbedded() && Boolean(this.getDirectiveArg('field', 'embedApi'));
216
- }
217
-
218
211
  /**
219
212
  * Can the field be changed after it's set
220
213
  */
@@ -35,6 +35,9 @@ module.exports = class SchemaDecorator extends TypeDefApi {
35
35
  * Synchronously merge a schema
36
36
  */
37
37
  mergeSchema(schema, options = {}) {
38
+ // Ensure this is a schema of sorts otherwise skip it
39
+ if (typeof schema !== 'string' && ['context', 'typeDefs', 'resolvers', 'schemaDirectives'].every(key => !schema[key])) return this;
40
+
38
41
  // Here we want to normalize the schema into the shape { context, typeDefs, resolvers, schemaDirectives }
39
42
  // We do NOT want to modify the schema object because that may cause unwanted side-effects.
40
43
  const normalizedSchema = { ...schema };
@@ -96,8 +99,8 @@ module.exports = class SchemaDecorator extends TypeDefApi {
96
99
  */
97
100
  decorate() {
98
101
  this.initialize();
99
- this.mergeSchema(frameworkExt(this));
100
- this.mergeSchema(typeExt(this));
102
+ this.mergeSchema(frameworkExt(this), { passive: true });
103
+ this.mergeSchema(typeExt(this), { passive: true });
101
104
  this.initialize();
102
105
  this.mergeSchema(apiExt(this), { passive: true });
103
106
  this.finalize();
@@ -3,7 +3,7 @@ const { Kind } = require('graphql');
3
3
  const ServerResolver = require('../../core/ServerResolver');
4
4
  const { ucFirst, fromGUID } = require('../../service/app.service');
5
5
  const { findGQLModels } = require('../../service/schema.service');
6
- const { makeCreateAPI, makeReadAPI, makeUpdateAPI, makeDeleteAPI, makeSubscriptionAPI, makeInputSplice, makeQueryResolver, makeMutationResolver } = require('../../service/decorator.service');
6
+ const { makeCreateAPI, makeReadAPI, makeUpdateAPI, makeDeleteAPI, makeSubscriptionAPI, makeQueryResolver, makeMutationResolver } = require('../../service/decorator.service');
7
7
 
8
8
  const interfaceKinds = [Kind.INTERFACE_TYPE_DEFINITION, Kind.INTERFACE_TYPE_EXTENSION];
9
9
 
@@ -25,8 +25,6 @@ module.exports = (schema) => {
25
25
  const createModels = findGQLModels('c', markedModels, allModels);
26
26
  const readModels = findGQLModels('r', markedModels, allModels);
27
27
  const updateModels = findGQLModels('u', markedModels, allModels);
28
- const deleteModels = findGQLModels('d', markedModels, allModels);
29
- const spliceModels = [...new Set([...createModels, ...updateModels, ...deleteModels])];
30
28
 
31
29
  return ({
32
30
  typeDefs: [
@@ -40,7 +38,6 @@ module.exports = (schema) => {
40
38
  input ${model.getName()}InputUpdate {
41
39
  ${model.getFields().filter(field => field.hasGQLScope('u') && !field.isVirtual()).map(field => `${field.getName()}: ${field.getGQLType('InputUpdate')}`)}
42
40
  }
43
- # ${makeInputSplice(model)}
44
41
  `),
45
42
 
46
43
  ...readModels.map(model => `
@@ -50,19 +47,14 @@ module.exports = (schema) => {
50
47
  input ${model.getName()}InputSort {
51
48
  ${getGQLWhereFields(model).map(field => `${field.getName()}: ${field.getModelRef() ? `${ucFirst(field.getDataRef())}InputSort` : 'SortOrderEnum'}`)}
52
49
  }
53
- `),
54
-
55
- ...readModels.map(model => `
56
50
  extend ${interfaceKinds.indexOf(model.getKind()) > -1 ? 'interface' : 'type'} ${model.getName()} {
57
51
  ${model.getFields().filter(field => field.hasGQLScope('r')).map(field => `${field.getName()}${field.getExtendArgs()}: ${field.getPayloadType()}`)}
58
52
  }
59
-
60
53
  type ${model.getName()}Connection {
61
54
  pageInfo: PageInfo!
62
55
  edges: [${model.getName()}Edge]
63
56
  count: Int!
64
57
  }
65
-
66
58
  type ${model.getName()}Edge {
67
59
  node: ${model.getName()}
68
60
  cursor: String!
@@ -105,13 +97,6 @@ module.exports = (schema) => {
105
97
  ${model.getFields().filter(field => field.hasGQLScope('r')).map(field => `${field.getName()}: ${field.getPayloadType()}`)}
106
98
  }
107
99
  `),
108
-
109
- ...spliceModels.map(model => `
110
- #input ${model.getName()}InputSplice {
111
- # with: ${model}InputWhere
112
- # put: ${model}InputUpdate
113
- #}
114
- `),
115
100
  ].concat([
116
101
  `type PageInfo {
117
102
  startCursor: String!
@@ -40,7 +40,6 @@ module.exports = (schema) => {
40
40
  fieldScope: AutoGraphMixed # Dictate how a FIELD may use me
41
41
  persist: Boolean # Persist this field (default true)
42
42
  default: AutoGraphMixed # Define a default value
43
- embedApi: Boolean # Should we also create an embedded API from this (default false)
44
43
  connection: Boolean # Treat this field as a connection type (default false - rolling this out slowly)
45
44
 
46
45
  noRepeat: Boolean
@@ -251,7 +251,7 @@ module.exports = class Query {
251
251
  return {
252
252
  isNative: Boolean(this.props.native),
253
253
  model: model.getKey(),
254
- schema: Query.getSchema(model),
254
+ shape: model.getShape(),
255
255
  method: this.props.method,
256
256
  select: this.props.$select,
257
257
  joins: this.props.joins,
@@ -292,22 +292,4 @@ module.exports = class Query {
292
292
  options: this.props.options,
293
293
  };
294
294
  }
295
-
296
- static getSchema(model, name = false) {
297
- return model.getSelectFields().reduce((prev, field) => {
298
- const key = name ? field.getName() : field.getKey();
299
- // const modelRef = field.getModelRef();
300
- // const isEmbedded = field.isEmbedded();
301
-
302
- return Object.assign(prev, {
303
- [key]: {
304
- field,
305
- alias: name ? field.getKey() : field.getName(),
306
- type: field.getDataType(),
307
- isArray: field.isArray(),
308
- // schema: isEmbedded ? Query.getSchema(modelRef, name) : null,
309
- },
310
- });
311
- }, {});
312
- }
313
295
  };
@@ -1,6 +1,6 @@
1
1
  const Query = require('./Query');
2
2
  const QueryResolver = require('./QueryResolver');
3
- const { unravelObject } = require('../service/app.service');
3
+ const { toKeyObj, unravelObject } = require('../service/app.service');
4
4
 
5
5
  /*
6
6
  * QueryBuilder
@@ -14,7 +14,8 @@ module.exports = class QueryBuilder {
14
14
 
15
15
  // Chainable commands
16
16
  this.id = (id) => { this.query.id(id); return this; };
17
- this.select = (select) => { this.query.select(select); return this; };
17
+ // this.select = (select) => { this.query.select(select); return this; };
18
+ this.select = (select) => { this.query.select(Object.entries(toKeyObj(select)).reduce((prev, [key, value]) => Object.assign(prev, { [key.replace(/edges.node./g, '')]: !!value }), {})); return this; };
18
19
  this.where = (where) => { this.query.where(where); return this; };
19
20
  this.match = (match) => { this.query.match(match); return this; };
20
21
  this.native = (native) => { this.query.native(native); return this; };
@@ -33,14 +33,13 @@ module.exports = class QueryResolver {
33
33
  const { model, input, flags } = query.toObject();
34
34
  model.appendDefaultFields(query, input);
35
35
 
36
- return createSystemEvent('Mutation', { method: 'create', query }, () => {
36
+ return createSystemEvent('Mutation', { method: 'create', query }, async () => {
37
37
  const $input = model.serialize(query, model.appendCreateFields(input));
38
38
  query.$input($input);
39
- const promise = get(flags, 'novalidate') ? this.resolver.resolve(query) : model.validate(query, $input).then(() => this.resolver.resolve(query));
40
- return promise.then((doc) => {
41
- query.doc(doc);
42
- return doc;
43
- });
39
+ if (!get(flags, 'novalidate')) await model.validate(query, $input);
40
+ const doc = await this.resolver.resolve(query);
41
+ query.doc(doc);
42
+ return doc;
44
43
  });
45
44
  }
46
45
 
@@ -174,7 +173,7 @@ module.exports = class QueryResolver {
174
173
  return this.findMany(query.method('findMany'));
175
174
  }
176
175
 
177
- async resolve() {
176
+ resolve() {
178
177
  const { model, method, flags } = this.query.toObject();
179
178
 
180
179
  return this[method](this.query).then((data) => {