@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +21 -3
  2. package/index.js +2 -6
  3. package/package.json +4 -4
  4. package/src/.DS_Store +0 -0
  5. package/src/core/EventEmitter.js +2 -4
  6. package/src/core/Resolver.js +43 -60
  7. package/src/core/Schema.js +3 -36
  8. package/src/data/.DS_Store +0 -0
  9. package/src/data/DataLoader.js +71 -32
  10. package/src/data/DataService.js +59 -58
  11. package/src/data/Field.js +71 -121
  12. package/src/data/Model.js +98 -108
  13. package/src/data/Pipeline.js +174 -0
  14. package/src/data/Type.js +19 -74
  15. package/src/driver/MongoDriver.js +21 -19
  16. package/src/graphql/.DS_Store +0 -0
  17. package/src/graphql/ast/Field.js +43 -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 +107 -111
  21. package/src/graphql/extension/api.js +20 -18
  22. package/src/graphql/extension/framework.js +25 -33
  23. package/src/graphql/extension/type.js +2 -2
  24. package/src/query/Query.js +72 -14
  25. package/src/query/QueryBuilder.js +38 -30
  26. package/src/query/QueryBuilderTransaction.js +3 -3
  27. package/src/query/QueryResolver.js +92 -42
  28. package/src/query/QueryService.js +31 -34
  29. package/src/service/app.service.js +67 -9
  30. package/src/service/event.service.js +5 -79
  31. package/src/service/schema.service.js +5 -3
  32. package/src/core/Rule.js +0 -107
  33. package/src/core/SchemaDecorator.js +0 -46
  34. package/src/core/Transformer.js +0 -68
  35. package/src/data/Memoizer.js +0 -39
  36. package/src/data/ResultSet.js +0 -205
  37. package/src/data/stream/DataHydrator.js +0 -58
  38. package/src/data/stream/ResultSet.js +0 -34
  39. package/src/data/stream/ResultSetItem.js +0 -158
  40. package/src/data/stream/ResultSetItemProxy.js +0 -161
  41. package/src/graphql/ast/SchemaDecorator.js +0 -141
  42. 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 Rule = require('../core/Rule');
4
- const Transformer = require('../core/Transformer');
5
- const { map, uvl, isPlainObject, ensureArray, promiseChain } = require('../service/app.service');
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
- 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
- }
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
- Object.entries(this.getDirectiveArgs('field', {})).forEach(([key, value]) => {
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 === 'deserialize') transformers.push(...value.map(t => Transformer.getInstances()[t]));
61
- });
62
-
63
- return transformers.concat(this.type.getDeserializers());
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
- getResolvers() {
67
- const resolvers = [];
68
-
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
- });
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
- validate(query, value) {
78
- const modelRef = this.getModelRef();
79
- const rules = [...this.getRules()];
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 && !this.isEmbedded()) rules.push(Rule.ensureId());
56
+ if (modelRef && isPlainObject(ensureArray(value)[0])) return modelRef.validate(query, value); // Model delegation
82
57
 
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
- });
58
+ return value;
89
59
  }
90
60
 
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());
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
- // Determine transformers
99
- const transformers = [...(serdes === 'serialize' ? this.getSerializers() : this.getDeserializers()), ...this.getTransformers()];
65
+ // Default resolver return immediately!
66
+ if (isScalar || isEmbedded) return value;
100
67
 
101
- // Transform
102
- return transformers.reduce((prev, transformer) => {
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
- * 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();
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
- // If embedded, simply delgate
115
- if (isEmbedded) return modelRef.serialize(query, value, minimal);
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
- // 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
- }
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
- deserialize(query, value) {
124
- return this.transform(query, value, 'deserialize');
90
+ return resolver.match(modelRef).id(value).one({ required: isRequired });
125
91
  }
126
92
 
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));
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 { map, ucFirst, ensureArray } = require('../service/app.service');
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
- * Called when creating a new document. Will add attributes such as id, createdAt, updatedAt
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
- 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));
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
- appendUpdateFields(input, embed = false) {
66
- // id, updatedAt
67
- if (embed && !input.id && this.idKey()) input.id = this.idValue();
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
- // 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
- });
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
- return input;
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
- appendDefaultFields(query, input) {
79
- this.getDefaultedFields().filter(field => field.isPersistable()).forEach((field) => {
80
- input[field] = field.resolveBoundValue(query, input[field]);
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
- // Generate embedded default values
84
- this.getEmbeddedFields().filter(field => field.isPersistable()).forEach((field) => {
85
- map(input[field], v => field.getModelRef().appendDefaultFields(query, v));
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
- return input;
89
- }
121
+ // Adding useful shape info
122
+ shape.crud = crud;
123
+ shape.model = this;
124
+ shape.serdes = serdes;
90
125
 
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);
126
+ // Cache and return
127
+ this.shapesCache.set(cacheKey, shape);
128
+ return shape;
96
129
  }
97
130
 
98
- /**
99
- * Deserialize data from Data Model to Domain Model
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
- * 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
- 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
- * 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
- }
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
- 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
- }
144
+ return shape.reduce((prev, { field, from, to, path, type, isArray, defaultValue, transformers = [], shape: subShape }) => {
145
+ const startValue = parent[from];
152
146
 
153
- shape(data, serdes = (() => { throw new Error('No Sir Sir SerDes!'); }), shape) {
154
- shape = shape || this.getShape(serdes);
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
- 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
- }
153
+ // if (`${field}` === 'searchability') console.log(startValue, transformedValue, transformers);
164
154
 
165
- validate(query, data) {
166
- const normalized = this.deserialize(query, data);
155
+ // Determine if key should stay or be removed
156
+ if (transformedValue === undefined && !Object.prototype.hasOwnProperty.call(parent, from)) return prev;
167
157
 
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
- }));
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 });