@coderich/autograph 0.9.10 → 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.
- package/CHANGELOG.md +5 -0
- package/index.js +2 -0
- package/package.json +10 -7
- package/src/.DS_Store +0 -0
- package/src/core/Resolver.js +7 -1
- package/src/core/SchemaDecorator.js +46 -0
- package/src/core/ServerResolver.js +7 -93
- package/src/data/.DS_Store +0 -0
- package/src/data/DataLoader.js +28 -26
- package/src/data/Model.js +22 -10
- package/src/data/stream/DataHydrator.js +58 -0
- package/src/data/stream/ResultSet.js +34 -0
- package/src/data/stream/ResultSetItem.js +158 -0
- package/src/data/stream/ResultSetItemProxy.js +161 -0
- package/src/driver/MongoDriver.js +46 -18
- package/src/graphql/ast/.DS_Store +0 -0
- package/src/graphql/ast/Field.js +1 -2
- package/src/graphql/ast/Model.js +4 -0
- package/src/graphql/ast/Node.js +0 -7
- package/src/graphql/ast/Schema.js +2 -3
- package/src/graphql/ast/SchemaDecorator.js +141 -0
- package/src/graphql/ast/TypeDefApi.js +93 -0
- package/src/graphql/extension/api.js +1 -16
- package/src/graphql/extension/framework.js +0 -1
- package/src/query/Query.js +1 -19
- package/src/query/QueryBuilder.js +3 -2
- package/src/query/QueryResolver.js +6 -3
- package/src/service/app.service.js +6 -0
- package/src/service/decorator.service.js +21 -288
- package/src/service/event.service.js +1 -0
- package/src/service/graphql.service.js +1 -1
- package/src/data/ResultSet2.js +0 -210
- package/src/data/ResultSet3.js +0 -186
|
@@ -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,25 +3,29 @@ 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
|
|
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 =
|
|
14
|
+
options.ignoreUndefined = false;
|
|
16
15
|
return MongoClient.connect(uri, options);
|
|
17
16
|
}
|
|
18
17
|
|
|
18
|
+
disconnect() {
|
|
19
|
+
return this.connection.then(client => client.close());
|
|
20
|
+
}
|
|
21
|
+
|
|
19
22
|
raw(collection) {
|
|
20
23
|
return proxyPromise(this.connection.then(client => client.db().collection(collection)));
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
query(collection, method, ...args) {
|
|
24
|
-
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);
|
|
25
29
|
return this.raw(collection)[method](...args);
|
|
26
30
|
}
|
|
27
31
|
|
|
@@ -32,18 +36,21 @@ module.exports = class MongoDriver {
|
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
findOne(query) {
|
|
35
|
-
return this.findMany(Object.assign(query, { first: 1 })).then(
|
|
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
|
+
});
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
findMany(query) {
|
|
39
|
-
const { model, options = {},
|
|
49
|
+
const { model, options = {}, flags } = query;
|
|
40
50
|
Object.assign(options, this.config.query || {});
|
|
41
51
|
|
|
42
52
|
return this.query(model, 'aggregate', MongoDriver.aggregateQuery(query), options, flags).then((cursor) => {
|
|
43
|
-
return cursor.
|
|
44
|
-
if (last) return results.splice(-last);
|
|
45
|
-
return results;
|
|
46
|
-
});
|
|
53
|
+
return cursor.stream();
|
|
47
54
|
});
|
|
48
55
|
}
|
|
49
56
|
|
|
@@ -59,7 +66,7 @@ module.exports = class MongoDriver {
|
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
createOne({ model, input, options, flags }) {
|
|
62
|
-
return this.query(model, 'insertOne', input, options, flags).then(result => Object.assign(input, {
|
|
69
|
+
return this.query(model, 'insertOne', input, options, flags).then(result => Object.assign(input, { id: result.insertedId }));
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
updateOne({ model, where, $doc, options, flags }) {
|
|
@@ -72,7 +79,7 @@ module.exports = class MongoDriver {
|
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
deleteOne({ model, where, options, flags }) {
|
|
75
|
-
return this.query(model, 'deleteOne', where, options, flags);
|
|
82
|
+
return this.query(model, 'deleteOne', where, options, flags).then(() => true);
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
dropModel(model) {
|
|
@@ -144,20 +151,38 @@ module.exports = class MongoDriver {
|
|
|
144
151
|
}
|
|
145
152
|
|
|
146
153
|
static getAddFields(query) {
|
|
147
|
-
const {
|
|
154
|
+
const { shape, where } = query;
|
|
148
155
|
|
|
149
|
-
return
|
|
150
|
-
const value = where[
|
|
156
|
+
return shape.reduce((prev, { from, type }) => {
|
|
157
|
+
const value = where[from];
|
|
151
158
|
if (value === undefined) return prev;
|
|
152
159
|
if (!isScalarDataType(type)) return prev;
|
|
153
160
|
const stype = String((type === 'Float' || type === 'Int' ? 'Number' : type)).toLowerCase();
|
|
154
161
|
if (String(typeof value) === `${stype}`) return prev;
|
|
155
|
-
return Object.assign(prev, { [
|
|
162
|
+
return Object.assign(prev, { [from]: { $toString: `$${from}` } });
|
|
156
163
|
}, {});
|
|
157
164
|
}
|
|
158
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
|
+
|
|
159
184
|
static aggregateQuery(query, count = false) {
|
|
160
|
-
const { where: $match, sort, skip, limit, joins } = query;
|
|
185
|
+
const { where: $match, sort = {}, skip, limit, joins, shape, after, before, first } = query;
|
|
161
186
|
const $aggregate = [{ $match }];
|
|
162
187
|
|
|
163
188
|
// Used for $regex matching
|
|
@@ -186,10 +211,13 @@ module.exports = class MongoDriver {
|
|
|
186
211
|
if (limit) $aggregate.push({ $limit: limit });
|
|
187
212
|
|
|
188
213
|
// Pagination
|
|
189
|
-
const { after, before, first } = query;
|
|
190
214
|
if (after) $aggregate.push({ $match: { $or: Object.entries(after).reduce((prev, [key, value]) => prev.concat({ [key]: { [sort[key] === 1 ? '$gte' : '$lte']: value } }), []) } });
|
|
191
215
|
if (before) $aggregate.push({ $match: { $or: Object.entries(before).reduce((prev, [key, value]) => prev.concat({ [key]: { [sort[key] === 1 ? '$lte' : '$gte']: value } }), []) } });
|
|
192
216
|
if (first) $aggregate.push({ $limit: first });
|
|
217
|
+
|
|
218
|
+
// Projection
|
|
219
|
+
const $project = MongoDriver.getProjectFields(shape);
|
|
220
|
+
$aggregate.push({ $project });
|
|
193
221
|
}
|
|
194
222
|
|
|
195
223
|
return $aggregate;
|
|
Binary file
|
package/src/graphql/ast/Field.js
CHANGED
|
@@ -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 (
|
|
152
|
+
if (suffix === 'InputCreate' && this.isRequired() && !this.isDefaulted()) type += '!';
|
|
154
153
|
return type;
|
|
155
154
|
}
|
|
156
155
|
|
package/src/graphql/ast/Model.js
CHANGED
package/src/graphql/ast/Node.js
CHANGED
|
@@ -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
|
*/
|
|
@@ -1,14 +1,13 @@
|
|
|
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');
|
|
7
6
|
const Node = require('./Node');
|
|
8
7
|
const Model = require('./Model');
|
|
9
8
|
|
|
10
|
-
const loadFile = file => FS.readFileSync(
|
|
11
|
-
const reqFile = file => require(
|
|
9
|
+
const loadFile = file => FS.readFileSync(file, 'utf8');
|
|
10
|
+
const reqFile = file => require(file); // eslint-disable-line global-require,import/no-dynamic-require
|
|
12
11
|
|
|
13
12
|
module.exports = class Schema extends Node {
|
|
14
13
|
constructor(schema) {
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const FS = require('fs');
|
|
2
|
+
const Glob = require('glob');
|
|
3
|
+
const Merge = require('deepmerge');
|
|
4
|
+
const { Kind, print, parse, visit } = require('graphql');
|
|
5
|
+
const { mergeASTArray, makeExecutableSchema } = require('../../service/graphql.service');
|
|
6
|
+
const { deleteKeys } = require('../../service/app.service');
|
|
7
|
+
const frameworkExt = require('../extension/framework');
|
|
8
|
+
const typeExt = require('../extension/type');
|
|
9
|
+
const apiExt = require('../extension/api');
|
|
10
|
+
const TypeDefApi = require('./TypeDefApi');
|
|
11
|
+
const Node = require('./Node');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* SchemaDecorator
|
|
15
|
+
*
|
|
16
|
+
* This class helps facilitate dynamic modification of a schema before it is passed to makeExecutableSchema(). It allows
|
|
17
|
+
* for "intelligent" merging of schemas and exposes an API wrapper for typeDefs.
|
|
18
|
+
*
|
|
19
|
+
* A "schema" is defined by the following object attributes:
|
|
20
|
+
*
|
|
21
|
+
* context <Object> - Globally shared object by all resolvers
|
|
22
|
+
* typeDefs <String|Object> - GQL String or AST Object (also supports a mixed array of both)
|
|
23
|
+
* resolvers <Object> - GraphQL resolvers
|
|
24
|
+
* schemaDirectives <Object> - GraphQL directives
|
|
25
|
+
*
|
|
26
|
+
*/
|
|
27
|
+
module.exports = class SchemaDecorator extends TypeDefApi {
|
|
28
|
+
constructor(schema) {
|
|
29
|
+
super();
|
|
30
|
+
this.schema = { context: {}, typeDefs: [], resolvers: {}, schemaDirectives: {} };
|
|
31
|
+
if (schema) this.mergeSchema(schema);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Synchronously merge a schema
|
|
36
|
+
*/
|
|
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
|
+
|
|
41
|
+
// Here we want to normalize the schema into the shape { context, typeDefs, resolvers, schemaDirectives }
|
|
42
|
+
// We do NOT want to modify the schema object because that may cause unwanted side-effects.
|
|
43
|
+
const normalizedSchema = { ...schema };
|
|
44
|
+
if (typeof schema === 'string') normalizedSchema.typeDefs = [schema];
|
|
45
|
+
else if (schema.typeDefs && !Array.isArray(schema.typeDefs)) normalizedSchema.typeDefs = [schema.typeDefs];
|
|
46
|
+
|
|
47
|
+
// For typeDefs we want the AST so that it can be intelligently merged. Here we convert
|
|
48
|
+
// GQL strings to AST objects and also filter out anything that does not parse to AST.
|
|
49
|
+
if (normalizedSchema.typeDefs && normalizedSchema.typeDefs.length) {
|
|
50
|
+
normalizedSchema.typeDefs = deleteKeys(normalizedSchema.typeDefs.map((td) => {
|
|
51
|
+
try {
|
|
52
|
+
const ast = typeof td === 'object' ? td : parse(td);
|
|
53
|
+
return ast.definitions;
|
|
54
|
+
} catch (e) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}), ['loc']).filter(Boolean).flat();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Now we're ready to merge the schema
|
|
61
|
+
const [left, right] = options.passive ? [normalizedSchema, this.schema] : [this.schema, normalizedSchema];
|
|
62
|
+
if (normalizedSchema.typeDefs && normalizedSchema.typeDefs.length) this.schema.typeDefs = mergeASTArray(left.typeDefs.concat(right.typeDefs));
|
|
63
|
+
if (normalizedSchema.context) this.schema.context = Merge(left.context, right.context);
|
|
64
|
+
if (normalizedSchema.resolvers) this.schema.resolvers = Merge(left.resolvers, right.resolvers);
|
|
65
|
+
if (normalizedSchema.schemaDirectives) this.schema.schemaDirectives = Merge(left.schemaDirectives, right.schemaDirectives);
|
|
66
|
+
|
|
67
|
+
// Chaining
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Asynchronously load files from a given glob pattern and merge each schema
|
|
73
|
+
*/
|
|
74
|
+
mergeSchemaFromFiles(globPattern, options) {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
Glob(globPattern, options, (err, files) => {
|
|
77
|
+
if (err) return reject(err);
|
|
78
|
+
|
|
79
|
+
return Promise.all(files.map((file) => {
|
|
80
|
+
return new Promise((res) => {
|
|
81
|
+
if (file.endsWith('.js')) res(require(file)); // eslint-disable-line global-require,import/no-dynamic-require
|
|
82
|
+
else res(FS.readFileSync(file, 'utf8'));
|
|
83
|
+
}).then(schema => this.mergeSchema(schema, options));
|
|
84
|
+
})).then(() => resolve(this)).catch(e => reject(e));
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Traverses the current schema's typeDefs in order to keep the TypeDefApi in sync. This operation
|
|
91
|
+
* only needs to be called when typeDefs have been changed and you want to keep the data model in sync.
|
|
92
|
+
*/
|
|
93
|
+
initialize() {
|
|
94
|
+
return super.initialize(this.schema.typeDefs);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Decorate the schema with Autograph's default api/definitions
|
|
99
|
+
*/
|
|
100
|
+
decorate() {
|
|
101
|
+
this.initialize();
|
|
102
|
+
this.mergeSchema(frameworkExt(this), { passive: true });
|
|
103
|
+
this.mergeSchema(typeExt(this), { passive: true });
|
|
104
|
+
this.initialize();
|
|
105
|
+
this.mergeSchema(apiExt(this), { passive: true });
|
|
106
|
+
this.finalize();
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* This should be called once before passing to makeExecutableSchema()
|
|
112
|
+
*/
|
|
113
|
+
finalize() {
|
|
114
|
+
const definitions = visit(this.schema.typeDefs, {
|
|
115
|
+
[Kind.FIELD_DEFINITION]: (node) => {
|
|
116
|
+
const scope = new Node(node, 'field').getDirectiveArg('field', 'gqlScope', 'crud');
|
|
117
|
+
if (scope === null || scope.indexOf('r') === -1) return null; // Delete node
|
|
118
|
+
return false; // Stop traversing this node
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
this.schema.typeDefs = { kind: Kind.DOCUMENT, definitions };
|
|
123
|
+
return this;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
makeExecutableSchema() {
|
|
127
|
+
return makeExecutableSchema(this.schema);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getContext() {
|
|
131
|
+
return this.schema.context;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
toObject() {
|
|
135
|
+
return this.schema;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
toString() {
|
|
139
|
+
return print(this.typeDefs);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const { Kind, visit } = require('graphql');
|
|
2
|
+
const Model = require('./Model');
|
|
3
|
+
const Node = require('./Node');
|
|
4
|
+
|
|
5
|
+
const operations = ['Query', 'Mutation', 'Subscription'];
|
|
6
|
+
const modelKinds = [Kind.OBJECT_TYPE_DEFINITION, Kind.OBJECT_TYPE_EXTENSION, Kind.INTERFACE_TYPE_DEFINITION, Kind.INTERFACE_TYPE_EXTENSION];
|
|
7
|
+
const inputKinds = [Kind.INPUT_OBJECT_TYPE_DEFINITION, Kind.INPUT_OBJECT_TYPE_EXTENSION];
|
|
8
|
+
const scalarKinds = [Kind.SCALAR_TYPE_DEFINITION, Kind.SCALAR_TYPE_EXTENSION];
|
|
9
|
+
const enumKinds = [Kind.ENUM_TYPE_DEFINITION, Kind.ENUM_TYPE_EXTENSION];
|
|
10
|
+
|
|
11
|
+
module.exports = class TypeDefApi {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.models = [];
|
|
14
|
+
this.scalars = [];
|
|
15
|
+
this.inputs = [];
|
|
16
|
+
this.enums = [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
initialize(typeDefs) {
|
|
20
|
+
this.models.length = 0;
|
|
21
|
+
this.scalars.length = 0;
|
|
22
|
+
this.inputs.length = 0;
|
|
23
|
+
this.enums.length = 0;
|
|
24
|
+
|
|
25
|
+
visit(typeDefs, {
|
|
26
|
+
enter: (node) => {
|
|
27
|
+
if (modelKinds.indexOf(node.kind) > -1 && operations.indexOf(node.name.value) === -1) {
|
|
28
|
+
this.models.push(new Model(this, node));
|
|
29
|
+
} else if (scalarKinds.indexOf(node.kind) > -1) {
|
|
30
|
+
this.scalars.push(new Node(node));
|
|
31
|
+
} else if (inputKinds.indexOf(node.kind) > -1) {
|
|
32
|
+
this.inputs.push(new Node(node));
|
|
33
|
+
} else if (enumKinds.indexOf(node.kind) > -1) {
|
|
34
|
+
this.enums.push(new Node(node));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return false; // Stop traversing this node
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Models
|
|
45
|
+
getModel(name) {
|
|
46
|
+
return this.models.find(m => m.getName() === name);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getModels() {
|
|
50
|
+
return this.models;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getModelNames() {
|
|
54
|
+
return this.getModels().map(model => model.getName());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getModelMap() {
|
|
58
|
+
return this.getModels().reduce((prev, model) => Object.assign(prev, { [model.getName()]: model }), {});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getMarkedModels() {
|
|
62
|
+
return Object.values(this.models).filter(model => model.isMarkedModel());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getEntityModels() {
|
|
66
|
+
return Object.values(this.models).filter(model => model.isEntity());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Definitions
|
|
70
|
+
getInput(name) {
|
|
71
|
+
return this.getInputs().find(input => input.getName() === name);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getInputs() {
|
|
75
|
+
return this.inputs;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getScalar(name) {
|
|
79
|
+
return this.getScalars().find(scalar => scalar.getName() === name);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getScalars() {
|
|
83
|
+
return this.scalars;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getEnum(name) {
|
|
87
|
+
return this.getEnums().find(el => el.getName() === name);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getEnums() {
|
|
91
|
+
return this.enums;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
@@ -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,
|
|
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
|