@coderich/autograph 0.10.1 → 0.10.4

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 CHANGED
@@ -2,26 +2,25 @@
2
2
 
3
3
  ## v0.10.x
4
4
  - Replaced ResultSet -> POJOs
5
- - Removed all $$ magic resolver methods
6
- - Removed all $ magic field methods
5
+ - Removed all $field methods (auto populated)
7
6
  - Removed .toObject()
7
+ - $model $save remove $delete $lookup $cursor $pageInfo
8
8
  - Removed embedded API completely
9
9
  - Removed Directives
10
10
  - embedApi -> no replacement
11
11
  - enforce -> use pipeline methods
12
12
  - resolve -> use graphql resolvers
13
13
  - @value -> use @field.instruct directive
14
- - Removed toId Transform -> use @field(id: '')
15
14
  - Removed Model.tform() -> use Model.shapeObject(shape, data)
16
- - Removed Resolver.toResultSet() -> ? TBD ?
17
15
  - Removed Transformer + Rule -> use Pipeline
18
16
  - Removed many pre-defined rules + transformers
19
- - Pre-defined names start with $ (eg. $toLowerCase)
20
17
  - Moved "validator" to dev dependency -> isEmail
21
18
  - Added QueryBuilder.resolve() terminal command
22
19
  - Exported SchemaDecorator -> Schema
23
20
  - Removed embedded schema SystemEvents (internal emitter also removed)
24
21
  - Removed spread of arguments in QueryBuilder terminal commands (must pass in array)
22
+ - Mutate "merged" instead of "input"
23
+ - Validate "payload"
25
24
 
26
25
  ## v0.9.x
27
26
  - Subscriptions API
package/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  const Schema = require('./src/core/Schema');
2
- const GraphQL = require('./src/core/GraphQL');
3
2
  const Resolver = require('./src/core/Resolver');
4
3
  const Pipeline = require('./src/data/Pipeline');
5
4
  const Driver = require('./src/driver');
@@ -7,7 +6,6 @@ const { eventEmitter: Emitter } = require('./src/service/event.service');
7
6
 
8
7
  module.exports = {
9
8
  Schema,
10
- GraphQL,
11
9
  Resolver,
12
10
  Driver,
13
11
  Emitter,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@coderich/autograph",
3
3
  "author": "Richard Livolsi (coderich)",
4
- "version": "0.10.1",
4
+ "version": "0.10.4",
5
5
  "description": "AutoGraph",
6
6
  "keywords": [
7
7
  "graphql",
@@ -23,16 +23,14 @@
23
23
  },
24
24
  "scripts": {
25
25
  "start": "APP_ROOT_PATH=$(pwd) node ./test/server",
26
- "test": "APP_ROOT_PATH=$(pwd) ratchet test --forceExit",
26
+ "test": "APP_ROOT_PATH=$(pwd) ratchet test",
27
27
  "test:debug": "APP_ROOT_PATH=$(pwd) node --inspect-brk ./node_modules/jest/bin/jest.js --watch --runInBand --logHeapUsage",
28
28
  "lint": "APP_ROOT_PATH=$(pwd) ratchet lint",
29
29
  "inspect": "APP_ROOT_PATH=$(pwd) node --expose-gc --inspect=9222 ./src/server",
30
30
  "ratchet": "ratchet"
31
31
  },
32
32
  "dependencies": {
33
- "@graphql-tools/schema": "^8.3.14",
34
33
  "@hapi/boom": "^9.1.0",
35
- "axios": "^0.21.4",
36
34
  "dataloader": "^2.0.0",
37
35
  "deepmerge": "^4.2.2",
38
36
  "fill-range": "^7.0.1",
@@ -41,11 +39,11 @@
41
39
  "lodash": "^4.17.21",
42
40
  "mongodb": "^4.8.0",
43
41
  "object-hash": "^2.0.1",
44
- "picomatch": "^2.1.1",
45
- "uuid": "^3.3.3"
42
+ "picomatch": "^2.1.1"
46
43
  },
47
44
  "devDependencies": {
48
45
  "@coderich/ratchet": "^1.5.7",
46
+ "@graphql-tools/schema": "^9.0.1",
49
47
  "graphql": "^15.5.0",
50
48
  "mongodb-memory-server": "^8.7.2",
51
49
  "neo4j-driver": "^4.0.0",
@@ -1,7 +1,10 @@
1
1
  const Model = require('../data/Model');
2
2
  const DataLoader = require('../data/DataLoader');
3
3
  const DataTransaction = require('../data/DataTransaction');
4
+ const Query = require('../query/Query');
4
5
  const QueryBuilder = require('../query/QueryBuilder');
6
+ const { finalizeResults } = require('../data/DataService');
7
+ const { createSystemEvent } = require('../service/event.service');
5
8
 
6
9
  module.exports = class Resolver {
7
10
  constructor(schema, context = {}) {
@@ -58,7 +61,8 @@ module.exports = class Resolver {
58
61
  case 'create': case 'update': case 'delete': {
59
62
  return model.getDriver().resolve(query.toDriver()).then((data) => {
60
63
  this.clear(model);
61
- return model.shapeObject(model.getShape(), data, query);
64
+ const rs = model.shapeObject(model.getShape(), data, query);
65
+ return finalizeResults(rs, query);
62
66
  });
63
67
  }
64
68
  default: {
@@ -99,25 +103,13 @@ module.exports = class Resolver {
99
103
  return entity;
100
104
  }
101
105
 
102
- // toResultSet(model, data, method) {
103
- // const crud = ['get', 'find', 'count'].indexOf(method) > -1 ? 'read' : method;
104
- // const doc = model.shape(data);
105
- // const result = doc;
106
- // const merged = doc;
107
-
108
- // return createSystemEvent('Response', {
109
- // model,
110
- // crud,
111
- // method,
112
- // result,
113
- // doc,
114
- // merged,
115
- // resolver: this,
116
- // key: `${method}${model}`,
117
- // context: this.getContext(),
118
- // query: query.doc(result).merged(result),
119
- // }, () => result);
120
- // }
106
+ toResultSet(model, data, method) {
107
+ model = this.toModel(model);
108
+ const query = new Query({ model, resolver: this, context: this.context, method });
109
+ const result = model.deserialize(data, query);
110
+ const event = { result, query, ...query.doc(result).merged(result).toObject() };
111
+ return createSystemEvent('Response', event, () => result);
112
+ }
121
113
 
122
114
  // DataLoader Proxy Methods
123
115
  clear(model) {
@@ -1,7 +1,7 @@
1
1
  const Model = require('../data/Model');
2
2
  const Schema = require('../graphql/ast/Schema');
3
3
  const { identifyOnDeletes } = require('../service/schema.service');
4
- const { createSystemEvent } = require('../service/event.service');
4
+ const { eventEmitter } = require('../service/event.service');
5
5
 
6
6
  // Export class
7
7
  module.exports = class extends Schema {
@@ -23,7 +23,7 @@ module.exports = class extends Schema {
23
23
  }
24
24
 
25
25
  setup() {
26
- return createSystemEvent('Setup', this, () => {
26
+ return eventEmitter.emit('setup', this).then(() => {
27
27
  const entities = this.models.filter(m => m.isEntity());
28
28
 
29
29
  // Create model indexes
@@ -37,10 +37,18 @@ module.exports = class extends Schema {
37
37
  });
38
38
  }
39
39
 
40
+ disconnect() {
41
+ return Promise.all(Object.values(this.drivers).map(({ dao }) => dao.disconnect()));
42
+ }
43
+
40
44
  initialize() {
41
45
  super.initialize();
42
46
  this.models = super.getModels().map(model => new Model(this, model, this.drivers[model.getDriverName()]));
43
- this.models.forEach(model => model.initialize());
47
+ return this;
48
+ }
49
+
50
+ finalize() {
51
+ super.finalize();
44
52
  this.models.forEach(model => model.referentialIntegrity(identifyOnDeletes(this.models, model)));
45
53
  return this;
46
54
  }
@@ -42,8 +42,6 @@ module.exports = class DataLoader extends FBDataLoader {
42
42
  */
43
43
  const whereShape = model.getShape('create', 'where');
44
44
 
45
- // console.log(Object.entries(batchQueries).map(([key, value]) => ({ [key]: value.length })));
46
-
47
45
  return Promise.all(Object.entries(batchQueries).map(([key, values]) => {
48
46
  switch (key) {
49
47
  case defaultBatchName: {
@@ -52,7 +50,7 @@ module.exports = class DataLoader extends FBDataLoader {
52
50
  default: {
53
51
  const keys = Array.from(new Set(values.map(({ where }) => map(where[key], el => `${el}`)).flat()));
54
52
  const batchQuery = new Query({ resolver, model, method: 'findMany', crud: 'read' });
55
- const batchWhere = model.shapeObject(whereShape, { [key]: keys }, batchQuery); // This will add back instructs etc
53
+ const batchWhere = model.shapeObject(whereShape, { ...values[0].where, [key]: keys }, batchQuery); // All where's should be the same - this is for idKey on keys etc
56
54
 
57
55
  return driver.resolve(batchQuery.where(batchWhere).toDriver()).then(data => handleData(data, model, batchQuery)).then((results) => {
58
56
  // One-time data transformation on results to make matching back faster (below)
@@ -1,27 +1,50 @@
1
1
  const { get, remove } = require('lodash');
2
2
  const { map, isPlainObject, objectContaining, mergeDeep, ensureArray, keyPaths } = require('../service/app.service');
3
3
 
4
- exports.paginateResultSet = (rs, query) => {
4
+ exports.finalizeResults = (rs, query) => {
5
+ const { model, resolver } = query.toObject();
6
+
7
+ return map(exports.paginateResults(rs, query), (doc) => {
8
+ return Object.defineProperties(doc, {
9
+ $model: { value: model },
10
+ $save: { value: input => resolver.match(model).id(doc.id).save({ ...doc, ...input }) },
11
+ $remove: { value: (...args) => resolver.match(model).id(doc.id).remove(...args) },
12
+ $delete: { value: (...args) => resolver.match(model).id(doc.id).delete(...args) },
13
+ $lookup: { value: (fieldName, args) => model.getFieldByName(fieldName).resolve(resolver, doc, args) },
14
+ // $resolve: { value: (fieldName, args) => model.getFieldByName(fieldName).resolve(resolver, doc, args, true) },
15
+ });
16
+ });
17
+ };
18
+
19
+ /**
20
+ * This is cursor-style pagination only
21
+ * You add 2 extra records to the result in order to determine previous/next
22
+ */
23
+ exports.paginateResults = (rs, query) => {
5
24
  const { first, after, last, before, sort } = query.toObject();
6
- const sortPaths = keyPaths(sort);
7
- const limiter = first || last;
25
+ const isPaginating = Boolean(first || last || after || before);
26
+
27
+ // Return right away if not paginating
28
+ if (!isPaginating) return rs;
29
+
8
30
  let hasNextPage = false;
9
31
  let hasPreviousPage = false;
32
+ const limiter = first || last;
33
+ const sortPaths = keyPaths(sort);
10
34
 
11
- // Add $$cursor data
35
+ // Add $cursor data
12
36
  map(rs, (doc) => {
13
37
  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');
38
+ Object.defineProperty(doc, '$cursor', { get() { return Buffer.from(JSON.stringify(sortValues)).toString('base64'); } });
16
39
  });
17
40
 
18
41
  // First try to take off the "bookends" ($gte | $lte)
19
- if (rs.length && rs[0].$$cursor === after) {
42
+ if (rs.length && rs[0].$cursor === after) {
20
43
  rs.shift();
21
44
  hasPreviousPage = true;
22
45
  }
23
46
 
24
- if (rs.length && rs[rs.length - 1].$$cursor === before) {
47
+ if (rs.length && rs[rs.length - 1].$cursor === before) {
25
48
  rs.pop();
26
49
  hasNextPage = true;
27
50
  }
@@ -42,18 +65,17 @@ exports.paginateResultSet = (rs, query) => {
42
65
  }
43
66
  }
44
67
 
45
- // Add $$pageInfo data (hidden)
68
+ // Add $pageInfo
46
69
  return Object.defineProperties(rs, {
47
- $$pageInfo: {
70
+ $pageInfo: {
48
71
  get() {
49
72
  return {
50
- startCursor: get(rs, '0.$$cursor', ''),
51
- endCursor: get(rs, `${rs.length - 1}.$$cursor`, ''),
73
+ startCursor: get(rs, '0.$cursor', ''),
74
+ endCursor: get(rs, `${rs.length - 1}.$cursor`, ''),
52
75
  hasPreviousPage,
53
76
  hasNextPage,
54
77
  };
55
78
  },
56
- enumerable: false,
57
79
  },
58
80
  });
59
81
  };
package/src/data/Field.js CHANGED
@@ -1,9 +1,7 @@
1
1
  const { isEmpty } = require('lodash');
2
2
  const Type = require('./Type');
3
3
  const Field = require('../graphql/ast/Field');
4
- const Boom = require('../core/Boom');
5
4
  const Pipeline = require('./Pipeline');
6
- const { isPlainObject, ensureArray } = require('../service/app.service');
7
5
 
8
6
  module.exports = class extends Field {
9
7
  constructor(model, field) {
@@ -15,49 +13,34 @@ module.exports = class extends Field {
15
13
  getStructures() {
16
14
  // Grab structures from the underlying type
17
15
  const structures = this.type.getStructures();
18
- const { isRequired, isPersistable, isVirtual, isPrimaryKeyId, isIdField } = this.props;
16
+ const { type, isPrimaryKeyId, isIdField, isRequired, isPersistable, isVirtual, isEmbedded, modelRef } = this.props;
19
17
 
20
18
  // Structures defined on the field
21
19
  const $structures = Object.entries(this.getDirectiveArgs('field', {})).reduce((prev, [key, value]) => {
22
20
  if (!Array.isArray(value)) value = [value];
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]));
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
30
  return prev;
31
31
  }, structures);
32
32
 
33
33
  // IDs (first - shift)
34
- if (isPrimaryKeyId) $structures.serializers.unshift(Pipeline.idKey);
34
+ if (isPrimaryKeyId && type === 'ID') $structures.serializers.unshift(Pipeline.idKey);
35
35
  if (isIdField) $structures.$serializers.unshift(Pipeline.idField);
36
36
 
37
37
  // Required (last - push)
38
- if (isRequired && isPersistable && !isVirtual) $structures.serializers.push(Pipeline.required);
38
+ if (isRequired && isPersistable && !isVirtual) $structures.validators.push(Pipeline.required);
39
+ if (modelRef && !isEmbedded) $structures.validators.push(Pipeline.ensureId);
39
40
 
40
41
  return $structures;
41
42
  }
42
43
 
43
- async validate(query, value) {
44
- if (value == null) return value;
45
- const { resolver } = query.toObject();
46
- const { type, modelRef, isEmbedded } = this.props;
47
-
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
- }
55
-
56
- if (modelRef && isPlainObject(ensureArray(value)[0])) return modelRef.validate(query, value); // Model delegation
57
-
58
- return value;
59
- }
60
-
61
44
  resolve(resolver, doc, args = {}) {
62
45
  const { name, isArray, isScalar, isVirtual, isRequired, isEmbedded, modelRef, virtualField } = this.props;
63
46
  const value = doc[name];
package/src/data/Model.js CHANGED
@@ -1,10 +1,9 @@
1
1
  const Stream = require('stream');
2
2
  const Field = require('./Field');
3
- const Pipeline = require('./Pipeline');
4
3
  const Model = require('../graphql/ast/Model');
5
- const { paginateResultSet } = require('./DataService');
6
4
  const { eventEmitter } = require('../service/event.service');
7
- const { map, seek, deseek, ensureArray } = require('../service/app.service');
5
+ const { finalizeResults } = require('./DataService');
6
+ const { map, mapPromise, seek, deseek } = require('../service/app.service');
8
7
 
9
8
  module.exports = class extends Model {
10
9
  constructor(schema, model, driver) {
@@ -48,80 +47,86 @@ module.exports = class extends Model {
48
47
  return this.referentials;
49
48
  }
50
49
 
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
-
67
50
  /**
68
51
  * Convenience method to deserialize data from a data source (such as a database)
69
52
  */
70
53
  deserialize(mixed, query) {
71
- const { flags = {} } = query.toObject();
72
- const { pipeline = true } = flags;
73
54
  const shape = this.getShape();
74
55
 
75
56
  return new Promise((resolve, reject) => {
76
57
  if (!(mixed instanceof Stream)) {
77
- resolve(pipeline ? this.shapeObject(shape, mixed, query) : mixed);
58
+ resolve(this.shapeObject(shape, mixed, query));
78
59
  } else {
79
60
  const results = [];
80
- mixed.on('data', (data) => { results.push(pipeline ? this.shapeObject(shape, data, query) : data); });
61
+ mixed.on('data', (data) => { results.push(this.shapeObject(shape, data, query)); });
81
62
  mixed.on('end', () => { resolve(results); });
82
63
  mixed.on('error', reject);
83
64
  }
84
- }).then((results) => {
85
- return results.length && pipeline ? paginateResultSet(results, query) : results;
86
- });
65
+ }).then(rs => finalizeResults(rs, query));
87
66
  }
88
67
 
89
68
  getShape(crud = 'read', target = 'doc', paths = []) {
69
+ // Cache check
90
70
  const cacheKey = `${crud}:${target}`;
91
71
  if (this.shapesCache.has(cacheKey)) return this.shapesCache.get(cacheKey);
92
72
 
93
73
  const serdes = crud === 'read' ? 'deserialize' : 'serialize';
94
74
  const fields = serdes === 'deserialize' ? this.getSelectFields() : this.getPersistableFields();
95
75
  const crudMap = { create: ['constructs'], update: ['restructs'], delete: ['destructs'], remove: ['destructs'] };
76
+ const sortKeys = ['isIdField', 'isBasicType', 'isEmbedded'];
96
77
  const crudKeys = crudMap[crud] || [];
97
78
 
79
+ // Define target mapping
98
80
  const targetMap = {
99
- doc: ['defaultValue', 'ensureArrayValue', 'castValue', ...crudKeys, `$${serdes}rs`, 'instructs', 'transformers', `${serdes}rs`],
100
- where: ['castValue', `$${serdes}rs`, 'instructs'],
81
+ doc: ['defaultValue', 'castValue', 'ensureArrayValue', 'normalizers', 'instructs', ...crudKeys, `$${serdes}rs`, `${serdes}rs`, 'transforms'],
82
+ input: ['defaultValue', 'castValue', 'ensureArrayValue', 'normalizers', 'instructs', ...crudKeys, `$${serdes}rs`, `${serdes}rs`, 'transforms'],
83
+ // input: ['defaultValue', 'castValue', 'ensureArrayValue'],
84
+ where: ['castValue', 'instructs', `$${serdes}rs`],
101
85
  };
102
86
 
103
87
  const structureKeys = targetMap[target] || ['castValue'];
104
88
 
105
- // Create shape, recursive
106
- const shape = fields.map((field) => {
89
+ // Create sorted shape, recursive
90
+ const shape = fields.sort((a, b) => {
91
+ const aObject = a.toObject();
92
+ const bObject = b.toObject();
93
+
94
+ // PK first
95
+ if (aObject.isPrimaryKeyId) return -1;
96
+ if (bObject.isPrimaryKeyId) return 1;
97
+
98
+ // Arrays last
99
+ if (aObject.isArray && !bObject.isArray) return 1;
100
+ if (bObject.isArray && !aObject.isArray) return -1;
101
+
102
+ // Now, follow sort keys
103
+ const aNum = sortKeys.findIndex(key => aObject[key]);
104
+ const bNum = sortKeys.findIndex(key => bObject[key]);
105
+ if (aNum < bNum) return -1;
106
+ if (aNum > bNum) return 1;
107
+ return 0;
108
+ }).map((field) => {
109
+ let instructed = false;
107
110
  const structures = field.getStructures();
108
- const [key, name, type, isArray] = [field.getKey(), field.getName(), field.getType(), field.isArray(), field.isIdField()];
111
+ const { key, name, type, isArray, isEmbedded, modelRef } = field.toObject();
109
112
  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;
115
-
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 };
113
+ const actualTo = target === 'input' || target === 'splice' ? from : to;
114
+ const path = paths.concat(actualTo);
115
+ const subCrud = crud === 'update' && isArray ? 'create' : crud; // Due to limitation to update embedded array
116
+ const subShape = isEmbedded ? modelRef.getShape(subCrud, target, path) : null;
117
+ const transformers = structureKeys.reduce((prev, struct) => {
118
+ const structs = structures[struct];
119
+ if (struct === 'instructs' && structs.length) instructed = true;
120
+ return prev.concat(structs);
121
+ }, []).filter(Boolean);
122
+ return { instructed, field, path, from, to: actualTo, type, isArray, transformers, validators: structures.validators, shape: subShape };
119
123
  });
120
124
 
121
125
  // Adding useful shape info
122
126
  shape.crud = crud;
123
127
  shape.model = this;
124
128
  shape.serdes = serdes;
129
+ shape.target = target;
125
130
 
126
131
  // Cache and return
127
132
  this.shapesCache.set(cacheKey, shape);
@@ -130,7 +135,11 @@ module.exports = class extends Model {
130
135
 
131
136
  shapeObject(shape, obj, query, root) {
132
137
  const { serdes, model } = shape;
133
- const { context, doc = {} } = query.toObject();
138
+ const { context, resolver, doc = {}, flags = {} } = query.toObject();
139
+ const { pipeline } = flags;
140
+
141
+ if (!pipeline) return obj;
142
+ // const filters = pipeline === true ? [] : Object.entries(pipeline).map(([k, v]) => (v === false ? k : null)).filter(Boolean);
134
143
 
135
144
  return map(obj, (parent) => {
136
145
  // "root" is the base of the object
@@ -141,19 +150,19 @@ module.exports = class extends Model {
141
150
  const rootPath = (p, hint) => (serdes === 'serialize' ? seek(root, p, hint) : deseek(shape, root, p, hint));
142
151
  const parentPath = (p, hint) => (serdes === 'serialize' ? seek(parent, p, hint) : deseek(shape, parent, p, hint));
143
152
 
144
- return shape.reduce((prev, { field, from, to, path, type, isArray, defaultValue, transformers = [], shape: subShape }) => {
153
+ return shape.reduce((prev, { instructed, field, from, to, path, type, isArray, defaultValue, transformers = [], shape: subShape }) => {
145
154
  const startValue = parent[from];
155
+ // transformers = filters.length ? transformers.filter() : transformers;
146
156
 
147
157
  // Transform value
148
158
  const transformedValue = transformers.reduce((value, t) => {
149
- const v = t({ model, field, path, docPath, rootPath, parentPath, startValue, value, context });
159
+ const v = t({ model, field, path, docPath, rootPath, parentPath, startValue, value, resolver, context });
150
160
  return v === undefined ? value : v;
151
161
  }, startValue);
152
162
 
153
- // if (`${field}` === 'searchability') console.log(startValue, transformedValue, transformers);
154
-
155
163
  // Determine if key should stay or be removed
156
- if (transformedValue === undefined && !Object.prototype.hasOwnProperty.call(parent, from)) return prev;
164
+ if (!instructed && transformedValue === undefined && !Object.prototype.hasOwnProperty.call(parent, from)) return prev;
165
+ if (!instructed && subShape && typeof transformedValue !== 'object') return prev;
157
166
 
158
167
  // Rename key & assign value
159
168
  prev[to] = (!subShape || transformedValue == null) ? transformedValue : this.shapeObject(subShape, transformedValue, query, root);
@@ -162,4 +171,36 @@ module.exports = class extends Model {
162
171
  }, {});
163
172
  });
164
173
  }
174
+
175
+ validateObject(shape, obj, query, root, silent = false) {
176
+ const { model } = shape;
177
+ const { context, resolver, doc = {}, flags = {} } = query.toObject();
178
+ const { validate = true } = flags;
179
+
180
+ if (!validate) return Promise.resolve();
181
+
182
+ return mapPromise(obj, (parent) => {
183
+ // "root" is the base of the object
184
+ root = root || parent;
185
+
186
+ // Lookup helper functions
187
+ const docPath = (p, hint) => seek(doc, p, hint);
188
+ const rootPath = (p, hint) => seek(root, p, hint);
189
+ const parentPath = (p, hint) => seek(parent, p, hint);
190
+
191
+ return Promise.all(shape.map(({ field, from, path, validators, shape: subShape }) => {
192
+ const value = parent[from]; // It hasn't been shaped yet
193
+
194
+ return Promise.all(validators.map((v) => {
195
+ return new Promise((resolve, reject) => {
196
+ return Promise.resolve(v({ model, field, path, docPath, rootPath, parentPath, startValue: value, value, resolver, context })).then(resolve).catch(reject);
197
+ });
198
+ })).then(() => {
199
+ return subShape ? this.validateObject(subShape, value, query, root, true) : Promise.resolve();
200
+ });
201
+ }));
202
+ }).then(() => {
203
+ return silent ? Promise.resolve() : eventEmitter.emit('validate', query.toObject());
204
+ });
205
+ }
165
206
  };
@@ -1,5 +1,5 @@
1
1
  const { uniqWith } = require('lodash');
2
- const { map, hashObject } = require('../service/app.service');
2
+ const { map, ensureArray, hashObject } = require('../service/app.service');
3
3
  const Boom = require('../core/Boom');
4
4
 
5
5
  module.exports = class Pipeline {
@@ -29,17 +29,17 @@ module.exports = class Pipeline {
29
29
  }, 'name', { value: name });
30
30
 
31
31
  // Attach enumerable method to the Pipeline
32
- Object.defineProperty(Pipeline, name, {
32
+ return Object.defineProperty(Pipeline, name, {
33
33
  value: wrapper,
34
34
  configurable,
35
35
  enumerable: true,
36
- });
36
+ })[name];
37
37
  }
38
38
 
39
39
  static factory(name, thunk, options = {}) {
40
40
  if (typeof thunk !== 'function') throw new Error(`Pipeline factory for "${name}" must be a thunk`);
41
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 }) });
42
+ return Object.defineProperty(Pipeline, name, { value: (...args) => Object.defineProperty(thunk(...args), 'options', { value: options }) })[name];
43
43
  }
44
44
 
45
45
  // static wrapper(name, factory, { ignoreNull, itemize }) {
@@ -66,13 +66,24 @@ module.exports = class Pipeline {
66
66
  // Additional Transformers
67
67
  Pipeline.define('toTitleCase', ({ value }) => value.replace(/\w\S*/g, w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()));
68
68
  Pipeline.define('toSentenceCase', ({ value }) => value.charAt(0).toUpperCase() + value.slice(1));
69
+ Pipeline.define('toId', ({ model, value }) => model.idValue(value));
69
70
  Pipeline.define('toArray', ({ value }) => (Array.isArray(value) ? value : [value]), { itemize: false });
70
71
  Pipeline.define('toDate', ({ value }) => new Date(value), { configurable: true });
71
72
  Pipeline.define('timestamp', ({ value }) => Date.now(), { ignoreNull: false });
72
73
  Pipeline.define('createdAt', ({ value }) => value || Date.now(), { ignoreNull: false });
73
74
  Pipeline.define('dedupe', ({ value }) => uniqWith(value, (b, c) => hashObject(b) === hashObject(c)), { itemize: false });
74
75
  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
+ 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 });
76
87
 
77
88
  Pipeline.define('defaultValue', ({ field, value }) => {
78
89
  const { defaultValue } = field.toObject();
@@ -117,7 +128,7 @@ module.exports = class Pipeline {
117
128
  }, { ignoreNull: false });
118
129
 
119
130
  // A field cannot hold a reference to itself
120
- Pipeline.define('selfless', ({ model, field, parentPath, value }) => {
131
+ Pipeline.define('selfless', ({ model, field, parent, parentPath, value }) => {
121
132
  if (`${value}` === `${parentPath('id')}`) throw Boom.badRequest(`${model}.${field} cannot hold a reference to itself`);
122
133
  });
123
134
 
@@ -129,17 +140,17 @@ module.exports = class Pipeline {
129
140
  });
130
141
 
131
142
  // List of allowed values
132
- Pipeline.factory('allow', (...args) => function allow({ model, field, value }) {
143
+ Pipeline.factory('Allow', (...args) => function allow({ model, field, value }) {
133
144
  if (args.indexOf(value) === -1) throw Boom.badRequest(`${model}.${field} allows ${args}; found '${value}'`);
134
145
  });
135
146
 
136
147
  // List of disallowed values
137
- Pipeline.factory('deny', (...args) => function deny({ model, field, value }) {
148
+ Pipeline.factory('Deny', (...args) => function deny({ model, field, value }) {
138
149
  if (args.indexOf(value) > -1) throw Boom.badRequest(`${model}.${field} denys ${args}; found '${value}'`);
139
150
  });
140
151
 
141
152
  // Min/Max range
142
- Pipeline.factory('range', (min, max) => {
153
+ Pipeline.factory('Range', (min, max) => {
143
154
  if (min == null) min = undefined;
144
155
  if (max == null) max = undefined;
145
156
 
@@ -152,7 +163,6 @@ module.exports = class Pipeline {
152
163
  }
153
164
  };
154
165
 
155
-
156
166
  // const jsStringMethods = [
157
167
  // 'charAt', 'charCodeAt', 'codePointAt', 'concat', 'indexOf', 'lastIndexOf', 'localeCompare',
158
168
  // 'normalize', 'padEnd', 'padStart', 'repeat', 'replace', 'search', 'slice', 'split', 'substr', 'substring',