godprotocol 2.3.46 → 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 -94
- 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,109 +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
|
+
};
|
|
136
241
|
}
|
|
137
242
|
}
|
|
138
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
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
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
|
+
}
|
|
168
384
|
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
}
|
|
171
409
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
+
}
|
|
179
421
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
+
}
|
|
430
|
+
|
|
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
|
-
let actual = Array.isArray(data_value) ? "array" : typeof data_value;
|
|
202
|
-
|
|
203
|
-
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])) {
|
|
204
467
|
return {
|
|
205
468
|
ok: false,
|
|
206
|
-
message: `Field '${
|
|
469
|
+
message: `Field '${fullPath}' failed '${op}' constraint (expected ${op} ${rest[op]})`,
|
|
470
|
+
path: fullPath,
|
|
207
471
|
};
|
|
208
472
|
}
|
|
209
473
|
}
|
|
210
474
|
}
|
|
211
475
|
|
|
212
|
-
//
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
ok
|
|
221
|
-
|
|
222
|
-
|
|
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
|
+
}
|
|
223
519
|
}
|
|
224
520
|
|
|
225
521
|
return { ok: true };
|
|
@@ -233,47 +529,45 @@ class Route_table extends Headers {
|
|
|
233
529
|
for (const prop in rules) {
|
|
234
530
|
const rule = rules[prop];
|
|
235
531
|
|
|
532
|
+
// ---------- $logic (or / and) ----------
|
|
236
533
|
if (prop === "$logic") {
|
|
237
|
-
// OR
|
|
534
|
+
// OR groups: at least one of the listed properties must satisfy the constraints
|
|
238
535
|
if (rule.or) {
|
|
239
536
|
for (const orRule of rule.or) {
|
|
240
537
|
let passed = false;
|
|
538
|
+
const failedProps = [];
|
|
241
539
|
|
|
242
540
|
for (const property of orRule.properties) {
|
|
243
541
|
const result = await this.check_validation(
|
|
244
|
-
{ prop: property, ...orRule },
|
|
542
|
+
{ prop: property, section, ...orRule },
|
|
245
543
|
target,
|
|
246
544
|
);
|
|
247
|
-
|
|
248
545
|
if (result.ok) {
|
|
249
546
|
passed = true;
|
|
250
547
|
break;
|
|
251
548
|
}
|
|
549
|
+
failedProps.push(property);
|
|
252
550
|
}
|
|
253
551
|
|
|
254
552
|
if (orRule.required && !passed) {
|
|
255
553
|
return {
|
|
256
554
|
ok: false,
|
|
257
|
-
message: `At least one of the following fields is required: ${orRule.properties.join(
|
|
258
|
-
|
|
259
|
-
)}`,
|
|
555
|
+
message: `At least one of the following fields is required in '${section}': ${orRule.properties.join(", ")}`,
|
|
556
|
+
path: `$logic.or`,
|
|
260
557
|
};
|
|
261
558
|
}
|
|
262
559
|
}
|
|
263
560
|
}
|
|
264
561
|
|
|
265
|
-
// AND
|
|
562
|
+
// AND groups: every listed property must satisfy the constraints
|
|
266
563
|
if (rule.and) {
|
|
267
564
|
for (const andRule of rule.and) {
|
|
268
565
|
for (const property of andRule.properties) {
|
|
269
566
|
const result = await this.check_validation(
|
|
270
|
-
{ prop: property, ...andRule },
|
|
567
|
+
{ prop: property, section, ...andRule },
|
|
271
568
|
target,
|
|
272
569
|
);
|
|
273
|
-
|
|
274
|
-
if (!result.ok) {
|
|
275
|
-
return result;
|
|
276
|
-
}
|
|
570
|
+
if (!result.ok) return result;
|
|
277
571
|
}
|
|
278
572
|
}
|
|
279
573
|
}
|
|
@@ -281,11 +575,12 @@ class Route_table extends Headers {
|
|
|
281
575
|
continue;
|
|
282
576
|
}
|
|
283
577
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
578
|
+
// ---------- normal field ----------
|
|
579
|
+
const result = await this.check_validation(
|
|
580
|
+
{ prop, section, ...rule },
|
|
581
|
+
target,
|
|
582
|
+
);
|
|
583
|
+
if (!result.ok) return result;
|
|
289
584
|
}
|
|
290
585
|
}
|
|
291
586
|
|
|
@@ -295,7 +590,7 @@ class Route_table extends Headers {
|
|
|
295
590
|
get_route_config = async (name) => {
|
|
296
591
|
let route = await this.get_route(name);
|
|
297
592
|
|
|
298
|
-
return route
|
|
593
|
+
return route?.config;
|
|
299
594
|
};
|
|
300
595
|
|
|
301
596
|
// =========================
|
|
@@ -311,20 +606,23 @@ class Route_table extends Headers {
|
|
|
311
606
|
let route = await this.get_route(name);
|
|
312
607
|
|
|
313
608
|
if (!route) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
data: { path: `/${name}`, name },
|
|
321
|
-
};
|
|
322
|
-
}
|
|
609
|
+
return {
|
|
610
|
+
ok: false,
|
|
611
|
+
status: 404,
|
|
612
|
+
message: "Route not found",
|
|
613
|
+
data: { path: `/${name}`, name },
|
|
614
|
+
};
|
|
323
615
|
}
|
|
324
616
|
|
|
325
|
-
|
|
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
|
+
}
|
|
326
625
|
|
|
327
|
-
// 📜 schema
|
|
328
626
|
if (config.schema) {
|
|
329
627
|
let resp = await this.validate(config.schema, payload);
|
|
330
628
|
|
|
@@ -334,14 +632,13 @@ class Route_table extends Headers {
|
|
|
334
632
|
endpoint: name,
|
|
335
633
|
args: payload.body,
|
|
336
634
|
response: resp,
|
|
337
|
-
conversation: payload.headers
|
|
635
|
+
conversation: payload.headers?.meta?.conversation_id,
|
|
338
636
|
});
|
|
339
637
|
}
|
|
340
638
|
return resp;
|
|
341
639
|
}
|
|
342
640
|
}
|
|
343
641
|
|
|
344
|
-
// ⚙️ middleware
|
|
345
642
|
for (let middleware of config.middlewares || []) {
|
|
346
643
|
let result = await middleware(payload);
|
|
347
644
|
if (!result?.ok) {
|
|
@@ -350,22 +647,19 @@ class Route_table extends Headers {
|
|
|
350
647
|
endpoint: name,
|
|
351
648
|
args: payload.body,
|
|
352
649
|
response: result,
|
|
353
|
-
// conversation: payload.headers.meta.conversation_id,
|
|
354
650
|
});
|
|
355
651
|
}
|
|
356
652
|
return result;
|
|
357
653
|
}
|
|
358
654
|
}
|
|
359
655
|
|
|
360
|
-
// 🎯 handler
|
|
361
656
|
let response = await handler(payload);
|
|
362
|
-
if (response
|
|
657
|
+
if (response?.agent) {
|
|
363
658
|
await this.call_agency(
|
|
364
659
|
{
|
|
365
660
|
endpoint: name,
|
|
366
661
|
args: payload.body,
|
|
367
662
|
response,
|
|
368
|
-
// conversation: payload.headers.meta.conversation_id,
|
|
369
663
|
},
|
|
370
664
|
response.agent,
|
|
371
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);
|