@xitkov/core 2.4.10 → 3.0.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/README.md
CHANGED
|
@@ -25,6 +25,29 @@ src/
|
|
|
25
25
|
└─ billing.ts # optional external service client
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
### Nested services
|
|
29
|
+
|
|
30
|
+
A service folder inside another is served beneath it. Position is the only
|
|
31
|
+
declaration needed:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
services/
|
|
35
|
+
├─ skills/
|
|
36
|
+
│ ├─ service.ts # /skills
|
|
37
|
+
│ └─ versions/
|
|
38
|
+
│ └─ service.ts # /skills/:parentId/versions
|
|
39
|
+
└─ conversations/
|
|
40
|
+
├─ service.ts # /conversations
|
|
41
|
+
└─ messages/
|
|
42
|
+
└─ service.ts # /conversations/:parentId/messages
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Read the parent from `req.params.parentId`, and address the service by its
|
|
46
|
+
path: `Services.get('skills.versions')`.
|
|
47
|
+
|
|
48
|
+
A folder with no `service` module is a grouping folder, not a service; the
|
|
49
|
+
services inside it are still found.
|
|
50
|
+
|
|
28
51
|
`src/index.ts`:
|
|
29
52
|
|
|
30
53
|
```ts
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import type { Application } from 'express';
|
|
2
2
|
import { Config, IAuth } from '..';
|
|
3
|
+
type Functions = 'find' | 'get' | 'create' | 'update' | 'patch' | 'remove';
|
|
4
|
+
/**
|
|
5
|
+
* Validate one method's input and return the coerced result.
|
|
6
|
+
*
|
|
7
|
+
* Joi converts as well as checks: `"25"` becomes `25`, and a declared default
|
|
8
|
+
* fills a missing field. The caller must therefore use the returned value —
|
|
9
|
+
* discarding it would validate the request and then hand the service the raw
|
|
10
|
+
* one, so defaults would silently never arrive.
|
|
11
|
+
*/
|
|
12
|
+
export declare function validate(Validator: any, func: string, value: any): Promise<any>;
|
|
13
|
+
/** Where a method's validated input sits in the service call. */
|
|
14
|
+
export declare const VALIDATED_INDEX: Record<Functions, number>;
|
|
15
|
+
/** Every service folder under `services`, nested ones included. */
|
|
16
|
+
export declare function discoverServices(root: string, segments?: string[]): Promise<string[][]>;
|
|
17
|
+
/** `['skills','versions']` -> `/skills/:parentId/versions`. */
|
|
18
|
+
export declare function mountPath(segments: string[]): string;
|
|
3
19
|
export declare function registerServices(app: Application, auth?: IAuth, database?: Config['database']): Promise<{
|
|
4
20
|
[serviceName: string]: any;
|
|
5
21
|
}>;
|
|
22
|
+
export {};
|
|
@@ -27,7 +27,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
27
27
|
};
|
|
28
28
|
var _a, _b, _c, _d, _e;
|
|
29
29
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
-
exports.registerServices = void 0;
|
|
30
|
+
exports.registerServices = exports.mountPath = exports.discoverServices = exports.VALIDATED_INDEX = exports.validate = void 0;
|
|
31
31
|
// eslint-disable-next-line node/no-unsupported-features/node-builtins
|
|
32
32
|
const fs_1 = require("fs");
|
|
33
33
|
const path_1 = __importDefault(require("path"));
|
|
@@ -35,6 +35,14 @@ const express_1 = require("express");
|
|
|
35
35
|
const __1 = require("..");
|
|
36
36
|
const errors_1 = require("../errors");
|
|
37
37
|
const auth_1 = require("./auth");
|
|
38
|
+
/**
|
|
39
|
+
* Validate one method's input and return the coerced result.
|
|
40
|
+
*
|
|
41
|
+
* Joi converts as well as checks: `"25"` becomes `25`, and a declared default
|
|
42
|
+
* fills a missing field. The caller must therefore use the returned value —
|
|
43
|
+
* discarding it would validate the request and then hand the service the raw
|
|
44
|
+
* one, so defaults would silently never arrive.
|
|
45
|
+
*/
|
|
38
46
|
async function validate(Validator, func, value) {
|
|
39
47
|
const schema = await Validator[func];
|
|
40
48
|
if (schema) {
|
|
@@ -48,6 +56,16 @@ async function validate(Validator, func, value) {
|
|
|
48
56
|
}
|
|
49
57
|
return value;
|
|
50
58
|
}
|
|
59
|
+
exports.validate = validate;
|
|
60
|
+
/** Where a method's validated input sits in the service call. */
|
|
61
|
+
exports.VALIDATED_INDEX = {
|
|
62
|
+
find: 0,
|
|
63
|
+
get: 1,
|
|
64
|
+
create: 0,
|
|
65
|
+
patch: 1,
|
|
66
|
+
update: 1,
|
|
67
|
+
remove: 1,
|
|
68
|
+
};
|
|
51
69
|
async function hooks(Hooks, func, parameters, methodName) {
|
|
52
70
|
var _a;
|
|
53
71
|
const allHooks = Hooks[func];
|
|
@@ -67,6 +85,32 @@ function getJWTDecoded(accessToken) {
|
|
|
67
85
|
return {};
|
|
68
86
|
}
|
|
69
87
|
}
|
|
88
|
+
/** Every service folder under `services`, nested ones included. */
|
|
89
|
+
async function discoverServices(root, segments = []) {
|
|
90
|
+
const entries = await fs_1.promises.readdir(path_1.default.join(root, ...segments), { withFileTypes: true });
|
|
91
|
+
const found = [];
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
if (!entry.isDirectory() || entry.name === 'hooks')
|
|
94
|
+
continue;
|
|
95
|
+
const child = [...segments, entry.name];
|
|
96
|
+
const files = await fs_1.promises.readdir(path_1.default.join(root, ...child));
|
|
97
|
+
if (files.some(file => /^service\.(ts|js)$/.test(file)))
|
|
98
|
+
found.push(child);
|
|
99
|
+
// Recurse either way: a grouping folder holds no service of its own.
|
|
100
|
+
found.push(...(await discoverServices(root, child)));
|
|
101
|
+
}
|
|
102
|
+
return found;
|
|
103
|
+
}
|
|
104
|
+
exports.discoverServices = discoverServices;
|
|
105
|
+
/** `['skills','versions']` -> `/skills/:parentId/versions`. */
|
|
106
|
+
function mountPath(segments) {
|
|
107
|
+
const parents = segments.slice(0, -1).map((name, i) =>
|
|
108
|
+
// Express rejects two parameters with the same name, so only the closest
|
|
109
|
+
// parent gets the plain one.
|
|
110
|
+
`/${name}/:parentId${i === segments.length - 2 ? '' : segments.length - 2 - i}`);
|
|
111
|
+
return `${parents.join('')}/${segments[segments.length - 1]}`;
|
|
112
|
+
}
|
|
113
|
+
exports.mountPath = mountPath;
|
|
70
114
|
async function registerServices(app, auth, database) {
|
|
71
115
|
var _a, _b, _c;
|
|
72
116
|
if (((_a = require.main) === null || _a === void 0 ? void 0 : _a.filename) == undefined) {
|
|
@@ -74,7 +118,8 @@ async function registerServices(app, auth, database) {
|
|
|
74
118
|
}
|
|
75
119
|
const appDir = path_1.default.dirname((_b = require.main) === null || _b === void 0 ? void 0 : _b.filename);
|
|
76
120
|
const serviceDirPath = appDir + '/services';
|
|
77
|
-
|
|
121
|
+
// Deepest first: `/skills/:id` would otherwise match `/skills/versions`.
|
|
122
|
+
const discovered = (await discoverServices(serviceDirPath)).sort((a, b) => b.length - a.length);
|
|
78
123
|
const methods = [
|
|
79
124
|
{
|
|
80
125
|
method: 'get',
|
|
@@ -108,11 +153,14 @@ async function registerServices(app, auth, database) {
|
|
|
108
153
|
},
|
|
109
154
|
];
|
|
110
155
|
const services = {};
|
|
111
|
-
for (const
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
const
|
|
156
|
+
for (const segments of discovered) {
|
|
157
|
+
// `skills/versions` is keyed `skills.versions` and mounted beneath skills.
|
|
158
|
+
const serviceName = segments.join('.');
|
|
159
|
+
const directory = path_1.default.join(serviceDirPath, ...segments);
|
|
160
|
+
const servicePath = `${directory}/service`;
|
|
161
|
+
const modelPath = `${directory}/model`;
|
|
162
|
+
const validatorPath = `${directory}/validator`;
|
|
163
|
+
const hooksPath = `${directory}/hooks`;
|
|
116
164
|
let Model = null;
|
|
117
165
|
let Validator = null;
|
|
118
166
|
let Hooks = null;
|
|
@@ -146,7 +194,9 @@ async function registerServices(app, auth, database) {
|
|
|
146
194
|
serviceConfig.dialect = (_c = database === null || database === void 0 ? void 0 : database.dialect) !== null && _c !== void 0 ? _c : 'mongodb';
|
|
147
195
|
const service = new (await (_e = servicePath, Promise.resolve().then(() => __importStar(require(_e))))).default(serviceConfig);
|
|
148
196
|
services[serviceName] = service;
|
|
149
|
-
|
|
197
|
+
// `mergeParams` lets a nested handler read the parent id from the mount
|
|
198
|
+
// path rather than only its own parameters.
|
|
199
|
+
const router = (0, express_1.Router)({ mergeParams: true });
|
|
150
200
|
for (const { method, url, func } of methods) {
|
|
151
201
|
const callback = async (req, res) => {
|
|
152
202
|
var _a, _b;
|
|
@@ -185,47 +235,48 @@ async function registerServices(app, auth, database) {
|
|
|
185
235
|
const user = req.user;
|
|
186
236
|
const { id } = params;
|
|
187
237
|
let parameters = [];
|
|
188
|
-
let validatorValue;
|
|
189
238
|
switch (func) {
|
|
190
239
|
case 'find':
|
|
191
240
|
parameters = [query, user, req];
|
|
192
|
-
validatorValue = query;
|
|
193
241
|
break;
|
|
194
242
|
case 'get':
|
|
195
243
|
parameters = [id, query, user, req];
|
|
196
|
-
validatorValue = query;
|
|
197
244
|
break;
|
|
198
245
|
case 'create':
|
|
199
246
|
parameters = [body, query, user, req];
|
|
200
|
-
validatorValue = body;
|
|
201
247
|
break;
|
|
202
248
|
case 'patch':
|
|
203
249
|
parameters = [id, body, query, user, req];
|
|
204
|
-
validatorValue = body;
|
|
205
250
|
break;
|
|
206
251
|
case 'update':
|
|
207
252
|
parameters = [id, body, query, user, req];
|
|
208
|
-
validatorValue = body;
|
|
209
253
|
break;
|
|
210
254
|
case 'remove':
|
|
211
255
|
parameters = [id, query, user, req];
|
|
212
|
-
validatorValue = query;
|
|
213
256
|
break;
|
|
214
257
|
}
|
|
215
|
-
if (Hooks) {
|
|
216
|
-
await hooks(Hooks, 'all', parameters, func);
|
|
217
|
-
await hooks(Hooks, func, parameters, func);
|
|
218
|
-
}
|
|
219
|
-
if (service.filtering !== false && func === 'find') {
|
|
220
|
-
parameters[0] = (0, __1.addQuerySupport)(...parameters, func);
|
|
221
|
-
}
|
|
222
258
|
if (service[func] == undefined) {
|
|
223
259
|
return res.status(404).send({
|
|
224
260
|
message: "Not found"
|
|
225
261
|
});
|
|
226
262
|
}
|
|
263
|
+
// Validation runs before the hooks so a hook inspects the same
|
|
264
|
+
// coerced value the service receives, and writing the result back
|
|
265
|
+
// into `parameters` is what carries Joi's defaults and type
|
|
266
|
+
// conversions through to the service.
|
|
227
267
|
if (Validator) {
|
|
228
|
-
|
|
268
|
+
const index = exports.VALIDATED_INDEX[func];
|
|
269
|
+
parameters[index] = await validate(Validator, func, parameters[index]);
|
|
270
|
+
}
|
|
271
|
+
if (Hooks) {
|
|
272
|
+
await hooks(Hooks, 'all', parameters, func);
|
|
273
|
+
await hooks(Hooks, func, parameters, func);
|
|
274
|
+
}
|
|
275
|
+
// Filter translation comes last: it rewrites the query in place, so
|
|
276
|
+
// running it before validation would validate a value the service
|
|
277
|
+
// never sees.
|
|
278
|
+
if (service.filtering !== false && func === 'find') {
|
|
279
|
+
parameters[0] = (0, __1.addQuerySupport)(...parameters, func);
|
|
229
280
|
}
|
|
230
281
|
const response = await service[func](...parameters);
|
|
231
282
|
let data = response;
|
|
@@ -264,7 +315,9 @@ async function registerServices(app, auth, database) {
|
|
|
264
315
|
router[method](url, callback);
|
|
265
316
|
}
|
|
266
317
|
app.set('services', services);
|
|
267
|
-
|
|
318
|
+
// The folder's position is the mount path, so a nested service is only
|
|
319
|
+
// reachable through its parent without naming that parent in code.
|
|
320
|
+
app.use(mountPath(segments), router);
|
|
268
321
|
}
|
|
269
322
|
return services;
|
|
270
323
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register-services.js","sourceRoot":"","sources":["../../src/services/register-services.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA,sEAAsE;AACtE,2BAA4B;AAC5B,gDAAwB;AACxB,qCAA+B;AAC/B,0BAAmE;AAEnE,sCAAqD;AACrD,iCAAqC;AAcrC,KAAK,UAAU,QAAQ,CAAC,SAAc,EAAE,IAAY,EAAE,KAAU;
|
|
1
|
+
{"version":3,"file":"register-services.js","sourceRoot":"","sources":["../../src/services/register-services.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xitkov/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"@types/node": "^14.11.2",
|
|
32
32
|
"gts": "^3.1.0",
|
|
33
33
|
"jest": "^30.4.2",
|
|
34
|
+
"joi": "^18.2.9",
|
|
34
35
|
"sqlite3": "^6.0.1",
|
|
35
36
|
"ts-jest": "^29.4.11",
|
|
36
37
|
"typescript": "^4.0.3"
|
|
@@ -20,7 +20,15 @@ interface Methods {
|
|
|
20
20
|
func: Functions;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Validate one method's input and return the coerced result.
|
|
25
|
+
*
|
|
26
|
+
* Joi converts as well as checks: `"25"` becomes `25`, and a declared default
|
|
27
|
+
* fills a missing field. The caller must therefore use the returned value —
|
|
28
|
+
* discarding it would validate the request and then hand the service the raw
|
|
29
|
+
* one, so defaults would silently never arrive.
|
|
30
|
+
*/
|
|
31
|
+
export async function validate(Validator: any, func: string, value: any) {
|
|
24
32
|
const schema = await Validator[func]
|
|
25
33
|
if (schema) {
|
|
26
34
|
try {
|
|
@@ -33,6 +41,16 @@ async function validate(Validator: any, func: string, value: any) {
|
|
|
33
41
|
return value
|
|
34
42
|
}
|
|
35
43
|
|
|
44
|
+
/** Where a method's validated input sits in the service call. */
|
|
45
|
+
export const VALIDATED_INDEX: Record<Functions, number> = {
|
|
46
|
+
find: 0,
|
|
47
|
+
get: 1,
|
|
48
|
+
create: 0,
|
|
49
|
+
patch: 1,
|
|
50
|
+
update: 1,
|
|
51
|
+
remove: 1,
|
|
52
|
+
}
|
|
53
|
+
|
|
36
54
|
async function hooks(Hooks: any, func: string, parameters: any[], methodName: string) {
|
|
37
55
|
const allHooks = Hooks[func]
|
|
38
56
|
|
|
@@ -53,6 +71,33 @@ function getJWTDecoded(accessToken: string) {
|
|
|
53
71
|
}
|
|
54
72
|
}
|
|
55
73
|
|
|
74
|
+
/** Every service folder under `services`, nested ones included. */
|
|
75
|
+
export async function discoverServices(root: string, segments: string[] = []) {
|
|
76
|
+
const entries = await promises.readdir(path.join(root, ...segments), {withFileTypes: true});
|
|
77
|
+
const found: string[][] = [];
|
|
78
|
+
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
if (!entry.isDirectory() || entry.name === 'hooks') continue;
|
|
81
|
+
const child = [...segments, entry.name];
|
|
82
|
+
const files = await promises.readdir(path.join(root, ...child));
|
|
83
|
+
if (files.some(file => /^service\.(ts|js)$/.test(file))) found.push(child);
|
|
84
|
+
// Recurse either way: a grouping folder holds no service of its own.
|
|
85
|
+
found.push(...(await discoverServices(root, child)));
|
|
86
|
+
}
|
|
87
|
+
return found;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** `['skills','versions']` -> `/skills/:parentId/versions`. */
|
|
91
|
+
export function mountPath(segments: string[]) {
|
|
92
|
+
const parents = segments.slice(0, -1).map((name, i) =>
|
|
93
|
+
// Express rejects two parameters with the same name, so only the closest
|
|
94
|
+
// parent gets the plain one.
|
|
95
|
+
`/${name}/:parentId${i === segments.length - 2 ? '' : segments.length - 2 - i}`
|
|
96
|
+
);
|
|
97
|
+
return `${parents.join('')}/${segments[segments.length - 1]}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
56
101
|
export async function registerServices(app: Application, auth?: IAuth, database?: Config['database']) {
|
|
57
102
|
if (require.main?.filename == undefined) {
|
|
58
103
|
throw new Error("Please provide service path")
|
|
@@ -60,8 +105,9 @@ export async function registerServices(app: Application, auth?: IAuth, database?
|
|
|
60
105
|
const appDir = path.dirname(require.main?.filename as string);
|
|
61
106
|
const serviceDirPath = appDir + '/services';
|
|
62
107
|
|
|
63
|
-
|
|
64
|
-
|
|
108
|
+
// Deepest first: `/skills/:id` would otherwise match `/skills/versions`.
|
|
109
|
+
const discovered = (await discoverServices(serviceDirPath)).sort(
|
|
110
|
+
(a, b) => b.length - a.length
|
|
65
111
|
);
|
|
66
112
|
|
|
67
113
|
const methods: Methods[] = [
|
|
@@ -99,11 +145,14 @@ export async function registerServices(app: Application, auth?: IAuth, database?
|
|
|
99
145
|
|
|
100
146
|
const services: {[serviceName: string]: any} = {}
|
|
101
147
|
|
|
102
|
-
for (const
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
const
|
|
148
|
+
for (const segments of discovered) {
|
|
149
|
+
// `skills/versions` is keyed `skills.versions` and mounted beneath skills.
|
|
150
|
+
const serviceName = segments.join('.');
|
|
151
|
+
const directory = path.join(serviceDirPath, ...segments);
|
|
152
|
+
const servicePath = `${directory}/service`;
|
|
153
|
+
const modelPath = `${directory}/model`;
|
|
154
|
+
const validatorPath = `${directory}/validator`;
|
|
155
|
+
const hooksPath = `${directory}/hooks`;
|
|
107
156
|
let Model = null
|
|
108
157
|
let Validator: any = null
|
|
109
158
|
let Hooks: any = null
|
|
@@ -137,7 +186,9 @@ export async function registerServices(app: Application, auth?: IAuth, database?
|
|
|
137
186
|
|
|
138
187
|
const service = new (await import(servicePath)).default(serviceConfig);
|
|
139
188
|
services[serviceName] = service
|
|
140
|
-
|
|
189
|
+
// `mergeParams` lets a nested handler read the parent id from the mount
|
|
190
|
+
// path rather than only its own parameters.
|
|
191
|
+
const router = Router({mergeParams: true});
|
|
141
192
|
for (const {method, url, func} of methods) {
|
|
142
193
|
const callback = async (req: Request, res: Response) => {
|
|
143
194
|
const { required, baseURL } = auth ?? {}
|
|
@@ -180,48 +231,52 @@ export async function registerServices(app: Application, auth?: IAuth, database?
|
|
|
180
231
|
const user = (req as any).user
|
|
181
232
|
const {id} = params;
|
|
182
233
|
let parameters: any[] = [];
|
|
183
|
-
let validatorValue
|
|
184
234
|
switch (func) {
|
|
185
235
|
case 'find':
|
|
186
236
|
parameters = [query, user, req];
|
|
187
|
-
validatorValue = query
|
|
188
237
|
break;
|
|
189
238
|
case 'get':
|
|
190
239
|
parameters = [id, query, user, req];
|
|
191
|
-
validatorValue = query
|
|
192
240
|
break;
|
|
193
241
|
case 'create':
|
|
194
242
|
parameters = [body, query, user, req];
|
|
195
|
-
validatorValue = body
|
|
196
243
|
break;
|
|
197
244
|
case 'patch':
|
|
198
245
|
parameters = [id, body, query, user, req];
|
|
199
|
-
validatorValue = body
|
|
200
246
|
break;
|
|
201
247
|
case 'update':
|
|
202
248
|
parameters = [id, body, query, user, req];
|
|
203
|
-
validatorValue = body
|
|
204
249
|
break;
|
|
205
250
|
case 'remove':
|
|
206
251
|
parameters = [id, query, user, req];
|
|
207
|
-
validatorValue = query
|
|
208
252
|
break;
|
|
209
253
|
}
|
|
210
|
-
if (Hooks) {
|
|
211
|
-
await hooks(Hooks, 'all', parameters, func)
|
|
212
|
-
await hooks(Hooks, func, parameters, func)
|
|
213
|
-
}
|
|
214
|
-
if (service.filtering !== false && func === 'find') {
|
|
215
|
-
parameters[0] = addQuerySupport(...parameters, func)
|
|
216
|
-
}
|
|
217
254
|
|
|
218
255
|
if(service[func] == undefined) {
|
|
219
256
|
return res.status(404).send({
|
|
220
257
|
message: "Not found"
|
|
221
258
|
});
|
|
222
259
|
}
|
|
260
|
+
|
|
261
|
+
// Validation runs before the hooks so a hook inspects the same
|
|
262
|
+
// coerced value the service receives, and writing the result back
|
|
263
|
+
// into `parameters` is what carries Joi's defaults and type
|
|
264
|
+
// conversions through to the service.
|
|
223
265
|
if (Validator) {
|
|
224
|
-
|
|
266
|
+
const index = VALIDATED_INDEX[func]
|
|
267
|
+
parameters[index] = await validate(Validator, func, parameters[index])
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (Hooks) {
|
|
271
|
+
await hooks(Hooks, 'all', parameters, func)
|
|
272
|
+
await hooks(Hooks, func, parameters, func)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Filter translation comes last: it rewrites the query in place, so
|
|
276
|
+
// running it before validation would validate a value the service
|
|
277
|
+
// never sees.
|
|
278
|
+
if (service.filtering !== false && func === 'find') {
|
|
279
|
+
parameters[0] = addQuerySupport(...parameters, func)
|
|
225
280
|
}
|
|
226
281
|
|
|
227
282
|
const response = await service[func](...parameters)
|
|
@@ -262,7 +317,9 @@ export async function registerServices(app: Application, auth?: IAuth, database?
|
|
|
262
317
|
}
|
|
263
318
|
app.set('services', services)
|
|
264
319
|
|
|
265
|
-
|
|
320
|
+
// The folder's position is the mount path, so a nested service is only
|
|
321
|
+
// reachable through its parent without naming that parent in code.
|
|
322
|
+
app.use(mountPath(segments), router);
|
|
266
323
|
}
|
|
267
324
|
return services;
|
|
268
325
|
}
|