@coderich/autograph 0.10.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -3
- package/index.js +2 -6
- package/package.json +4 -4
- package/src/.DS_Store +0 -0
- package/src/core/EventEmitter.js +2 -4
- package/src/core/Resolver.js +43 -60
- package/src/core/Schema.js +3 -36
- package/src/data/.DS_Store +0 -0
- package/src/data/DataLoader.js +71 -32
- package/src/data/DataService.js +59 -58
- package/src/data/Field.js +71 -121
- package/src/data/Model.js +98 -108
- package/src/data/Pipeline.js +174 -0
- package/src/data/Type.js +19 -74
- package/src/driver/MongoDriver.js +21 -19
- package/src/graphql/.DS_Store +0 -0
- package/src/graphql/ast/Field.js +43 -24
- package/src/graphql/ast/Model.js +5 -16
- package/src/graphql/ast/Node.js +0 -25
- package/src/graphql/ast/Schema.js +107 -111
- package/src/graphql/extension/api.js +20 -18
- package/src/graphql/extension/framework.js +25 -33
- package/src/graphql/extension/type.js +2 -2
- package/src/query/Query.js +72 -14
- package/src/query/QueryBuilder.js +38 -30
- package/src/query/QueryBuilderTransaction.js +3 -3
- package/src/query/QueryResolver.js +92 -42
- package/src/query/QueryService.js +31 -34
- package/src/service/app.service.js +67 -9
- package/src/service/event.service.js +5 -79
- package/src/service/schema.service.js +5 -3
- package/src/core/Rule.js +0 -107
- package/src/core/SchemaDecorator.js +0 -46
- package/src/core/Transformer.js +0 -68
- package/src/data/Memoizer.js +0 -39
- package/src/data/ResultSet.js +0 -205
- package/src/data/stream/DataHydrator.js +0 -58
- package/src/data/stream/ResultSet.js +0 -34
- package/src/data/stream/ResultSetItem.js +0 -158
- package/src/data/stream/ResultSetItemProxy.js +0 -161
- package/src/graphql/ast/SchemaDecorator.js +0 -141
- package/src/graphql/directive/authz.directive.js +0 -84
package/src/data/Field.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
const { isEmpty } = require('lodash');
|
|
1
2
|
const Type = require('./Type');
|
|
2
3
|
const Field = require('../graphql/ast/Field');
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const {
|
|
4
|
+
const Boom = require('../core/Boom');
|
|
5
|
+
const Pipeline = require('./Pipeline');
|
|
6
|
+
const { isPlainObject, ensureArray } = require('../service/app.service');
|
|
6
7
|
|
|
7
8
|
module.exports = class extends Field {
|
|
8
9
|
constructor(model, field) {
|
|
@@ -11,140 +12,89 @@ module.exports = class extends Field {
|
|
|
11
12
|
this.model = model;
|
|
12
13
|
}
|
|
13
14
|
|
|
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
|
-
}
|
|
54
|
-
|
|
55
|
-
getDeserializers() {
|
|
56
|
-
const transformers = [];
|
|
15
|
+
getStructures() {
|
|
16
|
+
// Grab structures from the underlying type
|
|
17
|
+
const structures = this.type.getStructures();
|
|
18
|
+
const { isRequired, isPersistable, isVirtual, isPrimaryKeyId, isIdField } = this.props;
|
|
57
19
|
|
|
58
|
-
|
|
20
|
+
// Structures defined on the field
|
|
21
|
+
const $structures = Object.entries(this.getDirectiveArgs('field', {})).reduce((prev, [key, value]) => {
|
|
59
22
|
if (!Array.isArray(value)) value = [value];
|
|
60
|
-
if (key === '
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
23
|
+
if (key === 'instruct') prev.instructs.unshift(...value.map(t => Pipeline[t]));
|
|
24
|
+
if (key === 'restruct') prev.restructs.unshift(...value.map(t => Pipeline[t]));
|
|
25
|
+
if (key === 'destruct') prev.destructs.unshift(...value.map(t => Pipeline[t]));
|
|
26
|
+
if (key === 'construct') prev.constructs.unshift(...value.map(t => Pipeline[t]));
|
|
27
|
+
if (key === 'serialize') prev.serializers.unshift(...value.map(t => Pipeline[t]));
|
|
28
|
+
if (key === 'deserialize') prev.deserializers.unshift(...value.map(t => Pipeline[t]));
|
|
29
|
+
if (key === 'transform') prev.transformers.unshift(...value.map(t => Pipeline[t]));
|
|
30
|
+
return prev;
|
|
31
|
+
}, structures);
|
|
32
|
+
|
|
33
|
+
// IDs (first - shift)
|
|
34
|
+
if (isPrimaryKeyId) $structures.serializers.unshift(Pipeline.idKey);
|
|
35
|
+
if (isIdField) $structures.$serializers.unshift(Pipeline.idField);
|
|
36
|
+
|
|
37
|
+
// Required (last - push)
|
|
38
|
+
if (isRequired && isPersistable && !isVirtual) $structures.serializers.push(Pipeline.required);
|
|
39
|
+
|
|
40
|
+
return $structures;
|
|
64
41
|
}
|
|
65
42
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (!Array.isArray(value)) value = [value];
|
|
71
|
-
if (key === 'resolve') resolvers.push(...value.map(t => Transformer.getInstances()[t]));
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
return resolvers.concat(this.type.getResolvers());
|
|
75
|
-
}
|
|
43
|
+
async validate(query, value) {
|
|
44
|
+
if (value == null) return value;
|
|
45
|
+
const { resolver } = query.toObject();
|
|
46
|
+
const { type, modelRef, isEmbedded } = this.props;
|
|
76
47
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
}
|
|
80
55
|
|
|
81
|
-
if (modelRef &&
|
|
56
|
+
if (modelRef && isPlainObject(ensureArray(value)[0])) return modelRef.validate(query, value); // Model delegation
|
|
82
57
|
|
|
83
|
-
return
|
|
84
|
-
return rule(this, value, query);
|
|
85
|
-
})).then((res) => {
|
|
86
|
-
if (modelRef && isPlainObject(ensureArray(value)[0])) return modelRef.validate(query, value); // Model delegation
|
|
87
|
-
return res;
|
|
88
|
-
});
|
|
58
|
+
return value;
|
|
89
59
|
}
|
|
90
60
|
|
|
91
|
-
|
|
92
|
-
|
|
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());
|
|
61
|
+
resolve(resolver, doc, args = {}) {
|
|
62
|
+
const { name, isArray, isScalar, isVirtual, isRequired, isEmbedded, modelRef, virtualField } = this.props;
|
|
63
|
+
const value = doc[name];
|
|
97
64
|
|
|
98
|
-
//
|
|
99
|
-
|
|
65
|
+
// Default resolver return immediately!
|
|
66
|
+
if (isScalar || isEmbedded) return value;
|
|
100
67
|
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
return transformer(this, prev, query);
|
|
104
|
-
}, this.cast($value));
|
|
105
|
-
}
|
|
68
|
+
// Ensure where clause for DB lookup
|
|
69
|
+
args.where = args.where || {};
|
|
106
70
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
71
|
+
if (isArray) {
|
|
72
|
+
if (isVirtual) {
|
|
73
|
+
if (isEmpty(args.where)) args.batch = `${virtualField}`;
|
|
74
|
+
args.where[virtualField] = doc.id;
|
|
75
|
+
return resolver.match(modelRef).merge(args).many();
|
|
76
|
+
}
|
|
113
77
|
|
|
114
|
-
|
|
115
|
-
|
|
78
|
+
// Not a "required" query + strip out nulls
|
|
79
|
+
if (isEmpty(args.where)) args.batch = 'id';
|
|
80
|
+
args.where.id = value;
|
|
81
|
+
return resolver.match(modelRef).merge(args).many();
|
|
82
|
+
}
|
|
116
83
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
84
|
+
if (isVirtual) {
|
|
85
|
+
if (isEmpty(args.where)) args.batch = `${virtualField}`;
|
|
86
|
+
args.where[virtualField] = doc.id;
|
|
87
|
+
return resolver.match(modelRef).merge(args).one();
|
|
88
|
+
}
|
|
122
89
|
|
|
123
|
-
|
|
124
|
-
return this.transform(query, value, 'deserialize');
|
|
90
|
+
return resolver.match(modelRef).id(value).one({ required: isRequired });
|
|
125
91
|
}
|
|
126
92
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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));
|
|
93
|
+
count(resolver, doc, args = {}) {
|
|
94
|
+
const { name, isVirtual, modelRef, virtualField } = this.props;
|
|
95
|
+
args.where = args.where || {};
|
|
96
|
+
if (isVirtual) args.where[virtualField] = doc.id;
|
|
97
|
+
else args.where.id = doc[name];
|
|
98
|
+
return resolver.match(modelRef).merge(args).count();
|
|
149
99
|
}
|
|
150
100
|
};
|
package/src/data/Model.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
const Stream = require('stream');
|
|
1
2
|
const Field = require('./Field');
|
|
3
|
+
const Pipeline = require('./Pipeline');
|
|
2
4
|
const Model = require('../graphql/ast/Model');
|
|
3
|
-
const {
|
|
5
|
+
const { paginateResultSet } = require('./DataService');
|
|
6
|
+
const { eventEmitter } = require('../service/event.service');
|
|
7
|
+
const { map, seek, deseek, ensureArray } = require('../service/app.service');
|
|
4
8
|
|
|
5
9
|
module.exports = class extends Model {
|
|
6
10
|
constructor(schema, model, driver) {
|
|
@@ -8,6 +12,7 @@ module.exports = class extends Model {
|
|
|
8
12
|
this.driver = driver;
|
|
9
13
|
this.fields = super.getFields().map(field => new Field(this, field));
|
|
10
14
|
this.namedQueries = {};
|
|
15
|
+
this.shapesCache = new Map();
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
raw() {
|
|
@@ -43,133 +48,118 @@ module.exports = class extends Model {
|
|
|
43
48
|
return this.referentials;
|
|
44
49
|
}
|
|
45
50
|
|
|
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
|
+
|
|
46
67
|
/**
|
|
47
|
-
*
|
|
48
|
-
* while ensuring that all defaulted values are set appropriately
|
|
68
|
+
* Convenience method to deserialize data from a data source (such as a database)
|
|
49
69
|
*/
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
deserialize(mixed, query) {
|
|
71
|
+
const { flags = {} } = query.toObject();
|
|
72
|
+
const { pipeline = true } = flags;
|
|
73
|
+
const shape = this.getShape();
|
|
74
|
+
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
if (!(mixed instanceof Stream)) {
|
|
77
|
+
resolve(pipeline ? this.shapeObject(shape, mixed, query) : mixed);
|
|
78
|
+
} else {
|
|
79
|
+
const results = [];
|
|
80
|
+
mixed.on('data', (data) => { results.push(pipeline ? this.shapeObject(shape, data, query) : data); });
|
|
81
|
+
mixed.on('end', () => { resolve(results); });
|
|
82
|
+
mixed.on('error', reject);
|
|
83
|
+
}
|
|
84
|
+
}).then((results) => {
|
|
85
|
+
return results.length && pipeline ? paginateResultSet(results, query) : results;
|
|
60
86
|
});
|
|
61
|
-
|
|
62
|
-
return input;
|
|
63
87
|
}
|
|
64
88
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (
|
|
68
|
-
input.updatedAt = new Date();
|
|
89
|
+
getShape(crud = 'read', target = 'doc', paths = []) {
|
|
90
|
+
const cacheKey = `${crud}:${target}`;
|
|
91
|
+
if (this.shapesCache.has(cacheKey)) return this.shapesCache.get(cacheKey);
|
|
69
92
|
|
|
70
|
-
|
|
71
|
-
this.
|
|
72
|
-
|
|
73
|
-
|
|
93
|
+
const serdes = crud === 'read' ? 'deserialize' : 'serialize';
|
|
94
|
+
const fields = serdes === 'deserialize' ? this.getSelectFields() : this.getPersistableFields();
|
|
95
|
+
const crudMap = { create: ['constructs'], update: ['restructs'], delete: ['destructs'], remove: ['destructs'] };
|
|
96
|
+
const crudKeys = crudMap[crud] || [];
|
|
74
97
|
|
|
75
|
-
|
|
76
|
-
|
|
98
|
+
const targetMap = {
|
|
99
|
+
doc: ['defaultValue', 'ensureArrayValue', 'castValue', ...crudKeys, `$${serdes}rs`, 'instructs', 'transformers', `${serdes}rs`],
|
|
100
|
+
where: ['castValue', `$${serdes}rs`, 'instructs'],
|
|
101
|
+
};
|
|
77
102
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
103
|
+
const structureKeys = targetMap[target] || ['castValue'];
|
|
104
|
+
|
|
105
|
+
// Create shape, recursive
|
|
106
|
+
const shape = fields.map((field) => {
|
|
107
|
+
const structures = field.getStructures();
|
|
108
|
+
const [key, name, type, isArray] = [field.getKey(), field.getName(), field.getType(), field.isArray(), field.isIdField()];
|
|
109
|
+
const [from, to] = serdes === 'serialize' ? [name, key] : [key, name];
|
|
110
|
+
const path = paths.concat(to);
|
|
111
|
+
const subShape = field.isEmbedded() ? field.getModelRef().getShape(crud, target, path) : null;
|
|
112
|
+
|
|
113
|
+
structures.castValue = Pipeline.castValue;
|
|
114
|
+
structures.defaultValue = Pipeline.defaultValue;
|
|
82
115
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
116
|
+
structures.ensureArrayValue = ({ value }) => (value != null && isArray && !Array.isArray(value) ? [value] : value);
|
|
117
|
+
const transformers = structureKeys.reduce((prev, struct) => prev.concat(structures[struct]), []).filter(Boolean);
|
|
118
|
+
return { field, path, from, to, type, isArray, transformers, shape: subShape };
|
|
86
119
|
});
|
|
87
120
|
|
|
88
|
-
|
|
89
|
-
|
|
121
|
+
// Adding useful shape info
|
|
122
|
+
shape.crud = crud;
|
|
123
|
+
shape.model = this;
|
|
124
|
+
shape.serdes = serdes;
|
|
90
125
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
serialize(query, data, minimal = false) {
|
|
95
|
-
return this.transform(query, data, 'serialize', minimal);
|
|
126
|
+
// Cache and return
|
|
127
|
+
this.shapesCache.set(cacheKey, shape);
|
|
128
|
+
return shape;
|
|
96
129
|
}
|
|
97
130
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
deserialize(query, data) {
|
|
102
|
-
return this.transform(query, data, 'deserialize');
|
|
103
|
-
}
|
|
131
|
+
shapeObject(shape, obj, query, root) {
|
|
132
|
+
const { serdes, model } = shape;
|
|
133
|
+
const { context, doc = {} } = query.toObject();
|
|
104
134
|
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
return prev;
|
|
125
|
-
}, {});
|
|
126
|
-
});
|
|
127
|
-
}
|
|
135
|
+
return map(obj, (parent) => {
|
|
136
|
+
// "root" is the base of the object
|
|
137
|
+
root = root || parent;
|
|
128
138
|
|
|
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
|
-
}
|
|
139
|
+
// Lookup helper functions
|
|
140
|
+
const docPath = (p, hint) => seek(doc, p, hint); // doc is already serialized; so always a seek
|
|
141
|
+
const rootPath = (p, hint) => (serdes === 'serialize' ? seek(root, p, hint) : deseek(shape, root, p, hint));
|
|
142
|
+
const parentPath = (p, hint) => (serdes === 'serialize' ? seek(parent, p, hint) : deseek(shape, parent, p, hint));
|
|
145
143
|
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
}
|
|
144
|
+
return shape.reduce((prev, { field, from, to, path, type, isArray, defaultValue, transformers = [], shape: subShape }) => {
|
|
145
|
+
const startValue = parent[from];
|
|
152
146
|
|
|
153
|
-
|
|
154
|
-
|
|
147
|
+
// Transform value
|
|
148
|
+
const transformedValue = transformers.reduce((value, t) => {
|
|
149
|
+
const v = t({ model, field, path, docPath, rootPath, parentPath, startValue, value, context });
|
|
150
|
+
return v === undefined ? value : v;
|
|
151
|
+
}, startValue);
|
|
155
152
|
|
|
156
|
-
|
|
157
|
-
return shape.reduce((prev, { from, to, shape: subShape }) => {
|
|
158
|
-
const value = doc[from];
|
|
159
|
-
if (value === undefined) return prev;
|
|
160
|
-
return Object.assign(prev, { [to]: subShape ? this.shape(value, serdes, subShape) : value });
|
|
161
|
-
}, {});
|
|
162
|
-
});
|
|
163
|
-
}
|
|
153
|
+
// if (`${field}` === 'searchability') console.log(startValue, transformedValue, transformers);
|
|
164
154
|
|
|
165
|
-
|
|
166
|
-
|
|
155
|
+
// Determine if key should stay or be removed
|
|
156
|
+
if (transformedValue === undefined && !Object.prototype.hasOwnProperty.call(parent, from)) return prev;
|
|
167
157
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return
|
|
172
|
-
})
|
|
173
|
-
})
|
|
158
|
+
// Rename key & assign value
|
|
159
|
+
prev[to] = (!subShape || transformedValue == null) ? transformedValue : this.shapeObject(subShape, transformedValue, query, root);
|
|
160
|
+
|
|
161
|
+
return prev;
|
|
162
|
+
}, {});
|
|
163
|
+
});
|
|
174
164
|
}
|
|
175
165
|
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
const { uniqWith } = require('lodash');
|
|
2
|
+
const { map, 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
|
+
Object.defineProperty(Pipeline, name, {
|
|
33
|
+
value: wrapper,
|
|
34
|
+
configurable,
|
|
35
|
+
enumerable: true,
|
|
36
|
+
});
|
|
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
|
+
Object.defineProperty(Pipeline, name, { value: Object.defineProperty(thunk, 'options', { value: options }) });
|
|
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('toArray', ({ value }) => (Array.isArray(value) ? value : [value]), { itemize: false });
|
|
70
|
+
Pipeline.define('toDate', ({ value }) => new Date(value), { configurable: true });
|
|
71
|
+
Pipeline.define('timestamp', ({ value }) => Date.now(), { ignoreNull: false });
|
|
72
|
+
Pipeline.define('createdAt', ({ value }) => value || Date.now(), { ignoreNull: false });
|
|
73
|
+
Pipeline.define('dedupe', ({ value }) => uniqWith(value, (b, c) => hashObject(b) === hashObject(c)), { itemize: false });
|
|
74
|
+
Pipeline.define('idKey', ({ model, value }) => (value == null ? model.idValue() : value), { ignoreNull: false });
|
|
75
|
+
Pipeline.define('idField', ({ field, value }) => map(value, v => field.getIdModel().idValue(v.id || v)));
|
|
76
|
+
|
|
77
|
+
Pipeline.define('defaultValue', ({ field, value }) => {
|
|
78
|
+
const { defaultValue } = field.toObject();
|
|
79
|
+
return value === undefined ? defaultValue : value;
|
|
80
|
+
}, { ignoreNull: false });
|
|
81
|
+
|
|
82
|
+
Pipeline.define('castValue', ({ field, value }) => {
|
|
83
|
+
const { type, isEmbedded } = field.toObject();
|
|
84
|
+
|
|
85
|
+
if (isEmbedded) return value;
|
|
86
|
+
|
|
87
|
+
return map(value, (v) => {
|
|
88
|
+
switch (type) {
|
|
89
|
+
case 'String': {
|
|
90
|
+
return `${v}`;
|
|
91
|
+
}
|
|
92
|
+
case 'Float': case 'Number': {
|
|
93
|
+
const num = Number(v);
|
|
94
|
+
if (!Number.isNaN(num)) return num;
|
|
95
|
+
return v;
|
|
96
|
+
}
|
|
97
|
+
case 'Int': {
|
|
98
|
+
const num = Number(v);
|
|
99
|
+
if (!Number.isNaN(num)) return parseInt(v, 10);
|
|
100
|
+
return v;
|
|
101
|
+
}
|
|
102
|
+
case 'Boolean': {
|
|
103
|
+
if (v === 'true') return true;
|
|
104
|
+
if (v === 'false') return false;
|
|
105
|
+
return v;
|
|
106
|
+
}
|
|
107
|
+
default: {
|
|
108
|
+
return v;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}, { itemize: false });
|
|
113
|
+
|
|
114
|
+
// Required fields
|
|
115
|
+
Pipeline.define('required', ({ model, field, value }) => {
|
|
116
|
+
if (value == null) throw Boom.badRequest(`${model}.${field} is required`);
|
|
117
|
+
}, { ignoreNull: false });
|
|
118
|
+
|
|
119
|
+
// A field cannot hold a reference to itself
|
|
120
|
+
Pipeline.define('selfless', ({ model, field, parentPath, value }) => {
|
|
121
|
+
if (`${value}` === `${parentPath('id')}`) throw Boom.badRequest(`${model}.${field} cannot hold a reference to itself`);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Once set it cannot be changed
|
|
125
|
+
Pipeline.define('immutable', ({ model, field, docPath, parentPath, path, value }) => {
|
|
126
|
+
const hint = { id: parentPath('id') };
|
|
127
|
+
const oldVal = docPath(path, hint);
|
|
128
|
+
if (oldVal !== undefined && value !== undefined && `${hashObject(oldVal)}` !== `${hashObject(value)}`) throw Boom.badRequest(`${model}.${field} is immutable; cannot be changed once set ${oldVal} -> ${value}`);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// List of allowed values
|
|
132
|
+
Pipeline.factory('allow', (...args) => function allow({ model, field, value }) {
|
|
133
|
+
if (args.indexOf(value) === -1) throw Boom.badRequest(`${model}.${field} allows ${args}; found '${value}'`);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// List of disallowed values
|
|
137
|
+
Pipeline.factory('deny', (...args) => function deny({ model, field, value }) {
|
|
138
|
+
if (args.indexOf(value) > -1) throw Boom.badRequest(`${model}.${field} denys ${args}; found '${value}'`);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Min/Max range
|
|
142
|
+
Pipeline.factory('range', (min, max) => {
|
|
143
|
+
if (min == null) min = undefined;
|
|
144
|
+
if (max == null) max = undefined;
|
|
145
|
+
|
|
146
|
+
return function range({ model, field, value }) {
|
|
147
|
+
const num = +value; // Coerce to number if possible
|
|
148
|
+
const test = Number.isNaN(num) ? value.length : num;
|
|
149
|
+
if (test < min || test > max) throw Boom.badRequest(`${model}.${field} must satisfy range ${min}:${max}; found '${value}'`);
|
|
150
|
+
};
|
|
151
|
+
}, { itemize: false });
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
// const jsStringMethods = [
|
|
157
|
+
// 'charAt', 'charCodeAt', 'codePointAt', 'concat', 'indexOf', 'lastIndexOf', 'localeCompare',
|
|
158
|
+
// 'normalize', 'padEnd', 'padStart', 'repeat', 'replace', 'search', 'slice', 'split', 'substr', 'substring',
|
|
159
|
+
// 'toLocaleLowerCase', 'toLocaleUpperCase', 'toLowerCase', 'toString', 'toUpperCase', 'trim', 'trimEnd', 'trimStart', 'raw',
|
|
160
|
+
// ];
|
|
161
|
+
|
|
162
|
+
// Transformer.factory('toTitleCase', () => ({ value }) => value.replace(/\w\S*/g, w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()), { enumerable: true });
|
|
163
|
+
// Transformer.factory('toLocaleTitleCase', (...args) => ({ value }) => value.replace(/\w\S*/g, w => w.charAt(0).toLocaleUpperCase(...args) + w.slice(1).toLocaleLowerCase()));
|
|
164
|
+
// Transformer.factory('toSentenceCase', () => ({ value }) => value.charAt(0).toUpperCase() + value.slice(1), { enumerable: true });
|
|
165
|
+
// Transformer.factory('toLocaleSentenceCase', (...args) => ({ value }) => value.charAt(0).toLocaleUpperCase(...args) + value.slice(1));
|
|
166
|
+
// Transformer.factory('toArray', () => ({ value }) => (Array.isArray(value) ? value : [value]), { itemize: false, enumerable: true });
|
|
167
|
+
// Transformer.factory('toDate', () => ({ value }) => new Date(value), { enumerable: true, writable: true });
|
|
168
|
+
// Transformer.factory('dedupe', () => ({ value }) => uniqWith(value, (b, c) => hashObject(b) === hashObject(c)), { ignoreNull: false, enumerable: true });
|
|
169
|
+
// Transformer.factory('dedupeBy', key => ({ value }) => uniqWith(value, (b, c) => hashObject(b[key]) === hashObject(c[key])), { ignoreNull: false, enumerable: true });
|
|
170
|
+
// Transformer.factory('timestamp', () => () => Date.now(), { enumerable: true, ignoreNull: false });
|
|
171
|
+
// Transformer.factory('createdAt', () => ({ value }) => value || Date.now(), { enumerable: true, ignoreNull: false });
|
|
172
|
+
// Transformer.factory('first', () => ({ value }) => (Array.isArray(value) ? value[0] : value), { enumerable: true });
|
|
173
|
+
// Transformer.factory('get', path => ({ value }) => get(value, path), { enumerable: true });
|
|
174
|
+
// Transformer.factory('set', path => ({ value }) => set({}, path, value), { enumerable: true });
|