@coderich/autograph 0.10.0 → 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 +20 -3
- package/index.js +2 -8
- package/package.json +5 -7
- package/src/.DS_Store +0 -0
- package/src/core/EventEmitter.js +2 -4
- package/src/core/Resolver.js +32 -57
- package/src/core/Schema.js +5 -38
- package/src/data/.DS_Store +0 -0
- package/src/data/DataLoader.js +71 -32
- package/src/data/DataService.js +82 -59
- package/src/data/Field.js +59 -126
- package/src/data/Model.js +113 -105
- package/src/data/Pipeline.js +184 -0
- package/src/data/Type.js +38 -74
- package/src/driver/MongoDriver.js +27 -22
- package/src/graphql/.DS_Store +0 -0
- package/src/graphql/ast/Field.js +46 -24
- package/src/graphql/ast/Model.js +5 -16
- package/src/graphql/ast/Node.js +0 -25
- package/src/graphql/ast/Schema.js +105 -112
- package/src/graphql/extension/api.js +20 -18
- package/src/graphql/extension/framework.js +27 -33
- package/src/graphql/extension/type.js +2 -2
- package/src/query/Query.js +82 -14
- package/src/query/QueryBuilder.js +38 -30
- package/src/query/QueryBuilderTransaction.js +3 -3
- package/src/query/QueryResolver.js +77 -41
- package/src/query/QueryService.js +24 -42
- package/src/service/app.service.js +70 -13
- package/src/service/event.service.js +30 -73
- package/src/service/graphql.service.js +0 -9
- package/src/service/schema.service.js +5 -3
- package/src/core/GraphQL.js +0 -21
- 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
package/src/data/Field.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
+
const { isEmpty } = require('lodash');
|
|
1
2
|
const Type = require('./Type');
|
|
2
3
|
const Field = require('../graphql/ast/Field');
|
|
3
|
-
const
|
|
4
|
-
const Transformer = require('../core/Transformer');
|
|
5
|
-
const { map, uvl, isPlainObject, ensureArray, promiseChain } = require('../service/app.service');
|
|
4
|
+
const Pipeline = require('./Pipeline');
|
|
6
5
|
|
|
7
6
|
module.exports = class extends Field {
|
|
8
7
|
constructor(model, field) {
|
|
@@ -11,140 +10,74 @@ module.exports = class extends Field {
|
|
|
11
10
|
this.model = model;
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
getRules() {
|
|
21
|
-
const rules = [];
|
|
22
|
-
|
|
23
|
-
Object.entries(this.getDirectiveArgs('field', {})).forEach(([key, value]) => {
|
|
24
|
-
if (!Array.isArray(value)) value = [value];
|
|
25
|
-
if (key === 'enforce') rules.push(...value.map(r => Rule.getInstances()[r]));
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
if (this.isRequired() && this.isPersistable() && !this.isVirtual()) rules.push(Rule.required());
|
|
29
|
-
|
|
30
|
-
return rules.concat(this.type.getRules());
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
getTransformers() {
|
|
34
|
-
const transformers = [];
|
|
35
|
-
|
|
36
|
-
Object.entries(this.getDirectiveArgs('field', {})).forEach(([key, value]) => {
|
|
37
|
-
if (!Array.isArray(value)) value = [value];
|
|
38
|
-
if (key === 'transform') transformers.push(...value.map(t => Transformer.getInstances()[t]));
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
return transformers.concat(this.type.getTransformers());
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
getSerializers() {
|
|
45
|
-
const transformers = [];
|
|
46
|
-
|
|
47
|
-
Object.entries(this.getDirectiveArgs('field', {})).forEach(([key, value]) => {
|
|
48
|
-
if (!Array.isArray(value)) value = [value];
|
|
49
|
-
if (key === 'serialize') transformers.push(...value.map(t => Transformer.getInstances()[t]));
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
return transformers.concat(this.type.getSerializers());
|
|
53
|
-
}
|
|
13
|
+
getStructures() {
|
|
14
|
+
// Grab structures from the underlying type
|
|
15
|
+
const structures = this.type.getStructures();
|
|
16
|
+
const { type, isPrimaryKeyId, isIdField, isRequired, isPersistable, isVirtual, isEmbedded, modelRef } = this.props;
|
|
54
17
|
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
Object.entries(this.getDirectiveArgs('field', {})).forEach(([key, value]) => {
|
|
18
|
+
// Structures defined on the field
|
|
19
|
+
const $structures = Object.entries(this.getDirectiveArgs('field', {})).reduce((prev, [key, value]) => {
|
|
59
20
|
if (!Array.isArray(value)) value = [value];
|
|
60
|
-
if (key === '
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
+
return prev;
|
|
31
|
+
}, structures);
|
|
32
|
+
|
|
33
|
+
// IDs (first - shift)
|
|
34
|
+
if (isPrimaryKeyId && type === 'ID') $structures.serializers.unshift(Pipeline.idKey);
|
|
35
|
+
if (isIdField) $structures.$serializers.unshift(Pipeline.idField);
|
|
36
|
+
|
|
37
|
+
// Required (last - push)
|
|
38
|
+
if (isRequired && isPersistable && !isVirtual) $structures.validators.push(Pipeline.required);
|
|
39
|
+
if (modelRef && !isEmbedded) $structures.validators.push(Pipeline.ensureId);
|
|
40
|
+
|
|
41
|
+
return $structures;
|
|
64
42
|
}
|
|
65
43
|
|
|
66
|
-
|
|
67
|
-
const
|
|
44
|
+
resolve(resolver, doc, args = {}) {
|
|
45
|
+
const { name, isArray, isScalar, isVirtual, isRequired, isEmbedded, modelRef, virtualField } = this.props;
|
|
46
|
+
const value = doc[name];
|
|
68
47
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (key === 'resolve') resolvers.push(...value.map(t => Transformer.getInstances()[t]));
|
|
72
|
-
});
|
|
48
|
+
// Default resolver return immediately!
|
|
49
|
+
if (isScalar || isEmbedded) return value;
|
|
73
50
|
|
|
74
|
-
|
|
75
|
-
|
|
51
|
+
// Ensure where clause for DB lookup
|
|
52
|
+
args.where = args.where || {};
|
|
76
53
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
54
|
+
if (isArray) {
|
|
55
|
+
if (isVirtual) {
|
|
56
|
+
if (isEmpty(args.where)) args.batch = `${virtualField}`;
|
|
57
|
+
args.where[virtualField] = doc.id;
|
|
58
|
+
return resolver.match(modelRef).merge(args).many();
|
|
59
|
+
}
|
|
80
60
|
|
|
81
|
-
|
|
61
|
+
// Not a "required" query + strip out nulls
|
|
62
|
+
if (isEmpty(args.where)) args.batch = 'id';
|
|
63
|
+
args.where.id = value;
|
|
64
|
+
return resolver.match(modelRef).merge(args).many();
|
|
65
|
+
}
|
|
82
66
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
});
|
|
89
|
-
}
|
|
67
|
+
if (isVirtual) {
|
|
68
|
+
if (isEmpty(args.where)) args.batch = `${virtualField}`;
|
|
69
|
+
args.where[virtualField] = doc.id;
|
|
70
|
+
return resolver.match(modelRef).merge(args).one();
|
|
71
|
+
}
|
|
90
72
|
|
|
91
|
-
|
|
92
|
-
* Ensures the value of the field via bound @value + transformations
|
|
93
|
-
*/
|
|
94
|
-
transform(query, value, serdes = (() => { throw new Error('No Sir Sir SerDes!'); })()) {
|
|
95
|
-
// Determine value
|
|
96
|
-
const $value = serdes === 'serialize' ? this.resolveBoundValue(query, value) : uvl(value, this.getDefaultValue());
|
|
97
|
-
|
|
98
|
-
// Determine transformers
|
|
99
|
-
const transformers = [...(serdes === 'serialize' ? this.getSerializers() : this.getDeserializers()), ...this.getTransformers()];
|
|
100
|
-
|
|
101
|
-
// Transform
|
|
102
|
-
return transformers.reduce((prev, transformer) => {
|
|
103
|
-
return transformer(this, prev, query);
|
|
104
|
-
}, this.cast($value));
|
|
73
|
+
return resolver.match(modelRef).id(value).one({ required: isRequired });
|
|
105
74
|
}
|
|
106
75
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
// If embedded, simply delgate
|
|
115
|
-
if (isEmbedded) return modelRef.serialize(query, value, minimal);
|
|
116
|
-
|
|
117
|
-
// Now, normalize and resolve
|
|
118
|
-
const $value = this.transform(query, value, 'serialize');
|
|
119
|
-
if (modelRef && !isEmbedded) return map($value, v => modelRef.idValue(v.id || v));
|
|
120
|
-
return $value;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
deserialize(query, value) {
|
|
124
|
-
return this.transform(query, value, 'deserialize');
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Applies any user-defined @field(resolve: [...methods]) in series
|
|
129
|
-
* This is ONLY run when resolving a value via the $<name> attribute
|
|
130
|
-
*/
|
|
131
|
-
resolve(query, value) {
|
|
132
|
-
const resolvers = [...this.getResolvers()];
|
|
133
|
-
|
|
134
|
-
return promiseChain(resolvers.map(fn => (chain) => {
|
|
135
|
-
return Promise.resolve(fn(this, uvl(this.cast(chain.pop()), value), query));
|
|
136
|
-
})).then((results) => {
|
|
137
|
-
return uvl(this.cast(results.pop()), value);
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
tform(query, value) {
|
|
142
|
-
// Determine transformers
|
|
143
|
-
const transformers = this.getTransformers();
|
|
144
|
-
|
|
145
|
-
// Transform
|
|
146
|
-
return transformers.reduce((prev, transformer) => {
|
|
147
|
-
return transformer(this, prev, query);
|
|
148
|
-
}, this.cast(value));
|
|
76
|
+
count(resolver, doc, args = {}) {
|
|
77
|
+
const { name, isVirtual, modelRef, virtualField } = this.props;
|
|
78
|
+
args.where = args.where || {};
|
|
79
|
+
if (isVirtual) args.where[virtualField] = doc.id;
|
|
80
|
+
else args.where.id = doc[name];
|
|
81
|
+
return resolver.match(modelRef).merge(args).count();
|
|
149
82
|
}
|
|
150
83
|
};
|
package/src/data/Model.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
const Stream = require('stream');
|
|
1
2
|
const Field = require('./Field');
|
|
2
3
|
const Model = require('../graphql/ast/Model');
|
|
3
|
-
const {
|
|
4
|
+
const { eventEmitter } = require('../service/event.service');
|
|
5
|
+
const { finalizeResults } = require('./DataService');
|
|
6
|
+
const { map, mapPromise, seek, deseek } = require('../service/app.service');
|
|
4
7
|
|
|
5
8
|
module.exports = class extends Model {
|
|
6
9
|
constructor(schema, model, driver) {
|
|
@@ -8,6 +11,7 @@ module.exports = class extends Model {
|
|
|
8
11
|
this.driver = driver;
|
|
9
12
|
this.fields = super.getFields().map(field => new Field(this, field));
|
|
10
13
|
this.namedQueries = {};
|
|
14
|
+
this.shapesCache = new Map();
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
raw() {
|
|
@@ -44,132 +48,136 @@ module.exports = class extends Model {
|
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
/**
|
|
47
|
-
*
|
|
48
|
-
* while ensuring that all defaulted values are set appropriately
|
|
51
|
+
* Convenience method to deserialize data from a data source (such as a database)
|
|
49
52
|
*/
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
deserialize(mixed, query) {
|
|
54
|
+
const shape = this.getShape();
|
|
55
|
+
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
if (!(mixed instanceof Stream)) {
|
|
58
|
+
resolve(this.shapeObject(shape, mixed, query));
|
|
59
|
+
} else {
|
|
60
|
+
const results = [];
|
|
61
|
+
mixed.on('data', (data) => { results.push(this.shapeObject(shape, data, query)); });
|
|
62
|
+
mixed.on('end', () => { resolve(results); });
|
|
63
|
+
mixed.on('error', reject);
|
|
64
|
+
}
|
|
65
|
+
}).then(rs => finalizeResults(rs, query));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getShape(crud = 'read', target = 'doc', paths = []) {
|
|
69
|
+
// Cache check
|
|
70
|
+
const cacheKey = `${crud}:${target}`;
|
|
71
|
+
if (this.shapesCache.has(cacheKey)) return this.shapesCache.get(cacheKey);
|
|
72
|
+
|
|
73
|
+
const serdes = crud === 'read' ? 'deserialize' : 'serialize';
|
|
74
|
+
const fields = serdes === 'deserialize' ? this.getSelectFields() : this.getPersistableFields();
|
|
75
|
+
const crudMap = { create: ['constructs'], update: ['restructs'], delete: ['destructs'], remove: ['destructs'] };
|
|
76
|
+
const crudKeys = crudMap[crud] || [];
|
|
77
|
+
|
|
78
|
+
// Define target mapping
|
|
79
|
+
const targetMap = {
|
|
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`],
|
|
84
|
+
};
|
|
85
|
+
const structureKeys = targetMap[target] || ['castValue'];
|
|
86
|
+
|
|
87
|
+
// Create shape, recursive
|
|
88
|
+
const shape = fields.map((field) => {
|
|
89
|
+
let instructed = false;
|
|
90
|
+
const structures = field.getStructures();
|
|
91
|
+
const { key, name, type, isArray, isEmbedded, modelRef } = field.toObject();
|
|
92
|
+
const [from, to] = serdes === 'serialize' ? [name, key] : [key, name];
|
|
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 };
|
|
60
104
|
});
|
|
61
105
|
|
|
62
|
-
|
|
106
|
+
// Adding useful shape info
|
|
107
|
+
shape.crud = crud;
|
|
108
|
+
shape.model = this;
|
|
109
|
+
shape.serdes = serdes;
|
|
110
|
+
shape.target = target;
|
|
111
|
+
|
|
112
|
+
// Cache and return
|
|
113
|
+
this.shapesCache.set(cacheKey, shape);
|
|
114
|
+
return shape;
|
|
63
115
|
}
|
|
64
116
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
117
|
+
shapeObject(shape, obj, query, root) {
|
|
118
|
+
const { serdes, model } = shape;
|
|
119
|
+
const { context, resolver, doc = {}, flags = {} } = query.toObject();
|
|
120
|
+
const { pipeline } = flags;
|
|
69
121
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (input[field]) map(input[field], v => field.getModelRef().appendUpdateFields(v, field.isArray())); // Only embedded when it's an array (because then we'll ensure ids)
|
|
73
|
-
});
|
|
122
|
+
if (!pipeline) return obj;
|
|
123
|
+
// const filters = pipeline === true ? [] : Object.entries(pipeline).map(([k, v]) => (v === false ? k : null)).filter(Boolean);
|
|
74
124
|
|
|
75
|
-
return
|
|
76
|
-
|
|
125
|
+
return map(obj, (parent) => {
|
|
126
|
+
// "root" is the base of the object
|
|
127
|
+
root = root || parent;
|
|
77
128
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
129
|
+
// Lookup helper functions
|
|
130
|
+
const docPath = (p, hint) => seek(doc, p, hint); // doc is already serialized; so always a seek
|
|
131
|
+
const rootPath = (p, hint) => (serdes === 'serialize' ? seek(root, p, hint) : deseek(shape, root, p, hint));
|
|
132
|
+
const parentPath = (p, hint) => (serdes === 'serialize' ? seek(parent, p, hint) : deseek(shape, parent, p, hint));
|
|
82
133
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
});
|
|
134
|
+
return shape.reduce((prev, { instructed, field, from, to, path, type, isArray, defaultValue, transformers = [], shape: subShape }) => {
|
|
135
|
+
const startValue = parent[from];
|
|
136
|
+
// transformers = filters.length ? transformers.filter() : transformers;
|
|
87
137
|
|
|
88
|
-
|
|
89
|
-
|
|
138
|
+
// Transform value
|
|
139
|
+
const transformedValue = transformers.reduce((value, t) => {
|
|
140
|
+
const v = t({ model, field, path, docPath, rootPath, parentPath, startValue, value, resolver, context });
|
|
141
|
+
return v === undefined ? value : v;
|
|
142
|
+
}, startValue);
|
|
90
143
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
serialize(query, data, minimal = false) {
|
|
95
|
-
return this.transform(query, data, 'serialize', minimal);
|
|
96
|
-
}
|
|
144
|
+
// Determine if key should stay or be removed
|
|
145
|
+
if (!instructed && transformedValue === undefined && !Object.prototype.hasOwnProperty.call(parent, from)) return prev;
|
|
146
|
+
if (!instructed && subShape && typeof transformedValue !== 'object') return prev;
|
|
97
147
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
*/
|
|
101
|
-
deserialize(query, data) {
|
|
102
|
-
return this.transform(query, data, 'deserialize');
|
|
103
|
-
}
|
|
148
|
+
// Rename key & assign value
|
|
149
|
+
prev[to] = (!subShape || transformedValue == null) ? transformedValue : this.shapeObject(subShape, transformedValue, query, root);
|
|
104
150
|
|
|
105
|
-
/**
|
|
106
|
-
* Serializer/Deserializer
|
|
107
|
-
*/
|
|
108
|
-
transform(query, data, serdes = (() => { throw new Error('No Sir Sir SerDes!'); })(), minimal = false) {
|
|
109
|
-
// Serialize always gets the bound values
|
|
110
|
-
const appendFields = (serdes === 'serialize' ? [...this.getBoundValueFields()] : []);
|
|
111
|
-
|
|
112
|
-
// Certain cases do not want custom serdes or defaults
|
|
113
|
-
if (!minimal) appendFields.push(...this[`get${ucFirst(serdes)}Fields`](), ...this.getDefaultFields());
|
|
114
|
-
|
|
115
|
-
// Transform all the data
|
|
116
|
-
return map(data, (doc) => {
|
|
117
|
-
// We want the appendFields + those in the data, deduped
|
|
118
|
-
const fields = [...new Set(appendFields.concat(Object.keys(doc).map(k => this.getField(k))))].filter(Boolean);
|
|
119
|
-
|
|
120
|
-
// Loop through the fields and delegate (renaming keys appropriately)
|
|
121
|
-
return fields.reduce((prev, field) => {
|
|
122
|
-
const [key, name] = serdes === 'serialize' ? [field.getKey(), field.getName()] : [field.getName(), field.getKey()];
|
|
123
|
-
prev[key] = field[serdes](query, doc[name], minimal);
|
|
124
151
|
return prev;
|
|
125
152
|
}, {});
|
|
126
153
|
});
|
|
127
154
|
}
|
|
128
155
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
// Transform all the data
|
|
134
|
-
return map(data, (doc) => {
|
|
135
|
-
const fields = Object.keys(doc).map(k => this.getField(k)).filter(Boolean);
|
|
136
|
-
|
|
137
|
-
// Loop through the fields and delegate (renaming keys appropriately)
|
|
138
|
-
return fields.reduce((prev, field) => {
|
|
139
|
-
const [key, name] = serdes === 'serialize' ? [field.getKey(), field.getName()] : [field.getName(), field.getKey()];
|
|
140
|
-
prev[key] = keysOnly ? doc[name] : field[serdes](query, doc[name], true);
|
|
141
|
-
return prev;
|
|
142
|
-
}, {});
|
|
143
|
-
});
|
|
144
|
-
}
|
|
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;
|
|
145
160
|
|
|
146
|
-
|
|
147
|
-
return this.getSelectFields().map((field) => {
|
|
148
|
-
const [from, to] = serdes === 'serialize' ? [field.getName(), field.getKey()] : [field.getKey(), field.getName()];
|
|
149
|
-
return { from, to, type: field.getDataType(), isArray: field.isArray(), shape: field.isEmbedded() ? field.getModelRef().getShape(serdes) : null };
|
|
150
|
-
});
|
|
151
|
-
}
|
|
161
|
+
if (!validate) return Promise.resolve();
|
|
152
162
|
|
|
153
|
-
|
|
154
|
-
|
|
163
|
+
return mapPromise(obj, (parent) => {
|
|
164
|
+
// "root" is the base of the object
|
|
165
|
+
root = root || parent;
|
|
155
166
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return Object.assign(prev, { [to]: subShape ? this.shape(value, serdes, subShape) : value });
|
|
161
|
-
}, {});
|
|
162
|
-
});
|
|
163
|
-
}
|
|
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);
|
|
164
171
|
|
|
165
|
-
|
|
166
|
-
|
|
172
|
+
return Promise.all(shape.map(({ field, from, path, validators, shape: subShape }) => {
|
|
173
|
+
const value = parent[from]; // It hasn't been shaped yet
|
|
167
174
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
+
});
|
|
174
182
|
}
|
|
175
183
|
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
const { uniqWith } = require('lodash');
|
|
2
|
+
const { map, ensureArray, hashObject } = require('../service/app.service');
|
|
3
|
+
const Boom = require('../core/Boom');
|
|
4
|
+
|
|
5
|
+
module.exports = class Pipeline {
|
|
6
|
+
constructor() {
|
|
7
|
+
throw new Error('Pipeline is a singleton; use the static {define|factory} methods');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
static define(name, factory, options = {}) {
|
|
11
|
+
// A factory must be a function
|
|
12
|
+
if (typeof factory !== 'function') throw new Error(`Pipeline definition for "${name}" must be a function`);
|
|
13
|
+
|
|
14
|
+
// Determine options; which may come from the factory function
|
|
15
|
+
const { ignoreNull = true, itemize = true, configurable = false } = Object.assign({}, factory.options, options);
|
|
16
|
+
|
|
17
|
+
const wrapper = Object.defineProperty((args) => {
|
|
18
|
+
if (ignoreNull && args.value == null) return args.value;
|
|
19
|
+
|
|
20
|
+
if (ignoreNull && itemize) {
|
|
21
|
+
return map(args.value, (val, index) => {
|
|
22
|
+
const v = factory({ ...args, value: val, index });
|
|
23
|
+
return v === undefined ? val : v;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const val = factory(args);
|
|
28
|
+
return val === undefined ? args.value : val;
|
|
29
|
+
}, 'name', { value: name });
|
|
30
|
+
|
|
31
|
+
// Attach enumerable method to the Pipeline
|
|
32
|
+
return Object.defineProperty(Pipeline, name, {
|
|
33
|
+
value: wrapper,
|
|
34
|
+
configurable,
|
|
35
|
+
enumerable: true,
|
|
36
|
+
})[name];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static factory(name, thunk, options = {}) {
|
|
40
|
+
if (typeof thunk !== 'function') throw new Error(`Pipeline factory for "${name}" must be a thunk`);
|
|
41
|
+
if (typeof thunk() !== 'function') throw new Error(`Factory thunk() for "${name}" must return a function`);
|
|
42
|
+
return Object.defineProperty(Pipeline, name, { value: (...args) => Object.defineProperty(thunk(...args), 'options', { value: options }) })[name];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// static wrapper(name, factory, { ignoreNull, itemize }) {
|
|
46
|
+
// return Object.defineProperty((args) => {
|
|
47
|
+
// if (ignoreNull && args.value == null) return args.value;
|
|
48
|
+
|
|
49
|
+
// if (ignoreNull && itemize) {
|
|
50
|
+
// return map(args.value, (val, index) => {
|
|
51
|
+
// const v = factory({ ...args, value: val, index });
|
|
52
|
+
// return v === undefined ? val : v;
|
|
53
|
+
// });
|
|
54
|
+
// }
|
|
55
|
+
|
|
56
|
+
// const val = factory(args);
|
|
57
|
+
// return val === undefined ? args.value : val;
|
|
58
|
+
// }, 'name', { value: name });
|
|
59
|
+
// }
|
|
60
|
+
|
|
61
|
+
static createPresets() {
|
|
62
|
+
// Built-In Javascript String Transformers
|
|
63
|
+
const jsStringTransformers = ['toLowerCase', 'toUpperCase', 'toString', 'trim', 'trimEnd', 'trimStart'];
|
|
64
|
+
jsStringTransformers.forEach(name => Pipeline.define(`${name}`, ({ value }) => String(value)[name]()));
|
|
65
|
+
|
|
66
|
+
// Additional Transformers
|
|
67
|
+
Pipeline.define('toTitleCase', ({ value }) => value.replace(/\w\S*/g, w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()));
|
|
68
|
+
Pipeline.define('toSentenceCase', ({ value }) => value.charAt(0).toUpperCase() + value.slice(1));
|
|
69
|
+
Pipeline.define('toId', ({ model, value }) => model.idValue(value));
|
|
70
|
+
Pipeline.define('toArray', ({ value }) => (Array.isArray(value) ? value : [value]), { itemize: false });
|
|
71
|
+
Pipeline.define('toDate', ({ value }) => new Date(value), { configurable: true });
|
|
72
|
+
Pipeline.define('timestamp', ({ value }) => Date.now(), { ignoreNull: false });
|
|
73
|
+
Pipeline.define('createdAt', ({ value }) => value || Date.now(), { ignoreNull: false });
|
|
74
|
+
Pipeline.define('dedupe', ({ value }) => uniqWith(value, (b, c) => hashObject(b) === hashObject(c)), { itemize: false });
|
|
75
|
+
Pipeline.define('idKey', ({ model, value }) => (value == null ? model.idValue() : value), { ignoreNull: false });
|
|
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 });
|
|
87
|
+
|
|
88
|
+
Pipeline.define('defaultValue', ({ field, value }) => {
|
|
89
|
+
const { defaultValue } = field.toObject();
|
|
90
|
+
return value === undefined ? defaultValue : value;
|
|
91
|
+
}, { ignoreNull: false });
|
|
92
|
+
|
|
93
|
+
Pipeline.define('castValue', ({ field, value }) => {
|
|
94
|
+
const { type, isEmbedded } = field.toObject();
|
|
95
|
+
|
|
96
|
+
if (isEmbedded) return value;
|
|
97
|
+
|
|
98
|
+
return map(value, (v) => {
|
|
99
|
+
switch (type) {
|
|
100
|
+
case 'String': {
|
|
101
|
+
return `${v}`;
|
|
102
|
+
}
|
|
103
|
+
case 'Float': case 'Number': {
|
|
104
|
+
const num = Number(v);
|
|
105
|
+
if (!Number.isNaN(num)) return num;
|
|
106
|
+
return v;
|
|
107
|
+
}
|
|
108
|
+
case 'Int': {
|
|
109
|
+
const num = Number(v);
|
|
110
|
+
if (!Number.isNaN(num)) return parseInt(v, 10);
|
|
111
|
+
return v;
|
|
112
|
+
}
|
|
113
|
+
case 'Boolean': {
|
|
114
|
+
if (v === 'true') return true;
|
|
115
|
+
if (v === 'false') return false;
|
|
116
|
+
return v;
|
|
117
|
+
}
|
|
118
|
+
default: {
|
|
119
|
+
return v;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}, { itemize: false });
|
|
124
|
+
|
|
125
|
+
// Required fields
|
|
126
|
+
Pipeline.define('required', ({ model, field, value }) => {
|
|
127
|
+
if (value == null) throw Boom.badRequest(`${model}.${field} is required`);
|
|
128
|
+
}, { ignoreNull: false });
|
|
129
|
+
|
|
130
|
+
// A field cannot hold a reference to itself
|
|
131
|
+
Pipeline.define('selfless', ({ model, field, parent, parentPath, value }) => {
|
|
132
|
+
if (`${value}` === `${parentPath('id')}`) throw Boom.badRequest(`${model}.${field} cannot hold a reference to itself`);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Once set it cannot be changed
|
|
136
|
+
Pipeline.define('immutable', ({ model, field, docPath, parentPath, path, value }) => {
|
|
137
|
+
const hint = { id: parentPath('id') };
|
|
138
|
+
const oldVal = docPath(path, hint);
|
|
139
|
+
if (oldVal !== undefined && value !== undefined && `${hashObject(oldVal)}` !== `${hashObject(value)}`) throw Boom.badRequest(`${model}.${field} is immutable; cannot be changed once set ${oldVal} -> ${value}`);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// List of allowed values
|
|
143
|
+
Pipeline.factory('Allow', (...args) => function allow({ model, field, value }) {
|
|
144
|
+
if (args.indexOf(value) === -1) throw Boom.badRequest(`${model}.${field} allows ${args}; found '${value}'`);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// List of disallowed values
|
|
148
|
+
Pipeline.factory('Deny', (...args) => function deny({ model, field, value }) {
|
|
149
|
+
if (args.indexOf(value) > -1) throw Boom.badRequest(`${model}.${field} denys ${args}; found '${value}'`);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Min/Max range
|
|
153
|
+
Pipeline.factory('Range', (min, max) => {
|
|
154
|
+
if (min == null) min = undefined;
|
|
155
|
+
if (max == null) max = undefined;
|
|
156
|
+
|
|
157
|
+
return function range({ model, field, value }) {
|
|
158
|
+
const num = +value; // Coerce to number if possible
|
|
159
|
+
const test = Number.isNaN(num) ? value.length : num;
|
|
160
|
+
if (test < min || test > max) throw Boom.badRequest(`${model}.${field} must satisfy range ${min}:${max}; found '${value}'`);
|
|
161
|
+
};
|
|
162
|
+
}, { itemize: false });
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// const jsStringMethods = [
|
|
167
|
+
// 'charAt', 'charCodeAt', 'codePointAt', 'concat', 'indexOf', 'lastIndexOf', 'localeCompare',
|
|
168
|
+
// 'normalize', 'padEnd', 'padStart', 'repeat', 'replace', 'search', 'slice', 'split', 'substr', 'substring',
|
|
169
|
+
// 'toLocaleLowerCase', 'toLocaleUpperCase', 'toLowerCase', 'toString', 'toUpperCase', 'trim', 'trimEnd', 'trimStart', 'raw',
|
|
170
|
+
// ];
|
|
171
|
+
|
|
172
|
+
// Transformer.factory('toTitleCase', () => ({ value }) => value.replace(/\w\S*/g, w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()), { enumerable: true });
|
|
173
|
+
// Transformer.factory('toLocaleTitleCase', (...args) => ({ value }) => value.replace(/\w\S*/g, w => w.charAt(0).toLocaleUpperCase(...args) + w.slice(1).toLocaleLowerCase()));
|
|
174
|
+
// Transformer.factory('toSentenceCase', () => ({ value }) => value.charAt(0).toUpperCase() + value.slice(1), { enumerable: true });
|
|
175
|
+
// Transformer.factory('toLocaleSentenceCase', (...args) => ({ value }) => value.charAt(0).toLocaleUpperCase(...args) + value.slice(1));
|
|
176
|
+
// Transformer.factory('toArray', () => ({ value }) => (Array.isArray(value) ? value : [value]), { itemize: false, enumerable: true });
|
|
177
|
+
// Transformer.factory('toDate', () => ({ value }) => new Date(value), { enumerable: true, writable: true });
|
|
178
|
+
// Transformer.factory('dedupe', () => ({ value }) => uniqWith(value, (b, c) => hashObject(b) === hashObject(c)), { ignoreNull: false, enumerable: true });
|
|
179
|
+
// Transformer.factory('dedupeBy', key => ({ value }) => uniqWith(value, (b, c) => hashObject(b[key]) === hashObject(c[key])), { ignoreNull: false, enumerable: true });
|
|
180
|
+
// Transformer.factory('timestamp', () => () => Date.now(), { enumerable: true, ignoreNull: false });
|
|
181
|
+
// Transformer.factory('createdAt', () => ({ value }) => value || Date.now(), { enumerable: true, ignoreNull: false });
|
|
182
|
+
// Transformer.factory('first', () => ({ value }) => (Array.isArray(value) ? value[0] : value), { enumerable: true });
|
|
183
|
+
// Transformer.factory('get', path => ({ value }) => get(value, path), { enumerable: true });
|
|
184
|
+
// Transformer.factory('set', path => ({ value }) => set({}, path, value), { enumerable: true });
|