@coderich/autograph 0.10.2 → 0.10.3
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 +4 -6
- package/index.js +0 -2
- package/package.json +6 -5
- package/src/core/Resolver.js +12 -20
- package/src/core/Schema.js +4 -4
- package/src/data/DataService.js +35 -13
- package/src/data/Field.js +13 -30
- package/src/data/Model.js +63 -45
- package/src/data/Pipeline.js +20 -10
- package/src/data/Type.js +22 -3
- package/src/driver/MongoDriver.js +7 -4
- package/src/graphql/ast/Field.js +3 -0
- package/src/graphql/ast/Schema.js +2 -6
- package/src/graphql/extension/api.js +2 -2
- package/src/graphql/extension/framework.js +5 -3
- package/src/graphql/extension/type.js +1 -1
- package/src/query/Query.js +10 -0
- package/src/query/QueryResolver.js +36 -50
- package/src/query/QueryService.js +8 -23
- package/src/service/app.service.js +31 -6
- package/src/service/event.service.js +37 -6
- package/src/core/GraphQL.js +0 -21
package/CHANGELOG.md
CHANGED
|
@@ -2,27 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
## v0.10.x
|
|
4
4
|
- Replaced ResultSet -> POJOs
|
|
5
|
-
- Removed all
|
|
6
|
-
- Removed all $ magic field methods
|
|
5
|
+
- Removed all $field methods (auto populated)
|
|
7
6
|
- Removed .toObject()
|
|
7
|
+
- $model $save remove $delete $lookup $cursor $pageInfo
|
|
8
8
|
- Removed embedded API completely
|
|
9
9
|
- Removed Directives
|
|
10
10
|
- embedApi -> no replacement
|
|
11
11
|
- enforce -> use pipeline methods
|
|
12
12
|
- resolve -> use graphql resolvers
|
|
13
13
|
- @value -> use @field.instruct directive
|
|
14
|
-
- Removed toId Transform -> use @field(id: '')
|
|
15
14
|
- Removed Model.tform() -> use Model.shapeObject(shape, data)
|
|
16
|
-
- Removed Resolver.toResultSet() -> ? TBD ?
|
|
17
15
|
- Removed Transformer + Rule -> use Pipeline
|
|
18
16
|
- Removed many pre-defined rules + transformers
|
|
19
|
-
- Pre-defined names start with $ (eg. $toLowerCase)
|
|
20
17
|
- Moved "validator" to dev dependency -> isEmail
|
|
21
18
|
- Added QueryBuilder.resolve() terminal command
|
|
22
19
|
- Exported SchemaDecorator -> Schema
|
|
23
20
|
- Removed embedded schema SystemEvents (internal emitter also removed)
|
|
24
21
|
- Removed spread of arguments in QueryBuilder terminal commands (must pass in array)
|
|
25
|
-
-
|
|
22
|
+
- Mutate "merged" instead of "input"
|
|
23
|
+
- Validate "payload"
|
|
26
24
|
|
|
27
25
|
## v0.9.x
|
|
28
26
|
- Subscriptions API
|
package/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const Schema = require('./src/core/Schema');
|
|
2
|
-
const GraphQL = require('./src/core/GraphQL');
|
|
3
2
|
const Resolver = require('./src/core/Resolver');
|
|
4
3
|
const Pipeline = require('./src/data/Pipeline');
|
|
5
4
|
const Driver = require('./src/driver');
|
|
@@ -7,7 +6,6 @@ const { eventEmitter: Emitter } = require('./src/service/event.service');
|
|
|
7
6
|
|
|
8
7
|
module.exports = {
|
|
9
8
|
Schema,
|
|
10
|
-
GraphQL,
|
|
11
9
|
Resolver,
|
|
12
10
|
Driver,
|
|
13
11
|
Emitter,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coderich/autograph",
|
|
3
3
|
"author": "Richard Livolsi (coderich)",
|
|
4
|
-
"version": "0.10.
|
|
4
|
+
"version": "0.10.3",
|
|
5
5
|
"description": "AutoGraph",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"graphql",
|
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@hapi/boom": "^9.1.0",
|
|
34
|
-
"axios": "^0.21.4",
|
|
35
34
|
"dataloader": "^2.0.0",
|
|
36
35
|
"deepmerge": "^4.2.2",
|
|
37
36
|
"fill-range": "^7.0.1",
|
|
@@ -40,12 +39,11 @@
|
|
|
40
39
|
"lodash": "^4.17.21",
|
|
41
40
|
"mongodb": "^4.8.0",
|
|
42
41
|
"object-hash": "^2.0.1",
|
|
43
|
-
"picomatch": "^2.1.1"
|
|
44
|
-
"uuid": "^3.3.3"
|
|
42
|
+
"picomatch": "^2.1.1"
|
|
45
43
|
},
|
|
46
44
|
"devDependencies": {
|
|
47
45
|
"@coderich/ratchet": "^1.5.7",
|
|
48
|
-
"@graphql-tools/schema": "^
|
|
46
|
+
"@graphql-tools/schema": "^9.0.1",
|
|
49
47
|
"graphql": "^15.5.0",
|
|
50
48
|
"mongodb-memory-server": "^8.7.2",
|
|
51
49
|
"neo4j-driver": "^4.0.0",
|
|
@@ -54,6 +52,9 @@
|
|
|
54
52
|
"redis-mock": "^0.47.0",
|
|
55
53
|
"validator": "^13.7.0"
|
|
56
54
|
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"graphql": "*"
|
|
57
|
+
},
|
|
57
58
|
"repository": {
|
|
58
59
|
"type": "git",
|
|
59
60
|
"url": "git@github.com:coderich/autograph.git"
|
package/src/core/Resolver.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
const Model = require('../data/Model');
|
|
2
2
|
const DataLoader = require('../data/DataLoader');
|
|
3
3
|
const DataTransaction = require('../data/DataTransaction');
|
|
4
|
+
const Query = require('../query/Query');
|
|
4
5
|
const QueryBuilder = require('../query/QueryBuilder');
|
|
6
|
+
const { finalizeResults } = require('../data/DataService');
|
|
7
|
+
const { createSystemEvent } = require('../service/event.service');
|
|
5
8
|
|
|
6
9
|
module.exports = class Resolver {
|
|
7
10
|
constructor(schema, context = {}) {
|
|
@@ -58,7 +61,8 @@ module.exports = class Resolver {
|
|
|
58
61
|
case 'create': case 'update': case 'delete': {
|
|
59
62
|
return model.getDriver().resolve(query.toDriver()).then((data) => {
|
|
60
63
|
this.clear(model);
|
|
61
|
-
|
|
64
|
+
const rs = model.shapeObject(model.getShape(), data, query);
|
|
65
|
+
return finalizeResults(rs, query);
|
|
62
66
|
});
|
|
63
67
|
}
|
|
64
68
|
default: {
|
|
@@ -99,25 +103,13 @@ module.exports = class Resolver {
|
|
|
99
103
|
return entity;
|
|
100
104
|
}
|
|
101
105
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// model,
|
|
110
|
-
// crud,
|
|
111
|
-
// method,
|
|
112
|
-
// result,
|
|
113
|
-
// doc,
|
|
114
|
-
// merged,
|
|
115
|
-
// resolver: this,
|
|
116
|
-
// key: `${method}${model}`,
|
|
117
|
-
// context: this.getContext(),
|
|
118
|
-
// query: query.doc(result).merged(result),
|
|
119
|
-
// }, () => result);
|
|
120
|
-
// }
|
|
106
|
+
toResultSet(model, data, method) {
|
|
107
|
+
model = this.toModel(model);
|
|
108
|
+
const query = new Query({ model, resolver: this, context: this.context, method });
|
|
109
|
+
const result = model.deserialize(data, query);
|
|
110
|
+
const event = { result, query, ...query.doc(result).merged(result).toObject() };
|
|
111
|
+
return createSystemEvent('Response', event, () => result);
|
|
112
|
+
}
|
|
121
113
|
|
|
122
114
|
// DataLoader Proxy Methods
|
|
123
115
|
clear(model) {
|
package/src/core/Schema.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
const Model = require('../data/Model');
|
|
2
2
|
const Schema = require('../graphql/ast/Schema');
|
|
3
3
|
const { identifyOnDeletes } = require('../service/schema.service');
|
|
4
|
-
const {
|
|
4
|
+
const { eventEmitter } = require('../service/event.service');
|
|
5
5
|
|
|
6
6
|
// Export class
|
|
7
7
|
module.exports = class extends Schema {
|
|
8
|
-
constructor(schema, stores
|
|
9
|
-
super(schema
|
|
8
|
+
constructor(schema, stores) {
|
|
9
|
+
super(schema);
|
|
10
10
|
|
|
11
11
|
// Create drivers
|
|
12
12
|
this.drivers = Object.entries(stores).reduce((prev, [key, value]) => {
|
|
@@ -23,7 +23,7 @@ module.exports = class extends Schema {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
setup() {
|
|
26
|
-
return
|
|
26
|
+
return eventEmitter.emit('setup', this).then(() => {
|
|
27
27
|
const entities = this.models.filter(m => m.isEntity());
|
|
28
28
|
|
|
29
29
|
// Create model indexes
|
package/src/data/DataService.js
CHANGED
|
@@ -1,27 +1,50 @@
|
|
|
1
1
|
const { get, remove } = require('lodash');
|
|
2
2
|
const { map, isPlainObject, objectContaining, mergeDeep, ensureArray, keyPaths } = require('../service/app.service');
|
|
3
3
|
|
|
4
|
-
exports.
|
|
4
|
+
exports.finalizeResults = (rs, query) => {
|
|
5
|
+
const { model, resolver } = query.toObject();
|
|
6
|
+
|
|
7
|
+
return map(exports.paginateResults(rs, query), (doc) => {
|
|
8
|
+
return Object.defineProperties(doc, {
|
|
9
|
+
$model: { value: model },
|
|
10
|
+
$save: { value: input => resolver.match(model).id(doc.id).save({ ...doc, ...input }) },
|
|
11
|
+
$remove: { value: (...args) => resolver.match(model).id(doc.id).remove(...args) },
|
|
12
|
+
$delete: { value: (...args) => resolver.match(model).id(doc.id).delete(...args) },
|
|
13
|
+
$lookup: { value: (fieldName, args) => model.getFieldByName(fieldName).resolve(resolver, doc, args) },
|
|
14
|
+
// $resolve: { value: (fieldName, args) => model.getFieldByName(fieldName).resolve(resolver, doc, args, true) },
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* This is cursor-style pagination only
|
|
21
|
+
* You add 2 extra records to the result in order to determine previous/next
|
|
22
|
+
*/
|
|
23
|
+
exports.paginateResults = (rs, query) => {
|
|
5
24
|
const { first, after, last, before, sort } = query.toObject();
|
|
6
|
-
const
|
|
7
|
-
|
|
25
|
+
const isPaginating = Boolean(first || last || after || before);
|
|
26
|
+
|
|
27
|
+
// Return right away if not paginating
|
|
28
|
+
if (!isPaginating) return rs;
|
|
29
|
+
|
|
8
30
|
let hasNextPage = false;
|
|
9
31
|
let hasPreviousPage = false;
|
|
32
|
+
const limiter = first || last;
|
|
33
|
+
const sortPaths = keyPaths(sort);
|
|
10
34
|
|
|
11
|
-
// Add
|
|
35
|
+
// Add $cursor data
|
|
12
36
|
map(rs, (doc) => {
|
|
13
37
|
const sortValues = sortPaths.reduce((prv, path) => Object.assign(prv, { [path]: get(doc, path) }), {});
|
|
14
|
-
|
|
15
|
-
doc.$$cursor = Buffer.from(sortJSON).toString('base64');
|
|
38
|
+
Object.defineProperty(doc, '$cursor', { get() { return Buffer.from(JSON.stringify(sortValues)).toString('base64'); } });
|
|
16
39
|
});
|
|
17
40
|
|
|
18
41
|
// First try to take off the "bookends" ($gte | $lte)
|
|
19
|
-
if (rs.length && rs[0]
|
|
42
|
+
if (rs.length && rs[0].$cursor === after) {
|
|
20
43
|
rs.shift();
|
|
21
44
|
hasPreviousPage = true;
|
|
22
45
|
}
|
|
23
46
|
|
|
24
|
-
if (rs.length && rs[rs.length - 1]
|
|
47
|
+
if (rs.length && rs[rs.length - 1].$cursor === before) {
|
|
25
48
|
rs.pop();
|
|
26
49
|
hasNextPage = true;
|
|
27
50
|
}
|
|
@@ -42,18 +65,17 @@ exports.paginateResultSet = (rs, query) => {
|
|
|
42
65
|
}
|
|
43
66
|
}
|
|
44
67
|
|
|
45
|
-
// Add
|
|
68
|
+
// Add $pageInfo
|
|
46
69
|
return Object.defineProperties(rs, {
|
|
47
|
-
|
|
70
|
+
$pageInfo: {
|
|
48
71
|
get() {
|
|
49
72
|
return {
|
|
50
|
-
startCursor: get(rs, '0
|
|
51
|
-
endCursor: get(rs, `${rs.length - 1}
|
|
73
|
+
startCursor: get(rs, '0.$cursor', ''),
|
|
74
|
+
endCursor: get(rs, `${rs.length - 1}.$cursor`, ''),
|
|
52
75
|
hasPreviousPage,
|
|
53
76
|
hasNextPage,
|
|
54
77
|
};
|
|
55
78
|
},
|
|
56
|
-
enumerable: false,
|
|
57
79
|
},
|
|
58
80
|
});
|
|
59
81
|
};
|
package/src/data/Field.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
const { isEmpty } = require('lodash');
|
|
2
2
|
const Type = require('./Type');
|
|
3
3
|
const Field = require('../graphql/ast/Field');
|
|
4
|
-
const Boom = require('../core/Boom');
|
|
5
4
|
const Pipeline = require('./Pipeline');
|
|
6
|
-
const { isPlainObject, ensureArray } = require('../service/app.service');
|
|
7
5
|
|
|
8
6
|
module.exports = class extends Field {
|
|
9
7
|
constructor(model, field) {
|
|
@@ -15,49 +13,34 @@ module.exports = class extends Field {
|
|
|
15
13
|
getStructures() {
|
|
16
14
|
// Grab structures from the underlying type
|
|
17
15
|
const structures = this.type.getStructures();
|
|
18
|
-
const { isRequired, isPersistable, isVirtual,
|
|
16
|
+
const { type, isPrimaryKeyId, isIdField, isRequired, isPersistable, isVirtual, isEmbedded, modelRef } = this.props;
|
|
19
17
|
|
|
20
18
|
// Structures defined on the field
|
|
21
19
|
const $structures = Object.entries(this.getDirectiveArgs('field', {})).reduce((prev, [key, value]) => {
|
|
22
20
|
if (!Array.isArray(value)) value = [value];
|
|
23
|
-
if (key === '
|
|
24
|
-
if (key === '
|
|
25
|
-
if (key === '
|
|
26
|
-
if (key === '
|
|
27
|
-
if (key === '
|
|
28
|
-
if (key === '
|
|
29
|
-
if (key === '
|
|
21
|
+
if (key === 'validate') prev.validators.push(...value.map(t => Pipeline[t]));
|
|
22
|
+
if (key === 'instruct') prev.instructs.push(...value.map(t => Pipeline[t]));
|
|
23
|
+
if (key === 'restruct') prev.restructs.push(...value.map(t => Pipeline[t]));
|
|
24
|
+
if (key === 'destruct') prev.destructs.push(...value.map(t => Pipeline[t]));
|
|
25
|
+
if (key === 'construct') prev.constructs.push(...value.map(t => Pipeline[t]));
|
|
26
|
+
if (key === 'transform') prev.transforms.push(...value.map(t => Pipeline[t]));
|
|
27
|
+
if (key === 'normalize') prev.normalizers.push(...value.map(t => Pipeline[t]));
|
|
28
|
+
if (key === 'serialize') prev.serializers.push(...value.map(t => Pipeline[t]));
|
|
29
|
+
if (key === 'deserialize') prev.deserializers.push(...value.map(t => Pipeline[t]));
|
|
30
30
|
return prev;
|
|
31
31
|
}, structures);
|
|
32
32
|
|
|
33
33
|
// IDs (first - shift)
|
|
34
|
-
if (isPrimaryKeyId) $structures.serializers.unshift(Pipeline.idKey);
|
|
34
|
+
if (isPrimaryKeyId && type === 'ID') $structures.serializers.unshift(Pipeline.idKey);
|
|
35
35
|
if (isIdField) $structures.$serializers.unshift(Pipeline.idField);
|
|
36
36
|
|
|
37
37
|
// Required (last - push)
|
|
38
|
-
if (isRequired && isPersistable && !isVirtual) $structures.
|
|
38
|
+
if (isRequired && isPersistable && !isVirtual) $structures.validators.push(Pipeline.required);
|
|
39
|
+
if (modelRef && !isEmbedded) $structures.validators.push(Pipeline.ensureId);
|
|
39
40
|
|
|
40
41
|
return $structures;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
async validate(query, value) {
|
|
44
|
-
if (value == null) return value;
|
|
45
|
-
const { resolver } = query.toObject();
|
|
46
|
-
const { type, modelRef, isEmbedded } = this.props;
|
|
47
|
-
|
|
48
|
-
if (modelRef && !isEmbedded) {
|
|
49
|
-
const ids = Array.from(new Set(ensureArray(value).map(v => `${v}`)));
|
|
50
|
-
await resolver.match(type).where({ id: ids }).count().then((count) => {
|
|
51
|
-
// if (type === 'Category') console.log(ids, count);
|
|
52
|
-
if (count !== ids.length) throw Boom.notFound(`${type} Not Found`);
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (modelRef && isPlainObject(ensureArray(value)[0])) return modelRef.validate(query, value); // Model delegation
|
|
57
|
-
|
|
58
|
-
return value;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
44
|
resolve(resolver, doc, args = {}) {
|
|
62
45
|
const { name, isArray, isScalar, isVirtual, isRequired, isEmbedded, modelRef, virtualField } = this.props;
|
|
63
46
|
const value = doc[name];
|
package/src/data/Model.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
const Stream = require('stream');
|
|
2
2
|
const Field = require('./Field');
|
|
3
|
-
const Pipeline = require('./Pipeline');
|
|
4
3
|
const Model = require('../graphql/ast/Model');
|
|
5
|
-
const { paginateResultSet } = require('./DataService');
|
|
6
4
|
const { eventEmitter } = require('../service/event.service');
|
|
7
|
-
const {
|
|
5
|
+
const { finalizeResults } = require('./DataService');
|
|
6
|
+
const { map, mapPromise, seek, deseek } = require('../service/app.service');
|
|
8
7
|
|
|
9
8
|
module.exports = class extends Model {
|
|
10
9
|
constructor(schema, model, driver) {
|
|
@@ -48,45 +47,26 @@ module.exports = class extends Model {
|
|
|
48
47
|
return this.referentials;
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
validate(query, data) {
|
|
52
|
-
const { flags = {} } = query.toObject();
|
|
53
|
-
const { validate = true } = flags;
|
|
54
|
-
|
|
55
|
-
if (!validate) return Promise.resolve();
|
|
56
|
-
|
|
57
|
-
return Promise.all(this.getFields().map((field) => {
|
|
58
|
-
return Promise.all(ensureArray(map(data, (obj) => {
|
|
59
|
-
if (obj == null) return Promise.resolve();
|
|
60
|
-
return field.validate(query, obj[field.getKey()]);
|
|
61
|
-
})));
|
|
62
|
-
})).then(() => {
|
|
63
|
-
return eventEmitter.emit('validate', query);
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
50
|
/**
|
|
68
51
|
* Convenience method to deserialize data from a data source (such as a database)
|
|
69
52
|
*/
|
|
70
53
|
deserialize(mixed, query) {
|
|
71
|
-
const { flags = {} } = query.toObject();
|
|
72
|
-
const { pipeline = true } = flags;
|
|
73
54
|
const shape = this.getShape();
|
|
74
55
|
|
|
75
56
|
return new Promise((resolve, reject) => {
|
|
76
57
|
if (!(mixed instanceof Stream)) {
|
|
77
|
-
resolve(
|
|
58
|
+
resolve(this.shapeObject(shape, mixed, query));
|
|
78
59
|
} else {
|
|
79
60
|
const results = [];
|
|
80
|
-
mixed.on('data', (data) => { results.push(
|
|
61
|
+
mixed.on('data', (data) => { results.push(this.shapeObject(shape, data, query)); });
|
|
81
62
|
mixed.on('end', () => { resolve(results); });
|
|
82
63
|
mixed.on('error', reject);
|
|
83
64
|
}
|
|
84
|
-
}).then(
|
|
85
|
-
return results.length && pipeline ? paginateResultSet(results, query) : results;
|
|
86
|
-
});
|
|
65
|
+
}).then(rs => finalizeResults(rs, query));
|
|
87
66
|
}
|
|
88
67
|
|
|
89
68
|
getShape(crud = 'read', target = 'doc', paths = []) {
|
|
69
|
+
// Cache check
|
|
90
70
|
const cacheKey = `${crud}:${target}`;
|
|
91
71
|
if (this.shapesCache.has(cacheKey)) return this.shapesCache.get(cacheKey);
|
|
92
72
|
|
|
@@ -95,33 +75,39 @@ module.exports = class extends Model {
|
|
|
95
75
|
const crudMap = { create: ['constructs'], update: ['restructs'], delete: ['destructs'], remove: ['destructs'] };
|
|
96
76
|
const crudKeys = crudMap[crud] || [];
|
|
97
77
|
|
|
78
|
+
// Define target mapping
|
|
98
79
|
const targetMap = {
|
|
99
|
-
doc: ['defaultValue', 'ensureArrayValue', '
|
|
100
|
-
|
|
80
|
+
doc: ['defaultValue', 'castValue', 'ensureArrayValue', 'normalizers', 'instructs', ...crudKeys, `$${serdes}rs`, `${serdes}rs`, 'transforms'],
|
|
81
|
+
input: ['defaultValue', 'castValue', 'ensureArrayValue', 'normalizers', 'instructs', ...crudKeys, `$${serdes}rs`, `${serdes}rs`, 'transforms'],
|
|
82
|
+
// input: ['defaultValue', 'castValue', 'ensureArrayValue'],
|
|
83
|
+
where: ['castValue', 'instructs', `$${serdes}rs`],
|
|
101
84
|
};
|
|
102
|
-
|
|
103
85
|
const structureKeys = targetMap[target] || ['castValue'];
|
|
104
86
|
|
|
105
87
|
// Create shape, recursive
|
|
106
88
|
const shape = fields.map((field) => {
|
|
89
|
+
let instructed = false;
|
|
107
90
|
const structures = field.getStructures();
|
|
108
|
-
const
|
|
91
|
+
const { key, name, type, isArray, isEmbedded, modelRef } = field.toObject();
|
|
109
92
|
const [from, to] = serdes === 'serialize' ? [name, key] : [key, name];
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
93
|
+
const actualTo = target === 'input' || target === 'splice' ? from : to;
|
|
94
|
+
const path = paths.concat(actualTo);
|
|
95
|
+
const subCrud = crud === 'update' && isArray ? 'create' : crud; // Due to limitation to update embedded array
|
|
96
|
+
const subShape = isEmbedded ? modelRef.getShape(subCrud, target, path) : null;
|
|
97
|
+
const transformers = structureKeys.reduce((prev, struct) => {
|
|
98
|
+
if (instructed) return prev;
|
|
99
|
+
const structs = structures[struct];
|
|
100
|
+
if (struct === 'instructs' && structs.length) instructed = true;
|
|
101
|
+
return prev.concat(structs);
|
|
102
|
+
}, []).filter(Boolean);
|
|
103
|
+
return { instructed, field, path, from, to: actualTo, type, isArray, transformers, validators: structures.validators, shape: subShape };
|
|
119
104
|
});
|
|
120
105
|
|
|
121
106
|
// Adding useful shape info
|
|
122
107
|
shape.crud = crud;
|
|
123
108
|
shape.model = this;
|
|
124
109
|
shape.serdes = serdes;
|
|
110
|
+
shape.target = target;
|
|
125
111
|
|
|
126
112
|
// Cache and return
|
|
127
113
|
this.shapesCache.set(cacheKey, shape);
|
|
@@ -130,7 +116,11 @@ module.exports = class extends Model {
|
|
|
130
116
|
|
|
131
117
|
shapeObject(shape, obj, query, root) {
|
|
132
118
|
const { serdes, model } = shape;
|
|
133
|
-
const { context, doc = {} } = query.toObject();
|
|
119
|
+
const { context, resolver, doc = {}, flags = {} } = query.toObject();
|
|
120
|
+
const { pipeline } = flags;
|
|
121
|
+
|
|
122
|
+
if (!pipeline) return obj;
|
|
123
|
+
// const filters = pipeline === true ? [] : Object.entries(pipeline).map(([k, v]) => (v === false ? k : null)).filter(Boolean);
|
|
134
124
|
|
|
135
125
|
return map(obj, (parent) => {
|
|
136
126
|
// "root" is the base of the object
|
|
@@ -141,19 +131,19 @@ module.exports = class extends Model {
|
|
|
141
131
|
const rootPath = (p, hint) => (serdes === 'serialize' ? seek(root, p, hint) : deseek(shape, root, p, hint));
|
|
142
132
|
const parentPath = (p, hint) => (serdes === 'serialize' ? seek(parent, p, hint) : deseek(shape, parent, p, hint));
|
|
143
133
|
|
|
144
|
-
return shape.reduce((prev, { field, from, to, path, type, isArray, defaultValue, transformers = [], shape: subShape }) => {
|
|
134
|
+
return shape.reduce((prev, { instructed, field, from, to, path, type, isArray, defaultValue, transformers = [], shape: subShape }) => {
|
|
145
135
|
const startValue = parent[from];
|
|
136
|
+
// transformers = filters.length ? transformers.filter() : transformers;
|
|
146
137
|
|
|
147
138
|
// Transform value
|
|
148
139
|
const transformedValue = transformers.reduce((value, t) => {
|
|
149
|
-
const v = t({ model, field, path, docPath, rootPath, parentPath, startValue, value, context });
|
|
140
|
+
const v = t({ model, field, path, docPath, rootPath, parentPath, startValue, value, resolver, context });
|
|
150
141
|
return v === undefined ? value : v;
|
|
151
142
|
}, startValue);
|
|
152
143
|
|
|
153
|
-
// if (`${field}` === 'searchability') console.log(startValue, transformedValue, transformers);
|
|
154
|
-
|
|
155
144
|
// Determine if key should stay or be removed
|
|
156
|
-
if (transformedValue === undefined && !Object.prototype.hasOwnProperty.call(parent, from)) return prev;
|
|
145
|
+
if (!instructed && transformedValue === undefined && !Object.prototype.hasOwnProperty.call(parent, from)) return prev;
|
|
146
|
+
if (!instructed && subShape && typeof transformedValue !== 'object') return prev;
|
|
157
147
|
|
|
158
148
|
// Rename key & assign value
|
|
159
149
|
prev[to] = (!subShape || transformedValue == null) ? transformedValue : this.shapeObject(subShape, transformedValue, query, root);
|
|
@@ -162,4 +152,32 @@ module.exports = class extends Model {
|
|
|
162
152
|
}, {});
|
|
163
153
|
});
|
|
164
154
|
}
|
|
155
|
+
|
|
156
|
+
validateObject(shape, obj, query, root, silent = false) {
|
|
157
|
+
const { model } = shape;
|
|
158
|
+
const { context, resolver, doc = {}, flags = {} } = query.toObject();
|
|
159
|
+
const { validate = true } = flags;
|
|
160
|
+
|
|
161
|
+
if (!validate) return Promise.resolve();
|
|
162
|
+
|
|
163
|
+
return mapPromise(obj, (parent) => {
|
|
164
|
+
// "root" is the base of the object
|
|
165
|
+
root = root || parent;
|
|
166
|
+
|
|
167
|
+
// Lookup helper functions
|
|
168
|
+
const docPath = (p, hint) => seek(doc, p, hint);
|
|
169
|
+
const rootPath = (p, hint) => seek(root, p, hint);
|
|
170
|
+
const parentPath = (p, hint) => seek(parent, p, hint);
|
|
171
|
+
|
|
172
|
+
return Promise.all(shape.map(({ field, from, path, validators, shape: subShape }) => {
|
|
173
|
+
const value = parent[from]; // It hasn't been shaped yet
|
|
174
|
+
|
|
175
|
+
return Promise.all(validators.map(v => v({ model, field, path, docPath, rootPath, parentPath, startValue: value, value, resolver, context }))).then(() => {
|
|
176
|
+
return subShape ? this.validateObject(subShape, value, query, root, true) : Promise.resolve();
|
|
177
|
+
});
|
|
178
|
+
}));
|
|
179
|
+
}).then(() => {
|
|
180
|
+
return silent ? Promise.resolve() : eventEmitter.emit('validate', query.toObject());
|
|
181
|
+
});
|
|
182
|
+
}
|
|
165
183
|
};
|
package/src/data/Pipeline.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const { uniqWith } = require('lodash');
|
|
2
|
-
const { map, hashObject } = require('../service/app.service');
|
|
2
|
+
const { map, ensureArray, hashObject } = require('../service/app.service');
|
|
3
3
|
const Boom = require('../core/Boom');
|
|
4
4
|
|
|
5
5
|
module.exports = class Pipeline {
|
|
@@ -29,17 +29,17 @@ module.exports = class Pipeline {
|
|
|
29
29
|
}, 'name', { value: name });
|
|
30
30
|
|
|
31
31
|
// Attach enumerable method to the Pipeline
|
|
32
|
-
Object.defineProperty(Pipeline, name, {
|
|
32
|
+
return Object.defineProperty(Pipeline, name, {
|
|
33
33
|
value: wrapper,
|
|
34
34
|
configurable,
|
|
35
35
|
enumerable: true,
|
|
36
|
-
});
|
|
36
|
+
})[name];
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
static factory(name, thunk, options = {}) {
|
|
40
40
|
if (typeof thunk !== 'function') throw new Error(`Pipeline factory for "${name}" must be a thunk`);
|
|
41
41
|
if (typeof thunk() !== 'function') throw new Error(`Factory thunk() for "${name}" must return a function`);
|
|
42
|
-
Object.defineProperty(Pipeline, name, { value: Object.defineProperty(thunk, 'options', { value: options }) });
|
|
42
|
+
return Object.defineProperty(Pipeline, name, { value: (...args) => Object.defineProperty(thunk(...args), 'options', { value: options }) })[name];
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// static wrapper(name, factory, { ignoreNull, itemize }) {
|
|
@@ -66,13 +66,24 @@ module.exports = class Pipeline {
|
|
|
66
66
|
// Additional Transformers
|
|
67
67
|
Pipeline.define('toTitleCase', ({ value }) => value.replace(/\w\S*/g, w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()));
|
|
68
68
|
Pipeline.define('toSentenceCase', ({ value }) => value.charAt(0).toUpperCase() + value.slice(1));
|
|
69
|
+
Pipeline.define('toId', ({ model, value }) => model.idValue(value));
|
|
69
70
|
Pipeline.define('toArray', ({ value }) => (Array.isArray(value) ? value : [value]), { itemize: false });
|
|
70
71
|
Pipeline.define('toDate', ({ value }) => new Date(value), { configurable: true });
|
|
71
72
|
Pipeline.define('timestamp', ({ value }) => Date.now(), { ignoreNull: false });
|
|
72
73
|
Pipeline.define('createdAt', ({ value }) => value || Date.now(), { ignoreNull: false });
|
|
73
74
|
Pipeline.define('dedupe', ({ value }) => uniqWith(value, (b, c) => hashObject(b) === hashObject(c)), { itemize: false });
|
|
74
75
|
Pipeline.define('idKey', ({ model, value }) => (value == null ? model.idValue() : value), { ignoreNull: false });
|
|
75
|
-
Pipeline.define('idField', ({ field, value }) =>
|
|
76
|
+
Pipeline.define('idField', ({ model, field, value }) => field.getIdModel().idValue(value.id || value));
|
|
77
|
+
Pipeline.define('ensureArrayValue', ({ field, value }) => (field.toObject().isArray && !Array.isArray(value) ? [value] : value), { itemize: false });
|
|
78
|
+
|
|
79
|
+
Pipeline.define('ensureId', ({ resolver, field, value }) => {
|
|
80
|
+
const { type } = field.toObject();
|
|
81
|
+
const ids = Array.from(new Set(ensureArray(value).map(v => `${v}`)));
|
|
82
|
+
|
|
83
|
+
return resolver.match(type).where({ id: ids }).count().then((count) => {
|
|
84
|
+
if (count !== ids.length) throw Boom.notFound(`${type} Not Found`);
|
|
85
|
+
});
|
|
86
|
+
}, { itemize: false });
|
|
76
87
|
|
|
77
88
|
Pipeline.define('defaultValue', ({ field, value }) => {
|
|
78
89
|
const { defaultValue } = field.toObject();
|
|
@@ -117,7 +128,7 @@ module.exports = class Pipeline {
|
|
|
117
128
|
}, { ignoreNull: false });
|
|
118
129
|
|
|
119
130
|
// A field cannot hold a reference to itself
|
|
120
|
-
Pipeline.define('selfless', ({ model, field, parentPath, value }) => {
|
|
131
|
+
Pipeline.define('selfless', ({ model, field, parent, parentPath, value }) => {
|
|
121
132
|
if (`${value}` === `${parentPath('id')}`) throw Boom.badRequest(`${model}.${field} cannot hold a reference to itself`);
|
|
122
133
|
});
|
|
123
134
|
|
|
@@ -129,17 +140,17 @@ module.exports = class Pipeline {
|
|
|
129
140
|
});
|
|
130
141
|
|
|
131
142
|
// List of allowed values
|
|
132
|
-
Pipeline.factory('
|
|
143
|
+
Pipeline.factory('Allow', (...args) => function allow({ model, field, value }) {
|
|
133
144
|
if (args.indexOf(value) === -1) throw Boom.badRequest(`${model}.${field} allows ${args}; found '${value}'`);
|
|
134
145
|
});
|
|
135
146
|
|
|
136
147
|
// List of disallowed values
|
|
137
|
-
Pipeline.factory('
|
|
148
|
+
Pipeline.factory('Deny', (...args) => function deny({ model, field, value }) {
|
|
138
149
|
if (args.indexOf(value) > -1) throw Boom.badRequest(`${model}.${field} denys ${args}; found '${value}'`);
|
|
139
150
|
});
|
|
140
151
|
|
|
141
152
|
// Min/Max range
|
|
142
|
-
Pipeline.factory('
|
|
153
|
+
Pipeline.factory('Range', (min, max) => {
|
|
143
154
|
if (min == null) min = undefined;
|
|
144
155
|
if (max == null) max = undefined;
|
|
145
156
|
|
|
@@ -152,7 +163,6 @@ module.exports = class Pipeline {
|
|
|
152
163
|
}
|
|
153
164
|
};
|
|
154
165
|
|
|
155
|
-
|
|
156
166
|
// const jsStringMethods = [
|
|
157
167
|
// 'charAt', 'charCodeAt', 'codePointAt', 'concat', 'indexOf', 'lastIndexOf', 'localeCompare',
|
|
158
168
|
// 'normalize', 'padEnd', 'padStart', 'repeat', 'replace', 'search', 'slice', 'split', 'substr', 'substring',
|
package/src/data/Type.js
CHANGED
|
@@ -11,20 +11,39 @@ module.exports = class extends Type {
|
|
|
11
11
|
const type = this.field.getType();
|
|
12
12
|
const enumType = this.field.getEnumRef();
|
|
13
13
|
const scalarType = this.field.getScalarRef();
|
|
14
|
-
const structures = {
|
|
14
|
+
const structures = {
|
|
15
|
+
validators: [],
|
|
16
|
+
instructs: [],
|
|
17
|
+
restructs: [],
|
|
18
|
+
destructs: [],
|
|
19
|
+
constructs: [],
|
|
20
|
+
normalizers: [],
|
|
21
|
+
$serializers: [],
|
|
22
|
+
$deserializers: [],
|
|
23
|
+
serializers: [],
|
|
24
|
+
deserializers: [],
|
|
25
|
+
transforms: [],
|
|
26
|
+
};
|
|
15
27
|
|
|
16
|
-
|
|
28
|
+
// Built-in pipelines
|
|
29
|
+
structures.castValue = Pipeline.castValue;
|
|
30
|
+
structures.defaultValue = Pipeline.defaultValue;
|
|
31
|
+
structures.ensureArrayValue = Pipeline.ensureArrayValue;
|
|
32
|
+
|
|
33
|
+
if (enumType) structures.validators.push(Pipeline.define(`allow:${type}`, Pipeline.Allow(...enumType.getValue()), { configurable: true }));
|
|
17
34
|
if (!scalarType) return structures;
|
|
18
35
|
|
|
19
36
|
return Object.entries(scalarType.getDirectiveArgs('field', {})).reduce((prev, [key, value]) => {
|
|
20
37
|
if (!Array.isArray(value)) value = [value];
|
|
38
|
+
if (key === 'validate') prev.validators.push(...value.map(t => Pipeline[t]));
|
|
21
39
|
if (key === 'instruct') prev.instructs.push(...value.map(t => Pipeline[t]));
|
|
22
40
|
if (key === 'restruct') prev.restructs.push(...value.map(t => Pipeline[t]));
|
|
23
41
|
if (key === 'destruct') prev.destructs.push(...value.map(t => Pipeline[t]));
|
|
24
42
|
if (key === 'construct') prev.constructs.push(...value.map(t => Pipeline[t]));
|
|
43
|
+
if (key === 'transform') prev.transforms.push(...value.map(t => Pipeline[t]));
|
|
44
|
+
if (key === 'normalize') prev.normalizers.push(...value.map(t => Pipeline[t]));
|
|
25
45
|
if (key === 'serialize') prev.serializers.push(...value.map(t => Pipeline[t]));
|
|
26
46
|
if (key === 'deserialize') prev.deserializers.push(...value.map(t => Pipeline[t]));
|
|
27
|
-
if (key === 'transform') prev.transformers.push(...value.map(t => Pipeline[t]));
|
|
28
47
|
return prev;
|
|
29
48
|
}, structures);
|
|
30
49
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const Util = require('util');
|
|
2
2
|
const { get } = require('lodash');
|
|
3
|
-
const { MongoClient,
|
|
3
|
+
const { MongoClient, ObjectId } = require('mongodb');
|
|
4
4
|
const { map, ensureArray, proxyDeep, toKeyObj, globToRegex, proxyPromise, isScalarDataType, promiseRetry } = require('../service/app.service');
|
|
5
5
|
|
|
6
6
|
module.exports = class MongoDriver {
|
|
@@ -126,10 +126,10 @@ module.exports = class MongoDriver {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
static idValue(value) {
|
|
129
|
-
if (value instanceof
|
|
129
|
+
if (value instanceof ObjectId) return value;
|
|
130
130
|
|
|
131
131
|
try {
|
|
132
|
-
const id =
|
|
132
|
+
const id = ObjectId(value);
|
|
133
133
|
return id;
|
|
134
134
|
} catch (e) {
|
|
135
135
|
return value;
|
|
@@ -142,7 +142,10 @@ module.exports = class MongoDriver {
|
|
|
142
142
|
const value = Reflect.get(target, prop, rec);
|
|
143
143
|
if (typeof value === 'function') return value.bind(target);
|
|
144
144
|
const $value = map(value, v => (typeof v === 'string' ? globToRegex(v, { nocase: true, regex: true }) : v));
|
|
145
|
-
if (Array.isArray($value))
|
|
145
|
+
if (Array.isArray($value)) {
|
|
146
|
+
// console.log(Util.inspect({ value, $value }, { depth: null, showHidden: false, colors: true }));
|
|
147
|
+
return { $in: $value };
|
|
148
|
+
}
|
|
146
149
|
return $value;
|
|
147
150
|
},
|
|
148
151
|
}).toObject();
|
package/src/graphql/ast/Field.js
CHANGED
|
@@ -175,8 +175,10 @@ module.exports = class Field extends Node {
|
|
|
175
175
|
|
|
176
176
|
initialize() {
|
|
177
177
|
this.props = {
|
|
178
|
+
key: this.getKey(),
|
|
178
179
|
name: this.getName(),
|
|
179
180
|
type: this.getType(),
|
|
181
|
+
model: this.model,
|
|
180
182
|
datatype: this.getDataType(),
|
|
181
183
|
defaultValue: this.getDefaultValue(),
|
|
182
184
|
isArray: this.isArray(),
|
|
@@ -187,6 +189,7 @@ module.exports = class Field extends Node {
|
|
|
187
189
|
isIdField: this.isIdField(),
|
|
188
190
|
isPrimaryKeyId: this.isPrimaryKeyId(),
|
|
189
191
|
isPersistable: this.isPersistable(),
|
|
192
|
+
idModel: this.getIdModel(),
|
|
190
193
|
modelRef: this.getModelRef(),
|
|
191
194
|
virtualRef: this.getVirtualRef(),
|
|
192
195
|
virtualField: this.getVirtualField(),
|
|
@@ -24,9 +24,8 @@ const Node = require('./Node');
|
|
|
24
24
|
*
|
|
25
25
|
*/
|
|
26
26
|
module.exports = class Schema extends TypeDefApi {
|
|
27
|
-
constructor(schema
|
|
27
|
+
constructor(schema) {
|
|
28
28
|
super();
|
|
29
|
-
this.toExecutableSchema = toExecutableSchema;
|
|
30
29
|
this.schema = { typeDefs: [], resolvers: {}, schemaDirectives: {} };
|
|
31
30
|
if (schema) this.mergeSchema(schema);
|
|
32
31
|
}
|
|
@@ -121,13 +120,10 @@ module.exports = class Schema extends TypeDefApi {
|
|
|
121
120
|
});
|
|
122
121
|
|
|
123
122
|
this.schema.typeDefs = { kind: Kind.DOCUMENT, definitions };
|
|
123
|
+
// validateSchema(this.schema);
|
|
124
124
|
return this;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
makeExecutableSchema() {
|
|
128
|
-
return this.toExecutableSchema(this.schema);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
127
|
toObject() {
|
|
132
128
|
return this.schema;
|
|
133
129
|
}
|
|
@@ -163,8 +163,8 @@ module.exports = (schema) => {
|
|
|
163
163
|
[modelName]: fieldResolvers,
|
|
164
164
|
[`${modelName}Connection`]: {
|
|
165
165
|
count: ({ count }) => count(),
|
|
166
|
-
edges: ({ edges }) => edges().then(rs => rs.map(node => ({ cursor: get(node, '
|
|
167
|
-
pageInfo: ({ pageInfo }) => pageInfo().then(rs => get(rs, '
|
|
166
|
+
edges: ({ edges }) => edges().then(rs => rs.map(node => ({ cursor: get(node, '$cursor'), node }))),
|
|
167
|
+
pageInfo: ({ pageInfo }) => pageInfo().then(rs => get(rs, '$pageInfo')),
|
|
168
168
|
},
|
|
169
169
|
});
|
|
170
170
|
}, {
|
|
@@ -31,11 +31,11 @@ module.exports = (schema) => {
|
|
|
31
31
|
|
|
32
32
|
directive @field(
|
|
33
33
|
id: String # Specify the ModelRef this field FK References
|
|
34
|
+
ref: AutoGraphMixed # Specify the modelRef field's name (overrides isEmbedded)
|
|
34
35
|
key: String # Specify db key
|
|
35
36
|
persist: Boolean # Persist this field (default true)
|
|
36
37
|
connection: Boolean # Treat this field as a connection type (default false - rolling this out slowly)
|
|
37
38
|
default: AutoGraphMixed # Define a default value
|
|
38
|
-
ref: AutoGraphMixed # Specify the modelRef field's name (overrides isEmbedded)
|
|
39
39
|
gqlScope: AutoGraphMixed # Dictate how GraphQL API behaves
|
|
40
40
|
dalScope: AutoGraphMixed # Dictate how the DAL behaves
|
|
41
41
|
fieldScope: AutoGraphMixed # Dictate how a FIELD may use me
|
|
@@ -44,13 +44,15 @@ module.exports = (schema) => {
|
|
|
44
44
|
authz: AutoGraphAuthzEnum # Access level used for authorization (default: private)
|
|
45
45
|
|
|
46
46
|
# Pipeline Structure
|
|
47
|
+
validate: [AutoGraphPipelineEnum!]
|
|
47
48
|
instruct: [AutoGraphPipelineEnum!]
|
|
48
|
-
destruct: [AutoGraphPipelineEnum!]
|
|
49
49
|
restruct: [AutoGraphPipelineEnum!]
|
|
50
|
+
destruct: [AutoGraphPipelineEnum!]
|
|
50
51
|
construct: [AutoGraphPipelineEnum!]
|
|
52
|
+
transform: [AutoGraphPipelineEnum!]
|
|
53
|
+
normalize: [AutoGraphPipelineEnum!]
|
|
51
54
|
serialize: [AutoGraphPipelineEnum!]
|
|
52
55
|
deserialize: [AutoGraphPipelineEnum!]
|
|
53
|
-
transform: [AutoGraphPipelineEnum!]
|
|
54
56
|
) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION | SCALAR
|
|
55
57
|
|
|
56
58
|
directive @link(
|
|
@@ -17,7 +17,7 @@ module.exports = (schema) => {
|
|
|
17
17
|
return `
|
|
18
18
|
extend type ${modelName}${interfacesGQL} {
|
|
19
19
|
${id ? `id: ID! @field(key: "${id}", gqlScope: r)` : ''}
|
|
20
|
-
${createdAt ? `createdAt: AutoGraphDateTime @field(key: "${createdAt}",
|
|
20
|
+
${createdAt ? `createdAt: AutoGraphDateTime @field(key: "${createdAt}", construct: createdAt, gqlScope: r)` : ''}
|
|
21
21
|
${updatedAt ? `updatedAt: AutoGraphDateTime @field(key: "${updatedAt}", serialize: timestamp, gqlScope: r)` : ''}
|
|
22
22
|
}
|
|
23
23
|
`;
|
package/src/query/Query.js
CHANGED
|
@@ -272,6 +272,16 @@ module.exports = class Query {
|
|
|
272
272
|
return this;
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
payload(payload) {
|
|
276
|
+
this.props.payload = payload;
|
|
277
|
+
return this;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
result(result) {
|
|
281
|
+
this.props.result = result;
|
|
282
|
+
return this;
|
|
283
|
+
}
|
|
284
|
+
|
|
275
285
|
$doc($doc) {
|
|
276
286
|
this.props.$doc = $doc;
|
|
277
287
|
return this;
|
|
@@ -16,7 +16,7 @@ module.exports = class QueryResolver {
|
|
|
16
16
|
const { args } = query.toObject();
|
|
17
17
|
const [,,, info] = args;
|
|
18
18
|
|
|
19
|
-
switch (getGQLReturnType(info.returnType)) {
|
|
19
|
+
switch (getGQLReturnType(`${info.returnType}`)) {
|
|
20
20
|
case 'array': {
|
|
21
21
|
return new QueryResolver(this.query.clone().method('findMany')).resolve();
|
|
22
22
|
}
|
|
@@ -36,42 +36,35 @@ module.exports = class QueryResolver {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return createSystemEvent('Query', { query }, () => {
|
|
39
|
+
findOne(query) {
|
|
40
|
+
return createSystemEvent('Query', { query }, async () => {
|
|
43
41
|
return this.resolver.resolve(query);
|
|
44
42
|
});
|
|
45
43
|
}
|
|
46
44
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return createSystemEvent('Query', { query }, () => {
|
|
45
|
+
findMany(query) {
|
|
46
|
+
return createSystemEvent('Query', { query }, async () => {
|
|
51
47
|
return this.resolver.resolve(query);
|
|
52
48
|
});
|
|
53
49
|
}
|
|
54
50
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return createSystemEvent('Query', { query }, () => {
|
|
51
|
+
count(query) {
|
|
52
|
+
return createSystemEvent('Query', { query }, async () => {
|
|
59
53
|
return this.resolver.resolve(query);
|
|
60
54
|
});
|
|
61
55
|
}
|
|
62
56
|
|
|
63
|
-
|
|
57
|
+
createOne(query) {
|
|
64
58
|
const { model, input } = query.toObject();
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
query.
|
|
74
|
-
return doc;
|
|
59
|
+
const inputShape = model.getShape('create', 'input');
|
|
60
|
+
const docShape = model.getShape('create', 'doc');
|
|
61
|
+
const doc = model.shapeObject(inputShape, {}, query); // We use input shape here
|
|
62
|
+
const merged = mergeDeep(doc, input);
|
|
63
|
+
|
|
64
|
+
return createSystemEvent('Mutation', { query: query.doc(doc).merged(merged) }, async () => {
|
|
65
|
+
const payload = model.shapeObject(inputShape, merged, query);
|
|
66
|
+
await model.validateObject(inputShape, payload, query.payload(payload));
|
|
67
|
+
return this.resolver.resolve(query.$input(model.shapeObject(docShape, payload, query)));
|
|
75
68
|
});
|
|
76
69
|
}
|
|
77
70
|
|
|
@@ -84,17 +77,16 @@ module.exports = class QueryResolver {
|
|
|
84
77
|
|
|
85
78
|
async updateOne(query) {
|
|
86
79
|
const { model, match, input } = query.toObject();
|
|
80
|
+
const inputShape = model.getShape('update', 'input');
|
|
81
|
+
const docShape = model.getShape('update', 'doc');
|
|
87
82
|
|
|
88
|
-
return this.resolver.match(model).match(match).one({ required: true }).then(
|
|
89
|
-
const
|
|
90
|
-
const merged = model.shapeObject(shape, mergeDeep(doc, input), query);
|
|
91
|
-
|
|
92
|
-
await QueryService.resolveQuery(query);
|
|
83
|
+
return this.resolver.match(model).match(match).one({ required: true }).then((doc) => {
|
|
84
|
+
const merged = mergeDeep(doc, input);
|
|
93
85
|
|
|
94
86
|
return createSystemEvent('Mutation', { query: query.doc(doc).merged(merged) }, async () => {
|
|
95
|
-
const
|
|
96
|
-
await model.
|
|
97
|
-
return this.resolver.resolve(query.$doc(
|
|
87
|
+
const payload = model.shapeObject(inputShape, merged, query);
|
|
88
|
+
await model.validateObject(inputShape, payload, query.payload(payload));
|
|
89
|
+
return this.resolver.resolve(query.$doc(model.shapeObject(docShape, payload, query)));
|
|
98
90
|
});
|
|
99
91
|
});
|
|
100
92
|
}
|
|
@@ -110,11 +102,9 @@ module.exports = class QueryResolver {
|
|
|
110
102
|
}
|
|
111
103
|
|
|
112
104
|
deleteOne(query) {
|
|
113
|
-
const { model, id
|
|
114
|
-
|
|
115
|
-
return this.resolver.match(model).id(id).flags(flags).one({ required: true }).then(async (doc) => {
|
|
116
|
-
await QueryService.resolveQuery(query);
|
|
105
|
+
const { model, id } = query.toObject();
|
|
117
106
|
|
|
107
|
+
return this.resolver.match(model).id(id).one({ required: true }).then(async (doc) => {
|
|
118
108
|
return createSystemEvent('Mutation', { query: query.doc(doc) }, () => {
|
|
119
109
|
return QueryService.resolveReferentialIntegrity(query).then(() => {
|
|
120
110
|
return this.resolver.resolve(query).then(() => doc);
|
|
@@ -186,7 +176,10 @@ module.exports = class QueryResolver {
|
|
|
186
176
|
}
|
|
187
177
|
|
|
188
178
|
splice(query) {
|
|
189
|
-
const { model, match, args
|
|
179
|
+
const { model, match, args } = query.toObject();
|
|
180
|
+
const docShape = model.getShape('update', 'doc');
|
|
181
|
+
const inputShape = model.getShape('update', 'input');
|
|
182
|
+
const spliceShape = model.getShape('update', 'splice');
|
|
190
183
|
const [key, from, to] = args;
|
|
191
184
|
|
|
192
185
|
// Can only splice arrays
|
|
@@ -194,20 +187,16 @@ module.exports = class QueryResolver {
|
|
|
194
187
|
const isArray = field.isArray();
|
|
195
188
|
if (!isArray) throw Boom.badRequest(`Cannot splice field '${model}.${field}'`);
|
|
196
189
|
|
|
197
|
-
return this.resolver.match(model).match(match).
|
|
190
|
+
return this.resolver.match(model).match(match).one({ required: true }).then(async (doc) => {
|
|
198
191
|
const array = get(doc, key) || [];
|
|
199
|
-
const
|
|
200
|
-
const $
|
|
201
|
-
const $from = model.shapeObject(paramShape, { [key]: from }, query)[key] || from;
|
|
192
|
+
const $to = model.shapeObject(spliceShape, { [key]: to }, query)[key] || to;
|
|
193
|
+
const $from = model.shapeObject(spliceShape, { [key]: from }, query)[key] || from;
|
|
202
194
|
set(doc, key, DataService.spliceEmbeddedArray(array, $from, $to));
|
|
203
195
|
|
|
204
|
-
await QueryService.resolveQuery(query);
|
|
205
|
-
|
|
206
196
|
return createSystemEvent('Mutation', { query: query.method('updateOne').doc(doc).merged(doc) }, async () => {
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
return this.resolver.resolve(query.$doc($doc));
|
|
197
|
+
const payload = model.shapeObject(inputShape, doc, query);
|
|
198
|
+
await model.validateObject(inputShape, payload, query.payload(payload));
|
|
199
|
+
return this.resolver.resolve(query.$doc(model.shapeObject(docShape, payload, query)));
|
|
211
200
|
});
|
|
212
201
|
});
|
|
213
202
|
}
|
|
@@ -223,9 +212,6 @@ module.exports = class QueryResolver {
|
|
|
223
212
|
async resolve() {
|
|
224
213
|
const { model, method, flags } = this.query.toObject();
|
|
225
214
|
|
|
226
|
-
// const resolveQueryMethods = ['findOne', 'findMany', 'count', 'createOne', 'updateOne', 'deleteOne', 'splice'];
|
|
227
|
-
// if (resolveQueryMethods.indexOf(method) > -1) await QueryService.resolveQuery(this.query);
|
|
228
|
-
|
|
229
215
|
return this[method](this.query).then((data) => {
|
|
230
216
|
if (flags.required && isEmpty(data)) throw Boom.notFound(`${model} Not Found`);
|
|
231
217
|
if (data == null) return null; // Explicitly return null here
|
|
@@ -6,7 +6,7 @@ const { keyPaths, ensureArray, isPlainObject } = require('../service/app.service
|
|
|
6
6
|
* This can happen because the where clause reaches into the schema via refs/virtual refs
|
|
7
7
|
*/
|
|
8
8
|
exports.resolveWhereClause = (query) => {
|
|
9
|
-
const { resolver, model, match: where = {}
|
|
9
|
+
const { resolver, model, match: where = {} } = query.toObject();
|
|
10
10
|
const shape = model.getShape('create', 'where');
|
|
11
11
|
|
|
12
12
|
const $where = Object.entries(where).reduce((prev, [from, value]) => {
|
|
@@ -16,12 +16,12 @@ exports.resolveWhereClause = (query) => {
|
|
|
16
16
|
const { isVirtual, isEmbedded, modelRef, virtualRef } = el.field.toObject();
|
|
17
17
|
|
|
18
18
|
if (isVirtual) {
|
|
19
|
-
const ids = Promise.all(ensureArray(value).map(v => resolver.match(modelRef).where(isPlainObject(v) ? v : { id: v }).many(
|
|
19
|
+
const ids = Promise.all(ensureArray(value).map(v => resolver.match(modelRef).where(isPlainObject(v) ? v : { id: v }).many().then(docs => docs.map(doc => doc[virtualRef])))).then(results => uniq(flattenDeep(results)));
|
|
20
20
|
return Object.assign(prev, { id: ids });
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
if (modelRef && !isEmbedded) {
|
|
24
|
-
const ids = Promise.all(ensureArray(value).map(v => (isPlainObject(v) ? resolver.match(modelRef).where(v).many(
|
|
24
|
+
const ids = Promise.all(ensureArray(value).map(v => (isPlainObject(v) ? resolver.match(modelRef).where(v).many().then(docs => docs.map(doc => doc.id)) : Promise.resolve(v)))).then(results => uniq(flattenDeep(results)));
|
|
25
25
|
return Object.assign(prev, { [from]: ids });
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -67,7 +67,7 @@ exports.resolveSortBy = (query) => {
|
|
|
67
67
|
};
|
|
68
68
|
|
|
69
69
|
exports.resolveReferentialIntegrity = (query) => {
|
|
70
|
-
const { id, model, resolver, transaction
|
|
70
|
+
const { id, model, resolver, transaction } = query.toObject();
|
|
71
71
|
const txn = resolver.transaction(transaction);
|
|
72
72
|
|
|
73
73
|
return new Promise((resolve, reject) => {
|
|
@@ -79,18 +79,18 @@ exports.resolveReferentialIntegrity = (query) => {
|
|
|
79
79
|
switch (op) {
|
|
80
80
|
case 'cascade': {
|
|
81
81
|
if (isArray) {
|
|
82
|
-
txn.match(ref).where($where).
|
|
82
|
+
txn.match(ref).where($where).pull(fieldStr, id);
|
|
83
83
|
} else {
|
|
84
|
-
txn.match(ref).where($where).
|
|
84
|
+
txn.match(ref).where($where).remove();
|
|
85
85
|
}
|
|
86
86
|
break;
|
|
87
87
|
}
|
|
88
88
|
case 'nullify': {
|
|
89
|
-
txn.match(ref).where($where).
|
|
89
|
+
txn.match(ref).where($where).save({ [fieldStr]: null });
|
|
90
90
|
break;
|
|
91
91
|
}
|
|
92
92
|
case 'restrict': {
|
|
93
|
-
txn.match(ref).where($where).
|
|
93
|
+
txn.match(ref).where($where).count().then(count => (count ? reject(new Error('Restricted')) : count));
|
|
94
94
|
break;
|
|
95
95
|
}
|
|
96
96
|
case 'defer': {
|
|
@@ -109,18 +109,3 @@ exports.resolveReferentialIntegrity = (query) => {
|
|
|
109
109
|
}
|
|
110
110
|
});
|
|
111
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
|
-
};
|
|
@@ -2,7 +2,7 @@ const _ = require('lodash');
|
|
|
2
2
|
const PicoMatch = require('picomatch');
|
|
3
3
|
const FillRange = require('fill-range');
|
|
4
4
|
const DeepMerge = require('deepmerge');
|
|
5
|
-
const {
|
|
5
|
+
const { ObjectId } = require('mongodb');
|
|
6
6
|
const ObjectHash = require('object-hash');
|
|
7
7
|
|
|
8
8
|
// const combineMerge = (target, source, options) => {
|
|
@@ -32,15 +32,15 @@ exports.id = '3d896496-02a3-4ee5-8e42-2115eb215f7e';
|
|
|
32
32
|
exports.ucFirst = string => string.charAt(0).toUpperCase() + string.slice(1);
|
|
33
33
|
exports.lcFirst = string => string.charAt(0).toLowerCase() + string.slice(1);
|
|
34
34
|
exports.isNumber = value => typeof value === 'number' && Number.isFinite(value);
|
|
35
|
-
exports.isBasicObject = obj => obj != null && typeof obj === 'object' && !(
|
|
35
|
+
exports.isBasicObject = obj => obj != null && typeof obj === 'object' && !(ObjectId.isValid(obj)) && !(obj instanceof Date) && typeof (obj.then) !== 'function';
|
|
36
36
|
exports.isPlainObject = obj => exports.isBasicObject(obj) && !Array.isArray(obj);
|
|
37
37
|
exports.isScalarValue = value => typeof value !== 'object' && typeof value !== 'function';
|
|
38
38
|
exports.isScalarDataType = value => ['String', 'Float', 'Int', 'Boolean', 'DateTime'].indexOf(value) > -1;
|
|
39
|
-
exports.isIdValue = value => exports.isScalarValue(value) || value instanceof
|
|
39
|
+
exports.isIdValue = value => exports.isScalarValue(value) || value instanceof ObjectId;
|
|
40
40
|
exports.mergeDeep = (...args) => DeepMerge.all(args, { isMergeableObject: obj => (exports.isPlainObject(obj) || Array.isArray(obj)), arrayMerge: smartMerge });
|
|
41
41
|
exports.uniq = arr => [...new Set(arr.map(a => `${a}`))];
|
|
42
42
|
exports.timeout = ms => new Promise(res => setTimeout(res, ms));
|
|
43
|
-
exports.hashObject = obj => ObjectHash(obj, { respectType: false, respectFunctionNames: false, respectFunctionProperties: false, unorderedArrays: true, ignoreUnknown: true, replacer: r => (r instanceof
|
|
43
|
+
exports.hashObject = obj => ObjectHash(obj, { respectType: false, respectFunctionNames: false, respectFunctionProperties: false, unorderedArrays: true, ignoreUnknown: true, replacer: r => (r instanceof ObjectId ? `${r}` : r) });
|
|
44
44
|
exports.globToRegex = (glob, options = {}) => PicoMatch.makeRe(glob, { ...options, expandRange: (a, b) => `(${FillRange(a, b, { toRegex: true })})` });
|
|
45
45
|
exports.globToRegexp = (glob, options = {}) => PicoMatch.toRegex(exports.globToRegex(glob, options));
|
|
46
46
|
exports.toGUID = (model, id) => Buffer.from(`${model},${`${id}`}`).toString('base64');
|
|
@@ -95,8 +95,7 @@ exports.map = (mixed, fn) => {
|
|
|
95
95
|
if (mixed == null) return mixed;
|
|
96
96
|
const isArray = Array.isArray(mixed);
|
|
97
97
|
const arr = isArray ? mixed : [mixed];
|
|
98
|
-
|
|
99
|
-
const results = arr.map((el, i, a) => fn(el, isArray ? i : undefined, isArray ? a : undefined));
|
|
98
|
+
const results = isArray ? arr.map((...args) => fn(...args)) : arr.map(el => fn(el));
|
|
100
99
|
return isArray ? results : results[0];
|
|
101
100
|
};
|
|
102
101
|
|
|
@@ -105,6 +104,32 @@ exports.mapPromise = (mixed, fn) => {
|
|
|
105
104
|
return Array.isArray(map) ? Promise.all(map) : Promise.resolve(map);
|
|
106
105
|
};
|
|
107
106
|
|
|
107
|
+
exports.castCmp = (type, value) => {
|
|
108
|
+
switch (type) {
|
|
109
|
+
case 'String': {
|
|
110
|
+
return `${value}`;
|
|
111
|
+
}
|
|
112
|
+
case 'Float': case 'Number': {
|
|
113
|
+
const num = Number(value);
|
|
114
|
+
if (!Number.isNaN(num)) return num;
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
case 'Int': {
|
|
118
|
+
const num = Number(value);
|
|
119
|
+
if (!Number.isNaN(num)) return parseInt(value, 10);
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
case 'Boolean': {
|
|
123
|
+
if (value === 'true') return true;
|
|
124
|
+
if (value === 'false') return false;
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
default: {
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
108
133
|
exports.objectContaining = (a, b) => {
|
|
109
134
|
if (a === b) return true;
|
|
110
135
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const QueryService = require('../query/QueryService');
|
|
1
2
|
const EventEmitter = require('../core/EventEmitter');
|
|
2
3
|
const { ucFirst } = require('./app.service');
|
|
3
4
|
|
|
@@ -8,19 +9,49 @@ const systemEvent = new EventEmitter().setMaxListeners(100).on('system', async (
|
|
|
8
9
|
next(await eventEmitter.emit(type, data)); // Return result from user-defined middleware
|
|
9
10
|
});
|
|
10
11
|
|
|
12
|
+
const makeEvent = (mixed) => {
|
|
13
|
+
const { query } = mixed;
|
|
14
|
+
const event = query.toObject();
|
|
15
|
+
event.query = query;
|
|
16
|
+
return event;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const makeMiddleware = () => {
|
|
20
|
+
return (mixed) => {
|
|
21
|
+
const { query } = mixed;
|
|
22
|
+
const { model, native, sort, match, batch } = query.toObject();
|
|
23
|
+
|
|
24
|
+
return new Promise(async (resolve) => {
|
|
25
|
+
if (!native) {
|
|
26
|
+
const whereShape = model.getShape('create', 'where');
|
|
27
|
+
const $where = batch ? match : await QueryService.resolveWhereClause(query);
|
|
28
|
+
const $$where = model.shapeObject(whereShape, $where, query);
|
|
29
|
+
query.match($$where);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (sort) {
|
|
33
|
+
query.$sort(QueryService.resolveSortBy(query));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
resolve();
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
11
41
|
//
|
|
12
42
|
exports.createSystemEvent = (name, mixed = {}, thunk = () => {}) => {
|
|
13
43
|
let event = mixed;
|
|
44
|
+
let middleware = () => Promise.resolve();
|
|
14
45
|
const type = ucFirst(name);
|
|
15
46
|
|
|
16
|
-
if (name !== '
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
event.query = query;
|
|
47
|
+
if (name !== 'Response') {
|
|
48
|
+
event = makeEvent(mixed);
|
|
49
|
+
middleware = makeMiddleware();
|
|
20
50
|
}
|
|
21
51
|
|
|
22
|
-
return systemEvent.emit('system', { type: `pre${type}`, data: event }).then((result) => {
|
|
23
|
-
|
|
52
|
+
return systemEvent.emit('system', { type: `pre${type}`, data: event }).then(async (result) => {
|
|
53
|
+
if (result !== undefined) return result; // Allowing middleware to dictate result
|
|
54
|
+
return middleware(mixed).then(thunk);
|
|
24
55
|
}).then((result) => {
|
|
25
56
|
event.result = result;
|
|
26
57
|
if (event.crud === 'create') event.doc = event.query.toObject().doc;
|
package/src/core/GraphQL.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
const { graphql } = require('graphql');
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* GraphQL.
|
|
5
|
-
*
|
|
6
|
-
* This is a wrapper class to the underlying GraphQL Executable Schema.
|
|
7
|
-
* It can be useful for testing and/or exercising the API as an outside caller would.
|
|
8
|
-
*
|
|
9
|
-
* Reference: https://github.com/graphql/graphql-js/blob/master/src/graphql.js#L32-L33
|
|
10
|
-
*/
|
|
11
|
-
module.exports = class GraphQL {
|
|
12
|
-
constructor(schema, resolver) {
|
|
13
|
-
this.schema = schema.makeExecutableSchema();
|
|
14
|
-
this.contextValue = resolver.getContext();
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
exec(source, variableValues) {
|
|
18
|
-
const { schema, contextValue = {} } = this;
|
|
19
|
-
return graphql({ schema, source, variableValues, contextValue });
|
|
20
|
-
}
|
|
21
|
-
};
|