godprotocol 2.3.47 → 2.3.50
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/package.json +1 -1
- package/server/Routable/Route_table.js +388 -92
- package/server/version_control.js +23 -12
package/package.json
CHANGED
|
@@ -8,6 +8,28 @@ let securities = {
|
|
|
8
8
|
none: [],
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
// Sections whose raw values arrive as strings (URLSearchParams / regex
|
|
12
|
+
const STRINGY_SECTIONS = ["query", "params"];
|
|
13
|
+
|
|
14
|
+
// Supported arithmetic operators (value-level)
|
|
15
|
+
const ARITH_OPS = {
|
|
16
|
+
min: (a, b) => a >= b,
|
|
17
|
+
max: (a, b) => a <= b,
|
|
18
|
+
gt: (a, b) => a > b,
|
|
19
|
+
gte: (a, b) => a >= b,
|
|
20
|
+
lt: (a, b) => a < b,
|
|
21
|
+
lte: (a, b) => a <= b,
|
|
22
|
+
eq: (a, b) => a === b,
|
|
23
|
+
ne: (a, b) => a !== b,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Length variants (work on string / array / object)
|
|
27
|
+
const LENGTH_OPS = {
|
|
28
|
+
minLength: (a, b) => (a?.length ?? Object.keys(a || {}).length) >= b,
|
|
29
|
+
maxLength: (a, b) => (a?.length ?? Object.keys(a || {}).length) <= b,
|
|
30
|
+
length: (a, b) => (a?.length ?? Object.keys(a || {}).length) === b,
|
|
31
|
+
};
|
|
32
|
+
|
|
11
33
|
class Route_table extends Headers {
|
|
12
34
|
constructor(gp) {
|
|
13
35
|
super();
|
|
@@ -36,7 +58,6 @@ class Route_table extends Headers {
|
|
|
36
58
|
for (const callback of this.before_callbacks) {
|
|
37
59
|
const result = await callback(payload);
|
|
38
60
|
|
|
39
|
-
// allow middleware-style rejection
|
|
40
61
|
if (result && result.ok === false) {
|
|
41
62
|
return result;
|
|
42
63
|
}
|
|
@@ -81,7 +102,13 @@ class Route_table extends Headers {
|
|
|
81
102
|
};
|
|
82
103
|
|
|
83
104
|
init_version = async (version, config) => {
|
|
84
|
-
this.versions[version] = {
|
|
105
|
+
this.versions[version] = {
|
|
106
|
+
config,
|
|
107
|
+
routes: {},
|
|
108
|
+
param_routes: [],
|
|
109
|
+
regex_routes: [],
|
|
110
|
+
wildcard: null,
|
|
111
|
+
};
|
|
85
112
|
|
|
86
113
|
this.active_version = version;
|
|
87
114
|
};
|
|
@@ -117,107 +144,378 @@ class Route_table extends Headers {
|
|
|
117
144
|
}
|
|
118
145
|
};
|
|
119
146
|
|
|
147
|
+
// =========================
|
|
148
|
+
// 🧭 ROUTE PATTERN COMPILATION
|
|
149
|
+
// =========================
|
|
150
|
+
compile_route_pattern = (pattern) => {
|
|
151
|
+
const keys = [];
|
|
152
|
+
|
|
153
|
+
const segments = pattern.split("/");
|
|
154
|
+
|
|
155
|
+
const regex_parts = segments.map((segment) => {
|
|
156
|
+
if (segment.startsWith(":")) {
|
|
157
|
+
keys.push(segment.slice(1));
|
|
158
|
+
return "([^/]+)";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (segment === "*") {
|
|
162
|
+
keys.push("*");
|
|
163
|
+
return "(.*)";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
regex: new RegExp(`^${regex_parts.join("/")}$`),
|
|
171
|
+
keys,
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
|
|
120
175
|
add_route = async (name, handler, config) => {
|
|
121
|
-
this.versions[this.active_version]
|
|
176
|
+
const version = this.versions[this.active_version];
|
|
177
|
+
|
|
178
|
+
version.routes[name] = { handler, config };
|
|
179
|
+
|
|
180
|
+
if (name === "*") {
|
|
181
|
+
version.wildcard = { handler, config, route_pattern: name };
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (name.startsWith("^")) {
|
|
186
|
+
version.regex_routes.push({
|
|
187
|
+
pattern: name,
|
|
188
|
+
regex: new RegExp(name),
|
|
189
|
+
handler,
|
|
190
|
+
config,
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (name.includes(":") || name.includes("*")) {
|
|
196
|
+
const { regex, keys } = this.compile_route_pattern(name);
|
|
197
|
+
|
|
198
|
+
version.param_routes.push({
|
|
199
|
+
pattern: name,
|
|
200
|
+
regex,
|
|
201
|
+
keys,
|
|
202
|
+
handler,
|
|
203
|
+
config,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
122
206
|
};
|
|
123
207
|
|
|
208
|
+
// =========================
|
|
209
|
+
// 🔎 ROUTE RESOLUTION
|
|
210
|
+
// Precedence: exact -> named param -> regex -> wildcard
|
|
211
|
+
// =========================
|
|
124
212
|
get_route = async (name) => {
|
|
125
213
|
const version = this.versions[this.active_version];
|
|
126
214
|
|
|
215
|
+
if (!version) return null;
|
|
216
|
+
|
|
127
217
|
if (version.routes[name]) {
|
|
128
|
-
return
|
|
218
|
+
return {
|
|
219
|
+
...version.routes[name],
|
|
220
|
+
params: {},
|
|
221
|
+
route_pattern: name,
|
|
222
|
+
};
|
|
129
223
|
}
|
|
130
224
|
|
|
131
|
-
for (const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
225
|
+
for (const route of version.param_routes) {
|
|
226
|
+
const match = route.regex.exec(name);
|
|
227
|
+
|
|
228
|
+
if (match) {
|
|
229
|
+
const params = {};
|
|
230
|
+
|
|
231
|
+
route.keys.forEach((key, i) => {
|
|
232
|
+
params[key] = decodeURIComponent(match[i + 1]);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
handler: route.handler,
|
|
237
|
+
config: route.config,
|
|
238
|
+
params,
|
|
239
|
+
route_pattern: route.pattern,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
for (const route of version.regex_routes) {
|
|
245
|
+
if (route.regex.test(name)) {
|
|
246
|
+
return {
|
|
247
|
+
handler: route.handler,
|
|
248
|
+
config: route.config,
|
|
249
|
+
params: {},
|
|
250
|
+
route_pattern: route.pattern,
|
|
251
|
+
};
|
|
136
252
|
}
|
|
137
253
|
}
|
|
138
254
|
|
|
255
|
+
if (version.wildcard) {
|
|
256
|
+
return { ...version.wildcard, params: {} };
|
|
257
|
+
}
|
|
258
|
+
|
|
139
259
|
return null;
|
|
140
260
|
};
|
|
141
261
|
|
|
142
262
|
getValue = (obj, path) => path.split(".").reduce((o, key) => o?.[key], obj);
|
|
143
263
|
|
|
144
|
-
|
|
145
|
-
|
|
264
|
+
// =========================
|
|
265
|
+
// 🔁 TYPE COERCION (query / params only)
|
|
266
|
+
// =========================
|
|
267
|
+
coerce_value = (value, expected) => {
|
|
268
|
+
if (value === null || value === undefined) return { ok: false };
|
|
269
|
+
|
|
270
|
+
switch (expected) {
|
|
271
|
+
case "number": {
|
|
272
|
+
if (typeof value === "number") return { ok: true, value };
|
|
273
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
274
|
+
return { ok: false };
|
|
275
|
+
const n = Number(value);
|
|
276
|
+
if (Number.isNaN(n)) return { ok: false };
|
|
277
|
+
return { ok: true, value: n };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
case "boolean": {
|
|
281
|
+
if (typeof value === "boolean") return { ok: true, value };
|
|
282
|
+
if (value === "true") return { ok: true, value: true };
|
|
283
|
+
if (value === "false") return { ok: true, value: false };
|
|
284
|
+
return { ok: false };
|
|
285
|
+
}
|
|
146
286
|
|
|
287
|
+
case "array": {
|
|
288
|
+
if (Array.isArray(value)) return { ok: true, value };
|
|
289
|
+
try {
|
|
290
|
+
const parsed = JSON.parse(value);
|
|
291
|
+
if (Array.isArray(parsed)) return { ok: true, value: parsed };
|
|
292
|
+
} catch (_) {}
|
|
293
|
+
if (typeof value === "string" && value.length) {
|
|
294
|
+
return { ok: true, value: value.split(",").map((v) => v.trim()) };
|
|
295
|
+
}
|
|
296
|
+
return { ok: false };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
case "object": {
|
|
300
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
301
|
+
return { ok: true, value };
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
const parsed = JSON.parse(value);
|
|
305
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
306
|
+
return { ok: true, value: parsed };
|
|
307
|
+
}
|
|
308
|
+
} catch (_) {}
|
|
309
|
+
return { ok: false };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
case "string":
|
|
313
|
+
return { ok: true, value: String(value) };
|
|
314
|
+
|
|
315
|
+
default:
|
|
316
|
+
return { ok: false };
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Deep validation of a single field (or nested structure).
|
|
322
|
+
* Supports:
|
|
323
|
+
* - type
|
|
324
|
+
* - required / default_value
|
|
325
|
+
* - enum
|
|
326
|
+
* - pattern (RegExp or string)
|
|
327
|
+
* - arithmetic operators (min, max, gt, gte, lt, lte, eq, ne)
|
|
328
|
+
* - length operators (minLength, maxLength, length)
|
|
329
|
+
* - nested schema (for object)
|
|
330
|
+
* - items schema (for array)
|
|
331
|
+
* - custom validator name (via this.validators)
|
|
332
|
+
*/
|
|
333
|
+
check_validation = async (rule, data, path = "") => {
|
|
334
|
+
let {
|
|
335
|
+
prop,
|
|
336
|
+
section,
|
|
337
|
+
type,
|
|
338
|
+
required = false,
|
|
339
|
+
enum: allowed,
|
|
340
|
+
pattern,
|
|
341
|
+
schema, // nested object schema
|
|
342
|
+
items, // array item schema
|
|
343
|
+
validator, // custom named validator
|
|
344
|
+
default_value,
|
|
345
|
+
...rest // arithmetic + length ops land here
|
|
346
|
+
} = rule;
|
|
347
|
+
|
|
348
|
+
const fullPath = path ? `${path}.${prop}` : prop;
|
|
147
349
|
let data_value = this.getValue(data, prop);
|
|
148
350
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
351
|
+
// ---------- default ----------
|
|
352
|
+
if (
|
|
353
|
+
data_value === undefined &&
|
|
354
|
+
Object.prototype.hasOwnProperty.call(rule, "default_value")
|
|
355
|
+
) {
|
|
356
|
+
data_value = default_value;
|
|
357
|
+
// write back so later stages see the default
|
|
358
|
+
const parts = prop.split(".");
|
|
359
|
+
let cur = data;
|
|
360
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
361
|
+
if (cur[parts[i]] == null) cur[parts[i]] = {};
|
|
362
|
+
cur = cur[parts[i]];
|
|
363
|
+
}
|
|
364
|
+
cur[parts[parts.length - 1]] = data_value;
|
|
152
365
|
}
|
|
153
366
|
|
|
367
|
+
// ---------- required ----------
|
|
154
368
|
if (data_value === undefined || data_value === null) {
|
|
155
369
|
if (required) {
|
|
156
370
|
return {
|
|
157
371
|
ok: false,
|
|
158
|
-
message: `Field '${
|
|
372
|
+
message: `Field '${fullPath}' is required in '${section}'`,
|
|
373
|
+
path: fullPath,
|
|
159
374
|
};
|
|
160
375
|
}
|
|
161
376
|
return { ok: true };
|
|
162
377
|
}
|
|
163
378
|
|
|
164
|
-
// type
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
379
|
+
// ---------- resolve type descriptor ----------
|
|
380
|
+
let expected = type;
|
|
381
|
+
if (typeof type === "object" && type !== null && !Array.isArray(type)) {
|
|
382
|
+
expected = type.value ?? type;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ---------- coercion (query / params only) ----------
|
|
386
|
+
if (
|
|
387
|
+
STRINGY_SECTIONS.includes(section) &&
|
|
388
|
+
expected &&
|
|
389
|
+
expected !== "string" &&
|
|
390
|
+
typeof data_value === "string"
|
|
391
|
+
) {
|
|
392
|
+
const coerced = this.coerce_value(data_value, expected);
|
|
393
|
+
if (!coerced.ok) {
|
|
394
|
+
return {
|
|
395
|
+
ok: false,
|
|
396
|
+
message: `Field '${fullPath}' in '${section}' could not be parsed as type '${expected}'`,
|
|
397
|
+
path: fullPath,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
data_value = coerced.value;
|
|
401
|
+
// write coerced value back
|
|
402
|
+
const parts = prop.split(".");
|
|
403
|
+
let cur = data;
|
|
404
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
405
|
+
cur = cur[parts[i]];
|
|
406
|
+
}
|
|
407
|
+
cur[parts[parts.length - 1]] = data_value;
|
|
408
|
+
}
|
|
168
409
|
|
|
169
|
-
|
|
170
|
-
|
|
410
|
+
// ---------- base type check ----------
|
|
411
|
+
if (expected) {
|
|
412
|
+
const actual = Array.isArray(data_value) ? "array" : typeof data_value;
|
|
413
|
+
if (actual !== expected) {
|
|
414
|
+
return {
|
|
415
|
+
ok: false,
|
|
416
|
+
message: `Field '${fullPath}' in '${section}' must be of type '${expected}', got '${actual}'`,
|
|
417
|
+
path: fullPath,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
}
|
|
171
421
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
422
|
+
// ---------- enum ----------
|
|
423
|
+
if (allowed && !allowed.includes(data_value)) {
|
|
424
|
+
return {
|
|
425
|
+
ok: false,
|
|
426
|
+
message: `Field '${fullPath}' in '${section}' must be one of: ${allowed.join(", ")}`,
|
|
427
|
+
path: fullPath,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
179
430
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
431
|
+
// ---------- regex / pattern ----------
|
|
432
|
+
if (pattern) {
|
|
433
|
+
const re = pattern instanceof RegExp ? pattern : new RegExp(pattern);
|
|
434
|
+
if (typeof data_value !== "string" || !re.test(data_value)) {
|
|
435
|
+
return {
|
|
436
|
+
ok: false,
|
|
437
|
+
message: `Field '${fullPath}' in '${section}' does not match required pattern`,
|
|
438
|
+
path: fullPath,
|
|
439
|
+
};
|
|
184
440
|
}
|
|
441
|
+
}
|
|
185
442
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
443
|
+
// ---------- arithmetic operators ----------
|
|
444
|
+
for (const [op, fn] of Object.entries(ARITH_OPS)) {
|
|
445
|
+
if (rest[op] !== undefined) {
|
|
446
|
+
if (typeof data_value !== "number") {
|
|
447
|
+
return {
|
|
448
|
+
ok: false,
|
|
449
|
+
message: `Field '${fullPath}' must be a number to apply '${op}'`,
|
|
450
|
+
path: fullPath,
|
|
194
451
|
};
|
|
195
452
|
}
|
|
453
|
+
if (!fn(data_value, rest[op])) {
|
|
454
|
+
return {
|
|
455
|
+
ok: false,
|
|
456
|
+
message: `Field '${fullPath}' failed '${op}' constraint (expected ${op} ${rest[op]}, got ${data_value})`,
|
|
457
|
+
path: fullPath,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
196
462
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (actual !== expected) {
|
|
463
|
+
// ---------- length operators ----------
|
|
464
|
+
for (const [op, fn] of Object.entries(LENGTH_OPS)) {
|
|
465
|
+
if (rest[op] !== undefined) {
|
|
466
|
+
if (!fn(data_value, rest[op])) {
|
|
202
467
|
return {
|
|
203
468
|
ok: false,
|
|
204
|
-
message: `Field '${
|
|
469
|
+
message: `Field '${fullPath}' failed '${op}' constraint (expected ${op} ${rest[op]})`,
|
|
470
|
+
path: fullPath,
|
|
205
471
|
};
|
|
206
472
|
}
|
|
207
473
|
}
|
|
208
474
|
}
|
|
209
475
|
|
|
210
|
-
//
|
|
211
|
-
if (
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
ok
|
|
219
|
-
|
|
220
|
-
|
|
476
|
+
// ---------- nested object schema ----------
|
|
477
|
+
if (expected === "object" && schema) {
|
|
478
|
+
for (const [nestedProp, nestedRule] of Object.entries(schema)) {
|
|
479
|
+
const result = await this.check_validation(
|
|
480
|
+
{ prop: nestedProp, section, ...nestedRule },
|
|
481
|
+
data_value,
|
|
482
|
+
fullPath,
|
|
483
|
+
);
|
|
484
|
+
if (!result.ok) return result;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ---------- array items schema ----------
|
|
489
|
+
if (expected === "array" && items) {
|
|
490
|
+
if (!Array.isArray(data_value)) {
|
|
491
|
+
return {
|
|
492
|
+
ok: false,
|
|
493
|
+
message: `Field '${fullPath}' must be an array`,
|
|
494
|
+
path: fullPath,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
for (let i = 0; i < data_value.length; i++) {
|
|
498
|
+
const result = await this.check_validation(
|
|
499
|
+
{ prop: String(i), section, ...items },
|
|
500
|
+
{ [String(i)]: data_value[i] },
|
|
501
|
+
`${fullPath}[${i}]`,
|
|
502
|
+
);
|
|
503
|
+
if (!result.ok) return result;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ---------- custom validator ----------
|
|
508
|
+
if (validator && this.validators[validator]) {
|
|
509
|
+
const result = await this.validators[validator](data_value, rule, data);
|
|
510
|
+
if (result && result.ok === false) {
|
|
511
|
+
return {
|
|
512
|
+
ok: false,
|
|
513
|
+
message:
|
|
514
|
+
result.message ||
|
|
515
|
+
`Custom validator '${validator}' failed for '${fullPath}'`,
|
|
516
|
+
path: fullPath,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
221
519
|
}
|
|
222
520
|
|
|
223
521
|
return { ok: true };
|
|
@@ -231,47 +529,45 @@ class Route_table extends Headers {
|
|
|
231
529
|
for (const prop in rules) {
|
|
232
530
|
const rule = rules[prop];
|
|
233
531
|
|
|
532
|
+
// ---------- $logic (or / and) ----------
|
|
234
533
|
if (prop === "$logic") {
|
|
235
|
-
// OR
|
|
534
|
+
// OR groups: at least one of the listed properties must satisfy the constraints
|
|
236
535
|
if (rule.or) {
|
|
237
536
|
for (const orRule of rule.or) {
|
|
238
537
|
let passed = false;
|
|
538
|
+
const failedProps = [];
|
|
239
539
|
|
|
240
540
|
for (const property of orRule.properties) {
|
|
241
541
|
const result = await this.check_validation(
|
|
242
|
-
{ prop: property, ...orRule },
|
|
542
|
+
{ prop: property, section, ...orRule },
|
|
243
543
|
target,
|
|
244
544
|
);
|
|
245
|
-
|
|
246
545
|
if (result.ok) {
|
|
247
546
|
passed = true;
|
|
248
547
|
break;
|
|
249
548
|
}
|
|
549
|
+
failedProps.push(property);
|
|
250
550
|
}
|
|
251
551
|
|
|
252
552
|
if (orRule.required && !passed) {
|
|
253
553
|
return {
|
|
254
554
|
ok: false,
|
|
255
|
-
message: `At least one of the following fields is required: ${orRule.properties.join(
|
|
256
|
-
|
|
257
|
-
)}`,
|
|
555
|
+
message: `At least one of the following fields is required in '${section}': ${orRule.properties.join(", ")}`,
|
|
556
|
+
path: `$logic.or`,
|
|
258
557
|
};
|
|
259
558
|
}
|
|
260
559
|
}
|
|
261
560
|
}
|
|
262
561
|
|
|
263
|
-
// AND
|
|
562
|
+
// AND groups: every listed property must satisfy the constraints
|
|
264
563
|
if (rule.and) {
|
|
265
564
|
for (const andRule of rule.and) {
|
|
266
565
|
for (const property of andRule.properties) {
|
|
267
566
|
const result = await this.check_validation(
|
|
268
|
-
{ prop: property, ...andRule },
|
|
567
|
+
{ prop: property, section, ...andRule },
|
|
269
568
|
target,
|
|
270
569
|
);
|
|
271
|
-
|
|
272
|
-
if (!result.ok) {
|
|
273
|
-
return result;
|
|
274
|
-
}
|
|
570
|
+
if (!result.ok) return result;
|
|
275
571
|
}
|
|
276
572
|
}
|
|
277
573
|
}
|
|
@@ -279,11 +575,12 @@ class Route_table extends Headers {
|
|
|
279
575
|
continue;
|
|
280
576
|
}
|
|
281
577
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
578
|
+
// ---------- normal field ----------
|
|
579
|
+
const result = await this.check_validation(
|
|
580
|
+
{ prop, section, ...rule },
|
|
581
|
+
target,
|
|
582
|
+
);
|
|
583
|
+
if (!result.ok) return result;
|
|
287
584
|
}
|
|
288
585
|
}
|
|
289
586
|
|
|
@@ -293,7 +590,7 @@ class Route_table extends Headers {
|
|
|
293
590
|
get_route_config = async (name) => {
|
|
294
591
|
let route = await this.get_route(name);
|
|
295
592
|
|
|
296
|
-
return route
|
|
593
|
+
return route?.config;
|
|
297
594
|
};
|
|
298
595
|
|
|
299
596
|
// =========================
|
|
@@ -309,20 +606,23 @@ class Route_table extends Headers {
|
|
|
309
606
|
let route = await this.get_route(name);
|
|
310
607
|
|
|
311
608
|
if (!route) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
data: { path: `/${name}`, name },
|
|
319
|
-
};
|
|
320
|
-
}
|
|
609
|
+
return {
|
|
610
|
+
ok: false,
|
|
611
|
+
status: 404,
|
|
612
|
+
message: "Route not found",
|
|
613
|
+
data: { path: `/${name}`, name },
|
|
614
|
+
};
|
|
321
615
|
}
|
|
322
616
|
|
|
323
|
-
|
|
617
|
+
const { handler, config, params = {}, route_pattern } = route;
|
|
618
|
+
|
|
619
|
+
payload.params = params;
|
|
620
|
+
payload.route_pattern = route_pattern;
|
|
621
|
+
|
|
622
|
+
if (payload.req) {
|
|
623
|
+
payload.req.params = params;
|
|
624
|
+
}
|
|
324
625
|
|
|
325
|
-
// 📜 schema
|
|
326
626
|
if (config.schema) {
|
|
327
627
|
let resp = await this.validate(config.schema, payload);
|
|
328
628
|
|
|
@@ -332,14 +632,13 @@ class Route_table extends Headers {
|
|
|
332
632
|
endpoint: name,
|
|
333
633
|
args: payload.body,
|
|
334
634
|
response: resp,
|
|
335
|
-
conversation: payload.headers
|
|
635
|
+
conversation: payload.headers?.meta?.conversation_id,
|
|
336
636
|
});
|
|
337
637
|
}
|
|
338
638
|
return resp;
|
|
339
639
|
}
|
|
340
640
|
}
|
|
341
641
|
|
|
342
|
-
// ⚙️ middleware
|
|
343
642
|
for (let middleware of config.middlewares || []) {
|
|
344
643
|
let result = await middleware(payload);
|
|
345
644
|
if (!result?.ok) {
|
|
@@ -348,22 +647,19 @@ class Route_table extends Headers {
|
|
|
348
647
|
endpoint: name,
|
|
349
648
|
args: payload.body,
|
|
350
649
|
response: result,
|
|
351
|
-
// conversation: payload.headers.meta.conversation_id,
|
|
352
650
|
});
|
|
353
651
|
}
|
|
354
652
|
return result;
|
|
355
653
|
}
|
|
356
654
|
}
|
|
357
655
|
|
|
358
|
-
// 🎯 handler
|
|
359
656
|
let response = await handler(payload);
|
|
360
|
-
if (response
|
|
657
|
+
if (response?.agent) {
|
|
361
658
|
await this.call_agency(
|
|
362
659
|
{
|
|
363
660
|
endpoint: name,
|
|
364
661
|
args: payload.body,
|
|
365
662
|
response,
|
|
366
|
-
// conversation: payload.headers.meta.conversation_id,
|
|
367
663
|
},
|
|
368
664
|
response.agent,
|
|
369
665
|
);
|
|
@@ -64,10 +64,14 @@ const respond = (result, res) => {
|
|
|
64
64
|
|
|
65
65
|
const response = {
|
|
66
66
|
ok: result?.ok ?? false,
|
|
67
|
-
message: result?.message ?? "",
|
|
68
|
-
data: result?.data,
|
|
69
67
|
};
|
|
70
68
|
|
|
69
|
+
if (result?.message) {
|
|
70
|
+
response.message = result.message;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
response.data = result?.data;
|
|
74
|
+
|
|
71
75
|
if (result?.pagination) {
|
|
72
76
|
response.pagination = result.pagination;
|
|
73
77
|
}
|
|
@@ -111,6 +115,10 @@ const version_middleware = async (req, res, routers) => {
|
|
|
111
115
|
const parsed_url = new URL(req.url, `http://${req.headers.host}`);
|
|
112
116
|
const pathname = parsed_url.pathname;
|
|
113
117
|
|
|
118
|
+
// ⬇️ query params captured for every method now
|
|
119
|
+
const query = Object.fromEntries(parsed_url.searchParams.entries());
|
|
120
|
+
req.query = query;
|
|
121
|
+
|
|
114
122
|
req.request_id = crypto.randomUUID();
|
|
115
123
|
routers.gp?.resource?.incoming_request(req);
|
|
116
124
|
res.gp = routers.gp;
|
|
@@ -119,7 +127,7 @@ const version_middleware = async (req, res, routers) => {
|
|
|
119
127
|
// STATIC ROUTE DETECTION
|
|
120
128
|
let version;
|
|
121
129
|
|
|
122
|
-
const versionMatch = pathname.match(/^\/api\/(
|
|
130
|
+
const versionMatch = pathname.match(/^\/api\/([^\/]+)(\/|$)/i);
|
|
123
131
|
if (req.method === "GET") {
|
|
124
132
|
if (routers.static_path && pathname.startsWith(routers.static_path)) {
|
|
125
133
|
res.gp = routers.gp;
|
|
@@ -159,9 +167,8 @@ const version_middleware = async (req, res, routers) => {
|
|
|
159
167
|
let body = {};
|
|
160
168
|
|
|
161
169
|
if (req.method === "GET") {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
body = Object.fromEntries(url.searchParams.entries());
|
|
170
|
+
// GET keeps existing behavior: query params double as body
|
|
171
|
+
body = query;
|
|
165
172
|
} else {
|
|
166
173
|
const contentType = req.headers["content-type"] || "";
|
|
167
174
|
|
|
@@ -181,12 +188,10 @@ const version_middleware = async (req, res, routers) => {
|
|
|
181
188
|
const router = routers;
|
|
182
189
|
|
|
183
190
|
// remove query string from route
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
let routePath = parsedUrl.pathname;
|
|
191
|
+
let routePath = pathname;
|
|
187
192
|
|
|
188
|
-
// Remove /api
|
|
189
|
-
routePath = routePath.replace(/^\/api\/
|
|
193
|
+
// Remove /api/<version>
|
|
194
|
+
routePath = routePath.replace(/^\/api\/[^\/]+(?=\/|$)/i, "");
|
|
190
195
|
|
|
191
196
|
// Ensure leading slash exists
|
|
192
197
|
if (!routePath.startsWith("/")) {
|
|
@@ -195,7 +200,7 @@ const version_middleware = async (req, res, routers) => {
|
|
|
195
200
|
|
|
196
201
|
req.url = routePath;
|
|
197
202
|
|
|
198
|
-
|
|
203
|
+
let name = routePath.replace(/^\//, "");
|
|
199
204
|
|
|
200
205
|
const isWebhook = req.headers["is-webhook"];
|
|
201
206
|
|
|
@@ -215,6 +220,7 @@ const version_middleware = async (req, res, routers) => {
|
|
|
215
220
|
{
|
|
216
221
|
req,
|
|
217
222
|
body,
|
|
223
|
+
query,
|
|
218
224
|
route: name,
|
|
219
225
|
db: db.db,
|
|
220
226
|
},
|
|
@@ -225,6 +231,8 @@ const version_middleware = async (req, res, routers) => {
|
|
|
225
231
|
|
|
226
232
|
if (callback?.body) body = callback.body;
|
|
227
233
|
|
|
234
|
+
if (callback?.query) query = callback.query; // only if you make `query` a `let` — see note below
|
|
235
|
+
|
|
228
236
|
if (callback?.headers) req.headers = callback.headers;
|
|
229
237
|
|
|
230
238
|
if (callback?.db) db.db = callback.db;
|
|
@@ -234,12 +242,15 @@ const version_middleware = async (req, res, routers) => {
|
|
|
234
242
|
error_stage = "Handler";
|
|
235
243
|
let payload = {
|
|
236
244
|
body,
|
|
245
|
+
query,
|
|
237
246
|
route: name,
|
|
238
247
|
headers: req.headers,
|
|
248
|
+
req,
|
|
239
249
|
db: db.db,
|
|
240
250
|
gp: router.gp,
|
|
241
251
|
services: router.get_service,
|
|
242
252
|
method: req.method,
|
|
253
|
+
params: {},
|
|
243
254
|
memory: router.gp.memory,
|
|
244
255
|
};
|
|
245
256
|
const result = await router.execute(name, payload);
|