@coderich/autograph 0.9.15 → 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 (39) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/index.js +2 -6
  3. package/package.json +13 -10
  4. package/src/.DS_Store +0 -0
  5. package/src/core/EventEmitter.js +2 -4
  6. package/src/core/Resolver.js +43 -59
  7. package/src/core/Schema.js +3 -36
  8. package/src/core/ServerResolver.js +7 -93
  9. package/src/data/DataLoader.js +68 -27
  10. package/src/data/DataService.js +59 -58
  11. package/src/data/Field.js +71 -96
  12. package/src/data/Model.js +95 -113
  13. package/src/data/Pipeline.js +174 -0
  14. package/src/data/Type.js +19 -60
  15. package/src/driver/MongoDriver.js +52 -27
  16. package/src/graphql/ast/Field.js +44 -26
  17. package/src/graphql/ast/Model.js +5 -16
  18. package/src/graphql/ast/Node.js +0 -32
  19. package/src/graphql/ast/Schema.js +107 -111
  20. package/src/graphql/extension/api.js +22 -35
  21. package/src/graphql/extension/framework.js +25 -33
  22. package/src/graphql/extension/type.js +2 -2
  23. package/src/query/Query.js +73 -15
  24. package/src/query/QueryBuilder.js +37 -28
  25. package/src/query/QueryBuilderTransaction.js +3 -3
  26. package/src/query/QueryResolver.js +93 -44
  27. package/src/query/QueryService.js +31 -34
  28. package/src/service/app.service.js +56 -9
  29. package/src/service/decorator.service.js +21 -288
  30. package/src/service/event.service.js +5 -79
  31. package/src/service/graphql.service.js +1 -1
  32. package/src/service/schema.service.js +5 -3
  33. package/src/core/Rule.js +0 -107
  34. package/src/core/SchemaDecorator.js +0 -46
  35. package/src/core/Transformer.js +0 -68
  36. package/src/data/Memoizer.js +0 -39
  37. package/src/data/ResultSet.js +0 -246
  38. package/src/graphql/ast/SchemaDecorator.js +0 -138
  39. package/src/graphql/directive/authz.directive.js +0 -84
@@ -1,11 +1,19 @@
1
- const { remove } = require('lodash');
2
- const Boom = require('../core/Boom');
3
- const { isPlainObject, objectContaining, mergeDeep, map } = require('../service/app.service');
1
+ const { get, remove } = require('lodash');
2
+ const { map, isPlainObject, objectContaining, mergeDeep, ensureArray, keyPaths } = require('../service/app.service');
4
3
 
5
- exports.paginateResultSet = (rs, first, after, last, before) => {
4
+ exports.paginateResultSet = (rs, query) => {
5
+ const { first, after, last, before, sort } = query.toObject();
6
+ const sortPaths = keyPaths(sort);
7
+ const limiter = first || last;
6
8
  let hasNextPage = false;
7
9
  let hasPreviousPage = false;
8
- const limiter = first || last;
10
+
11
+ // Add $$cursor data
12
+ map(rs, (doc) => {
13
+ const sortValues = sortPaths.reduce((prv, path) => Object.assign(prv, { [path]: get(doc, path) }), {});
14
+ const sortJSON = JSON.stringify(sortValues);
15
+ doc.$$cursor = Buffer.from(sortJSON).toString('base64');
16
+ });
9
17
 
10
18
  // First try to take off the "bookends" ($gte | $lte)
11
19
  if (rs.length && rs[0].$$cursor === after) {
@@ -34,64 +42,57 @@ exports.paginateResultSet = (rs, first, after, last, before) => {
34
42
  }
35
43
  }
36
44
 
37
- return { hasNextPage, hasPreviousPage };
45
+ // Add $$pageInfo data (hidden)
46
+ return Object.defineProperties(rs, {
47
+ $$pageInfo: {
48
+ get() {
49
+ return {
50
+ startCursor: get(rs, '0.$$cursor', ''),
51
+ endCursor: get(rs, `${rs.length - 1}.$$cursor`, ''),
52
+ hasPreviousPage,
53
+ hasNextPage,
54
+ };
55
+ },
56
+ enumerable: false,
57
+ },
58
+ });
38
59
  };
39
60
 
40
- /**
41
- * @param from <Array>
42
- * @param to <Array>
43
- */
44
- exports.spliceEmbeddedArray = (query, doc, key, from, to) => {
45
- const { model } = query.toObject();
46
- const field = model.getField(key);
47
- const modelRef = field.getModelRef();
61
+ exports.spliceEmbeddedArray = (array, from, to) => {
48
62
  const op = from && to ? 'edit' : (from ? 'pull' : 'push'); // eslint-disable-line no-nested-ternary
49
- const promises = [];
50
-
51
- // Can only splice arrays
52
- if (!field || !field.isArray()) return Promise.reject(Boom.badRequest(`Cannot splice field '${key}'`));
53
-
54
- // We have to deserialize because this normalizes the data (casting etc)
55
- let $to = model.deserialize(query, { [key]: to })[key] || to;
56
- const $from = model.deserialize(query, { [key]: from })[key] || from;
57
-
58
- // If it's embedded we need to append default/create fields for insertion
59
- if ($to && field.isEmbedded()) $to = $to.map(el => modelRef.appendDefaultFields(query, modelRef.appendCreateFields(el, true)));
60
63
 
61
64
  // Convenience so the user does not have to explicity type out the same value over and over to replace
62
- if ($from && $from.length > 1 && $to && $to.length === 1) $to = Array.from($from).fill($to[0]);
63
-
64
- // Traverse the document till we find the segment to modify (in place)
65
- return key.split('.').reduce((prev, segment, i, arr) => {
66
- if (prev == null) return prev;
65
+ if (from && from.length > 1 && to && to.length === 1) to = Array.from(from).fill(to[0]);
67
66
 
68
- return map(prev, (data) => {
69
- if (i < (arr.length - 1)) return data[segment]; // We have not found the target segment yet
70
- data[segment] = data[segment] || []; // Ensuring target segment is an array
71
-
72
- switch (op) {
73
- case 'edit': {
74
- data[segment].forEach((el, j) => {
75
- $from.forEach((val, k) => {
76
- if (objectContaining(el, val)) data[segment][j] = isPlainObject(el) ? mergeDeep(el, $to[k]) : $to[k];
77
- });
78
- });
79
- break;
80
- }
81
- case 'push': {
82
- data[segment].push(...$to);
83
- break;
84
- }
85
- case 'pull': {
86
- remove(data[segment], el => $from.find(val => objectContaining(el, val)));
87
- break;
88
- }
89
- default: {
90
- break;
91
- }
92
- }
67
+ switch (op) {
68
+ case 'edit': {
69
+ array.forEach((el, j) => {
70
+ ensureArray(from).forEach((val, k) => {
71
+ if (objectContaining(el, val)) array[j] = isPlainObject(el) ? mergeDeep(el, ensureArray(to)[k]) : ensureArray(to)[k];
72
+ });
73
+ });
74
+ break;
75
+ }
76
+ // case 'edit': {
77
+ // ensureArray(from).forEach((f, i) => {
78
+ // const t = ensureArray(to)[i];
79
+ // const indexes = array.map((el, j) => (el === f ? j : -1)).filter(index => index !== -1);
80
+ // indexes.forEach(index => (array[index] = t));
81
+ // });
82
+ // break;
83
+ // }
84
+ case 'push': {
85
+ array.push(...to);
86
+ break;
87
+ }
88
+ case 'pull': {
89
+ remove(array, el => from.find(val => objectContaining(el, val)));
90
+ break;
91
+ }
92
+ default: {
93
+ break;
94
+ }
95
+ }
93
96
 
94
- return Promise.all(promises);
95
- });
96
- }, doc);
97
+ return array;
97
98
  };
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 } = 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,115 +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());
15
+ getStructures() {
16
+ // Grab structures from the underlying type
17
+ const structures = this.type.getStructures();
18
+ const { isRequired, isPersistable, isVirtual, isPrimaryKeyId, isIdField } = this.props;
29
19
 
30
- return rules.concat(this.type.getRules());
31
- }
32
-
33
- getTransformers() {
34
- const transformers = [];
35
-
36
- 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]) => {
37
22
  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());
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;
42
41
  }
43
42
 
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 = [];
57
-
58
- Object.entries(this.getDirectiveArgs('field', {})).forEach(([key, value]) => {
59
- 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());
64
- }
43
+ async validate(query, value) {
44
+ if (value == null) return value;
45
+ const { resolver } = query.toObject();
46
+ const { type, modelRef, isEmbedded } = this.props;
65
47
 
66
- validate(query, value) {
67
- const modelRef = this.getModelRef();
68
- 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
+ }
69
55
 
70
- if (modelRef && !this.isEmbedded()) rules.push(Rule.ensureId());
56
+ if (modelRef && isPlainObject(ensureArray(value)[0])) return modelRef.validate(query, value); // Model delegation
71
57
 
72
- return Promise.all(rules.map((rule) => {
73
- return rule(this, value, query);
74
- })).then((res) => {
75
- if (modelRef && isPlainObject(ensureArray(value)[0])) return modelRef.validate(query, value); // Model delegation
76
- return res;
77
- });
58
+ return value;
78
59
  }
79
60
 
80
- /**
81
- * Ensures the value of the field via bound @value + transformations
82
- */
83
- transform(query, value, serdes = (() => { throw new Error('No Sir Sir SerDes!'); })()) {
84
- // Determine value
85
- 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];
86
64
 
87
- // Determine transformers
88
- const transformers = [...(serdes === 'serialize' ? this.getSerializers() : this.getDeserializers()), ...this.getTransformers()];
65
+ // Default resolver return immediately!
66
+ if (isScalar || isEmbedded) return value;
89
67
 
90
- // Transform
91
- return transformers.reduce((prev, transformer) => {
92
- return transformer(this, prev, query);
93
- }, this.cast($value));
94
- }
68
+ // Ensure where clause for DB lookup
69
+ args.where = args.where || {};
95
70
 
96
- /**
97
- * Ensures the value of the field is appropriate for the driver
98
- */
99
- serialize(query, value, minimal = false) {
100
- const modelRef = uvl(this.getModelRef(), this.getType() === 'ID' ? this.model : undefined);
101
- 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
+ }
102
77
 
103
- // If embedded, simply delgate
104
- 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
+ }
105
83
 
106
- // Now, normalize and resolve
107
- const $value = this.transform(query, value, 'serialize');
108
- if (modelRef && !isEmbedded) return map($value, v => modelRef.idValue(v.id || v));
109
- return $value;
110
- }
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
+ }
111
89
 
112
- deserialize(query, value) {
113
- return this.transform(query, value, 'deserialize');
90
+ return resolver.match(modelRef).id(value).one({ required: isRequired });
114
91
  }
115
92
 
116
- tform(query, value) {
117
- // Determine transformers
118
- const transformers = this.getTransformers();
119
-
120
- // Transform
121
- return transformers.reduce((prev, transformer) => {
122
- return transformer(this, prev, query);
123
- }, 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();
124
99
  }
125
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,140 +48,117 @@ 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'];
82
104
 
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
- });
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;
87
112
 
88
- return input;
89
- }
113
+ structures.castValue = Pipeline.castValue;
114
+ structures.defaultValue = Pipeline.defaultValue;
90
115
 
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
- }
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 };
119
+ });
97
120
 
98
- /**
99
- * Deserialize data from Data Model to Domain Model
100
- */
101
- deserialize(query, data) {
102
- return this.transform(query, data, 'deserialize');
103
- }
121
+ // Adding useful shape info
122
+ shape.crud = crud;
123
+ shape.model = this;
124
+ shape.serdes = serdes;
104
125
 
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
- });
126
+ // Cache and return
127
+ this.shapesCache.set(cacheKey, shape);
128
+ return shape;
127
129
  }
128
130
 
129
- normalize(query, data, serdes = (() => { throw new Error('No Sir Sir SerDes!'); }), keysOnly = false) {
130
- // Transform all the data
131
- return map(data, (doc) => {
132
- const fields = Object.keys(doc).map(k => this.getField(k)).filter(Boolean);
131
+ shapeObject(shape, obj, query, root) {
132
+ const { serdes, model } = shape;
133
+ const { context, doc = {} } = query.toObject();
133
134
 
134
- // Loop through the fields and delegate (renaming keys appropriately)
135
- return fields.reduce((prev, field) => {
136
- const [key, name] = serdes === 'serialize' ? [field.getKey(), field.getName()] : [field.getName(), field.getKey()];
137
- prev[key] = keysOnly ? doc[name] : field[serdes](query, doc[name], true);
138
- return prev;
139
- }, {});
140
- });
141
- }
135
+ return map(obj, (parent) => {
136
+ // "root" is the base of the object
137
+ root = root || parent;
142
138
 
143
- validate(query, data) {
144
- const normalized = this.deserialize(query, data);
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
- return Promise.all(this.getFields().map((field) => {
147
- return Promise.all(ensureArray(map(normalized, (obj) => {
148
- if (obj == null) return Promise.resolve();
149
- return field.validate(query, obj[field.getName()]);
150
- })));
151
- }));
152
- }
144
+ return shape.reduce((prev, { field, from, to, path, type, isArray, defaultValue, transformers = [], shape: subShape }) => {
145
+ const startValue = parent[from];
153
146
 
154
- tform(query, data) {
155
- return map(data, (doc) => {
156
- return Object.keys(doc).map(k => this.getField(k)).filter(Boolean).reduce((prev, curr) => {
157
- const key = curr.getName();
158
- const value = doc[key];
159
- return Object.assign(prev, { [key]: curr.tform(query, value) });
160
- }, {});
161
- });
162
- }
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);
163
152
 
164
- getShape(serdes = 'deserialize', recursive = true) {
165
- return this.getSelectFields().map((field) => {
166
- const [from, to] = serdes === 'serialize' ? [field.getName(), field.getKey()] : [field.getKey(), field.getName()];
167
- const shape = recursive && field.isEmbedded() ? field.getModelRef().getShape(serdes, recursive) : null;
168
- return { from, to, type: field.getDataType(), isArray: field.isArray(), shape };
169
- });
170
- }
153
+ // if (`${field}` === 'searchability') console.log(startValue, transformedValue, transformers);
171
154
 
172
- shape(data, serdes = (() => { throw new Error('No Sir Sir SerDes!'); }), shape) {
173
- shape = shape || this.getShape(serdes);
155
+ // Determine if key should stay or be removed
156
+ if (transformedValue === undefined && !Object.prototype.hasOwnProperty.call(parent, from)) return prev;
174
157
 
175
- return map(data, (doc) => {
176
- return shape.reduce((prev, { from, to, shape: subShape }) => {
177
- const value = doc[from];
178
- if (value === undefined) return prev;
179
- return Object.assign(prev, { [to]: subShape ? this.shape(value, serdes, subShape) : value });
158
+ // Rename key & assign value
159
+ prev[to] = (!subShape || transformedValue == null) ? transformedValue : this.shapeObject(subShape, transformedValue, query, root);
160
+
161
+ return prev;
180
162
  }, {});
181
163
  });
182
164
  }