@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +20 -3
  2. package/index.js +2 -8
  3. package/package.json +5 -7
  4. package/src/.DS_Store +0 -0
  5. package/src/core/EventEmitter.js +2 -4
  6. package/src/core/Resolver.js +32 -57
  7. package/src/core/Schema.js +5 -38
  8. package/src/data/.DS_Store +0 -0
  9. package/src/data/DataLoader.js +71 -32
  10. package/src/data/DataService.js +82 -59
  11. package/src/data/Field.js +59 -126
  12. package/src/data/Model.js +113 -105
  13. package/src/data/Pipeline.js +184 -0
  14. package/src/data/Type.js +38 -74
  15. package/src/driver/MongoDriver.js +27 -22
  16. package/src/graphql/.DS_Store +0 -0
  17. package/src/graphql/ast/Field.js +46 -24
  18. package/src/graphql/ast/Model.js +5 -16
  19. package/src/graphql/ast/Node.js +0 -25
  20. package/src/graphql/ast/Schema.js +105 -112
  21. package/src/graphql/extension/api.js +20 -18
  22. package/src/graphql/extension/framework.js +27 -33
  23. package/src/graphql/extension/type.js +2 -2
  24. package/src/query/Query.js +82 -14
  25. package/src/query/QueryBuilder.js +38 -30
  26. package/src/query/QueryBuilderTransaction.js +3 -3
  27. package/src/query/QueryResolver.js +77 -41
  28. package/src/query/QueryService.js +24 -42
  29. package/src/service/app.service.js +70 -13
  30. package/src/service/event.service.js +30 -73
  31. package/src/service/graphql.service.js +0 -9
  32. package/src/service/schema.service.js +5 -3
  33. package/src/core/GraphQL.js +0 -21
  34. package/src/core/Rule.js +0 -107
  35. package/src/core/SchemaDecorator.js +0 -46
  36. package/src/core/Transformer.js +0 -68
  37. package/src/data/Memoizer.js +0 -39
  38. package/src/data/ResultSet.js +0 -205
  39. package/src/data/stream/DataHydrator.js +0 -58
  40. package/src/data/stream/ResultSet.js +0 -34
  41. package/src/data/stream/ResultSetItem.js +0 -158
  42. package/src/data/stream/ResultSetItemProxy.js +0 -161
  43. package/src/graphql/ast/SchemaDecorator.js +0 -141
  44. 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 Rule = require('../core/Rule');
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
- cast(value) {
15
- if (value == null) return value;
16
- const casted = Transformer.cast(this.getType())(this, value);
17
- return this.isArray() ? ensureArray(casted) : casted;
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
- getDeserializers() {
56
- const transformers = [];
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 === 'deserialize') transformers.push(...value.map(t => Transformer.getInstances()[t]));
61
- });
62
-
63
- return transformers.concat(this.type.getDeserializers());
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
- getResolvers() {
67
- const resolvers = [];
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
- Object.entries(this.getDirectiveArgs('field', {})).forEach(([key, value]) => {
70
- if (!Array.isArray(value)) value = [value];
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
- return resolvers.concat(this.type.getResolvers());
75
- }
51
+ // Ensure where clause for DB lookup
52
+ args.where = args.where || {};
76
53
 
77
- validate(query, value) {
78
- const modelRef = this.getModelRef();
79
- const rules = [...this.getRules()];
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
- if (modelRef && !this.isEmbedded()) rules.push(Rule.ensureId());
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
- return Promise.all(rules.map((rule) => {
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
- });
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
- * Ensures the value of the field is appropriate for the driver
109
- */
110
- serialize(query, value, minimal = false) {
111
- const modelRef = uvl(this.getModelRef(), this.getType() === 'ID' ? this.model : undefined);
112
- const isEmbedded = this.isEmbedded();
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 { map, ucFirst, ensureArray } = require('../service/app.service');
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
- * Called when creating a new document. Will add attributes such as id, createdAt, updatedAt
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
- appendCreateFields(input, embed = false) {
51
- // id, createdAt, updatedAt
52
- const timestamp = new Date();
53
- if (embed && !input.id && this.idKey()) input.id = this.idValue();
54
- if (!input.createdAt) input.createdAt = timestamp;
55
- input.updatedAt = timestamp;
56
-
57
- // Generate embedded default values
58
- this.getEmbeddedFields().filter(field => field.isPersistable()).forEach((field) => {
59
- if (input[field]) map(input[field], v => field.getModelRef().appendCreateFields(v, true));
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
- return input;
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
- appendUpdateFields(input, embed = false) {
66
- // id, updatedAt
67
- if (embed && !input.id && this.idKey()) input.id = this.idValue();
68
- input.updatedAt = new Date();
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
- // Generate embedded default values
71
- this.getEmbeddedFields().filter(field => field.isPersistable()).forEach((field) => {
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 input;
76
- }
125
+ return map(obj, (parent) => {
126
+ // "root" is the base of the object
127
+ root = root || parent;
77
128
 
78
- appendDefaultFields(query, input) {
79
- this.getDefaultedFields().filter(field => field.isPersistable()).forEach((field) => {
80
- input[field] = field.resolveBoundValue(query, input[field]);
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
- // Generate embedded default values
84
- this.getEmbeddedFields().filter(field => field.isPersistable()).forEach((field) => {
85
- map(input[field], v => field.getModelRef().appendDefaultFields(query, v));
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
- return input;
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
- * Serialize data from Domain Model to Data Model (where clause has special handling)
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
- * Deserialize data from Data Model to Domain Model
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
- * Normalizes data by renaming keys and serdes on field values (unless keysOnly)
131
- */
132
- normalize(query, data, serdes = (() => { throw new Error('No Sir Sir SerDes!'); }), keysOnly = false) {
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
- getShape(serdes = 'deserialize') {
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
- shape(data, serdes = (() => { throw new Error('No Sir Sir SerDes!'); }), shape) {
154
- shape = shape || this.getShape(serdes);
163
+ return mapPromise(obj, (parent) => {
164
+ // "root" is the base of the object
165
+ root = root || parent;
155
166
 
156
- return map(data, (doc) => {
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
- }
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
- validate(query, data) {
166
- const normalized = this.deserialize(query, data);
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
- return Promise.all(this.getFields().map((field) => {
169
- return Promise.all(ensureArray(map(normalized, (obj) => {
170
- if (obj == null) return Promise.resolve();
171
- return field.validate(query, obj[field.getName()]);
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 });