@coderich/autograph 0.8.13 → 0.9.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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## v0.9.x
4
+ - Subscriptions API
5
+ - postMutation no longer mutates "doc" and adds "result"
6
+
3
7
  ## v0.8.x
4
8
  - Engine 14+
5
9
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@coderich/autograph",
3
3
  "author": "Richard Livolsi (coderich)",
4
- "version": "0.8.13",
4
+ "version": "0.9.0",
5
5
  "description": "AutoGraph",
6
6
  "keywords": [
7
7
  "graphql",
@@ -8,7 +8,7 @@ const { ensureArray } = require('../service/app.service');
8
8
  * If it expects more than 1 we block and wait for it to finish.
9
9
  */
10
10
  module.exports = class extends EventEmitter {
11
- emit(event, data) {
11
+ emit(event, data, prev) {
12
12
  return Promise.all(this.rawListeners(event).map((wrapper) => {
13
13
  return new Promise((resolve, reject) => {
14
14
  const next = result => resolve(result); // If a result is passed this will bypass middleware thunk()
@@ -17,7 +17,9 @@ module.exports = class extends EventEmitter {
17
17
  if (numArgs < 2) next();
18
18
  });
19
19
  })).then((results) => {
20
- return results.find(result => result !== undefined); // There can be only one (result)
20
+ if (event === 'preMutation') return this.emit('validate', data, results);
21
+ const target = event === 'validate' ? prev : results;
22
+ return target.find(r => r !== undefined); // There can be only one (result)
21
23
  });
22
24
  }
23
25
 
@@ -7,14 +7,10 @@ const QueryBuilder = require('../query/QueryBuilder');
7
7
 
8
8
  module.exports = class Resolver {
9
9
  constructor(schema, context = {}) {
10
+ this.models = schema.getModels();
10
11
  this.schema = schema;
11
12
  this.context = context;
12
- this.loader = new DataLoader();
13
-
14
- // DataLoader Proxy Methods
15
- this.clear = key => this.loader.clear(key);
16
- this.clearAll = () => this.loader.clearAll();
17
- this.prime = (key, value) => this.loader.prime(key, value);
13
+ this.loaders = this.models.reduce((prev, model) => prev.set(model, new DataLoader(this, model)), new WeakMap());
18
14
 
19
15
  //
20
16
  this.getSchema = () => this.schema;
@@ -55,15 +51,18 @@ module.exports = class Resolver {
55
51
  switch (crud) {
56
52
  case 'create': case 'update': case 'delete': {
57
53
  return model.getDriver().resolve(query.toDriver()).then((data) => {
58
- this.clearAll();
54
+ this.clear(model);
59
55
  return new ResultSet(query, data);
60
56
  });
61
57
  }
62
58
  default: {
59
+ // This is needed in SF tests...
63
60
  const key = model.idKey();
64
61
  const { where } = query.toDriver();
65
62
  if (Object.prototype.hasOwnProperty.call(where, key) && where[key] == null) return Promise.resolve(null);
66
- return this.loader.load(query);
63
+
64
+ //
65
+ return this.loaders.get(model).load(query);
67
66
  }
68
67
  }
69
68
  }
@@ -90,4 +89,15 @@ module.exports = class Resolver {
90
89
  toResultSet(model, data) {
91
90
  return new ResultSet(new Query({ model: this.toModel(model), resolver: this }), data);
92
91
  }
92
+
93
+ // DataLoader Proxy Methods
94
+ clear(model) {
95
+ this.loaders.get(this.toModel(model)).clearAll();
96
+ return this;
97
+ }
98
+
99
+ clearAll() {
100
+ this.models.forEach(model => this.clear(model));
101
+ return this;
102
+ }
93
103
  };
@@ -1,84 +1,42 @@
1
- // const { flatten } = require('lodash');
2
1
  const FBDataLoader = require('dataloader');
3
2
  const ResultSet = require('./ResultSet');
3
+ const Query = require('../query/Query');
4
4
  const { hashObject } = require('../service/app.service');
5
5
 
6
6
  // let counter = 0;
7
7
  module.exports = class DataLoader extends FBDataLoader {
8
- constructor() {
9
- return new FBDataLoader((queries) => {
10
- // const timeID = `${++counter}DataLoader(${queries.length})[${new Date().getTime()}]`;
11
- // console.time(timeID);
12
-
13
- // const queriesByModel = queries.reduce((prev, query, i) => {
14
- // const toDriver = query.toDriver();
15
- // const { model, method } = query.toObject();
16
- // const key = model.idKey();
17
- // prev[model] = (prev[model] || { query, model, key, get: {}, find: [] });
18
- // toDriver.$index = i;
19
-
20
- // if (method === 'findOne' && Object.prototype.hasOwnProperty.call(toDriver.where, key) && !Array.isArray(toDriver.where[key])) {
21
- // const { [key]: id, ...rest } = toDriver.where;
22
- // const hash = hashObject(rest);
23
- // prev[model].get[hash] = prev[model].get[hash] || [];
24
- // prev[model].get[hash].push(toDriver);
25
- // return prev;
26
- // }
27
-
28
- // prev[model].find.push(toDriver);
29
- // return prev;
30
- // }, {});
31
-
32
- // return new Promise((resolve, reject) => {
33
- // const results = [];
34
-
35
- // Promise.all(Object.values(queriesByModel).map(({ query, model, key, get, find }) => {
36
- // return Promise.all(flatten([
37
- // ...find.map(q => model.getDriver().resolve(q)),
38
- // ...Object.values(get).map((set) => {
39
- // const ids = [...new Set(set.map(({ where }) => where[key]))];
40
- // const toDriver = { ...set[0] };
41
- // toDriver.method = 'findMany';
42
- // toDriver.where[key] = ids;
43
- // return model.getDriver().resolve(toDriver).then(data => (typeof data === 'object' ? new ResultSet(query, data) : data));
44
- // // return model.getDriver().resolve(toDriver);
45
- // }),
46
- // ]));
47
- // })).then((resultsByModel) => {
48
- // console.timeEnd(timeID);
49
- // resultsByModel.forEach((modelResults, i) => {
50
- // const { key, get, find } = Object.values(queriesByModel)[i];
8
+ constructor(resolver, model) {
9
+ const idKey = model.idKey();
10
+ const driver = model.getDriver();
51
11
 
52
- // modelResults.splice(0, find.length).forEach((result, j) => (results[find[j].$index] = result));
53
-
54
- // Object.values(get).forEach((set) => {
55
- // const bundle = modelResults.shift();
56
-
57
- // set.forEach(({ where, $index }) => {
58
- // const id = where[key];
59
- // results[$index] = bundle.find(res => `${res[key]}` === `${id}`) || null;
60
- // });
61
- // // modelResults.splice(0, set.length).forEach((result, j) => (results[set[j].$index] = result));
62
- // });
12
+ return new FBDataLoader((queries) => {
13
+ // The idea is to group the "findOne by id" queries together to make 1 query instead
14
+ const { findOneByIdQueries, allOtherQueries } = queries.reduce((prev, query, i) => {
15
+ const { id, method } = query.toObject();
16
+ const key = method === 'findOne' && id ? 'findOneByIdQueries' : 'allOtherQueries';
17
+ prev[key].push({ id, query, i });
18
+ return prev;
19
+ }, { findOneByIdQueries: [], allOtherQueries: [] });
20
+
21
+ // Aggregate ids
22
+ const ids = Array.from(new Set(findOneByIdQueries.map(el => `${el.id}`)));
23
+ const batchQuery = new Query({ resolver, model, method: 'findMany', crud: 'read' });
24
+ const batchWhere = model.transform(batchQuery, { id: ids }, 'serialize', true);
25
+ const promises = [Promise.all(allOtherQueries.map(({ query, i }) => driver.resolve(query.toDriver()).then(data => ({ data, query, i }))))];
26
+ if (ids.length) promises.push(driver.resolve(batchQuery.where(batchWhere).toDriver()).then(results => findOneByIdQueries.map(({ query, id, i }) => ({ i, query, data: results.find(r => `${r[idKey]}` === `${id}`) || null }))));
27
+
28
+ return Promise.all(promises).then((results) => {
29
+ const sorted = results.flat().filter(Boolean).sort((a, b) => a.i - b.i);
30
+ return sorted.map(({ query, data }) => (data != null && typeof data === 'object' ? new ResultSet(query, data) : data));
31
+ });
63
32
 
64
- // return results;
65
- // });
33
+ // return Promise.all(queries.map((query) => {
34
+ // return driver.resolve(query.toDriver()).then((data) => {
35
+ // return (data != null && typeof data === 'object' ? new ResultSet(query, data) : data);
66
36
  // });
67
- // });
68
-
69
- return Promise.all(queries.map((query) => {
70
- const { model } = query.toObject();
71
- return model.getDriver().resolve(query.toDriver()).then((data) => {
72
- return (data != null && typeof data === 'object' ? new ResultSet(query, data) : data);
73
- });
74
- })).then((results) => {
75
- // console.timeEnd(timeID);
76
- // console.log(new Date().getTime());
77
- return results;
78
- });
37
+ // }));
79
38
  }, {
80
- // cache: false,
81
- // maxBatchSize: 50,
39
+ cache: true,
82
40
  cacheKeyFn: query => hashObject(query.getCacheKey()),
83
41
  });
84
42
  }
package/src/data/Field.js CHANGED
@@ -137,4 +137,14 @@ module.exports = class extends Field {
137
137
  return uvl(this.cast(results.pop()), value);
138
138
  });
139
139
  }
140
+
141
+ tform(query, value) {
142
+ // Determine transformers
143
+ const transformers = this.getTransformers();
144
+
145
+ // Transform
146
+ return transformers.reduce((prev, transformer) => {
147
+ return transformer(this, prev, query);
148
+ }, this.cast(value));
149
+ }
140
150
  };
package/src/data/Model.js CHANGED
@@ -115,7 +115,7 @@ module.exports = class extends Model {
115
115
  // Transform all the data
116
116
  return map(data, (doc) => {
117
117
  // We want the appendFields + those in the data, deduped
118
- const fields = [...new Set(appendFields.concat(Object.keys(doc).map(k => this.getField(k))))].filter(f => f);
118
+ const fields = [...new Set(appendFields.concat(Object.keys(doc).map(k => this.getField(k))))].filter(Boolean);
119
119
 
120
120
  // Loop through the fields and delegate (renaming keys appropriately)
121
121
  return fields.reduce((prev, field) => {
@@ -129,7 +129,7 @@ module.exports = class extends Model {
129
129
  normalize(query, data, serdes = (() => { throw new Error('No Sir Sir SerDes!'); }), keysOnly = false) {
130
130
  // Transform all the data
131
131
  return map(data, (doc) => {
132
- const fields = Object.keys(doc).map(k => this.getField(k)).filter(f => f);
132
+ const fields = Object.keys(doc).map(k => this.getField(k)).filter(Boolean);
133
133
 
134
134
  // Loop through the fields and delegate (renaming keys appropriately)
135
135
  return fields.reduce((prev, field) => {
@@ -150,4 +150,14 @@ module.exports = class extends Model {
150
150
  })));
151
151
  }));
152
152
  }
153
+
154
+ tform(query, data) {
155
+ return map(data, (doc) => {
156
+ return Object.keys(doc).map(k => this.getField(k)).filter(Boolean).reduce((prev, curr) => {
157
+ const key = curr.getName();
158
+ const value = doc[key];
159
+ return Object.assign(prev, { [key]: curr.tform(query, value) });
160
+ }, {});
161
+ });
162
+ }
153
163
  };
@@ -13,162 +13,157 @@ module.exports = class ResultSet {
13
13
  //
14
14
  const cache = new Map();
15
15
 
16
- // Create and return ResultSetItem
17
- return Object.defineProperties({},
18
- fields.reduce((prev, field) => {
19
- const key = field.getKey();
20
- const name = field.getName();
21
- const $name = `$${name}`;
22
- const value = doc[key];
23
-
24
- // Field attributes
25
- prev[name] = {
26
- get() {
27
- if (cache.has(name)) return cache.get(name);
28
- let $value = field.deserialize(query, value);
29
- $value = $value != null && field.isEmbedded() ? new ResultSet(query.model(field.getModelRef()), $value, false) : $value;
30
- cache.set(name, $value);
31
- return $value;
32
- },
33
- set($value) {
34
- cache.set(name, $value);
35
- },
36
- enumerable: true,
37
- configurable: true, // Allows things like delete
38
- };
39
-
40
- // Hydrated field attributes
41
- prev[`$${name}`] = {
42
- get() {
43
- return (args = {}) => {
44
- // Ensure where clause
45
- args.where = args.where || {};
46
-
47
- // Cache
48
- const cacheKey = `${$name}-${hashObject(args)}`;
49
- if (cache.has(cacheKey)) return cache.get(cacheKey);
16
+ const definition = fields.reduce((prev, field) => {
17
+ const key = field.getKey();
18
+ const name = field.getName();
19
+ const $name = `$${name}`;
20
+ const value = doc[key];
21
+
22
+ // Field attributes
23
+ prev[name] = {
24
+ get() {
25
+ if (cache.has(name)) return cache.get(name);
26
+ let $value = field.deserialize(query, value);
27
+ $value = $value != null && field.isEmbedded() ? new ResultSet(query.model(field.getModelRef()), $value, false) : $value;
28
+ cache.set(name, $value);
29
+ return $value;
30
+ },
31
+ set($value) {
32
+ cache.set(name, $value);
33
+ },
34
+ enumerable: true,
35
+ configurable: true, // Allows things like delete
36
+ };
50
37
 
51
- const promise = new Promise((resolve, reject) => {
52
- (() => {
53
- const $value = this[name];
38
+ // Hydrated field attributes
39
+ prev[`$${name}`] = {
40
+ get() {
41
+ return (args = {}) => {
42
+ // Ensure where clause
43
+ args.where = args.where || {};
54
44
 
55
- if (field.isScalar() || field.isEmbedded()) return Promise.resolve($value);
45
+ // Cache
46
+ const cacheKey = `${$name}-${hashObject(args)}`;
47
+ if (cache.has(cacheKey)) return cache.get(cacheKey);
56
48
 
57
- const modelRef = field.getModelRef();
49
+ const promise = new Promise((resolve, reject) => {
50
+ (() => {
51
+ const $value = this[name];
58
52
 
59
- if (field.isArray()) {
60
- if (field.isVirtual()) {
61
- args.where[[field.getVirtualField()]] = this.id; // Is where[[field.getVirtualField()]] correct?
62
- return resolver.match(modelRef).merge(args).many();
63
- }
53
+ if (field.isScalar() || field.isEmbedded()) return Promise.resolve($value);
64
54
 
65
- // Not a "required" query + strip out nulls
66
- args.where.id = $value;
67
- return resolver.match(modelRef).merge(args).many();
68
- }
55
+ const modelRef = field.getModelRef();
69
56
 
57
+ if (field.isArray()) {
70
58
  if (field.isVirtual()) {
71
- args.where[[field.getVirtualField()]] = this.id;
72
- return resolver.match(modelRef).merge(args).one();
59
+ args.where[[field.getVirtualField()]] = this.id; // Is where[[field.getVirtualField()]] correct?
60
+ return resolver.match(modelRef).merge(args).many();
73
61
  }
74
62
 
75
- return resolver.match(modelRef).id($value).one({ required: field.isRequired() });
76
- })().then((results) => {
77
- if (results == null) return field.resolve(query, results); // Allow field to determine
78
- return mapPromise(results, result => field.resolve(query, result)).then(() => results); // Resolve the inside fields but still return "results"!!!!
79
- }).then((resolved) => {
80
- resolve(resolved);
81
- }).catch((e) => {
82
- reject(e);
83
- });
63
+ // Not a "required" query + strip out nulls
64
+ args.where.id = $value;
65
+ return resolver.match(modelRef).merge(args).many();
66
+ }
67
+
68
+ if (field.isVirtual()) {
69
+ args.where[[field.getVirtualField()]] = this.id;
70
+ return resolver.match(modelRef).merge(args).one();
71
+ }
72
+
73
+ return resolver.match(modelRef).id($value).one({ required: field.isRequired() });
74
+ })().then((results) => {
75
+ if (results == null) return field.resolve(query, results); // Allow field to determine
76
+ return mapPromise(results, result => field.resolve(query, result)).then(() => results); // Resolve the inside fields but still return "results"!!!!
77
+ }).then((resolved) => {
78
+ resolve(resolved);
79
+ }).catch((e) => {
80
+ reject(e);
84
81
  });
82
+ });
85
83
 
86
- cache.set(cacheKey, promise);
87
- return promise;
88
- };
89
- },
90
- enumerable: false,
91
- };
92
-
93
- // Field count (let's assume it's a Connection Type - meaning dont try with anything else)
94
- prev[`$${name}:count`] = {
95
- get() {
96
- return (q = {}) => {
97
- q.where = q.where || {};
98
- if (field.isVirtual()) q.where[field.getVirtualField()] = this.id;
99
- else q.where.id = this[name];
100
- return resolver.match(field.getModelRef()).merge(q).count();
101
- };
102
- },
103
- enumerable: false,
104
- };
105
-
106
- return prev;
107
- }, {
108
- id: {
109
- get() { return doc.id || doc[model.idKey()]; },
110
- set(id) { doc.id = id; }, // Embedded array of documents need to set id
111
- enumerable: true,
84
+ cache.set(cacheKey, promise);
85
+ return promise;
86
+ };
112
87
  },
113
-
114
- $id: {
115
- get() { return toGUID(model.getName(), this.id); },
116
- enumerable: false,
88
+ enumerable: false,
89
+ };
90
+
91
+ // Field count (let's assume it's a Connection Type - meaning dont try with anything else)
92
+ prev[`$${name}:count`] = {
93
+ get() {
94
+ return (q = {}) => {
95
+ q.where = q.where || {};
96
+ if (field.isVirtual()) q.where[field.getVirtualField()] = this.id;
97
+ else q.where.id = this[name];
98
+ return resolver.match(field.getModelRef()).merge(q).count();
99
+ };
117
100
  },
101
+ enumerable: false,
102
+ };
103
+
104
+ return prev;
105
+ }, {
106
+ id: {
107
+ get() { return doc.id || doc[model.idKey()]; },
108
+ set(id) { doc.id = id; }, // Embedded array of documents need to set id
109
+ enumerable: true,
110
+ },
118
111
 
119
- $$cursor: {
120
- get() {
121
- const sortPaths = keyPaths(sort);
122
- const sortValues = sortPaths.reduce((prv, path) => Object.assign(prv, { [path]: get(this, path) }), {});
123
- const sortJSON = JSON.stringify(sortValues);
124
- return Buffer.from(sortJSON).toString('base64');
125
- },
126
- enumerable: false,
127
- },
112
+ $id: {
113
+ get() { return toGUID(model.getName(), this.id); },
114
+ enumerable: false,
115
+ },
128
116
 
129
- $$model: {
130
- value: model,
131
- enumerable: false,
117
+ $$cursor: {
118
+ get() {
119
+ const sortPaths = keyPaths(sort);
120
+ const sortValues = sortPaths.reduce((prv, path) => Object.assign(prv, { [path]: get(this, path) }), {});
121
+ const sortJSON = JSON.stringify(sortValues);
122
+ return Buffer.from(sortJSON).toString('base64');
132
123
  },
124
+ enumerable: false,
125
+ },
133
126
 
134
- $$data: {
135
- value: data,
136
- enumerable: false,
137
- },
127
+ $$model: {
128
+ value: model,
129
+ enumerable: false,
130
+ },
138
131
 
139
- $$isResultSetItem: {
140
- value: true,
141
- enumerable: false,
142
- },
132
+ $$isResultSetItem: {
133
+ value: true,
134
+ enumerable: false,
135
+ },
143
136
 
144
- $$save: {
145
- get() { return input => resolver.match(model).id(this.id).save({ ...this, ...input }); },
146
- enumerable: false,
147
- },
137
+ $$save: {
138
+ get() { return input => resolver.match(model).id(this.id).save({ ...this, ...input }); },
139
+ enumerable: false,
140
+ },
148
141
 
149
- $$remove: {
150
- get() { return () => resolver.match(model).id(this.id).remove(); },
151
- enumerable: false,
152
- },
142
+ $$remove: {
143
+ get() { return () => resolver.match(model).id(this.id).remove(); },
144
+ enumerable: false,
145
+ },
153
146
 
154
- $$delete: {
155
- get() { return () => resolver.match(model).id(this.id).delete(); },
156
- enumerable: false,
157
- },
147
+ $$delete: {
148
+ get() { return () => resolver.match(model).id(this.id).delete(); },
149
+ enumerable: false,
150
+ },
158
151
 
159
- toObject: {
160
- get() {
161
- return () => map(this, obj => Object.entries(obj).reduce((prev, [key, value]) => {
162
- if (value === undefined) return prev;
163
- prev[key] = get(value, '$$isResultSet') ? value.toObject() : value;
164
- return prev;
165
- }, {}));
166
- },
167
- enumerable: false,
168
- configurable: true,
152
+ toObject: {
153
+ get() {
154
+ return () => map(this, obj => Object.entries(obj).reduce((prev, [key, value]) => {
155
+ if (value === undefined) return prev;
156
+ prev[key] = get(value, '$$isResultSet') ? value.toObject() : value;
157
+ return prev;
158
+ }, {}));
169
159
  },
170
- }),
171
- );
160
+ enumerable: false,
161
+ configurable: true,
162
+ },
163
+ });
164
+
165
+ // Create and return ResultSetItem
166
+ return Object.defineProperties({}, definition);
172
167
  });
173
168
 
174
169
  let hasNextPage = false;
@@ -0,0 +1,210 @@
1
+ const { get } = require('lodash');
2
+ const DataService = require('./DataService');
3
+ const { map, ensureArray, keyPaths, mapPromise, toGUID, hashObject } = require('../service/app.service');
4
+
5
+ module.exports = class ResultSet {
6
+ constructor(query, data, adjustForPagination = true) {
7
+ const { resolver, model, sort, first, after, last, before } = query.toObject();
8
+ const fields = model.getFields().filter(f => f.getName() !== 'id');
9
+
10
+ const rs = map(data, (doc) => {
11
+ if (doc == null || typeof doc !== 'object') return doc;
12
+
13
+ //
14
+ const cache = new Map();
15
+
16
+ // Base definition all results have
17
+ const definition = {
18
+ id: {
19
+ get() { return doc.id || doc[model.idKey()]; },
20
+ set(id) { doc.id = id; }, // Embedded array of documents need to set id
21
+ enumerable: true,
22
+ },
23
+
24
+ $id: {
25
+ get() { return toGUID(model.getName(), this.id); },
26
+ enumerable: false,
27
+ },
28
+
29
+ $$cursor: {
30
+ get() {
31
+ const sortPaths = keyPaths(sort);
32
+ const sortValues = sortPaths.reduce((prv, path) => Object.assign(prv, { [path]: get(this, path) }), {});
33
+ const sortJSON = JSON.stringify(sortValues);
34
+ return Buffer.from(sortJSON).toString('base64');
35
+ },
36
+ enumerable: false,
37
+ },
38
+
39
+ $$model: {
40
+ value: model,
41
+ enumerable: false,
42
+ },
43
+
44
+ $$data: {
45
+ value: data,
46
+ enumerable: false,
47
+ },
48
+
49
+ $$isResultSetItem: {
50
+ value: true,
51
+ enumerable: false,
52
+ },
53
+
54
+ $$save: {
55
+ get() { return input => resolver.match(model).id(this.id).save({ ...this, ...input }); },
56
+ enumerable: false,
57
+ },
58
+
59
+ $$remove: {
60
+ get() { return () => resolver.match(model).id(this.id).remove(); },
61
+ enumerable: false,
62
+ },
63
+
64
+ $$delete: {
65
+ get() { return () => resolver.match(model).id(this.id).delete(); },
66
+ enumerable: false,
67
+ },
68
+
69
+ toObject: {
70
+ get() {
71
+ return () => map(this, obj => Object.entries(obj).reduce((prev, [key, value]) => {
72
+ if (value === undefined) return prev;
73
+ prev[key] = get(value, '$$isResultSet') ? value.toObject() : value;
74
+ return prev;
75
+ }, {}));
76
+ },
77
+ enumerable: false,
78
+ configurable: true,
79
+ },
80
+ };
81
+
82
+ fields.forEach((field) => {
83
+ const key = field.getKey();
84
+ const name = field.getName();
85
+ const $name = `$${name}`;
86
+ const value = doc[key];
87
+
88
+ // Field attributes
89
+ definition[name] = {
90
+ get() {
91
+ if (cache.has(name)) return cache.get(name);
92
+ let $value = field.deserialize(query, value);
93
+ $value = $value != null && field.isEmbedded() ? new ResultSet(query.model(field.getModelRef()), $value, false) : $value;
94
+ cache.set(name, $value);
95
+ return $value;
96
+ },
97
+ set($value) {
98
+ cache.set(name, $value);
99
+ },
100
+ enumerable: true,
101
+ configurable: true, // Allows things like delete
102
+ };
103
+
104
+ // Hydrated field attributes
105
+ definition[`$${name}`] = {
106
+ get() {
107
+ return (args = {}) => {
108
+ // Ensure where clause
109
+ args.where = args.where || {};
110
+
111
+ // Cache
112
+ const cacheKey = `${$name}-${hashObject(args)}`;
113
+ if (cache.has(cacheKey)) return cache.get(cacheKey);
114
+
115
+ const promise = new Promise((resolve, reject) => {
116
+ (() => {
117
+ const $value = this[name];
118
+
119
+ if (field.isScalar() || field.isEmbedded()) return Promise.resolve($value);
120
+
121
+ const modelRef = field.getModelRef();
122
+
123
+ if (field.isArray()) {
124
+ if (field.isVirtual()) {
125
+ args.where[[field.getVirtualField()]] = this.id; // Is where[[field.getVirtualField()]] correct?
126
+ return resolver.match(modelRef).merge(args).many();
127
+ }
128
+
129
+ // Not a "required" query + strip out nulls
130
+ args.where.id = $value;
131
+ return resolver.match(modelRef).merge(args).many();
132
+ }
133
+
134
+ if (field.isVirtual()) {
135
+ args.where[[field.getVirtualField()]] = this.id;
136
+ return resolver.match(modelRef).merge(args).one();
137
+ }
138
+
139
+ return resolver.match(modelRef).id($value).one({ required: field.isRequired() });
140
+ })().then((results) => {
141
+ if (results == null) return field.resolve(query, results); // Allow field to determine
142
+ return mapPromise(results, result => field.resolve(query, result)).then(() => results); // Resolve the inside fields but still return "results"!!!!
143
+ }).then((resolved) => {
144
+ resolve(resolved);
145
+ }).catch((e) => {
146
+ reject(e);
147
+ });
148
+ });
149
+
150
+ cache.set(cacheKey, promise);
151
+ return promise;
152
+ };
153
+ },
154
+ enumerable: false,
155
+ };
156
+
157
+ // Field count (let's assume it's a Connection Type - meaning dont try with anything else)
158
+ definition[`$${name}:count`] = {
159
+ get() {
160
+ return (q = {}) => {
161
+ q.where = q.where || {};
162
+ if (field.isVirtual()) q.where[field.getVirtualField()] = this.id;
163
+ else q.where.id = this[name];
164
+ return resolver.match(field.getModelRef()).merge(q).count();
165
+ };
166
+ },
167
+ enumerable: false,
168
+ };
169
+ });
170
+
171
+ // Create and return ResultSetItem
172
+ return Object.defineProperties({}, definition);
173
+ });
174
+
175
+ let hasNextPage = false;
176
+ let hasPreviousPage = false;
177
+ if (adjustForPagination && rs.length) (({ hasPreviousPage, hasNextPage } = DataService.paginateResultSet(rs, first, after, last, before)));
178
+
179
+ return Object.defineProperties(rs, {
180
+ $$pageInfo: {
181
+ get() {
182
+ const edges = ensureArray(rs);
183
+
184
+ return {
185
+ startCursor: get(edges, '0.$$cursor', ''),
186
+ endCursor: get(edges, `${edges.length - 1}.$$cursor`, ''),
187
+ hasPreviousPage,
188
+ hasNextPage,
189
+ };
190
+ },
191
+ enumerable: false,
192
+ },
193
+ $$isResultSet: {
194
+ value: true,
195
+ enumerable: false,
196
+ },
197
+ toObject: {
198
+ get() {
199
+ return () => map(this, doc => Object.entries(doc).reduce((prev, [key, value]) => {
200
+ if (value === undefined) return prev;
201
+ prev[key] = get(value, '$$isResultSet') ? value.toObject() : value;
202
+ return prev;
203
+ }, {}));
204
+ },
205
+ enumerable: false,
206
+ configurable: true,
207
+ },
208
+ });
209
+ }
210
+ };
@@ -0,0 +1,186 @@
1
+ const { get } = require('lodash');
2
+ const DataService = require('./DataService');
3
+ const { map, ensureArray, keyPaths, mapPromise, toGUID, hashObject } = require('../service/app.service');
4
+
5
+ module.exports = class ResultSet {
6
+ constructor(query, data, adjustForPagination = true) {
7
+ const { resolver, model, sort, first, after, last, before } = query.toObject();
8
+ const fields = model.getFields().filter(f => f.getName() !== 'id');
9
+
10
+ const rs = map(data, (doc) => {
11
+ if (doc == null || typeof doc !== 'object') return doc;
12
+
13
+ const cache = new Map();
14
+
15
+ const validKeys = [];
16
+
17
+ const definition = {
18
+ get id() { return doc.id || doc[model.idKey()]; },
19
+ get $id() { return toGUID(model.getName(), this.id); },
20
+ get $$data() { return data; },
21
+ get $$model() { return model; },
22
+ get $$isResultSetItem() { return true; },
23
+ get $$save() { return input => resolver.match(model).id(this.id).save({ ...this, ...input }); },
24
+ get $$remove() { return () => resolver.match(model).id(this.id).remove(); },
25
+ get $$delete() { return () => resolver.match(model).id(this.id).delete(); },
26
+ get $$cursor() {
27
+ return () => {
28
+ const sortPaths = keyPaths(sort);
29
+ const sortValues = sortPaths.reduce((prv, path) => Object.assign(prv, { [path]: get(this, path) }), {});
30
+ const sortJSON = JSON.stringify(sortValues);
31
+ return Buffer.from(sortJSON).toString('base64');
32
+ };
33
+ },
34
+ get toObject() {
35
+ return () => validKeys.reduce((prev, key) => Object.assign(prev, { [key]: this[key] }), {});
36
+ },
37
+ };
38
+
39
+ fields.forEach((field) => {
40
+ const key = field.getKey();
41
+ const name = field.getName();
42
+ const $name = `$${name}`;
43
+ const value = doc[key];
44
+ validKeys.push(name);
45
+
46
+ // Field attributes
47
+ Object.assign(definition, {
48
+ get [name]() {
49
+ let $value = field.deserialize(query, value);
50
+ $value = $value != null && field.isEmbedded() ? new ResultSet(query.model(field.getModelRef()), $value, false) : $value;
51
+ return $value;
52
+ },
53
+ });
54
+
55
+ // Hydrated field attributes
56
+ Object.assign(definition, {
57
+ get [$name]() {
58
+ return (args = {}) => {
59
+ // Ensure where clause
60
+ args.where = args.where || {};
61
+
62
+ return new Promise((resolve, reject) => {
63
+ (() => {
64
+ const $value = this[name];
65
+
66
+ if (field.isScalar() || field.isEmbedded()) return Promise.resolve($value);
67
+
68
+ const modelRef = field.getModelRef();
69
+
70
+ if (field.isArray()) {
71
+ if (field.isVirtual()) {
72
+ args.where[[field.getVirtualField()]] = this.id; // Is where[[field.getVirtualField()]] correct?
73
+ return resolver.match(modelRef).merge(args).many();
74
+ }
75
+
76
+ // Not a "required" query + strip out nulls
77
+ args.where.id = $value;
78
+ return resolver.match(modelRef).merge(args).many();
79
+ }
80
+
81
+ if (field.isVirtual()) {
82
+ args.where[[field.getVirtualField()]] = this.id;
83
+ return resolver.match(modelRef).merge(args).one();
84
+ }
85
+
86
+ return resolver.match(modelRef).id($value).one({ required: field.isRequired() });
87
+ })().then((results) => {
88
+ if (results == null) return field.resolve(query, results); // Allow field to determine
89
+ return mapPromise(results, result => field.resolve(query, result)).then(() => results); // Resolve the inside fields but still return "results"!!!!
90
+ }).then((resolved) => {
91
+ resolve(resolved);
92
+ }).catch((e) => {
93
+ reject(e);
94
+ });
95
+ });
96
+ };
97
+ },
98
+ });
99
+
100
+ // Field count (let's assume it's a Connection Type - meaning dont try with anything else)
101
+ Object.assign(definition, {
102
+ get [`${$name}:count`]() {
103
+ return (q = {}) => {
104
+ q.where = q.where || {};
105
+ if (field.isVirtual()) q.where[field.getVirtualField()] = this.id;
106
+ else q.where.id = this[name];
107
+ return resolver.match(field.getModelRef()).merge(q).count();
108
+ };
109
+ },
110
+ });
111
+ });
112
+
113
+ // Create and return ResultSetItem
114
+ const idk = new Proxy(definition, {
115
+ get(target, prop, rec) {
116
+ if (cache.has(prop)) return cache.get(prop);
117
+ const value = Reflect.get(target, prop, rec);
118
+ if (typeof value === 'function') return value.bind(target);
119
+ cache.set(prop, value);
120
+ return value;
121
+ },
122
+ set(target, prop, value) {
123
+ cache.set(prop, value);
124
+ return true;
125
+ },
126
+ ownKeys() {
127
+ return validKeys;
128
+ },
129
+ getOwnPropertyDescriptor(target, prop) {
130
+ if (validKeys.indexOf(prop) === -1) {
131
+ return {
132
+ writable: true,
133
+ enumerable: true,
134
+ configurable: true,
135
+ };
136
+ }
137
+
138
+ return {
139
+ writable: false,
140
+ enumerable: false,
141
+ configurable: false,
142
+ };
143
+ },
144
+ });
145
+
146
+ // console.log(idk);
147
+ // // console.log(idk.toObject());
148
+ return idk;
149
+ });
150
+
151
+ let hasNextPage = false;
152
+ let hasPreviousPage = false;
153
+ if (adjustForPagination && rs.length) (({ hasPreviousPage, hasNextPage } = DataService.paginateResultSet(rs, first, after, last, before)));
154
+
155
+ return Object.defineProperties(rs, {
156
+ $$pageInfo: {
157
+ get() {
158
+ const edges = ensureArray(rs);
159
+
160
+ return {
161
+ startCursor: get(edges, '0.$$cursor', ''),
162
+ endCursor: get(edges, `${edges.length - 1}.$$cursor`, ''),
163
+ hasPreviousPage,
164
+ hasNextPage,
165
+ };
166
+ },
167
+ enumerable: false,
168
+ },
169
+ $$isResultSet: {
170
+ value: true,
171
+ enumerable: false,
172
+ },
173
+ toObject: {
174
+ get() {
175
+ return () => map(this, doc => Object.entries(doc).reduce((prev, [key, value]) => {
176
+ if (value === undefined) return prev;
177
+ prev[key] = get(value, '$$isResultSet') ? value.toObject() : value;
178
+ return prev;
179
+ }, {}));
180
+ },
181
+ enumerable: false,
182
+ configurable: true,
183
+ },
184
+ });
185
+ }
186
+ };
@@ -265,9 +265,9 @@ module.exports = class Node {
265
265
  switch (this.nodeType) {
266
266
  case 'model': {
267
267
  if (!this.isMarkedModel()) return '';
268
- return nvl(uvl(this.getDirectiveArg('model', 'gqlScope'), 'crud'), '');
268
+ return nvl(uvl(this.getDirectiveArg('model', 'gqlScope'), 'cruds'), '');
269
269
  }
270
- case 'field': return nvl(uvl(this.getDirectiveArg('field', 'gqlScope'), 'crud'), '');
270
+ case 'field': return nvl(uvl(this.getDirectiveArg('field', 'gqlScope'), 'cruds'), '');
271
271
  default: return '';
272
272
  }
273
273
  }
@@ -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, makeInputSplice, makeQueryResolver, makeMutationResolver } = require('../../service/decorator.service');
6
+ const { makeCreateAPI, makeReadAPI, makeUpdateAPI, makeDeleteAPI, makeSubscriptionAPI, makeInputSplice, makeQueryResolver, makeMutationResolver } = require('../../service/decorator.service');
7
7
 
8
8
  const interfaceKinds = [Kind.INTERFACE_TYPE_DEFINITION, Kind.INTERFACE_TYPE_EXTENSION];
9
9
 
@@ -20,6 +20,7 @@ const getGQLWhereFields = (model) => {
20
20
  module.exports = (schema) => {
21
21
  const resolver = new ServerResolver();
22
22
  const allModels = schema.getModels();
23
+ const entityModels = schema.getEntityModels();
23
24
  const markedModels = schema.getMarkedModels();
24
25
  const createModels = findGQLModels('c', markedModels, allModels);
25
26
  const readModels = findGQLModels('r', markedModels, allModels);
@@ -55,17 +56,43 @@ module.exports = (schema) => {
55
56
  extend ${interfaceKinds.indexOf(model.getKind()) > -1 ? 'interface' : 'type'} ${model.getName()} {
56
57
  ${model.getFields().filter(field => field.hasGQLScope('r')).map(field => `${field.getName()}${field.getExtendArgs()}: ${field.getPayloadType()}`)}
57
58
  }
59
+
58
60
  type ${model.getName()}Connection {
59
61
  pageInfo: PageInfo!
60
62
  edges: [${model.getName()}Edge]
61
63
  count: Int!
62
64
  }
65
+
63
66
  type ${model.getName()}Edge {
64
67
  node: ${model.getName()}
65
68
  cursor: String!
66
69
  }
67
70
  `),
68
71
 
72
+ ...entityModels.map(model => `
73
+ input ${model.getName()}SubscriptionInputWhere {
74
+ ${getGQLWhereFields(model).filter(field => field.isBasicType() || field.isEmbedded()).map(field => `${field.getName()}: ${field.getModelRef() ? `${ucFirst(field.getDataRef())}InputWhere` : 'AutoGraphMixed'}`)}
75
+ }
76
+
77
+ type ${model.getName()}SubscriptionPayloadEventData {
78
+ ${getGQLWhereFields(model).filter(field => field.isBasicType() || field.isEmbedded()).map(field => `${field.getName()}: ${field.getGQLType()}`)}
79
+ }
80
+
81
+ type ${model.getName()}SubscriptionPayloadEvent {
82
+ crud: SubscriptionCrudEnum!
83
+ data: ${model.getName()}SubscriptionPayloadEventData!
84
+ }
85
+
86
+ type ${model.getName()}SubscriptionPayload {
87
+ event: ${model.getName()}SubscriptionPayloadEvent
88
+ }
89
+
90
+ input ${model.getName()}SubscriptionInputFilter {
91
+ when: SubscriptionWhenEnum! = anytime
92
+ where: ${model.getName()}SubscriptionInputWhere! = {}
93
+ }
94
+ `),
95
+
69
96
  ...spliceModels.map(model => `
70
97
  #input ${model.getName()}InputSplice {
71
98
  # with: ${model}InputWhere
@@ -82,14 +109,19 @@ module.exports = (schema) => {
82
109
 
83
110
  `type Query {
84
111
  node(id: ID!): Node
85
- ${schema.getEntityModels().map(model => makeReadAPI(model.getName(), model))}
112
+ ${entityModels.map(model => makeReadAPI(model.getName(), model))}
86
113
  }`,
87
114
 
88
115
  `type Mutation {
89
116
  _noop: String
90
- ${schema.getEntityModels().map(model => makeCreateAPI(model.getName(), model))}
91
- ${schema.getEntityModels().map(model => makeUpdateAPI(model.getName(), model))}
92
- ${schema.getEntityModels().map(model => makeDeleteAPI(model.getName(), model))}
117
+ ${entityModels.map(model => makeCreateAPI(model.getName(), model))}
118
+ ${entityModels.map(model => makeUpdateAPI(model.getName(), model))}
119
+ ${entityModels.map(model => makeDeleteAPI(model.getName(), model))}
120
+ }`,
121
+
122
+ `type Subscription {
123
+ _noop: String
124
+ ${entityModels.map(model => makeSubscriptionAPI(model.getName(), model))}
93
125
  }`,
94
126
  ]),
95
127
  resolvers: readModels.reduce((prev, model) => {
@@ -130,10 +162,10 @@ module.exports = (schema) => {
130
162
  });
131
163
  }, {
132
164
  Node: {
133
- __resolveType: (root, args, context, info) => root.__typename || fromGUID(root.$id)[0],
165
+ __resolveType: (root, args, context, info) => root.__typename || fromGUID(root.$id)[0], // eslint-disable-line no-underscore-dangle
134
166
  },
135
167
 
136
- Query: schema.getEntityModels().reduce((prev, model) => {
168
+ Query: entityModels.reduce((prev, model) => {
137
169
  return Object.assign(prev, makeQueryResolver(model.getName(), model, resolver));
138
170
  }, {
139
171
  node: (root, args, context, info) => {
@@ -144,7 +176,7 @@ module.exports = (schema) => {
144
176
  },
145
177
  }),
146
178
 
147
- Mutation: schema.getEntityModels().reduce((prev, model) => {
179
+ Mutation: entityModels.reduce((prev, model) => {
148
180
  return Object.assign(prev, makeMutationResolver(model.getName(), model, resolver));
149
181
  }, {}),
150
182
  }),
@@ -23,6 +23,8 @@ module.exports = (schema) => {
23
23
  }).concat(`
24
24
  interface Node { id: ID! }
25
25
  enum SortOrderEnum { asc desc }
26
+ enum SubscriptionCrudEnum { create update delete } # Not going to support "read"
27
+ enum SubscriptionWhenEnum { anytime preEvent postEvent }
26
28
  `),
27
29
  });
28
30
  };
@@ -278,7 +278,7 @@ module.exports = class Query {
278
278
 
279
279
  getCacheKey() {
280
280
  return {
281
- model: `${this.props.model}`,
281
+ // model: `${this.props.model}`,
282
282
  method: this.props.method,
283
283
  where: this.props.match,
284
284
  search: this.props.search,
@@ -343,6 +343,19 @@ exports.makeDeleteAPI = (name, model, parent) => {
343
343
  return gql;
344
344
  };
345
345
 
346
+ exports.makeSubscriptionAPI = (name, model, parent) => {
347
+ let gql = '';
348
+
349
+ if (model.hasGQLScope('s')) {
350
+ gql += `${name} (
351
+ on: [SubscriptionCrudEnum!]! = [create, update, delete]
352
+ filter: ${name}SubscriptionInputFilter
353
+ ): ${name}SubscriptionPayload!`;
354
+ }
355
+
356
+ return gql;
357
+ };
358
+
346
359
  // Resolvers
347
360
  exports.makeQueryResolver = (name, model, resolver, embeds = []) => {
348
361
  const obj = {};
@@ -19,13 +19,14 @@ exports.createSystemEvent = (name, mixed = {}, thunk = () => {}) => {
19
19
 
20
20
  if (name !== 'Setup') {
21
21
  const { method, query } = mixed;
22
- const { resolver, model, meta, doc, id, input, sort, merged, native, root } = query.toObject();
22
+ const { resolver, model, meta, doc, id, input, sort, merged, native, root, crud } = query.toObject();
23
23
 
24
24
  event = {
25
25
  context: resolver.getContext(),
26
26
  key: `${method}${model}`,
27
27
  resolver,
28
28
  method,
29
+ crud,
29
30
  model,
30
31
  meta,
31
32
  id,
@@ -57,11 +58,8 @@ exports.createSystemEvent = (name, mixed = {}, thunk = () => {}) => {
57
58
  if (result !== undefined) return result; // Allowing middleware to dictate result
58
59
  return middleware().then(thunk);
59
60
  }).then((result) => {
60
- event.doc = result; // You do actually need this...
61
- if (name === 'Mutation') return systemEvent.emit('system', { type: 'preSave', data: event }).then(finalResult => finalResult || result);
62
- return Promise.resolve(result);
63
- }).then((result) => {
64
- event.doc = result; // You do actually need this...
61
+ // event.doc = result; // You do actually need this...
62
+ event.result = result;
65
63
  return systemEvent.emit('system', { type: `post${type}`, data: event }).then(finalResult => finalResult || result);
66
64
  });
67
65
  };