@coderich/autograph 0.11.1 → 0.13.0
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/index.js +4 -6
- package/package.json +30 -44
- package/src/data/DataLoader.js +77 -70
- package/src/data/Emitter.js +89 -0
- package/src/data/Loader.js +33 -0
- package/src/data/Pipeline.js +88 -96
- package/src/data/Resolver.js +304 -0
- package/src/data/Transaction.js +49 -0
- package/src/query/Query.js +159 -334
- package/src/query/QueryBuilder.js +228 -114
- package/src/query/QueryResolver.js +110 -216
- package/src/query/QueryResolverTransaction.js +16 -0
- package/src/schema/Schema.js +593 -0
- package/src/service/AppService.js +38 -0
- package/src/service/ErrorService.js +7 -0
- package/CHANGELOG.md +0 -41
- package/LICENSE +0 -21
- package/README.md +0 -76
- package/src/.DS_Store +0 -0
- package/src/core/.DS_Store +0 -0
- package/src/core/Boom.js +0 -9
- package/src/core/EventEmitter.js +0 -95
- package/src/core/Resolver.js +0 -124
- package/src/core/Schema.js +0 -55
- package/src/core/ServerResolver.js +0 -15
- package/src/data/.DS_Store +0 -0
- package/src/data/DataService.js +0 -120
- package/src/data/DataTransaction.js +0 -96
- package/src/data/Field.js +0 -83
- package/src/data/Model.js +0 -223
- package/src/data/TreeMap.js +0 -78
- package/src/data/Type.js +0 -50
- package/src/driver/.DS_Store +0 -0
- package/src/driver/MongoDriver.js +0 -227
- package/src/driver/index.js +0 -11
- package/src/graphql/.DS_Store +0 -0
- package/src/graphql/ast/.DS_Store +0 -0
- package/src/graphql/ast/Field.js +0 -206
- package/src/graphql/ast/Model.js +0 -145
- package/src/graphql/ast/Node.js +0 -291
- package/src/graphql/ast/Schema.js +0 -133
- package/src/graphql/ast/Type.js +0 -26
- package/src/graphql/ast/TypeDefApi.js +0 -93
- package/src/graphql/extension/.DS_Store +0 -0
- package/src/graphql/extension/api.js +0 -193
- package/src/graphql/extension/framework.js +0 -71
- package/src/graphql/extension/type.js +0 -34
- package/src/query/.DS_Store +0 -0
- package/src/query/QueryBuilderTransaction.js +0 -26
- package/src/query/QueryService.js +0 -111
- package/src/service/.DS_Store +0 -0
- package/src/service/app.service.js +0 -319
- package/src/service/decorator.service.js +0 -114
- package/src/service/event.service.js +0 -66
- package/src/service/graphql.service.js +0 -92
- package/src/service/schema.service.js +0 -95
package/index.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
const Schema = require('./src/
|
|
2
|
-
const Resolver = require('./src/
|
|
1
|
+
const Schema = require('./src/schema/Schema');
|
|
2
|
+
const Resolver = require('./src/data/Resolver');
|
|
3
3
|
const Pipeline = require('./src/data/Pipeline');
|
|
4
|
-
const
|
|
5
|
-
const { eventEmitter: Emitter } = require('./src/service/event.service');
|
|
4
|
+
const Emitter = require('./src/data/Emitter');
|
|
6
5
|
|
|
7
6
|
module.exports = {
|
|
8
7
|
Schema,
|
|
9
8
|
Resolver,
|
|
10
|
-
Driver,
|
|
11
|
-
Emitter,
|
|
12
9
|
Pipeline,
|
|
10
|
+
Emitter,
|
|
13
11
|
};
|
package/package.json
CHANGED
|
@@ -1,59 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coderich/autograph",
|
|
3
|
-
"author": "Richard Livolsi (coderich)",
|
|
4
|
-
"version": "0.11.1",
|
|
5
|
-
"description": "AutoGraph",
|
|
6
|
-
"keywords": [
|
|
7
|
-
"graphql",
|
|
8
|
-
"mongo",
|
|
9
|
-
"neo4j",
|
|
10
|
-
"redis",
|
|
11
|
-
"dataloader",
|
|
12
|
-
"resolver",
|
|
13
|
-
"orm"
|
|
14
|
-
],
|
|
15
3
|
"main": "index.js",
|
|
16
|
-
"
|
|
4
|
+
"version": "0.13.0",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
17
8
|
"files": [
|
|
18
|
-
"src
|
|
19
|
-
"
|
|
9
|
+
"src",
|
|
10
|
+
"!__mocks__"
|
|
20
11
|
],
|
|
21
|
-
"engines": {
|
|
22
|
-
"node": ">=14.17.0"
|
|
23
|
-
},
|
|
24
12
|
"scripts": {
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"lint": "APP_ROOT_PATH=$(pwd) ratchet lint",
|
|
29
|
-
"inspect": "APP_ROOT_PATH=$(pwd) node --expose-gc --inspect=9222 ./src/server",
|
|
30
|
-
"ratchet": "ratchet"
|
|
13
|
+
"test": "NODE_OPTIONS=\"--stack-trace-limit=1000\" jest --config=jest.config.js",
|
|
14
|
+
"lint": "eslint --config=.eslintrc ./",
|
|
15
|
+
"dev": "coderich-dev"
|
|
31
16
|
},
|
|
32
17
|
"dependencies": {
|
|
33
|
-
"@coderich/util": "
|
|
34
|
-
"@
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
18
|
+
"@coderich/util": "0.1.4",
|
|
19
|
+
"@graphql-tools/merge": "9.0.0",
|
|
20
|
+
"@graphql-tools/resolvers-composition": "7.0.0",
|
|
21
|
+
"@hapi/boom": "10.0.1",
|
|
22
|
+
"bson-objectid": "2.0.4",
|
|
23
|
+
"dataloader": "2.2.2",
|
|
24
|
+
"deepmerge": "4.3.1",
|
|
25
|
+
"fill-range": "7.0.1",
|
|
26
|
+
"lodash.get": "4.4.2",
|
|
27
|
+
"lodash.merge": "4.6.2",
|
|
28
|
+
"lodash.uniqwith": "4.5.0",
|
|
29
|
+
"object-hash": "3.0.0",
|
|
30
|
+
"picomatch": "2.3.1"
|
|
44
31
|
},
|
|
45
32
|
"devDependencies": {
|
|
46
|
-
"@
|
|
47
|
-
"@
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
33
|
+
"@apollo/server": "4.9.5",
|
|
34
|
+
"@coderich/autograph-mongodb": "0.0.1",
|
|
35
|
+
"@coderich/dev": "0.1.0",
|
|
36
|
+
"@graphql-tools/schema": "10.0.0",
|
|
37
|
+
"graphql": "16.6.0",
|
|
38
|
+
"mongodb": "5.7.0",
|
|
39
|
+
"mongodb-memory-server": "8.13.0",
|
|
40
|
+
"validator": "13.9.0"
|
|
51
41
|
},
|
|
52
42
|
"peerDependencies": {
|
|
53
43
|
"graphql": "*"
|
|
54
|
-
},
|
|
55
|
-
"repository": {
|
|
56
|
-
"type": "git",
|
|
57
|
-
"url": "git@github.com:coderich/autograph.git"
|
|
58
44
|
}
|
|
59
45
|
}
|
package/src/data/DataLoader.js
CHANGED
|
@@ -1,82 +1,89 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
const
|
|
1
|
+
const get = require('lodash.get');
|
|
2
|
+
const Util = require('@coderich/util');
|
|
3
|
+
const DataLoader = require('dataloader');
|
|
4
|
+
const { hashObject } = require('../service/AppService');
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
module.exports = class DataLoader extends FBDataLoader {
|
|
11
|
-
constructor(resolver, model) {
|
|
12
|
-
const driver = model.getDriver();
|
|
6
|
+
module.exports = class Loader {
|
|
7
|
+
#model;
|
|
8
|
+
#loader;
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
constructor(model) {
|
|
11
|
+
this.#model = model;
|
|
12
|
+
model.loader.cacheKeyFn ??= (query => hashObject(query.toCacheKey()));
|
|
13
|
+
this.#loader = new DataLoader(keys => this.#resolve(keys), model.loader);
|
|
14
|
+
}
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* queries to run; saving the order ("i") along with useful meta information
|
|
22
|
-
*/
|
|
23
|
-
const batchQueries = queries.reduce((prev, query, i) => {
|
|
24
|
-
const { batch = defaultBatchName, where, cmd } = query.toObject();
|
|
25
|
-
const key = batch && (cmd === 'one' || cmd === 'many') ? batch : defaultBatchName;
|
|
26
|
-
if (key !== defaultBatchName) performBatchQuery = true;
|
|
27
|
-
prev[key] = prev[key] || [];
|
|
28
|
-
prev[key].push({ query, where, cmd, i });
|
|
29
|
-
return prev;
|
|
30
|
-
}, {});
|
|
16
|
+
clearAll() {
|
|
17
|
+
return this.#loader.clearAll();
|
|
18
|
+
}
|
|
31
19
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return driver.resolve(query.toDriver()).then(data => handleData(data, model, query));
|
|
36
|
-
}));
|
|
37
|
-
}
|
|
20
|
+
resolve(query) {
|
|
21
|
+
return this.#loader.load(query);
|
|
22
|
+
}
|
|
38
23
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const whereShape = model.getShape('create', 'where');
|
|
24
|
+
#resolve(queries) {
|
|
25
|
+
return Promise.all(queries.map((query) => {
|
|
26
|
+
const dquery = query.toDriver();
|
|
27
|
+
const $query = dquery.toObject();
|
|
44
28
|
|
|
45
|
-
return
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const batchQuery = new Query({ resolver, model, method: 'findMany', crud: 'read' });
|
|
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
|
|
29
|
+
return this.#model.source.client.resolve($query).then((data) => {
|
|
30
|
+
if (data == null) return null; // Explicit return null;
|
|
31
|
+
if ($query.isCursorPaging && Array.isArray(data)) return Loader.#paginateResults(data, query.toObject());
|
|
32
|
+
return data;
|
|
33
|
+
});
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
54
36
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
});
|
|
62
|
-
return prev;
|
|
63
|
-
}, {});
|
|
37
|
+
static #paginateResults(rs, query) {
|
|
38
|
+
let hasNextPage = false;
|
|
39
|
+
let hasPreviousPage = false;
|
|
40
|
+
const { first, after, last, before, sort = {} } = query;
|
|
41
|
+
const sortPaths = Object.keys(Util.flatten(sort, { safe: true }));
|
|
42
|
+
const limiter = first || last;
|
|
64
43
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}).flat()).then((results) => {
|
|
75
|
-
return results.flat().sort((a, b) => a.i - b.i).map(({ data }) => data);
|
|
44
|
+
// Add $cursor data (but only if sort is defined!)
|
|
45
|
+
if (sortPaths.length) {
|
|
46
|
+
Util.map(rs, (doc) => {
|
|
47
|
+
const sortValues = sortPaths.reduce((prev, path) => Object.assign(prev, { [path]: get(doc, path) }), {});
|
|
48
|
+
Object.defineProperty(doc, '$cursor', { value: Buffer.from(JSON.stringify(sortValues)).toString('base64') });
|
|
76
49
|
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// First try to take off the "bookends" ($gte | $lte)
|
|
53
|
+
if (rs.length && rs[0].$cursor === after) {
|
|
54
|
+
rs.shift();
|
|
55
|
+
hasPreviousPage = true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (rs.length && rs[rs.length - 1].$cursor === before) {
|
|
59
|
+
rs.pop();
|
|
60
|
+
hasNextPage = true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Next, remove any overage
|
|
64
|
+
const overage = rs.length - (limiter - 2);
|
|
65
|
+
|
|
66
|
+
if (overage > 0) {
|
|
67
|
+
if (first) {
|
|
68
|
+
rs.splice(-overage);
|
|
69
|
+
hasNextPage = true;
|
|
70
|
+
} else if (last) {
|
|
71
|
+
rs.splice(0, overage);
|
|
72
|
+
hasPreviousPage = true;
|
|
73
|
+
} else {
|
|
74
|
+
rs.splice(-overage);
|
|
75
|
+
hasNextPage = true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Add $pageInfo
|
|
80
|
+
return Object.defineProperty(rs, '$pageInfo', {
|
|
81
|
+
value: {
|
|
82
|
+
startCursor: get(rs, '0.$cursor', ''),
|
|
83
|
+
endCursor: get(rs, `${rs.length - 1}.$cursor`, ''),
|
|
84
|
+
hasPreviousPage,
|
|
85
|
+
hasNextPage,
|
|
86
|
+
},
|
|
80
87
|
});
|
|
81
88
|
}
|
|
82
89
|
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const EventEmitter = require('events');
|
|
2
|
+
const Util = require('@coderich/util');
|
|
3
|
+
const { AbortEarlyError } = require('../service/ErrorService');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* EventEmitter.
|
|
7
|
+
*
|
|
8
|
+
* The difference is that I'm looking at each raw listeners to determine how many arguments it's expecting.
|
|
9
|
+
* If it expects more than 1 we block and wait for it to finish.
|
|
10
|
+
*/
|
|
11
|
+
class Emitter extends EventEmitter {
|
|
12
|
+
emit(event, data) {
|
|
13
|
+
// Here we pull out functions with "next" vs those without
|
|
14
|
+
const [basicFuncs, nextFuncs] = this.rawListeners(event).reduce((prev, wrapper) => {
|
|
15
|
+
const listener = wrapper.listener || wrapper;
|
|
16
|
+
const isBasic = listener.length < 2;
|
|
17
|
+
return prev[isBasic ? 0 : 1].push(wrapper) && prev;
|
|
18
|
+
}, [[], []]);
|
|
19
|
+
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
// Basic functions run first; if they return a value they abort the flow of execution
|
|
22
|
+
basicFuncs.forEach((fn) => {
|
|
23
|
+
const value = fn(data);
|
|
24
|
+
if (value !== undefined && !(value instanceof Promise)) throw new AbortEarlyError(value);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Next functions are async and control the timing of the next phase
|
|
28
|
+
Promise.all(nextFuncs.map((fn) => {
|
|
29
|
+
return new Promise((next) => {
|
|
30
|
+
Promise.resolve(fn(data, next));
|
|
31
|
+
}).then((result) => {
|
|
32
|
+
if (result !== undefined) throw new AbortEarlyError(result);
|
|
33
|
+
}).catch(reject);
|
|
34
|
+
})).then(() => resolve()); // Resolve to undefined
|
|
35
|
+
}).catch((e) => {
|
|
36
|
+
if (e instanceof AbortEarlyError) return e.data;
|
|
37
|
+
throw e;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Syntactic sugar to listen on query keys
|
|
43
|
+
*/
|
|
44
|
+
onKeys(...args) {
|
|
45
|
+
return this.#createWrapper(...args, 'key');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Syntactic sugar to listen once on query keys
|
|
50
|
+
*/
|
|
51
|
+
onceKeys(...args) {
|
|
52
|
+
return this.#createWrapper(...args, 'key', true);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Syntactic sugar to listen on query models
|
|
57
|
+
*/
|
|
58
|
+
onModels(...args) {
|
|
59
|
+
return this.#createWrapper(...args, 'model');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Syntactic sugar to listen once on query models
|
|
64
|
+
*/
|
|
65
|
+
onceModels(...args) {
|
|
66
|
+
return this.#createWrapper(...args, 'model', true);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#createWrapper(eventName, arr, listener, prop, once) {
|
|
70
|
+
arr = Util.ensureArray(arr);
|
|
71
|
+
|
|
72
|
+
const wrapper = listener.length < 2 ? (event) => {
|
|
73
|
+
if (arr.includes(`${event.query[prop]}`)) {
|
|
74
|
+
if (once) this.removeListener(eventName, wrapper);
|
|
75
|
+
return listener(event);
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
} : (event, next) => {
|
|
79
|
+
if (arr.includes(`${event.query[prop]}`)) {
|
|
80
|
+
if (once) this.removeListener(eventName, wrapper);
|
|
81
|
+
next(listener(event, next));
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return this.on(eventName, wrapper);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = new Emitter().setMaxListeners(100);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const DataLoader = require('dataloader');
|
|
2
|
+
const { hashObject } = require('../service/AppService');
|
|
3
|
+
|
|
4
|
+
module.exports = class Loader {
|
|
5
|
+
#loader;
|
|
6
|
+
#resolver;
|
|
7
|
+
|
|
8
|
+
constructor(resolver, config = {}) {
|
|
9
|
+
config.cacheKeyFn ??= event => hashObject(event.args);
|
|
10
|
+
this.#loader = new DataLoader(events => this.#resolve(events), config);
|
|
11
|
+
this.#resolver = resolver;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
load(args, context) {
|
|
15
|
+
return this.#loader.load({ args, context });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
prime(args, value) {
|
|
19
|
+
return this.#loader.prime({ args }, value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
clear(args) {
|
|
23
|
+
return this.#loader.clear({ args });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
clearAll() {
|
|
27
|
+
return this.#loader.clearAll();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#resolve(events) {
|
|
31
|
+
return Promise.all(events.map(event => this.#resolver(event.args, event.context)));
|
|
32
|
+
}
|
|
33
|
+
};
|
package/src/data/Pipeline.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
const
|
|
1
|
+
const Boom = require('@hapi/boom');
|
|
2
|
+
const get = require('lodash.get');
|
|
3
|
+
const Util = require('@coderich/util');
|
|
4
|
+
const uniqWith = require('lodash.uniqwith');
|
|
5
|
+
const { hashObject } = require('../service/AppService');
|
|
4
6
|
|
|
5
7
|
module.exports = class Pipeline {
|
|
6
8
|
constructor() {
|
|
@@ -12,20 +14,25 @@ module.exports = class Pipeline {
|
|
|
12
14
|
if (typeof factory !== 'function') throw new Error(`Pipeline definition for "${name}" must be a function`);
|
|
13
15
|
|
|
14
16
|
// Determine options; which may come from the factory function
|
|
15
|
-
const { ignoreNull = true, itemize = true, configurable = false } =
|
|
17
|
+
const { ignoreNull = true, itemize = true, configurable = false } = { ...factory.options, ...options };
|
|
16
18
|
|
|
17
19
|
const wrapper = Object.defineProperty((args) => {
|
|
18
|
-
|
|
20
|
+
try {
|
|
21
|
+
if (ignoreNull && args.value == null) return args.value;
|
|
22
|
+
|
|
23
|
+
if (ignoreNull && itemize) {
|
|
24
|
+
return Util.map(args.value, (value) => {
|
|
25
|
+
const v = factory({ ...args, value });
|
|
26
|
+
return v === undefined ? value : v;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
19
29
|
|
|
20
|
-
|
|
21
|
-
return
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
});
|
|
30
|
+
const value = factory(args);
|
|
31
|
+
return value === undefined ? args.value : value;
|
|
32
|
+
} catch (e) {
|
|
33
|
+
const { data = {} } = e;
|
|
34
|
+
throw Boom.boomify(e, { data: { ...args, ...data } });
|
|
25
35
|
}
|
|
26
|
-
|
|
27
|
-
const val = factory(args);
|
|
28
|
-
return val === undefined ? args.value : val;
|
|
29
36
|
}, 'name', { value: name });
|
|
30
37
|
|
|
31
38
|
// Attach enumerable method to the Pipeline
|
|
@@ -50,93 +57,89 @@ module.exports = class Pipeline {
|
|
|
50
57
|
// Additional Transformers
|
|
51
58
|
Pipeline.define('toTitleCase', ({ value }) => value.replace(/\w\S*/g, w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()));
|
|
52
59
|
Pipeline.define('toSentenceCase', ({ value }) => value.charAt(0).toUpperCase() + value.slice(1));
|
|
53
|
-
Pipeline.define('toId', ({ model, value }) => model.idValue(value));
|
|
54
60
|
Pipeline.define('toArray', ({ value }) => (Array.isArray(value) ? value : [value]), { itemize: false });
|
|
55
61
|
Pipeline.define('toDate', ({ value }) => new Date(value), { configurable: true });
|
|
56
|
-
Pipeline.define('
|
|
57
|
-
Pipeline.define('createdAt', ({ value }) => value || Date
|
|
62
|
+
Pipeline.define('updatedAt', () => new Date(), { ignoreNull: false });
|
|
63
|
+
Pipeline.define('createdAt', ({ value }) => value || new Date(), { ignoreNull: false });
|
|
64
|
+
Pipeline.define('timestamp', () => Date.now(), { ignoreNull: false });
|
|
58
65
|
Pipeline.define('dedupe', ({ value }) => uniqWith(value, (b, c) => hashObject(b) === hashObject(c)), { itemize: false });
|
|
59
|
-
Pipeline.define('idKey', ({ model, value }) => (value == null ? model.idValue() : value), { ignoreNull: false });
|
|
60
|
-
Pipeline.define('idField', ({ model, field, value }) => field.getIdModel().idValue(value.id || value));
|
|
61
|
-
Pipeline.define('ensureArrayValue', ({ field, value }) => (field.toObject().isArray && !Array.isArray(value) ? [value] : value), { itemize: false });
|
|
62
|
-
|
|
63
|
-
Pipeline.define('ensureId', ({ resolver, model, field, value }) => {
|
|
64
|
-
const path = `${model}.${field}`;
|
|
65
|
-
const { type } = field.toObject();
|
|
66
|
-
const ids = Array.from(new Set(ensureArray(value).map(v => `${v}`)));
|
|
67
|
-
|
|
68
|
-
return resolver.match(type).where({ id: ids }).count().then((count) => {
|
|
69
|
-
if (count !== ids.length) throw Boom.notFound(`${type} Not Found`, { path });
|
|
70
|
-
});
|
|
71
|
-
}, { itemize: false });
|
|
72
|
-
|
|
73
|
-
Pipeline.define('defaultValue', ({ field, value }) => {
|
|
74
|
-
const { defaultValue } = field.toObject();
|
|
75
|
-
return value === undefined ? defaultValue : value;
|
|
76
|
-
}, { ignoreNull: false });
|
|
77
|
-
|
|
78
|
-
Pipeline.define('castValue', ({ field, value }) => {
|
|
79
|
-
const { type, isEmbedded } = field.toObject();
|
|
80
66
|
|
|
67
|
+
// Structures
|
|
68
|
+
Pipeline.define('$instruct', params => Pipeline.resolve(params, 'instruct'), { ignoreNull: false });
|
|
69
|
+
Pipeline.define('$normalize', params => Pipeline.resolve(params, 'normalize'), { ignoreNull: false });
|
|
70
|
+
Pipeline.define('$construct', params => Pipeline.resolve(params, 'construct'), { ignoreNull: false });
|
|
71
|
+
Pipeline.define('$restruct', params => Pipeline.resolve(params, 'restruct'), { ignoreNull: false });
|
|
72
|
+
Pipeline.define('$serialize', params => Pipeline.resolve(params, 'serialize'), { ignoreNull: false });
|
|
73
|
+
Pipeline.define('$finalize', params => Pipeline.resolve(params, 'finalize'), { ignoreNull: false });
|
|
74
|
+
|
|
75
|
+
//
|
|
76
|
+
Pipeline.define('$pk', ({ query, model, value, path }) => model.source.idValue(get(query.doc, path) || value?.id || value), { ignoreNull: false });
|
|
77
|
+
Pipeline.define('$fk', ({ model, value }) => model.source.idValue(value.id || value));
|
|
78
|
+
Pipeline.define('$default', ({ field: { defaultValue }, value }) => (value === undefined ? defaultValue : value), { ignoreNull: false });
|
|
79
|
+
|
|
80
|
+
//
|
|
81
|
+
Pipeline.define('$cast', ({ field, value }) => {
|
|
82
|
+
const { type, isEmbedded } = field;
|
|
81
83
|
if (isEmbedded) return value;
|
|
82
84
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return v;
|
|
92
|
-
}
|
|
93
|
-
case 'Int': {
|
|
94
|
-
const num = Number(v);
|
|
95
|
-
if (!Number.isNaN(num)) return parseInt(v, 10);
|
|
96
|
-
return v;
|
|
97
|
-
}
|
|
98
|
-
case 'Boolean': {
|
|
99
|
-
if (v === 'true') return true;
|
|
100
|
-
if (v === 'false') return false;
|
|
101
|
-
return v;
|
|
102
|
-
}
|
|
103
|
-
default: {
|
|
104
|
-
return v;
|
|
105
|
-
}
|
|
85
|
+
switch (type.toLowerCase()) {
|
|
86
|
+
case 'string': {
|
|
87
|
+
return `${value}`;
|
|
88
|
+
}
|
|
89
|
+
case 'float': case 'number': {
|
|
90
|
+
const num = Number(value);
|
|
91
|
+
if (!Number.isNaN(num)) return num;
|
|
92
|
+
return value;
|
|
106
93
|
}
|
|
94
|
+
case 'int': {
|
|
95
|
+
const num = Number(value);
|
|
96
|
+
if (!Number.isNaN(num)) return parseInt(value, 10);
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
case 'boolean': {
|
|
100
|
+
if (value === 'true') return true;
|
|
101
|
+
if (value === 'false') return false;
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
default: {
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
//
|
|
111
|
+
Pipeline.define('ensureId', ({ query, resolver, model, field, value }) => {
|
|
112
|
+
const { type } = field;
|
|
113
|
+
const ids = Util.filterBy(Util.ensureArray(value), (a, b) => `${a}` === `${b}`);
|
|
114
|
+
return resolver.match(type).flags(query.flags).where({ id: ids }).count().then((count) => {
|
|
115
|
+
if (count !== ids.length) throw Boom.notFound(`${type} Not Found`);
|
|
107
116
|
});
|
|
108
117
|
}, { itemize: false });
|
|
109
118
|
|
|
110
119
|
// Required fields
|
|
111
|
-
Pipeline.define('required', ({ model, field, value }) => {
|
|
112
|
-
|
|
113
|
-
if (value == null) throw Boom.badRequest(`${path} is required`, { path });
|
|
120
|
+
Pipeline.define('required', ({ query, model, field, value }) => {
|
|
121
|
+
if ((query.crud === 'create' && value == null) || (query.crud === 'update' && value === null)) throw Boom.badRequest(`${model.name}.${field.name} is required`);
|
|
114
122
|
}, { ignoreNull: false });
|
|
115
123
|
|
|
116
124
|
// A field cannot hold a reference to itself
|
|
117
|
-
Pipeline.define('selfless', ({ model, field,
|
|
118
|
-
|
|
119
|
-
if (`${value}` === `${parentPath('id')}`) throw Boom.badData(`${path} cannot hold a reference to itself`, { path });
|
|
125
|
+
Pipeline.define('selfless', ({ query, model, field, value }) => {
|
|
126
|
+
if (`${value}` === `${query.doc?.id}`) throw Boom.badRequest(`${model}.${field} cannot hold a reference to itself`);
|
|
120
127
|
});
|
|
121
128
|
|
|
122
129
|
// Once set it cannot be changed
|
|
123
|
-
Pipeline.define('immutable', ({ model, field,
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
const oldVal = docPath(path, hint);
|
|
127
|
-
if (oldVal !== undefined && value !== undefined && `${hashObject(oldVal)}` !== `${hashObject(value)}`) throw Boom.badData(`${$path} is immutable; cannot be changed once set ${oldVal} -> ${value}`, { path: $path });
|
|
130
|
+
Pipeline.define('immutable', ({ query, model, field, value, path }) => {
|
|
131
|
+
const oldVal = get(query.doc, path);
|
|
132
|
+
if (oldVal !== undefined && value !== undefined && `${hashObject(oldVal)}` !== `${hashObject(value)}`) throw Boom.badRequest(`${model}.${field} is immutable; cannot be changed once set ${oldVal} -> ${value}`);
|
|
128
133
|
});
|
|
129
134
|
|
|
130
135
|
// List of allowed values
|
|
131
136
|
Pipeline.factory('Allow', (...args) => function allow({ model, field, value }) {
|
|
132
|
-
|
|
133
|
-
if (args.indexOf(value) === -1) throw Boom.badData(`${path} allows ${args}; found '${value}'`, { path });
|
|
137
|
+
if (args.indexOf(value) === -1) throw Boom.badRequest(`${model}.${field} allows ${args}; found '${value}'`);
|
|
134
138
|
});
|
|
135
139
|
|
|
136
140
|
// List of disallowed values
|
|
137
141
|
Pipeline.factory('Deny', (...args) => function deny({ model, field, value }) {
|
|
138
|
-
|
|
139
|
-
if (args.indexOf(value) > -1) throw Boom.badData(`${path} denys ${args}; found '${value}'`, { path });
|
|
142
|
+
if (args.indexOf(value) > -1) throw Boom.badRequest(`${model}.${field} denys ${args}; found '${value}'`);
|
|
140
143
|
});
|
|
141
144
|
|
|
142
145
|
// Min/Max range
|
|
@@ -145,31 +148,20 @@ module.exports = class Pipeline {
|
|
|
145
148
|
if (max == null) max = undefined;
|
|
146
149
|
|
|
147
150
|
return function range({ model, field, value }) {
|
|
148
|
-
const path = `${model}.${field}`;
|
|
149
151
|
const num = +value; // Coerce to number if possible
|
|
150
152
|
const test = Number.isNaN(num) ? value.length : num;
|
|
151
|
-
if (test < min || test > max) throw Boom.
|
|
153
|
+
if (test < min || test > max) throw Boom.badRequest(`${model}.${field} must satisfy range ${min}:${max}; found '${value}'`);
|
|
152
154
|
};
|
|
153
155
|
}, { itemize: false });
|
|
154
156
|
}
|
|
157
|
+
|
|
158
|
+
static resolve(params, pipeline) {
|
|
159
|
+
const transformers = params.field.pipelines[pipeline] || [];
|
|
160
|
+
|
|
161
|
+
return Util.pipeline(transformers.map(t => async (value) => {
|
|
162
|
+
return Pipeline[t]({ ...params, value });
|
|
163
|
+
}), params.value);
|
|
164
|
+
}
|
|
155
165
|
};
|
|
156
166
|
|
|
157
|
-
|
|
158
|
-
// 'charAt', 'charCodeAt', 'codePointAt', 'concat', 'indexOf', 'lastIndexOf', 'localeCompare',
|
|
159
|
-
// 'normalize', 'padEnd', 'padStart', 'repeat', 'replace', 'search', 'slice', 'split', 'substr', 'substring',
|
|
160
|
-
// 'toLocaleLowerCase', 'toLocaleUpperCase', 'toLowerCase', 'toString', 'toUpperCase', 'trim', 'trimEnd', 'trimStart', 'raw',
|
|
161
|
-
// ];
|
|
162
|
-
|
|
163
|
-
// Transformer.factory('toTitleCase', () => ({ value }) => value.replace(/\w\S*/g, w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()), { enumerable: true });
|
|
164
|
-
// Transformer.factory('toLocaleTitleCase', (...args) => ({ value }) => value.replace(/\w\S*/g, w => w.charAt(0).toLocaleUpperCase(...args) + w.slice(1).toLocaleLowerCase()));
|
|
165
|
-
// Transformer.factory('toSentenceCase', () => ({ value }) => value.charAt(0).toUpperCase() + value.slice(1), { enumerable: true });
|
|
166
|
-
// Transformer.factory('toLocaleSentenceCase', (...args) => ({ value }) => value.charAt(0).toLocaleUpperCase(...args) + value.slice(1));
|
|
167
|
-
// Transformer.factory('toArray', () => ({ value }) => (Array.isArray(value) ? value : [value]), { itemize: false, enumerable: true });
|
|
168
|
-
// Transformer.factory('toDate', () => ({ value }) => new Date(value), { enumerable: true, writable: true });
|
|
169
|
-
// Transformer.factory('dedupe', () => ({ value }) => uniqWith(value, (b, c) => hashObject(b) === hashObject(c)), { ignoreNull: false, enumerable: true });
|
|
170
|
-
// Transformer.factory('dedupeBy', key => ({ value }) => uniqWith(value, (b, c) => hashObject(b[key]) === hashObject(c[key])), { ignoreNull: false, enumerable: true });
|
|
171
|
-
// Transformer.factory('timestamp', () => () => Date.now(), { enumerable: true, ignoreNull: false });
|
|
172
|
-
// Transformer.factory('createdAt', () => ({ value }) => value || Date.now(), { enumerable: true, ignoreNull: false });
|
|
173
|
-
// Transformer.factory('first', () => ({ value }) => (Array.isArray(value) ? value[0] : value), { enumerable: true });
|
|
174
|
-
// Transformer.factory('get', path => ({ value }) => get(value, path), { enumerable: true });
|
|
175
|
-
// Transformer.factory('set', path => ({ value }) => set({}, path, value), { enumerable: true });
|
|
167
|
+
module.exports.createPresets();
|