@coderich/autograph 0.10.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -3
- package/index.js +2 -6
- package/package.json +4 -4
- package/src/.DS_Store +0 -0
- package/src/core/EventEmitter.js +2 -4
- package/src/core/Resolver.js +43 -60
- package/src/core/Schema.js +3 -36
- package/src/data/.DS_Store +0 -0
- package/src/data/DataLoader.js +71 -32
- package/src/data/DataService.js +59 -58
- package/src/data/Field.js +71 -121
- package/src/data/Model.js +98 -108
- package/src/data/Pipeline.js +174 -0
- package/src/data/Type.js +19 -74
- package/src/driver/MongoDriver.js +21 -19
- package/src/graphql/.DS_Store +0 -0
- package/src/graphql/ast/Field.js +43 -24
- package/src/graphql/ast/Model.js +5 -16
- package/src/graphql/ast/Node.js +0 -25
- package/src/graphql/ast/Schema.js +107 -111
- package/src/graphql/extension/api.js +20 -18
- package/src/graphql/extension/framework.js +25 -33
- package/src/graphql/extension/type.js +2 -2
- package/src/query/Query.js +72 -14
- package/src/query/QueryBuilder.js +38 -30
- package/src/query/QueryBuilderTransaction.js +3 -3
- package/src/query/QueryResolver.js +92 -42
- package/src/query/QueryService.js +31 -34
- package/src/service/app.service.js +67 -9
- package/src/service/event.service.js +5 -79
- package/src/service/schema.service.js +5 -3
- package/src/core/Rule.js +0 -107
- package/src/core/SchemaDecorator.js +0 -46
- package/src/core/Transformer.js +0 -68
- package/src/data/Memoizer.js +0 -39
- package/src/data/ResultSet.js +0 -205
- package/src/data/stream/DataHydrator.js +0 -58
- package/src/data/stream/ResultSet.js +0 -34
- package/src/data/stream/ResultSetItem.js +0 -158
- package/src/data/stream/ResultSetItemProxy.js +0 -161
- package/src/graphql/ast/SchemaDecorator.js +0 -141
- package/src/graphql/directive/authz.directive.js +0 -84
|
@@ -1,77 +1,110 @@
|
|
|
1
|
-
const { get, isEmpty } = require('lodash');
|
|
1
|
+
const { get, set, isEmpty } = require('lodash');
|
|
2
2
|
const Boom = require('../core/Boom');
|
|
3
3
|
const QueryService = require('./QueryService');
|
|
4
4
|
const DataService = require('../data/DataService');
|
|
5
5
|
const { createSystemEvent } = require('../service/event.service');
|
|
6
|
-
const { mergeDeep } = require('../service/app.service');
|
|
6
|
+
const { mergeDeep, getGQLReturnType } = require('../service/app.service');
|
|
7
7
|
|
|
8
8
|
module.exports = class QueryResolver {
|
|
9
9
|
constructor(query) {
|
|
10
10
|
this.query = query;
|
|
11
11
|
this.resolver = query.toObject().resolver;
|
|
12
|
+
this.context = this.resolver.getContext();
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
autoResolve(query) {
|
|
16
|
+
const { args } = query.toObject();
|
|
17
|
+
const [,,, info] = args;
|
|
18
|
+
|
|
19
|
+
switch (getGQLReturnType(info.returnType)) {
|
|
20
|
+
case 'array': {
|
|
21
|
+
return new QueryResolver(this.query.clone().method('findMany')).resolve();
|
|
22
|
+
}
|
|
23
|
+
case 'number': {
|
|
24
|
+
return new QueryResolver(this.query.clone().method('count')).resolve();
|
|
25
|
+
}
|
|
26
|
+
case 'connection': {
|
|
27
|
+
return Promise.resolve({
|
|
28
|
+
count: () => new QueryResolver(this.query.clone().method('count')).resolve(),
|
|
29
|
+
edges: () => new QueryResolver(this.query.clone().method('findMany')).resolve(),
|
|
30
|
+
pageInfo: () => new QueryResolver(this.query.clone().method('findMany')).resolve(),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
case 'scalar': default: {
|
|
34
|
+
return new QueryResolver(this.query.clone().method('findOne')).resolve();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async findOne(query) {
|
|
40
|
+
await QueryService.resolveQuery(query);
|
|
41
|
+
|
|
42
|
+
return createSystemEvent('Query', { query }, () => {
|
|
16
43
|
return this.resolver.resolve(query);
|
|
17
44
|
});
|
|
18
45
|
}
|
|
19
46
|
|
|
20
|
-
findMany(query) {
|
|
21
|
-
|
|
47
|
+
async findMany(query) {
|
|
48
|
+
await QueryService.resolveQuery(query);
|
|
49
|
+
|
|
50
|
+
return createSystemEvent('Query', { query }, () => {
|
|
22
51
|
return this.resolver.resolve(query);
|
|
23
52
|
});
|
|
24
53
|
}
|
|
25
54
|
|
|
26
|
-
count(query) {
|
|
27
|
-
|
|
55
|
+
async count(query) {
|
|
56
|
+
await QueryService.resolveQuery(query);
|
|
57
|
+
|
|
58
|
+
return createSystemEvent('Query', { query }, () => {
|
|
28
59
|
return this.resolver.resolve(query);
|
|
29
60
|
});
|
|
30
61
|
}
|
|
31
62
|
|
|
32
|
-
createOne(query) {
|
|
33
|
-
const { model, input
|
|
34
|
-
model.
|
|
63
|
+
async createOne(query) {
|
|
64
|
+
const { model, input } = query.toObject();
|
|
65
|
+
const shape = model.getShape('create');
|
|
35
66
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
67
|
+
await QueryService.resolveQuery(query);
|
|
68
|
+
|
|
69
|
+
return createSystemEvent('Mutation', { query }, async () => {
|
|
70
|
+
const $input = model.shapeObject(shape, input, query);
|
|
71
|
+
await model.validate(query, $input);
|
|
72
|
+
const doc = await this.resolver.resolve(query.$input($input));
|
|
41
73
|
query.doc(doc);
|
|
42
74
|
return doc;
|
|
43
75
|
});
|
|
44
76
|
}
|
|
45
77
|
|
|
46
78
|
createMany(query) {
|
|
47
|
-
const { model,
|
|
79
|
+
const { model, input, transaction } = query.toObject();
|
|
48
80
|
const txn = this.resolver.transaction(transaction);
|
|
49
|
-
|
|
81
|
+
input.forEach(arg => txn.match(model).save(arg));
|
|
50
82
|
return txn.run();
|
|
51
83
|
}
|
|
52
84
|
|
|
53
|
-
updateOne(query) {
|
|
54
|
-
const { model, match,
|
|
85
|
+
async updateOne(query) {
|
|
86
|
+
const { model, match, input } = query.toObject();
|
|
87
|
+
|
|
88
|
+
return this.resolver.match(model).match(match).one({ required: true }).then(async (doc) => {
|
|
89
|
+
const shape = model.getShape('update');
|
|
90
|
+
const merged = model.shapeObject(shape, mergeDeep(doc, input), query);
|
|
55
91
|
|
|
56
|
-
|
|
57
|
-
const { input } = query.toObject();
|
|
58
|
-
const merged = mergeDeep(doc, input);
|
|
92
|
+
await QueryService.resolveQuery(query);
|
|
59
93
|
|
|
60
|
-
return createSystemEvent('Mutation', {
|
|
61
|
-
const $
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return this.resolver.resolve(query.$doc($doc).$input($input));
|
|
94
|
+
return createSystemEvent('Mutation', { query: query.doc(doc).merged(merged) }, async () => {
|
|
95
|
+
const $doc = model.shapeObject(shape, mergeDeep(doc, input), query);
|
|
96
|
+
await model.validate(query, $doc);
|
|
97
|
+
return this.resolver.resolve(query.$doc($doc));
|
|
65
98
|
});
|
|
66
99
|
});
|
|
67
100
|
}
|
|
68
101
|
|
|
69
102
|
updateMany(query) {
|
|
70
|
-
const { model,
|
|
103
|
+
const { model, input, match, transaction, flags } = query.toObject();
|
|
71
104
|
|
|
72
|
-
return this.resolver.match(model).match(match).
|
|
105
|
+
return this.resolver.match(model).match(match).many(flags).then((docs) => {
|
|
73
106
|
const txn = this.resolver.transaction(transaction);
|
|
74
|
-
docs.forEach(doc => txn.match(model).id(doc.id).save(
|
|
107
|
+
docs.forEach(doc => txn.match(model).id(doc.id).save(input, flags));
|
|
75
108
|
return txn.run();
|
|
76
109
|
});
|
|
77
110
|
}
|
|
@@ -79,8 +112,10 @@ module.exports = class QueryResolver {
|
|
|
79
112
|
deleteOne(query) {
|
|
80
113
|
const { model, id, flags } = query.toObject();
|
|
81
114
|
|
|
82
|
-
return this.resolver.match(model).id(id).flags(flags).one({ required: true }).then((doc) => {
|
|
83
|
-
|
|
115
|
+
return this.resolver.match(model).id(id).flags(flags).one({ required: true }).then(async (doc) => {
|
|
116
|
+
await QueryService.resolveQuery(query);
|
|
117
|
+
|
|
118
|
+
return createSystemEvent('Mutation', { query: query.doc(doc) }, () => {
|
|
84
119
|
return QueryService.resolveReferentialIntegrity(query).then(() => {
|
|
85
120
|
return this.resolver.resolve(query).then(() => doc);
|
|
86
121
|
});
|
|
@@ -151,16 +186,28 @@ module.exports = class QueryResolver {
|
|
|
151
186
|
}
|
|
152
187
|
|
|
153
188
|
splice(query) {
|
|
154
|
-
const { model, match, args, flags } = query.toObject();
|
|
189
|
+
const { model, match, args, flags = {} } = query.toObject();
|
|
155
190
|
const [key, from, to] = args;
|
|
156
191
|
|
|
157
|
-
|
|
158
|
-
|
|
192
|
+
// Can only splice arrays
|
|
193
|
+
const field = model.getField(key);
|
|
194
|
+
const isArray = field.isArray();
|
|
195
|
+
if (!isArray) throw Boom.badRequest(`Cannot splice field '${model}.${field}'`);
|
|
159
196
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
197
|
+
return this.resolver.match(model).match(match).flags(flags).one({ required: true }).then(async (doc) => {
|
|
198
|
+
const array = get(doc, key) || [];
|
|
199
|
+
const paramShape = model.getShape('create', 'spliceTo');
|
|
200
|
+
const $to = model.shapeObject(paramShape, { [key]: to }, query)[key] || to;
|
|
201
|
+
const $from = model.shapeObject(paramShape, { [key]: from }, query)[key] || from;
|
|
202
|
+
set(doc, key, DataService.spliceEmbeddedArray(array, $from, $to));
|
|
203
|
+
|
|
204
|
+
await QueryService.resolveQuery(query);
|
|
205
|
+
|
|
206
|
+
return createSystemEvent('Mutation', { query: query.method('updateOne').doc(doc).merged(doc) }, async () => {
|
|
207
|
+
const shape = model.getShape('update');
|
|
208
|
+
const $doc = model.shapeObject(shape, doc, query);
|
|
209
|
+
await model.validate(query, $doc);
|
|
210
|
+
return this.resolver.resolve(query.$doc($doc));
|
|
164
211
|
});
|
|
165
212
|
});
|
|
166
213
|
}
|
|
@@ -173,11 +220,14 @@ module.exports = class QueryResolver {
|
|
|
173
220
|
return this.findMany(query.method('findMany'));
|
|
174
221
|
}
|
|
175
222
|
|
|
176
|
-
resolve() {
|
|
223
|
+
async resolve() {
|
|
177
224
|
const { model, method, flags } = this.query.toObject();
|
|
178
225
|
|
|
226
|
+
// const resolveQueryMethods = ['findOne', 'findMany', 'count', 'createOne', 'updateOne', 'deleteOne', 'splice'];
|
|
227
|
+
// if (resolveQueryMethods.indexOf(method) > -1) await QueryService.resolveQuery(this.query);
|
|
228
|
+
|
|
179
229
|
return this[method](this.query).then((data) => {
|
|
180
|
-
if (flags.required &&
|
|
230
|
+
if (flags.required && isEmpty(data)) throw Boom.notFound(`${model} Not Found`);
|
|
181
231
|
if (data == null) return null; // Explicitly return null here
|
|
182
232
|
return data;
|
|
183
233
|
});
|
|
@@ -1,50 +1,31 @@
|
|
|
1
1
|
const { get, set, uniq, flattenDeep } = require('lodash');
|
|
2
|
-
const {
|
|
3
|
-
|
|
4
|
-
const resolveEmbeddedWhere = (ref, key, value) => {
|
|
5
|
-
const resolved = ensureArray(map(value, (obj) => {
|
|
6
|
-
return Object.entries(obj).reduce((p, [k, v]) => {
|
|
7
|
-
const f = ref.getFieldByName(k);
|
|
8
|
-
|
|
9
|
-
if (k === 'id') return Object.assign(p, { [k]: ref.idValue(v) });
|
|
10
|
-
if (f.isScalar()) return Object.assign(p, { [k]: v });
|
|
11
|
-
if (f.isEmbedded()) return Object.assign(p, { [k]: resolveEmbeddedWhere(f.getModelRef(), k, v) });
|
|
12
|
-
return Object.assign(p, { [k]: v });
|
|
13
|
-
}, {});
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
return resolved.length > 1 ? resolved : resolved[0];
|
|
17
|
-
};
|
|
2
|
+
const { keyPaths, ensureArray, isPlainObject } = require('../service/app.service');
|
|
18
3
|
|
|
4
|
+
/**
|
|
5
|
+
* The where clause may contain attributes that are NOT in the model
|
|
6
|
+
* This can happen because the where clause reaches into the schema via refs/virtual refs
|
|
7
|
+
*/
|
|
19
8
|
exports.resolveWhereClause = (query) => {
|
|
20
9
|
const { resolver, model, match: where = {}, flags = {} } = query.toObject();
|
|
10
|
+
const shape = model.getShape('create', 'where');
|
|
21
11
|
|
|
22
|
-
|
|
23
|
-
|
|
12
|
+
const $where = Object.entries(where).reduce((prev, [from, value]) => {
|
|
13
|
+
const el = shape.find(s => s.from === from);
|
|
14
|
+
if (!el) return prev; // There's no knowing what this could be
|
|
24
15
|
|
|
25
|
-
|
|
26
|
-
const $where = Object.entries(where).reduce((prev, [key, value]) => {
|
|
27
|
-
const field = model.getField(key);
|
|
28
|
-
if (!field) return prev;
|
|
29
|
-
const modelRef = field.getModelRef();
|
|
16
|
+
const { isVirtual, isEmbedded, modelRef, virtualRef } = el.field.toObject();
|
|
30
17
|
|
|
31
|
-
if (
|
|
32
|
-
const virtualRef = field.getVirtualRef();
|
|
18
|
+
if (isVirtual) {
|
|
33
19
|
const ids = Promise.all(ensureArray(value).map(v => resolver.match(modelRef).where(isPlainObject(v) ? v : { id: v }).many(flags).then(docs => docs.map(doc => doc[virtualRef])))).then(results => uniq(flattenDeep(results)));
|
|
34
20
|
return Object.assign(prev, { id: ids });
|
|
35
21
|
}
|
|
36
22
|
|
|
37
|
-
if (modelRef && !
|
|
23
|
+
if (modelRef && !isEmbedded) {
|
|
38
24
|
const ids = Promise.all(ensureArray(value).map(v => (isPlainObject(v) ? resolver.match(modelRef).where(v).many(flags).then(docs => docs.map(doc => doc.id)) : Promise.resolve(v)))).then(results => uniq(flattenDeep(results)));
|
|
39
|
-
return Object.assign(prev, { [
|
|
25
|
+
return Object.assign(prev, { [from]: ids });
|
|
40
26
|
}
|
|
41
27
|
|
|
42
|
-
|
|
43
|
-
if (field.isEmbedded()) {
|
|
44
|
-
return Object.assign(prev, { [key]: resolveEmbeddedWhere(modelRef, key, value) });
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return Object.assign(prev, { [key]: value });
|
|
28
|
+
return Object.assign(prev, { [from]: value });
|
|
48
29
|
}, {});
|
|
49
30
|
|
|
50
31
|
// Resolve
|
|
@@ -61,7 +42,8 @@ exports.resolveWhereClause = (query) => {
|
|
|
61
42
|
|
|
62
43
|
exports.resolveSortBy = (query) => {
|
|
63
44
|
const { model, sort = {} } = query.toObject();
|
|
64
|
-
const
|
|
45
|
+
const shape = model.getShape('create', 'sortBy');
|
|
46
|
+
const $sort = model.shapeObject(shape, sort, query);
|
|
65
47
|
|
|
66
48
|
// Because normalize casts the value (sometimes to an array) need special handling
|
|
67
49
|
keyPaths($sort).forEach((path) => {
|
|
@@ -127,3 +109,18 @@ exports.resolveReferentialIntegrity = (query) => {
|
|
|
127
109
|
}
|
|
128
110
|
});
|
|
129
111
|
};
|
|
112
|
+
|
|
113
|
+
exports.resolveQuery = async (query) => {
|
|
114
|
+
const { model, sort, native, batch, match } = query.toObject();
|
|
115
|
+
|
|
116
|
+
if (!native) {
|
|
117
|
+
const shape = model.getShape('create', 'where');
|
|
118
|
+
const $where = batch ? match : await exports.resolveWhereClause(query);
|
|
119
|
+
const $$where = model.shapeObject(shape, $where, query);
|
|
120
|
+
query.match($$where);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (sort) {
|
|
124
|
+
query.$sort(exports.resolveSortBy(query));
|
|
125
|
+
}
|
|
126
|
+
};
|
|
@@ -54,6 +54,11 @@ exports.stripObjectUndefineds = obj => Object.entries(obj).reduce((prev, [key, v
|
|
|
54
54
|
exports.pushIt = (arr, it) => arr[arr.push(it) - 1];
|
|
55
55
|
exports.toKeyObj = obj => exports.keyPaths(obj).reduce((prev, path) => Object.assign(prev, { [path]: _.get(obj, path) }), {});
|
|
56
56
|
|
|
57
|
+
exports.getGQLReturnType = (returnType) => {
|
|
58
|
+
const typeMap = { array: /^\[.+\].?$/, connection: /.+Connection!?$/, number: /^(Int|Float)!?$/, scalar: /.*/ };
|
|
59
|
+
return Object.entries(typeMap).find(([type, pattern]) => returnType.match(pattern))[0];
|
|
60
|
+
};
|
|
61
|
+
|
|
57
62
|
exports.removeUndefinedDeep = (obj) => {
|
|
58
63
|
return exports.unravelObject(exports.keyPaths(obj).reduce((prev, path) => {
|
|
59
64
|
const value = _.get(obj, path);
|
|
@@ -90,7 +95,8 @@ exports.map = (mixed, fn) => {
|
|
|
90
95
|
if (mixed == null) return mixed;
|
|
91
96
|
const isArray = Array.isArray(mixed);
|
|
92
97
|
const arr = isArray ? mixed : [mixed];
|
|
93
|
-
const results = arr.map(
|
|
98
|
+
// const results = arr.map((...args) => fn(...args));
|
|
99
|
+
const results = arr.map((el, i, a) => fn(el, isArray ? i : undefined, isArray ? a : undefined));
|
|
94
100
|
return isArray ? results : results[0];
|
|
95
101
|
};
|
|
96
102
|
|
|
@@ -141,14 +147,6 @@ exports.objectContaining = (a, b) => {
|
|
|
141
147
|
return exports.hashObject(a) === exports.hashObject(b);
|
|
142
148
|
};
|
|
143
149
|
|
|
144
|
-
exports.serialize = (field, value) => {
|
|
145
|
-
if (!exports.isPlainObject(value)) return value;
|
|
146
|
-
const model = field.getModelRef();
|
|
147
|
-
if (!model) return value;
|
|
148
|
-
const key = model.idKey();
|
|
149
|
-
return value[key];
|
|
150
|
-
};
|
|
151
|
-
|
|
152
150
|
/**
|
|
153
151
|
* Transform an object with dot.notation keys into an expanded object.
|
|
154
152
|
* eg. { 'user.name': 'richard' } => { user: { name: 'richard' } }
|
|
@@ -266,3 +264,63 @@ exports.proxyDeep = (obj, handler, proxyMap = new WeakMap(), path = '') => {
|
|
|
266
264
|
|
|
267
265
|
return finalProxy;
|
|
268
266
|
};
|
|
267
|
+
|
|
268
|
+
exports.resolveDataObject = (obj) => {
|
|
269
|
+
return Promise.all(Object.keys(obj).map(async (key) => {
|
|
270
|
+
const value = await obj[key];
|
|
271
|
+
return { key, value };
|
|
272
|
+
})).then((results) => {
|
|
273
|
+
return results.reduce((prev, { key, value }) => {
|
|
274
|
+
return Object.assign(prev, { [key]: value });
|
|
275
|
+
}, {});
|
|
276
|
+
});
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
exports.seek = (obj, paths, hint) => {
|
|
280
|
+
// We first do a normal get
|
|
281
|
+
const value = _.get(obj, paths);
|
|
282
|
+
if (!hint || value !== undefined) return value;
|
|
283
|
+
|
|
284
|
+
// Normalize paths & hint for traversal
|
|
285
|
+
const $paths = Array.isArray(paths) ? paths : paths.split('.');
|
|
286
|
+
const $hint = exports.unravelObject(hint);
|
|
287
|
+
|
|
288
|
+
// Traverse paths and get as close to the value as possible
|
|
289
|
+
const { currentValue, pathsToGo } = $paths.reduce((prev, path, i, arr) => {
|
|
290
|
+
if (prev.currentValue === undefined) return prev;
|
|
291
|
+
if (!Object.prototype.hasOwnProperty.call(prev.currentValue, path)) return prev;
|
|
292
|
+
prev.currentValue = prev.currentValue[path];
|
|
293
|
+
prev.pathsToGo = arr.slice(i + 1);
|
|
294
|
+
return prev;
|
|
295
|
+
}, { currentValue: obj, pathsToGo: $paths });
|
|
296
|
+
|
|
297
|
+
// Only if we hit an array can we continue
|
|
298
|
+
if (!Array.isArray(currentValue)) return undefined;
|
|
299
|
+
|
|
300
|
+
// If we got to the last segment we need the hint in order to verify
|
|
301
|
+
const lastPath = Boolean(pathsToGo.length === 1);
|
|
302
|
+
const arr = lastPath ? currentValue.filter(v => exports.objectContaining(v, $hint)) : currentValue;
|
|
303
|
+
|
|
304
|
+
// We keep going, recursive, till we find the first value
|
|
305
|
+
return arr.reduce((prev, v) => prev || exports.seek(v, pathsToGo, $hint), undefined);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
exports.deseek = (shape, obj, paths, hint) => {
|
|
309
|
+
// Normalize paths
|
|
310
|
+
const $paths = (Array.isArray(paths) ? paths : paths.split('.')).map((path) => {
|
|
311
|
+
const item = shape.find(s => s.to === path); // Deserializing from unknown to expected
|
|
312
|
+
return item ? item.from : path;
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Normalize hint
|
|
316
|
+
const $hint = Object.entries(exports.toKeyObj(hint)).reduce((prev, [key, value]) => {
|
|
317
|
+
const segments = key.split('.').map((path) => {
|
|
318
|
+
const item = shape.find(s => s.to === path); // Deserializing from unknown to expected
|
|
319
|
+
return item ? item.from : path;
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
return Object.assign(prev, { [segments.join('.')]: value });
|
|
323
|
+
}, {});
|
|
324
|
+
|
|
325
|
+
return exports.seek(obj, $paths, $hint);
|
|
326
|
+
};
|
|
@@ -1,59 +1,26 @@
|
|
|
1
|
-
const QueryService = require('../query/QueryService');
|
|
2
1
|
const EventEmitter = require('../core/EventEmitter');
|
|
3
|
-
const {
|
|
2
|
+
const { ucFirst } = require('./app.service');
|
|
4
3
|
|
|
5
4
|
// Event emitters
|
|
6
5
|
const eventEmitter = new EventEmitter().setMaxListeners(100);
|
|
7
|
-
const internalEmitter = new EventEmitter().setMaxListeners(100);
|
|
8
6
|
const systemEvent = new EventEmitter().setMaxListeners(100).on('system', async (event, next) => {
|
|
9
7
|
const { type, data } = event;
|
|
10
|
-
await internalEmitter.emit(type, data);
|
|
11
8
|
next(await eventEmitter.emit(type, data)); // Return result from user-defined middleware
|
|
12
9
|
});
|
|
13
10
|
|
|
14
11
|
//
|
|
15
12
|
exports.createSystemEvent = (name, mixed = {}, thunk = () => {}) => {
|
|
16
13
|
let event = mixed;
|
|
17
|
-
let middleware = () => Promise.resolve();
|
|
18
14
|
const type = ucFirst(name);
|
|
19
15
|
|
|
20
16
|
if (name !== 'Setup' && name !== 'Response') {
|
|
21
|
-
const {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
event = {
|
|
25
|
-
context: resolver.getContext(),
|
|
26
|
-
key: `${method}${model}`,
|
|
27
|
-
resolver,
|
|
28
|
-
method,
|
|
29
|
-
crud,
|
|
30
|
-
model,
|
|
31
|
-
meta,
|
|
32
|
-
id,
|
|
33
|
-
input,
|
|
34
|
-
query,
|
|
35
|
-
doc,
|
|
36
|
-
merged,
|
|
37
|
-
root,
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
middleware = () => new Promise(async (resolve) => {
|
|
41
|
-
if (!native) {
|
|
42
|
-
const $where = await QueryService.resolveWhereClause(query);
|
|
43
|
-
query.match(model.serialize(query, $where, true));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (sort) {
|
|
47
|
-
query.$sort(QueryService.resolveSortBy(query));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
resolve();
|
|
51
|
-
});
|
|
17
|
+
const { query } = mixed;
|
|
18
|
+
event = query.toObject();
|
|
19
|
+
event.query = query;
|
|
52
20
|
}
|
|
53
21
|
|
|
54
22
|
return systemEvent.emit('system', { type: `pre${type}`, data: event }).then((result) => {
|
|
55
|
-
|
|
56
|
-
return middleware().then(thunk);
|
|
23
|
+
return (result !== undefined) ? result : thunk(); // Allowing middleware to dictate result
|
|
57
24
|
}).then((result) => {
|
|
58
25
|
event.result = result;
|
|
59
26
|
if (event.crud === 'create') event.doc = event.query.toObject().doc;
|
|
@@ -66,44 +33,3 @@ exports.createSystemEvent = (name, mixed = {}, thunk = () => {}) => {
|
|
|
66
33
|
};
|
|
67
34
|
|
|
68
35
|
exports.eventEmitter = eventEmitter;
|
|
69
|
-
exports.internalEmitter = internalEmitter;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Hook into the pre event only!
|
|
74
|
-
*
|
|
75
|
-
* Kick off system events for embedded fields
|
|
76
|
-
*/
|
|
77
|
-
const eventHandler = (event) => {
|
|
78
|
-
const { model, input, method, doc = input, query } = event;
|
|
79
|
-
|
|
80
|
-
return Promise.all(model.getEmbeddedFields().map((field) => {
|
|
81
|
-
return new Promise((resolve, reject) => {
|
|
82
|
-
if (Object.prototype.hasOwnProperty.call(input || {}, field.getName())) {
|
|
83
|
-
let i = 0;
|
|
84
|
-
const value = input[field.getName()];
|
|
85
|
-
const values = ensureArray(value).filter(el => el != null);
|
|
86
|
-
const newModel = field.getModelRef();
|
|
87
|
-
|
|
88
|
-
if (values.length) {
|
|
89
|
-
values.forEach((val) => {
|
|
90
|
-
const clone = query.clone().model(newModel).input(val).doc(doc);
|
|
91
|
-
exports.createSystemEvent('Mutation', { method, query: clone }, () => {
|
|
92
|
-
if (++i >= values.length) resolve();
|
|
93
|
-
}).catch(e => reject(e));
|
|
94
|
-
// const newEvent = { parent: doc, key: `${method}${field}`, method, model: newModel, resolver, query: new Query(resolver, newModel, { meta }), input: val };
|
|
95
|
-
// exports.createSystemEvent('Mutation', newEvent, () => {
|
|
96
|
-
// if (++i >= values.length) resolve();
|
|
97
|
-
// }).catch(e => reject(e));
|
|
98
|
-
});
|
|
99
|
-
} else {
|
|
100
|
-
resolve();
|
|
101
|
-
}
|
|
102
|
-
} else {
|
|
103
|
-
resolve();
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
}));
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
internalEmitter.on('preMutation', async (event, next) => eventHandler(event).then(next)); // Only preMutation!
|
|
@@ -54,11 +54,13 @@ exports.getSchemaData = (schema) => {
|
|
|
54
54
|
exports.identifyOnDeletes = (models, parentModel) => {
|
|
55
55
|
return models.reduce((prev, model) => {
|
|
56
56
|
model.getOnDeleteFields().forEach((field) => {
|
|
57
|
-
|
|
57
|
+
const { modelRef, isArray } = field.toObject();
|
|
58
|
+
|
|
59
|
+
if (`${modelRef}` === `${parentModel}`) {
|
|
58
60
|
if (model.isEntity()) {
|
|
59
|
-
prev.push({ model, field, isArray
|
|
61
|
+
prev.push({ model, field, isArray, op: field.getOnDelete() });
|
|
60
62
|
} else {
|
|
61
|
-
prev.push(...exports.identifyOnDeletes(models, model).map(od => Object.assign(od, { fieldRef: field, isArray
|
|
63
|
+
prev.push(...exports.identifyOnDeletes(models, model).map(od => Object.assign(od, { fieldRef: field, isArray, op: field.getOnDelete() })));
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
});
|
package/src/core/Rule.js
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
const { get } = require('lodash');
|
|
2
|
-
const isEmail = require('validator/lib/isEmail');
|
|
3
|
-
const Boom = require('./Boom');
|
|
4
|
-
const { map, ensureArray, hashObject } = require('../service/app.service');
|
|
5
|
-
|
|
6
|
-
const instances = {};
|
|
7
|
-
const jsStringMethods = ['endsWith', 'includes', 'match', 'search', 'startsWith'];
|
|
8
|
-
|
|
9
|
-
class Rule {
|
|
10
|
-
constructor(thunk, options = {}, name = 'Unknown') {
|
|
11
|
-
const {
|
|
12
|
-
ignoreNull = true,
|
|
13
|
-
itemize = true,
|
|
14
|
-
toError = (field, value, msg) => Boom.notAcceptable(`Rule (${name}) failed for { ${field.getModel()}.${field}: ${value} }`),
|
|
15
|
-
} = (options || {});
|
|
16
|
-
|
|
17
|
-
return Object.defineProperty((field, val, query) => {
|
|
18
|
-
return new Promise((resolve, reject) => {
|
|
19
|
-
if (ignoreNull && val == null) return resolve();
|
|
20
|
-
|
|
21
|
-
if (ignoreNull && itemize) {
|
|
22
|
-
return Promise.all(ensureArray(map(val, async (v) => {
|
|
23
|
-
const err = await thunk(field, v, query);
|
|
24
|
-
if (err) return Promise.reject(toError(field, v));
|
|
25
|
-
return Promise.resolve();
|
|
26
|
-
}))).then(v => resolve()).catch(e => reject(e));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return Promise.all([(async () => {
|
|
30
|
-
const err = await thunk(field, val, query);
|
|
31
|
-
if (err) return Promise.reject(toError(field, val));
|
|
32
|
-
return Promise.resolve();
|
|
33
|
-
})()]).then(v => resolve()).catch(e => reject(e));
|
|
34
|
-
});
|
|
35
|
-
}, 'type', { value: 'rule' });
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
static factory(name, thunk, options = {}) {
|
|
39
|
-
return Object.defineProperty(Rule, name, {
|
|
40
|
-
value: (...args) => Object.defineProperty(new Rule(thunk(...args), options, name), 'method', { value: name }),
|
|
41
|
-
writable: options.writable,
|
|
42
|
-
enumerable: options.enumerable,
|
|
43
|
-
})[name];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
static extend(name, instance) {
|
|
47
|
-
const invalidArg = () => { throw new Error('Invalid argument; expected Rule factory instance'); };
|
|
48
|
-
const { method = invalidArg(), type = invalidArg() } = instance;
|
|
49
|
-
if (type !== 'rule' || !Rule[method]) invalidArg();
|
|
50
|
-
return (instances[name] = instance);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
static getInstances() {
|
|
54
|
-
const defaultRules = Object.entries(Rule).map(([name, method]) => ({ name, instance: method() }));
|
|
55
|
-
const customRules = Object.entries(instances).map(([name, instance]) => ({ name, instance }));
|
|
56
|
-
const rules = defaultRules.concat(customRules);
|
|
57
|
-
return rules.reduce((prev, { name, instance }) => Object.assign(prev, { [name]: instance }), {});
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Factory methods
|
|
62
|
-
jsStringMethods.forEach(name => Rule.factory(name, (...args) => (f, v) => !String(v)[name](...args)));
|
|
63
|
-
|
|
64
|
-
// Ensures Foreign Key relationships
|
|
65
|
-
Rule.factory('ensureId', () => (f, v, q) => {
|
|
66
|
-
const { resolver } = q.toObject();
|
|
67
|
-
return resolver.match(f.getType()).id(v).one().then(doc => Boolean(doc == null));
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
// Enforces required fields (only during create)
|
|
71
|
-
Rule.factory('required', () => (f, v, q) => {
|
|
72
|
-
const { crud, input } = q.toObject();
|
|
73
|
-
return (crud === 'create' ? v == null : Object.prototype.hasOwnProperty.call(input, f.getName()) && v == null);
|
|
74
|
-
}, { ignoreNull: false, enumerable: true });
|
|
75
|
-
|
|
76
|
-
// A field cannot hold a reference to itself (model)
|
|
77
|
-
Rule.factory('selfless', () => (f, v, q) => {
|
|
78
|
-
const { doc } = q.toObject();
|
|
79
|
-
if (`${v}` === `${get(doc, 'id')}`) throw Boom.badRequest(`${f.getModel()}.${f.getName()} cannot hold a reference to itself`);
|
|
80
|
-
return false;
|
|
81
|
-
}, { enumerable: true });
|
|
82
|
-
|
|
83
|
-
// Once set it cannot be changed
|
|
84
|
-
Rule.factory('immutable', () => (f, v, q) => {
|
|
85
|
-
const { doc, crud } = q.toObject();
|
|
86
|
-
const path = `${f.getModel()}.${f.getName()}`;
|
|
87
|
-
const p = path.substr(path.indexOf('.') + 1);
|
|
88
|
-
const oldVal = get(doc, p);
|
|
89
|
-
if (crud === 'update' && oldVal !== undefined && v !== undefined && `${hashObject(v)}` !== `${hashObject(oldVal)}`) throw Boom.badRequest(`${path} is immutable; cannot be changed once set`);
|
|
90
|
-
return false;
|
|
91
|
-
}, { enumerable: true });
|
|
92
|
-
|
|
93
|
-
Rule.factory('allow', (...args) => (f, v) => args.indexOf(v) === -1);
|
|
94
|
-
Rule.factory('deny', (...args) => (f, v) => args.indexOf(v) > -1);
|
|
95
|
-
Rule.factory('range', (min, max) => {
|
|
96
|
-
if (min == null) min = undefined;
|
|
97
|
-
if (max == null) max = undefined;
|
|
98
|
-
return (f, v) => {
|
|
99
|
-
const num = +v; // Coerce to number if possible
|
|
100
|
-
const test = Number.isNaN(num) ? v.length : num;
|
|
101
|
-
return test < min || test > max;
|
|
102
|
-
};
|
|
103
|
-
}, { itemize: false });
|
|
104
|
-
Rule.factory('email', () => (f, v) => !isEmail(v), { enumerable: true });
|
|
105
|
-
Rule.factory('distinct', () => (f, v) => false, { enumerable: true });
|
|
106
|
-
|
|
107
|
-
module.exports = Rule;
|