@coderich/autograph 0.10.1 → 0.10.4
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 -5
- package/index.js +0 -2
- package/package.json +4 -6
- package/src/core/Resolver.js +12 -20
- package/src/core/Schema.js +11 -3
- package/src/data/DataLoader.js +1 -3
- package/src/data/DataService.js +35 -13
- package/src/data/Field.js +13 -30
- package/src/data/Model.js +87 -46
- package/src/data/Pipeline.js +20 -10
- package/src/data/Type.js +22 -3
- package/src/driver/MongoDriver.js +9 -9
- package/src/graphql/ast/Field.js +6 -1
- package/src/graphql/ast/Model.js +2 -2
- 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/QueryBuilder.js +5 -0
- package/src/query/QueryResolver.js +36 -50
- package/src/query/QueryService.js +8 -23
- package/src/service/app.service.js +5 -6
- package/src/service/event.service.js +37 -6
- package/src/service/graphql.service.js +0 -9
- package/src/core/GraphQL.js +0 -21
package/CHANGELOG.md
CHANGED
|
@@ -2,26 +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)
|
|
22
|
+
- Mutate "merged" instead of "input"
|
|
23
|
+
- Validate "payload"
|
|
25
24
|
|
|
26
25
|
## v0.9.x
|
|
27
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.4",
|
|
5
5
|
"description": "AutoGraph",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"graphql",
|
|
@@ -23,16 +23,14 @@
|
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"start": "APP_ROOT_PATH=$(pwd) node ./test/server",
|
|
26
|
-
"test": "APP_ROOT_PATH=$(pwd) ratchet test
|
|
26
|
+
"test": "APP_ROOT_PATH=$(pwd) ratchet test",
|
|
27
27
|
"test:debug": "APP_ROOT_PATH=$(pwd) node --inspect-brk ./node_modules/jest/bin/jest.js --watch --runInBand --logHeapUsage",
|
|
28
28
|
"lint": "APP_ROOT_PATH=$(pwd) ratchet lint",
|
|
29
29
|
"inspect": "APP_ROOT_PATH=$(pwd) node --expose-gc --inspect=9222 ./src/server",
|
|
30
30
|
"ratchet": "ratchet"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@graphql-tools/schema": "^8.3.14",
|
|
34
33
|
"@hapi/boom": "^9.1.0",
|
|
35
|
-
"axios": "^0.21.4",
|
|
36
34
|
"dataloader": "^2.0.0",
|
|
37
35
|
"deepmerge": "^4.2.2",
|
|
38
36
|
"fill-range": "^7.0.1",
|
|
@@ -41,11 +39,11 @@
|
|
|
41
39
|
"lodash": "^4.17.21",
|
|
42
40
|
"mongodb": "^4.8.0",
|
|
43
41
|
"object-hash": "^2.0.1",
|
|
44
|
-
"picomatch": "^2.1.1"
|
|
45
|
-
"uuid": "^3.3.3"
|
|
42
|
+
"picomatch": "^2.1.1"
|
|
46
43
|
},
|
|
47
44
|
"devDependencies": {
|
|
48
45
|
"@coderich/ratchet": "^1.5.7",
|
|
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",
|
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,7 +1,7 @@
|
|
|
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 {
|
|
@@ -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
|
|
@@ -37,10 +37,18 @@ module.exports = class extends Schema {
|
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
disconnect() {
|
|
41
|
+
return Promise.all(Object.values(this.drivers).map(({ dao }) => dao.disconnect()));
|
|
42
|
+
}
|
|
43
|
+
|
|
40
44
|
initialize() {
|
|
41
45
|
super.initialize();
|
|
42
46
|
this.models = super.getModels().map(model => new Model(this, model, this.drivers[model.getDriverName()]));
|
|
43
|
-
this
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
finalize() {
|
|
51
|
+
super.finalize();
|
|
44
52
|
this.models.forEach(model => model.referentialIntegrity(identifyOnDeletes(this.models, model)));
|
|
45
53
|
return this;
|
|
46
54
|
}
|
package/src/data/DataLoader.js
CHANGED
|
@@ -42,8 +42,6 @@ module.exports = class DataLoader extends FBDataLoader {
|
|
|
42
42
|
*/
|
|
43
43
|
const whereShape = model.getShape('create', 'where');
|
|
44
44
|
|
|
45
|
-
// console.log(Object.entries(batchQueries).map(([key, value]) => ({ [key]: value.length })));
|
|
46
|
-
|
|
47
45
|
return Promise.all(Object.entries(batchQueries).map(([key, values]) => {
|
|
48
46
|
switch (key) {
|
|
49
47
|
case defaultBatchName: {
|
|
@@ -52,7 +50,7 @@ module.exports = class DataLoader extends FBDataLoader {
|
|
|
52
50
|
default: {
|
|
53
51
|
const keys = Array.from(new Set(values.map(({ where }) => map(where[key], el => `${el}`)).flat()));
|
|
54
52
|
const batchQuery = new Query({ resolver, model, method: 'findMany', crud: 'read' });
|
|
55
|
-
const batchWhere = model.shapeObject(whereShape, { [key]: keys }, batchQuery); //
|
|
53
|
+
const batchWhere = model.shapeObject(whereShape, { ...values[0].where, [key]: keys }, batchQuery); // All where's should be the same - this is for idKey on keys etc
|
|
56
54
|
|
|
57
55
|
return driver.resolve(batchQuery.where(batchWhere).toDriver()).then(data => handleData(data, model, batchQuery)).then((results) => {
|
|
58
56
|
// One-time data transformation on results to make matching back faster (below)
|
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,80 +47,86 @@ 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
|
|
|
93
73
|
const serdes = crud === 'read' ? 'deserialize' : 'serialize';
|
|
94
74
|
const fields = serdes === 'deserialize' ? this.getSelectFields() : this.getPersistableFields();
|
|
95
75
|
const crudMap = { create: ['constructs'], update: ['restructs'], delete: ['destructs'], remove: ['destructs'] };
|
|
76
|
+
const sortKeys = ['isIdField', 'isBasicType', 'isEmbedded'];
|
|
96
77
|
const crudKeys = crudMap[crud] || [];
|
|
97
78
|
|
|
79
|
+
// Define target mapping
|
|
98
80
|
const targetMap = {
|
|
99
|
-
doc: ['defaultValue', 'ensureArrayValue', '
|
|
100
|
-
|
|
81
|
+
doc: ['defaultValue', 'castValue', 'ensureArrayValue', 'normalizers', 'instructs', ...crudKeys, `$${serdes}rs`, `${serdes}rs`, 'transforms'],
|
|
82
|
+
input: ['defaultValue', 'castValue', 'ensureArrayValue', 'normalizers', 'instructs', ...crudKeys, `$${serdes}rs`, `${serdes}rs`, 'transforms'],
|
|
83
|
+
// input: ['defaultValue', 'castValue', 'ensureArrayValue'],
|
|
84
|
+
where: ['castValue', 'instructs', `$${serdes}rs`],
|
|
101
85
|
};
|
|
102
86
|
|
|
103
87
|
const structureKeys = targetMap[target] || ['castValue'];
|
|
104
88
|
|
|
105
|
-
// Create shape, recursive
|
|
106
|
-
const shape = fields.
|
|
89
|
+
// Create sorted shape, recursive
|
|
90
|
+
const shape = fields.sort((a, b) => {
|
|
91
|
+
const aObject = a.toObject();
|
|
92
|
+
const bObject = b.toObject();
|
|
93
|
+
|
|
94
|
+
// PK first
|
|
95
|
+
if (aObject.isPrimaryKeyId) return -1;
|
|
96
|
+
if (bObject.isPrimaryKeyId) return 1;
|
|
97
|
+
|
|
98
|
+
// Arrays last
|
|
99
|
+
if (aObject.isArray && !bObject.isArray) return 1;
|
|
100
|
+
if (bObject.isArray && !aObject.isArray) return -1;
|
|
101
|
+
|
|
102
|
+
// Now, follow sort keys
|
|
103
|
+
const aNum = sortKeys.findIndex(key => aObject[key]);
|
|
104
|
+
const bNum = sortKeys.findIndex(key => bObject[key]);
|
|
105
|
+
if (aNum < bNum) return -1;
|
|
106
|
+
if (aNum > bNum) return 1;
|
|
107
|
+
return 0;
|
|
108
|
+
}).map((field) => {
|
|
109
|
+
let instructed = false;
|
|
107
110
|
const structures = field.getStructures();
|
|
108
|
-
const
|
|
111
|
+
const { key, name, type, isArray, isEmbedded, modelRef } = field.toObject();
|
|
109
112
|
const [from, to] = serdes === 'serialize' ? [name, key] : [key, name];
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
113
|
+
const actualTo = target === 'input' || target === 'splice' ? from : to;
|
|
114
|
+
const path = paths.concat(actualTo);
|
|
115
|
+
const subCrud = crud === 'update' && isArray ? 'create' : crud; // Due to limitation to update embedded array
|
|
116
|
+
const subShape = isEmbedded ? modelRef.getShape(subCrud, target, path) : null;
|
|
117
|
+
const transformers = structureKeys.reduce((prev, struct) => {
|
|
118
|
+
const structs = structures[struct];
|
|
119
|
+
if (struct === 'instructs' && structs.length) instructed = true;
|
|
120
|
+
return prev.concat(structs);
|
|
121
|
+
}, []).filter(Boolean);
|
|
122
|
+
return { instructed, field, path, from, to: actualTo, type, isArray, transformers, validators: structures.validators, shape: subShape };
|
|
119
123
|
});
|
|
120
124
|
|
|
121
125
|
// Adding useful shape info
|
|
122
126
|
shape.crud = crud;
|
|
123
127
|
shape.model = this;
|
|
124
128
|
shape.serdes = serdes;
|
|
129
|
+
shape.target = target;
|
|
125
130
|
|
|
126
131
|
// Cache and return
|
|
127
132
|
this.shapesCache.set(cacheKey, shape);
|
|
@@ -130,7 +135,11 @@ module.exports = class extends Model {
|
|
|
130
135
|
|
|
131
136
|
shapeObject(shape, obj, query, root) {
|
|
132
137
|
const { serdes, model } = shape;
|
|
133
|
-
const { context, doc = {} } = query.toObject();
|
|
138
|
+
const { context, resolver, doc = {}, flags = {} } = query.toObject();
|
|
139
|
+
const { pipeline } = flags;
|
|
140
|
+
|
|
141
|
+
if (!pipeline) return obj;
|
|
142
|
+
// const filters = pipeline === true ? [] : Object.entries(pipeline).map(([k, v]) => (v === false ? k : null)).filter(Boolean);
|
|
134
143
|
|
|
135
144
|
return map(obj, (parent) => {
|
|
136
145
|
// "root" is the base of the object
|
|
@@ -141,19 +150,19 @@ module.exports = class extends Model {
|
|
|
141
150
|
const rootPath = (p, hint) => (serdes === 'serialize' ? seek(root, p, hint) : deseek(shape, root, p, hint));
|
|
142
151
|
const parentPath = (p, hint) => (serdes === 'serialize' ? seek(parent, p, hint) : deseek(shape, parent, p, hint));
|
|
143
152
|
|
|
144
|
-
return shape.reduce((prev, { field, from, to, path, type, isArray, defaultValue, transformers = [], shape: subShape }) => {
|
|
153
|
+
return shape.reduce((prev, { instructed, field, from, to, path, type, isArray, defaultValue, transformers = [], shape: subShape }) => {
|
|
145
154
|
const startValue = parent[from];
|
|
155
|
+
// transformers = filters.length ? transformers.filter() : transformers;
|
|
146
156
|
|
|
147
157
|
// Transform value
|
|
148
158
|
const transformedValue = transformers.reduce((value, t) => {
|
|
149
|
-
const v = t({ model, field, path, docPath, rootPath, parentPath, startValue, value, context });
|
|
159
|
+
const v = t({ model, field, path, docPath, rootPath, parentPath, startValue, value, resolver, context });
|
|
150
160
|
return v === undefined ? value : v;
|
|
151
161
|
}, startValue);
|
|
152
162
|
|
|
153
|
-
// if (`${field}` === 'searchability') console.log(startValue, transformedValue, transformers);
|
|
154
|
-
|
|
155
163
|
// Determine if key should stay or be removed
|
|
156
|
-
if (transformedValue === undefined && !Object.prototype.hasOwnProperty.call(parent, from)) return prev;
|
|
164
|
+
if (!instructed && transformedValue === undefined && !Object.prototype.hasOwnProperty.call(parent, from)) return prev;
|
|
165
|
+
if (!instructed && subShape && typeof transformedValue !== 'object') return prev;
|
|
157
166
|
|
|
158
167
|
// Rename key & assign value
|
|
159
168
|
prev[to] = (!subShape || transformedValue == null) ? transformedValue : this.shapeObject(subShape, transformedValue, query, root);
|
|
@@ -162,4 +171,36 @@ module.exports = class extends Model {
|
|
|
162
171
|
}, {});
|
|
163
172
|
});
|
|
164
173
|
}
|
|
174
|
+
|
|
175
|
+
validateObject(shape, obj, query, root, silent = false) {
|
|
176
|
+
const { model } = shape;
|
|
177
|
+
const { context, resolver, doc = {}, flags = {} } = query.toObject();
|
|
178
|
+
const { validate = true } = flags;
|
|
179
|
+
|
|
180
|
+
if (!validate) return Promise.resolve();
|
|
181
|
+
|
|
182
|
+
return mapPromise(obj, (parent) => {
|
|
183
|
+
// "root" is the base of the object
|
|
184
|
+
root = root || parent;
|
|
185
|
+
|
|
186
|
+
// Lookup helper functions
|
|
187
|
+
const docPath = (p, hint) => seek(doc, p, hint);
|
|
188
|
+
const rootPath = (p, hint) => seek(root, p, hint);
|
|
189
|
+
const parentPath = (p, hint) => seek(parent, p, hint);
|
|
190
|
+
|
|
191
|
+
return Promise.all(shape.map(({ field, from, path, validators, shape: subShape }) => {
|
|
192
|
+
const value = parent[from]; // It hasn't been shaped yet
|
|
193
|
+
|
|
194
|
+
return Promise.all(validators.map((v) => {
|
|
195
|
+
return new Promise((resolve, reject) => {
|
|
196
|
+
return Promise.resolve(v({ model, field, path, docPath, rootPath, parentPath, startValue: value, value, resolver, context })).then(resolve).catch(reject);
|
|
197
|
+
});
|
|
198
|
+
})).then(() => {
|
|
199
|
+
return subShape ? this.validateObject(subShape, value, query, root, true) : Promise.resolve();
|
|
200
|
+
});
|
|
201
|
+
}));
|
|
202
|
+
}).then(() => {
|
|
203
|
+
return silent ? Promise.resolve() : eventEmitter.emit('validate', query.toObject());
|
|
204
|
+
});
|
|
205
|
+
}
|
|
165
206
|
};
|
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',
|