@coderich/autograph 0.11.1 → 0.13.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/index.js +4 -6
- package/package.json +30 -44
- package/src/data/DataLoader.js +77 -70
- package/src/data/Emitter.js +89 -0
- package/src/data/Loader.js +33 -0
- package/src/data/Pipeline.js +88 -96
- package/src/data/Resolver.js +304 -0
- package/src/data/Transaction.js +49 -0
- package/src/query/Query.js +159 -334
- package/src/query/QueryBuilder.js +228 -114
- package/src/query/QueryResolver.js +110 -216
- package/src/query/QueryResolverTransaction.js +16 -0
- package/src/schema/Schema.js +593 -0
- package/src/service/AppService.js +38 -0
- package/src/service/ErrorService.js +7 -0
- package/CHANGELOG.md +0 -41
- package/LICENSE +0 -21
- package/README.md +0 -76
- package/src/.DS_Store +0 -0
- package/src/core/.DS_Store +0 -0
- package/src/core/Boom.js +0 -9
- package/src/core/EventEmitter.js +0 -95
- package/src/core/Resolver.js +0 -124
- package/src/core/Schema.js +0 -55
- package/src/core/ServerResolver.js +0 -15
- package/src/data/.DS_Store +0 -0
- package/src/data/DataService.js +0 -120
- package/src/data/DataTransaction.js +0 -96
- package/src/data/Field.js +0 -83
- package/src/data/Model.js +0 -223
- package/src/data/TreeMap.js +0 -78
- package/src/data/Type.js +0 -50
- package/src/driver/.DS_Store +0 -0
- package/src/driver/MongoDriver.js +0 -227
- package/src/driver/index.js +0 -11
- package/src/graphql/.DS_Store +0 -0
- package/src/graphql/ast/.DS_Store +0 -0
- package/src/graphql/ast/Field.js +0 -206
- package/src/graphql/ast/Model.js +0 -145
- package/src/graphql/ast/Node.js +0 -291
- package/src/graphql/ast/Schema.js +0 -133
- package/src/graphql/ast/Type.js +0 -26
- package/src/graphql/ast/TypeDefApi.js +0 -93
- package/src/graphql/extension/.DS_Store +0 -0
- package/src/graphql/extension/api.js +0 -193
- package/src/graphql/extension/framework.js +0 -71
- package/src/graphql/extension/type.js +0 -34
- package/src/query/.DS_Store +0 -0
- package/src/query/QueryBuilderTransaction.js +0 -26
- package/src/query/QueryService.js +0 -111
- package/src/service/.DS_Store +0 -0
- package/src/service/app.service.js +0 -319
- package/src/service/decorator.service.js +0 -114
- package/src/service/event.service.js +0 -66
- package/src/service/graphql.service.js +0 -92
- package/src/service/schema.service.js +0 -95
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
/* eslint-disable indent */
|
|
2
|
+
|
|
3
|
+
const Util = require('@coderich/util');
|
|
4
|
+
const { Kind, parse, visit } = require('graphql');
|
|
5
|
+
const { mergeTypeDefs, mergeFields } = require('@graphql-tools/merge');
|
|
6
|
+
const { isLeafValue, isPlainObject, isBasicObject, mergeDeep, fromGUID } = require('../service/AppService');
|
|
7
|
+
const Pipeline = require('../data/Pipeline');
|
|
8
|
+
const Emitter = require('../data/Emitter');
|
|
9
|
+
|
|
10
|
+
const operations = ['Query', 'Mutation', 'Subscription'];
|
|
11
|
+
// const interfaceKinds = [Kind.INTERFACE_TYPE_DEFINITION, Kind.INTERFACE_TYPE_EXTENSION];
|
|
12
|
+
const modelKinds = [Kind.OBJECT_TYPE_DEFINITION, Kind.OBJECT_TYPE_EXTENSION, Kind.INTERFACE_TYPE_DEFINITION, Kind.INTERFACE_TYPE_EXTENSION];
|
|
13
|
+
const allowedKinds = modelKinds.concat(Kind.DOCUMENT, Kind.FIELD_DEFINITION, Kind.NON_NULL_TYPE, Kind.NAMED_TYPE, Kind.LIST_TYPE, Kind.DIRECTIVE);
|
|
14
|
+
const pipelines = ['finalize', 'construct', 'restruct', 'instruct', 'normalize', 'serialize'];
|
|
15
|
+
const inputPipelines = ['finalize', 'construct', 'instruct', 'normalize', 'serialize'];
|
|
16
|
+
const scalars = ['ID', 'String', 'Float', 'Int', 'Boolean'];
|
|
17
|
+
|
|
18
|
+
module.exports = class Schema {
|
|
19
|
+
#config;
|
|
20
|
+
#schema;
|
|
21
|
+
#typeDefs;
|
|
22
|
+
#resolvers = {};
|
|
23
|
+
|
|
24
|
+
constructor(config) {
|
|
25
|
+
this.#config = config;
|
|
26
|
+
this.#typeDefs = Schema.#framework();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Decorate each marked @model with config-driven field decorators
|
|
31
|
+
*/
|
|
32
|
+
decorate() {
|
|
33
|
+
this.#typeDefs = visit(this.#typeDefs, {
|
|
34
|
+
enter: (node) => {
|
|
35
|
+
if (modelKinds.includes(node.kind) && !operations.includes(node.name.value)) {
|
|
36
|
+
const directive = node.directives.find(({ name }) => name.value === 'model');
|
|
37
|
+
|
|
38
|
+
if (directive) {
|
|
39
|
+
const arg = directive.arguments.find(({ name }) => name.value === 'decorate');
|
|
40
|
+
const value = arg?.value.value || 'default';
|
|
41
|
+
const decorator = this.#config.decorators?.[value];
|
|
42
|
+
|
|
43
|
+
if (decorator) {
|
|
44
|
+
const { fields } = parse(`type decorator { ${decorator} }`).definitions[0];
|
|
45
|
+
node.fields = mergeFields(node, node.fields, fields, { noLocation: true, onFieldTypeConflict: a => a });
|
|
46
|
+
return node;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return undefined;
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Merge typeDefs and resolvers
|
|
62
|
+
*/
|
|
63
|
+
merge(schema = {}) {
|
|
64
|
+
if (typeof schema === 'string') schema = { typeDefs: schema };
|
|
65
|
+
else if (schema instanceof Schema) schema = schema.toObject();
|
|
66
|
+
const { typeDefs, resolvers } = schema;
|
|
67
|
+
if (typeDefs) this.#typeDefs = mergeTypeDefs([parse(typeDefs), this.#typeDefs], { noLocation: true, reverseDirectives: true, onFieldTypeConflict: a => a });
|
|
68
|
+
if (resolvers) this.#resolvers = mergeDeep(this.#resolvers, resolvers);
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parse typeDefs; returning a schema POJO
|
|
74
|
+
*/
|
|
75
|
+
parse() {
|
|
76
|
+
if (this.#schema) return this.#schema;
|
|
77
|
+
|
|
78
|
+
this.#schema = { models: {}, indexes: [] };
|
|
79
|
+
let model, field, isField, isList;
|
|
80
|
+
const thunks = [];
|
|
81
|
+
|
|
82
|
+
// Parse AST
|
|
83
|
+
visit(this.#typeDefs, {
|
|
84
|
+
enter: (node) => {
|
|
85
|
+
const name = node.name?.value;
|
|
86
|
+
if (!allowedKinds.includes(node.kind)) return false;
|
|
87
|
+
|
|
88
|
+
if (modelKinds.includes(node.kind) && !operations.includes(name)) {
|
|
89
|
+
model = this.#schema.models[name] = {
|
|
90
|
+
name,
|
|
91
|
+
key: name,
|
|
92
|
+
fields: {},
|
|
93
|
+
idField: 'id',
|
|
94
|
+
crud: 'crud',
|
|
95
|
+
scope: 'crud',
|
|
96
|
+
isPersistable: true,
|
|
97
|
+
source: this.#config.dataSources?.default,
|
|
98
|
+
loader: this.#config.dataLoaders?.default,
|
|
99
|
+
directives: {},
|
|
100
|
+
toString: () => name,
|
|
101
|
+
};
|
|
102
|
+
} else if (node.kind === Kind.FIELD_DEFINITION) {
|
|
103
|
+
isField = true;
|
|
104
|
+
field = model.fields[name] = {
|
|
105
|
+
name,
|
|
106
|
+
key: name,
|
|
107
|
+
crud: 'crud',
|
|
108
|
+
pipelines: pipelines.reduce((prev, key) => Object.assign(prev, { [key]: [] }), {}),
|
|
109
|
+
directives: {},
|
|
110
|
+
toString: () => name,
|
|
111
|
+
};
|
|
112
|
+
} else if (node.kind === Kind.NON_NULL_TYPE) {
|
|
113
|
+
field[isList ? 'isArrayRequired' : 'isRequired'] = true;
|
|
114
|
+
} else if (node.kind === Kind.NAMED_TYPE) {
|
|
115
|
+
field.type = node.name.value;
|
|
116
|
+
} else if (node.kind === Kind.LIST_TYPE) {
|
|
117
|
+
field.isArray = true;
|
|
118
|
+
isList = true;
|
|
119
|
+
} else if (node.kind === Kind.DIRECTIVE) {
|
|
120
|
+
const target = isField ? field : model;
|
|
121
|
+
target.directives[name] = target.directives[name] || {};
|
|
122
|
+
|
|
123
|
+
if (name === 'model') model.isMarkedModel = true;
|
|
124
|
+
else if (name === 'index') this.#schema.indexes.push({ model });
|
|
125
|
+
|
|
126
|
+
node.arguments.forEach((arg) => {
|
|
127
|
+
const key = arg.name.value;
|
|
128
|
+
const { value: val, values } = arg.value;
|
|
129
|
+
const value = values ? values.map(n => n.value) : val;
|
|
130
|
+
target.directives[name][key] = value;
|
|
131
|
+
|
|
132
|
+
if (name === 'index') this.#schema.indexes[this.#schema.indexes.length - 1][key] = value;
|
|
133
|
+
|
|
134
|
+
switch (`${name}-${key}`) {
|
|
135
|
+
// Model specific directives
|
|
136
|
+
case 'model-id': {
|
|
137
|
+
model.idField = value;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case 'model-source': {
|
|
141
|
+
model.source = this.#config.dataSources?.[value];
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case 'model-loader': {
|
|
145
|
+
model.loader = this.#config.dataLoaders?.[value];
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case 'model-embed': {
|
|
149
|
+
model.isEmbedded = value;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
// Field specific directives
|
|
153
|
+
case 'field-default': {
|
|
154
|
+
field.defaultValue = value;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
case 'field-connection': {
|
|
158
|
+
field.isConnection = value;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
case 'link-by': {
|
|
162
|
+
field.linkBy = value;
|
|
163
|
+
field.isVirtual = true;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
// Generic by target directives
|
|
167
|
+
case 'model-persist': case 'field-persist': {
|
|
168
|
+
target.isPersistable = value;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
case 'model-crud': case 'model-scope': case 'field-crud': {
|
|
172
|
+
target[key] = Util.nvl(value, '');
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
case 'model-key': case 'model-meta': case 'field-key': case 'field-onDelete': {
|
|
176
|
+
target[key] = value;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
default: {
|
|
180
|
+
if (pipelines.includes(key)) {
|
|
181
|
+
target.pipelines[key] = target.pipelines[key].concat(value).filter(Boolean);
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return undefined; // Continue
|
|
190
|
+
},
|
|
191
|
+
leave: (node) => {
|
|
192
|
+
if (modelKinds.includes(node.kind) && !operations.includes(node.name.value)) {
|
|
193
|
+
const $model = model;
|
|
194
|
+
// const idField = $model.fields[$model.idField];
|
|
195
|
+
// $model.primaryKey = Util.nvl(idField?.key, idField?.name, 'id');
|
|
196
|
+
|
|
197
|
+
// Model resolution after field resolution (push)
|
|
198
|
+
thunks.push(($schema) => {
|
|
199
|
+
$model.isEntity = Boolean($model.isMarkedModel && !$model.isEmbedded);
|
|
200
|
+
|
|
201
|
+
$model.resolvePath = (path, prop = 'name') => this.#schema.resolvePath(`${$model[prop]}.${path}`, prop);
|
|
202
|
+
|
|
203
|
+
$model.isJoinPath = (path, prop = 'name') => {
|
|
204
|
+
let foundJoin = false;
|
|
205
|
+
return !path.split('.').every((el, i, arr) => {
|
|
206
|
+
if (foundJoin) return false;
|
|
207
|
+
const $field = $model.resolvePath(arr.slice(0, i + 1).join('.'), prop);
|
|
208
|
+
foundJoin = $field.isVirtual || $field.isFKReference;
|
|
209
|
+
return !$field.isVirtual;
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
$model.walk = (data, fn, opts = {}) => {
|
|
214
|
+
if (data == null || !isPlainObject(data)) return data;
|
|
215
|
+
|
|
216
|
+
// Options
|
|
217
|
+
opts.key = opts.key ?? 'name';
|
|
218
|
+
opts.run = opts.run ?? [];
|
|
219
|
+
opts.path = opts.path ?? [];
|
|
220
|
+
opts.itemize = opts.itemize ?? true;
|
|
221
|
+
|
|
222
|
+
return Object.entries(data).reduce((prev, [key, value]) => {
|
|
223
|
+
// Find the field; remove it if not found
|
|
224
|
+
const $field = Object.values($model.fields).find(el => el[opts.key] === key);
|
|
225
|
+
if (!$field) return prev;
|
|
226
|
+
|
|
227
|
+
// Invoke callback function; allowing result to be modified in order to change key/value
|
|
228
|
+
let run = opts.run.concat($field[opts.key]);
|
|
229
|
+
const path = opts.path.concat($field[opts.key]);
|
|
230
|
+
const isLeaf = isLeafValue(value);
|
|
231
|
+
const $node = fn({ model: $model, field: $field, key, value, path, run, isLeaf });
|
|
232
|
+
if (!$node) return prev;
|
|
233
|
+
|
|
234
|
+
// Recursive walk
|
|
235
|
+
if (!$field.model?.isEmbedded) run = [];
|
|
236
|
+
const $value = opts.itemize && $field.model && isBasicObject($node.value) ? Util.map($node.value, el => $field.model.walk(el, fn, { ...opts, path, run })) : $node.value;
|
|
237
|
+
return Object.assign(prev, { [$node.key]: $value });
|
|
238
|
+
}, {});
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// Pre-processing
|
|
242
|
+
$model.pipelineFields = {
|
|
243
|
+
input: Object.values($model.fields).filter(f => f.defaultValue !== undefined || inputPipelines.some(k => f.pipelines[k].length)).reduce((prev, f) => Object.assign(prev, { [f.name]: undefined }), {}),
|
|
244
|
+
where: Object.values($model.fields).filter(f => f.pipelines.instruct.length).reduce((prev, f) => Object.assign(prev, { [f.name]: undefined }), {}),
|
|
245
|
+
};
|
|
246
|
+
});
|
|
247
|
+
} else if (node.kind === Kind.FIELD_DEFINITION) {
|
|
248
|
+
const $field = field;
|
|
249
|
+
const $model = model;
|
|
250
|
+
|
|
251
|
+
$field.isPrimaryKey = Boolean($field.name === model.idField);
|
|
252
|
+
$field.isPersistable = Util.uvl($field.isPersistable, model.isPersistable, true);
|
|
253
|
+
|
|
254
|
+
// Field resolution comes first (unshift)
|
|
255
|
+
thunks.unshift(($schema) => {
|
|
256
|
+
$field.model = $schema.models[$field.type];
|
|
257
|
+
$field.linkBy = $field.linkBy || $field.model?.idField;
|
|
258
|
+
$field.linkField = $field.isVirtual ? $model.fields[$model.idField] : $field;
|
|
259
|
+
$field.isFKReference = !$field.isPrimaryKey && $field.model?.isMarkedModel && !$field.model?.isEmbedded;
|
|
260
|
+
$field.isEmbedded = Boolean($field.model && !$field.isFKReference && !$field.isPrimaryKey);
|
|
261
|
+
$field.isScalar = Boolean(!$field.model || scalars.includes($field.type));
|
|
262
|
+
|
|
263
|
+
if ($field.isArray) $field.pipelines.normalize.unshift('toArray');
|
|
264
|
+
if ($field.isPrimaryKey) $field.pipelines.serialize.unshift('$pk'); // Will create/convert to FK type always
|
|
265
|
+
if ($field.isFKReference) $field.pipelines.serialize.unshift('$fk'); // Will convert to FK type IFF defined in payload
|
|
266
|
+
|
|
267
|
+
if ($field.isRequired && $field.isPersistable && !$field.isVirtual) $field.pipelines.finalize.push('required');
|
|
268
|
+
if ($field.isFKReference) {
|
|
269
|
+
const to = $field.model.key;
|
|
270
|
+
const on = $field.model.fields[$field.linkBy].key;
|
|
271
|
+
const from = $field.linkField.key;
|
|
272
|
+
const as = `join_${to}`;
|
|
273
|
+
$field.join = { to, on, from, as };
|
|
274
|
+
$field.pipelines.finalize.push('ensureId'); // Absolute Last
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
isField = false;
|
|
279
|
+
} else if (node.kind === Kind.LIST_TYPE) {
|
|
280
|
+
isList = false;
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Resolve data thunks
|
|
286
|
+
thunks.forEach(thunk => thunk(this.#schema));
|
|
287
|
+
|
|
288
|
+
// Resolve indexes
|
|
289
|
+
this.#schema.indexes = this.#schema.indexes.map((index) => {
|
|
290
|
+
const { key } = index.model;
|
|
291
|
+
const { name, type } = index;
|
|
292
|
+
const on = index.on.map(f => index.model.fields[f].key);
|
|
293
|
+
return { key, name, type, on };
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Resolve referential integrity
|
|
297
|
+
Object.values(this.#schema.models).forEach(($model) => {
|
|
298
|
+
$model.referentialIntegrity = Schema.#identifyOnDeletes(Object.values(this.#schema.models), $model.name);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Helper methods
|
|
302
|
+
this.#schema.resolvePath = (path, prop = 'key') => {
|
|
303
|
+
const [modelKey, ...fieldKeys] = path.split('.');
|
|
304
|
+
const $model = Object.values(this.#schema.models).find(el => el[prop] === modelKey);
|
|
305
|
+
if (!$model || !fieldKeys.length) return $model;
|
|
306
|
+
return fieldKeys.reduce((parent, key) => Object.values(parent.fields || parent.model.fields).find(el => el[prop] === key) || parent, $model);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// Return schema
|
|
310
|
+
return this.#schema;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
api() {
|
|
314
|
+
return this.merge(Schema.#api(this.parse()));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
setup() {
|
|
318
|
+
return Emitter.emit('setup', this.#schema);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
toObject() {
|
|
322
|
+
return {
|
|
323
|
+
typeDefs: this.#typeDefs,
|
|
324
|
+
resolvers: this.#resolvers,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
makeExecutableSchema() {
|
|
329
|
+
return this.#config.makeExecutableSchema(this.toObject());
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
static #framework() {
|
|
333
|
+
return parse(`
|
|
334
|
+
scalar AutoGraphMixed
|
|
335
|
+
|
|
336
|
+
enum AutoGraphIndexEnum { unique }
|
|
337
|
+
enum AutoGraphOnDeleteEnum { cascade nullify restrict defer }
|
|
338
|
+
enum AutoGraphPipelineEnum { ${Object.keys(Pipeline).filter(k => !k.startsWith('$')).join(' ')} }
|
|
339
|
+
|
|
340
|
+
directive @model(
|
|
341
|
+
id: String # Specify the ID/PK field (default "id")
|
|
342
|
+
key: String # Specify db table/collection name
|
|
343
|
+
crud: AutoGraphMixed # CRUD API
|
|
344
|
+
scope: AutoGraphMixed #
|
|
345
|
+
meta: AutoGraphMixed # Custom input "meta" field for mutations
|
|
346
|
+
source: AutoGraphMixed # Data source (default: "default")
|
|
347
|
+
embed: Boolean # Mark this an embedded model (default false)
|
|
348
|
+
persist: Boolean # Persist this model (default true)
|
|
349
|
+
) on OBJECT | INTERFACE
|
|
350
|
+
|
|
351
|
+
directive @field(
|
|
352
|
+
key: String # Specify db key
|
|
353
|
+
persist: Boolean # Persist this field (default true)
|
|
354
|
+
connection: Boolean # Treat this field as a connection type (default false - rolling this out slowly)
|
|
355
|
+
default: AutoGraphMixed # Define a default value
|
|
356
|
+
crud: AutoGraphMixed # CRUD API
|
|
357
|
+
onDelete: AutoGraphOnDeleteEnum # onDelete behavior
|
|
358
|
+
|
|
359
|
+
# Pipeline Structure
|
|
360
|
+
normalize: [AutoGraphPipelineEnum!]
|
|
361
|
+
instruct: [AutoGraphPipelineEnum!]
|
|
362
|
+
construct: [AutoGraphPipelineEnum!]
|
|
363
|
+
restruct: [AutoGraphPipelineEnum!]
|
|
364
|
+
serialize: [AutoGraphPipelineEnum!]
|
|
365
|
+
finalize: [AutoGraphPipelineEnum!]
|
|
366
|
+
) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION | SCALAR
|
|
367
|
+
|
|
368
|
+
directive @link(
|
|
369
|
+
to: AutoGraphMixed # The MODEL to link to (default's to modelRef)
|
|
370
|
+
by: AutoGraphMixed! # The FIELD to match yourself by
|
|
371
|
+
use: AutoGraphMixed # The VALUE to use (default's to @link'd value); useful for many-to-many relationships
|
|
372
|
+
) on FIELD_DEFINITION
|
|
373
|
+
|
|
374
|
+
directive @index(
|
|
375
|
+
name: String
|
|
376
|
+
on: [AutoGraphMixed!]!
|
|
377
|
+
type: AutoGraphIndexEnum!
|
|
378
|
+
) repeatable on OBJECT
|
|
379
|
+
`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
static #api(schema) {
|
|
383
|
+
// These models are for creating types
|
|
384
|
+
const readModels = Object.values(schema.models).filter(model => model.crud.includes('r'));
|
|
385
|
+
const createModels = Object.values(schema.models).filter(model => model.crud.includes('c'));
|
|
386
|
+
const updateModels = Object.values(schema.models).filter(model => model.crud.includes('u'));
|
|
387
|
+
|
|
388
|
+
// These are for defining schema queries/mutations
|
|
389
|
+
const entityModels = Object.values(schema.models).filter(model => model.isEntity);
|
|
390
|
+
const queryModels = entityModels.filter(model => model.crud.includes('r'));
|
|
391
|
+
const mutationModels = entityModels.filter(model => ['c', 'u', 'd'].some(el => model.crud.includes(el)));
|
|
392
|
+
const subscriptionModels = entityModels.filter(model => model.crud.includes('s'));
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
typeDefs: `
|
|
396
|
+
scalar AutoGraphMixed
|
|
397
|
+
|
|
398
|
+
interface Node { id: ID! }
|
|
399
|
+
|
|
400
|
+
enum SortOrderEnum { asc desc }
|
|
401
|
+
enum SubscriptionCrudEnum { create update delete }
|
|
402
|
+
enum SubscriptionWhenEnum { preEvent postEvent }
|
|
403
|
+
|
|
404
|
+
type PageInfo {
|
|
405
|
+
startCursor: String!
|
|
406
|
+
endCursor: String!
|
|
407
|
+
hasPreviousPage: Boolean!
|
|
408
|
+
hasNextPage: Boolean!
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
${entityModels.map(model => `
|
|
412
|
+
extend type ${model} implements Node {
|
|
413
|
+
id: ID!
|
|
414
|
+
}
|
|
415
|
+
`)}
|
|
416
|
+
|
|
417
|
+
${readModels.map((model) => {
|
|
418
|
+
const fields = Object.values(model.fields).filter(field => field.crud.includes('r'));
|
|
419
|
+
const connectionFields = fields.filter(field => field.isConnection);
|
|
420
|
+
|
|
421
|
+
return `
|
|
422
|
+
input ${model}InputWhere {
|
|
423
|
+
${fields.map(field => `${field}: ${field.model?.isEntity ? `${field.model}InputWhere` : 'AutoGraphMixed'}`)}
|
|
424
|
+
}
|
|
425
|
+
input ${model}InputSort {
|
|
426
|
+
${fields.map(field => `${field}: ${field.model?.isEntity ? `${field.model}InputSort` : 'SortOrderEnum'}`)}
|
|
427
|
+
}
|
|
428
|
+
type ${model}Connection {
|
|
429
|
+
count: Int!
|
|
430
|
+
pageInfo: PageInfo
|
|
431
|
+
edges: [${model}Edge]
|
|
432
|
+
}
|
|
433
|
+
type ${model}Edge {
|
|
434
|
+
node: ${model}
|
|
435
|
+
cursor: String
|
|
436
|
+
}
|
|
437
|
+
${connectionFields.length ? `
|
|
438
|
+
extend type ${model} {
|
|
439
|
+
${connectionFields.map(field => `${field}: ${field.model}Connection`)}
|
|
440
|
+
}
|
|
441
|
+
` : ''}
|
|
442
|
+
`;
|
|
443
|
+
})}
|
|
444
|
+
|
|
445
|
+
${createModels.map((model) => {
|
|
446
|
+
const fields = Object.values(model.fields).filter(field => field.crud.includes('c') && !field.isVirtual);
|
|
447
|
+
|
|
448
|
+
return `
|
|
449
|
+
input ${model}InputCreate {
|
|
450
|
+
${fields.map(field => `${field}: ${Schema.#getGQLType(field, 'InputCreate')}`)}
|
|
451
|
+
}
|
|
452
|
+
`;
|
|
453
|
+
})}
|
|
454
|
+
|
|
455
|
+
${updateModels.map((model) => {
|
|
456
|
+
const fields = Object.values(model.fields).filter(field => field.crud.includes('u') && !field.isVirtual);
|
|
457
|
+
|
|
458
|
+
return `
|
|
459
|
+
input ${model}InputUpdate {
|
|
460
|
+
${fields.map(field => `${field}: ${Schema.#getGQLType(field, 'InputUpdate')}`)}
|
|
461
|
+
}
|
|
462
|
+
`;
|
|
463
|
+
})}
|
|
464
|
+
|
|
465
|
+
type Query {
|
|
466
|
+
node(id: ID!): Node
|
|
467
|
+
${queryModels.map(model => `
|
|
468
|
+
get${model}(id: ID!): ${model}
|
|
469
|
+
find${model}(
|
|
470
|
+
where: ${model}InputWhere
|
|
471
|
+
sortBy: ${model}InputSort
|
|
472
|
+
limit: Int
|
|
473
|
+
skip: Int
|
|
474
|
+
first: Int
|
|
475
|
+
after: String
|
|
476
|
+
last: Int
|
|
477
|
+
before: String
|
|
478
|
+
): ${model}Connection!
|
|
479
|
+
`)}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
${mutationModels.length ? `
|
|
483
|
+
type Mutation {
|
|
484
|
+
${mutationModels.map((model) => {
|
|
485
|
+
const api = [];
|
|
486
|
+
const meta = model.meta ? `meta: ${model.meta}` : '';
|
|
487
|
+
if (model.crud.includes('c')) api.push(`create${model}(input: ${model}InputCreate! ${meta}): ${model}!`);
|
|
488
|
+
if (model.crud.includes('u')) api.push(`update${model}(id: ID! input: ${model}InputUpdate ${meta}): ${model}!`);
|
|
489
|
+
if (model.crud.includes('d')) api.push(`delete${model}(id: ID! ${meta}): ${model}!`);
|
|
490
|
+
return api.join('\n');
|
|
491
|
+
})}
|
|
492
|
+
}
|
|
493
|
+
` : ''}
|
|
494
|
+
|
|
495
|
+
${subscriptionModels.length ? `
|
|
496
|
+
type Subscription {
|
|
497
|
+
${subscriptionModels.map(model => `
|
|
498
|
+
${model}(
|
|
499
|
+
on: [SubscriptionCrudEnum!]! = [create, update, delete]
|
|
500
|
+
filter: ${model}SubscriptionInputFilter
|
|
501
|
+
): ${model}SubscriptionPayload!
|
|
502
|
+
`)}
|
|
503
|
+
}
|
|
504
|
+
` : ''}
|
|
505
|
+
`,
|
|
506
|
+
resolvers: {
|
|
507
|
+
Node: {
|
|
508
|
+
__resolveType: (doc, args, context, info) => doc.__typename, // eslint-disable-line no-underscore-dangle
|
|
509
|
+
},
|
|
510
|
+
...queryModels.reduce((prev, model) => {
|
|
511
|
+
return Object.assign(prev, {
|
|
512
|
+
[`${model}Connection`]: {
|
|
513
|
+
count: ({ count }) => count(),
|
|
514
|
+
edges: ({ edges }) => edges().then(rs => rs.map(node => ({ cursor: node.$cursor, node }))),
|
|
515
|
+
pageInfo: ({ pageInfo }) => pageInfo().then(rs => rs?.$pageInfo),
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
}, {}),
|
|
519
|
+
Query: queryModels.reduce((prev, model) => {
|
|
520
|
+
return Object.assign(prev, {
|
|
521
|
+
[`get${model}`]: (doc, args, context, info) => context.autograph.resolver.match(model).args(args).one({ required: true }),
|
|
522
|
+
[`find${model}`]: (doc, args, context, info) => {
|
|
523
|
+
return {
|
|
524
|
+
edges: () => context.autograph.resolver.match(model).args(args).many(),
|
|
525
|
+
count: () => context.autograph.resolver.match(model).args(args).count(),
|
|
526
|
+
pageInfo: () => context.autograph.resolver.match(model).args(args).many(),
|
|
527
|
+
};
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
}, {
|
|
531
|
+
node: (doc, args, context, info) => {
|
|
532
|
+
const { id } = args;
|
|
533
|
+
const [modelName] = fromGUID(id);
|
|
534
|
+
const model = schema.models[modelName];
|
|
535
|
+
return context.autograph.resolver.match(model).id(id).one().then((result) => {
|
|
536
|
+
if (result == null) return result;
|
|
537
|
+
result.__typename = modelName; // eslint-disable-line no-underscore-dangle
|
|
538
|
+
return result;
|
|
539
|
+
});
|
|
540
|
+
},
|
|
541
|
+
}),
|
|
542
|
+
...(mutationModels.length ? {
|
|
543
|
+
Mutation: mutationModels.reduce((prev, model) => {
|
|
544
|
+
if (model.crud.includes('c')) prev[`create${model}`] = (doc, args, context, info) => context.autograph.resolver.match(model).args(args).save(args.input);
|
|
545
|
+
if (model.crud.includes('u')) prev[`update${model}`] = (doc, args, context, info) => context.autograph.resolver.match(model).args(args).save(args.input);
|
|
546
|
+
if (model.crud.includes('d')) prev[`delete${model}`] = (doc, args, context, info) => context.autograph.resolver.match(model).args(args).delete();
|
|
547
|
+
return prev;
|
|
548
|
+
}, {}),
|
|
549
|
+
} : {}),
|
|
550
|
+
...readModels.reduce((prev, model) => {
|
|
551
|
+
return Object.assign(prev, {
|
|
552
|
+
[model]: Object.values(model.fields).filter(field => field.model?.isEntity).reduce((prev2, field) => {
|
|
553
|
+
return Object.assign(prev2, {
|
|
554
|
+
[field]: (doc, args, context, info) => {
|
|
555
|
+
return context.autograph.resolver.match(field.model).where({ [field.linkBy]: doc[field.linkField.name] }).args(args).resolve(info);
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
}, {}),
|
|
559
|
+
});
|
|
560
|
+
}, {}),
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
static #getGQLType(field, suffix) {
|
|
566
|
+
let { type } = field;
|
|
567
|
+
const { isEmbedded, isRequired, isScalar, isArray, isArrayRequired, defaultValue } = field;
|
|
568
|
+
const modelType = `${type}${suffix}`;
|
|
569
|
+
if (suffix && !isScalar) type = isEmbedded ? modelType : 'ID';
|
|
570
|
+
type = isArray ? `[${type}${isArrayRequired ? '!' : ''}]` : type;
|
|
571
|
+
if (!suffix && isRequired) type += '!';
|
|
572
|
+
if (suffix === 'InputCreate' && isRequired && defaultValue != null) type += '!';
|
|
573
|
+
return type;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
static #identifyOnDeletes(models, parentName) {
|
|
577
|
+
return models.reduce((prev, model) => {
|
|
578
|
+
Object.values(model.fields).filter(f => f.onDelete).forEach((field) => {
|
|
579
|
+
if (`${field.model.name}` === `${parentName}`) {
|
|
580
|
+
if (model.isEntity) {
|
|
581
|
+
prev.push({ model, field, isArray: field.isArray, op: field.onDelete });
|
|
582
|
+
}
|
|
583
|
+
// else {
|
|
584
|
+
// prev.push(...Schema.#identifyOnDeletes(models, model.name).map(od => Object.assign(od, { fieldRef: field.name, isArray: field.isArray, op: field.onDelete })));
|
|
585
|
+
// }
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
// Assign model referential integrity
|
|
590
|
+
return Util.filterBy(prev, (a, b) => `${a.model.name}:${a.field.name}:${a.fieldRef}:${a.op}` === `${b.model.name}:${b.field.name}:${b.fieldRef}:${b.op}`);
|
|
591
|
+
}, []);
|
|
592
|
+
}
|
|
593
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const Util = require('@coderich/util');
|
|
2
|
+
const PicoMatch = require('picomatch');
|
|
3
|
+
const FillRange = require('fill-range');
|
|
4
|
+
const ObjectHash = require('object-hash');
|
|
5
|
+
const ObjectId = require('bson-objectid');
|
|
6
|
+
const DeepMerge = require('deepmerge');
|
|
7
|
+
|
|
8
|
+
exports.isGlob = str => PicoMatch.scan(str).isGlob;
|
|
9
|
+
exports.globToRegex = (glob, options = {}) => PicoMatch.makeRe(glob, { nocase: true, ...options, expandRange: (a, b) => `(${FillRange(a, b, { toRegex: true })})` });
|
|
10
|
+
|
|
11
|
+
const smartMerge = (target, source, options) => source;
|
|
12
|
+
exports.isScalarValue = value => typeof value !== 'object' && typeof value !== 'function';
|
|
13
|
+
exports.isLeafValue = value => Array.isArray(value) || value instanceof Date || ObjectId.isValid(value) || exports.isScalarValue(value);
|
|
14
|
+
exports.isBasicObject = obj => obj != null && typeof obj === 'object' && !(ObjectId.isValid(obj)) && !(obj instanceof Date) && typeof (obj.then) !== 'function';
|
|
15
|
+
exports.isPlainObject = obj => exports.isBasicObject(obj) && !Array.isArray(obj);
|
|
16
|
+
exports.mergeDeep = (...args) => DeepMerge.all(args, { isMergeableObject: obj => (exports.isPlainObject(obj) || Array.isArray(obj)), arrayMerge: smartMerge });
|
|
17
|
+
exports.hashObject = obj => ObjectHash(obj, { respectType: false, respectFunctionNames: false, respectFunctionProperties: false, unorderedArrays: true, ignoreUnknown: true, replacer: r => (ObjectId.isValid(r) ? `${r}` : r) });
|
|
18
|
+
exports.fromGUID = guid => Buffer.from(`${guid}`, 'base64').toString('ascii').split(',');
|
|
19
|
+
exports.guidToId = (autograph, guid) => (autograph.legacyMode ? guid : exports.uvl(exports.fromGUID(guid)[1], guid));
|
|
20
|
+
|
|
21
|
+
exports.finalizeWhereClause = (obj, arrayOp = '$in') => {
|
|
22
|
+
return Object.entries(Util.flatten(obj, { safe: true })).reduce((prev, [key, value]) => {
|
|
23
|
+
const isArray = Array.isArray(value);
|
|
24
|
+
if (isArray) return Object.assign(prev, { [key]: { [arrayOp]: value } });
|
|
25
|
+
return Object.assign(prev, { [key]: value });
|
|
26
|
+
}, {});
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
exports.getGQLReturnType = (returnType) => {
|
|
30
|
+
const typeMap = { array: /^\[.+\].?$/, connection: /.+Connection!?$/, number: /^(Int|Float)!?$/, scalar: /.*/ };
|
|
31
|
+
return Object.entries(typeMap).find(([type, pattern]) => returnType.match(pattern))[0];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
exports.removeUndefinedDeep = (obj) => {
|
|
35
|
+
return Util.unflatten(Object.entries(Util.flatten(obj)).reduce((prev, [key, value]) => {
|
|
36
|
+
return value === undefined ? prev : Object.assign(prev, { [key]: value });
|
|
37
|
+
}, {}));
|
|
38
|
+
};
|
package/CHANGELOG.md
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# CHANGELOG
|
|
2
|
-
|
|
3
|
-
## v0.10.x
|
|
4
|
-
- Replaced ResultSet -> POJOs
|
|
5
|
-
- Removed all $field methods (auto populated)
|
|
6
|
-
- Removed .toObject()
|
|
7
|
-
- $model $save remove $delete $lookup $cursor $pageInfo
|
|
8
|
-
- Removed embedded API completely
|
|
9
|
-
- Removed Directives
|
|
10
|
-
- embedApi -> no replacement
|
|
11
|
-
- enforce -> use pipeline methods
|
|
12
|
-
- resolve -> use graphql resolvers
|
|
13
|
-
- @value -> use @field.instruct directive
|
|
14
|
-
- Removed Model.tform() -> use Model.shapeObject(shape, data)
|
|
15
|
-
- Removed Transformer + Rule -> use Pipeline
|
|
16
|
-
- Removed many pre-defined rules + transformers
|
|
17
|
-
- Moved "validator" to dev dependency -> isEmail
|
|
18
|
-
- Added QueryBuilder.resolve() terminal command
|
|
19
|
-
- Exported SchemaDecorator -> Schema
|
|
20
|
-
- Removed embedded schema SystemEvents (internal emitter also removed)
|
|
21
|
-
- Removed spread of arguments in QueryBuilder terminal commands (must pass in array)
|
|
22
|
-
- Mutate "merged" instead of "input"
|
|
23
|
-
- Validate "payload"
|
|
24
|
-
|
|
25
|
-
## v0.9.x
|
|
26
|
-
- Subscriptions API
|
|
27
|
-
- postMutation no longer mutates "doc" and adds "result"
|
|
28
|
-
- Added onDelete defer option
|
|
29
|
-
|
|
30
|
-
## v0.8.x
|
|
31
|
-
- Engine 14+
|
|
32
|
-
|
|
33
|
-
## v0.7.x
|
|
34
|
-
- Complete overhaul of Query to Mongo Driver (pagination, sorting, counts, etc)
|
|
35
|
-
- Removed countModel Queries from the API (now available as `count` property on `Connetion` types)
|
|
36
|
-
- Dropped Neo4J (temporarily)
|
|
37
|
-
|
|
38
|
-
## v0.6.x
|
|
39
|
-
- Mongo driver no longer checks for `version` directive
|
|
40
|
-
- Models no longer share a Connection type; removing the need to use `... on Model` for GraphQL queries
|
|
41
|
-
- Added `@field(connection: Boolean)` parameter to specifically indicate fields that should return a Connection type
|