@maiyunnet/kebab 9.16.1 → 9.17.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/doc/kebab-rag.md +107500 -548
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/lib/core.d.ts +3 -0
- package/lib/core.js +3 -0
- package/lib/fs.js +4 -1
- package/lib/undici/response.d.ts +1 -1
- package/package.json +17 -16
- package/sys/ctr.d.ts +85 -0
- package/sys/ctr.js +117 -5
- package/sys/route.js +1 -1
- package/www/example/ctr/test.d.ts +2 -0
- package/www/example/ctr/test.js +92 -0
- package/www/example/data/locale/en.test.json +18 -1
- package/www/example/data/locale/ja.test.json +18 -1
- package/www/example/data/locale/sc.test.json +18 -1
- package/www/example/data/locale/tc.test.json +18 -1
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* --- 本文件用来定义每个目录实体地址的常量 ---
|
|
7
7
|
*/
|
|
8
8
|
/** --- 当前系统版本号 --- */
|
|
9
|
-
export const VER = '9.
|
|
9
|
+
export const VER = '9.17.0';
|
|
10
10
|
// --- 服务端用的路径 ---
|
|
11
11
|
const imu = decodeURIComponent(import.meta.url).replace('file://', '').replace(/^\/(\w:)/, '$1');
|
|
12
12
|
/** --- /xxx/xxx --- */
|
package/lib/core.d.ts
CHANGED
|
@@ -2,10 +2,13 @@ import * as http from 'http';
|
|
|
2
2
|
import * as http2 from 'http2';
|
|
3
3
|
import * as stream from 'stream';
|
|
4
4
|
import * as net from 'net';
|
|
5
|
+
import * as valibot from 'valibot';
|
|
5
6
|
import * as kebab from '#kebab/index.js';
|
|
6
7
|
import * as lNetResponse from '#kebab/lib/net/response.js';
|
|
7
8
|
import * as lUndiciResponse from '#kebab/lib/undici/response.js';
|
|
8
9
|
import * as sCtr from '#kebab/sys/ctr.js';
|
|
10
|
+
/** --- Valibot 数据校验工具,业务侧可通过 lCore.v 使用 --- */
|
|
11
|
+
export { valibot as v };
|
|
9
12
|
/** --- 全局参数 --- */
|
|
10
13
|
export declare const globalConfig: kebab.IConfig & {
|
|
11
14
|
'httpPort': number;
|
package/lib/core.js
CHANGED
|
@@ -11,6 +11,7 @@ import * as net from 'net';
|
|
|
11
11
|
import * as crypto from 'crypto';
|
|
12
12
|
import Ajv from 'ajv';
|
|
13
13
|
import addFormats from 'ajv-formats';
|
|
14
|
+
import * as valibot from 'valibot';
|
|
14
15
|
import * as kebab from '#kebab/index.js';
|
|
15
16
|
import * as lTime from '#kebab/lib/time.js';
|
|
16
17
|
import * as lFs from '#kebab/lib/fs.js';
|
|
@@ -20,6 +21,8 @@ import * as lCrypto from '#kebab/lib/crypto.js';
|
|
|
20
21
|
import * as lNetResponse from '#kebab/lib/net/response.js';
|
|
21
22
|
import * as lUndiciResponse from '#kebab/lib/undici/response.js';
|
|
22
23
|
import * as sCtr from '#kebab/sys/ctr.js';
|
|
24
|
+
/** --- Valibot 数据校验工具,业务侧可通过 lCore.v 使用 --- */
|
|
25
|
+
export { valibot as v };
|
|
23
26
|
/** --- 全局参数 --- */
|
|
24
27
|
export const globalConfig = {};
|
|
25
28
|
/** --- JSON Schema 校验器 --- */
|
package/lib/fs.js
CHANGED
|
@@ -440,10 +440,13 @@ export async function readToResponse(path, req, res, stat) {
|
|
|
440
440
|
}
|
|
441
441
|
// --- 这些文件可能需要缓存 ---
|
|
442
442
|
if (['htm', 'html', 'css', 'js', 'mjs', 'xml', 'jpg', 'jpeg', 'svg', 'gif', 'png', 'json'].includes(mimeData.extension)) {
|
|
443
|
+
/** --- 静态文件默认缓存秒数 --- */
|
|
444
|
+
const cacheTTL = 600;
|
|
443
445
|
const hash = `W/"${stat.size.toString(16)}-${stat.mtime.getTime().toString(16)}"`;
|
|
444
446
|
const lastModified = stat.mtime.toUTCString();
|
|
445
447
|
res.setHeader('etag', hash);
|
|
446
|
-
res.setHeader('
|
|
448
|
+
res.setHeader('expires', new Date(Date.now() + cacheTTL * 1_000).toUTCString());
|
|
449
|
+
res.setHeader('cache-control', 'public, max-age=' + cacheTTL.toString());
|
|
447
450
|
// --- 判断返回 304 吗 ---
|
|
448
451
|
const noneMatch = req.headers['if-none-match'];
|
|
449
452
|
const modifiedSince = req.headers['if-modified-since'];
|
package/lib/undici/response.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export declare class Response {
|
|
|
31
31
|
/**
|
|
32
32
|
* --- 获取响应读取流对象 ---
|
|
33
33
|
*/
|
|
34
|
-
getStream():
|
|
34
|
+
getStream(): (import("undici/types/readable").default & undici.Dispatcher.BodyMixin) | zlib.Gunzip | zlib.Inflate | zlib.BrotliDecompress | null;
|
|
35
35
|
/**
|
|
36
36
|
* --- 获取原生响应读取流对象 ---
|
|
37
37
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maiyunnet/kebab",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.17.0",
|
|
4
4
|
"description": "Simple, easy-to-use, and fully-featured Node.js framework that is ready-to-use out of the box.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
"#kebab/*": "./*"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@aws-sdk/client-s3": "^3.
|
|
26
|
-
"@aws-sdk/lib-storage": "^3.
|
|
25
|
+
"@aws-sdk/client-s3": "^3.1120.0",
|
|
26
|
+
"@aws-sdk/lib-storage": "^3.1120.0",
|
|
27
27
|
"@litert/http-client": "^1.1.2",
|
|
28
|
-
"@litert/mime": "^0.
|
|
28
|
+
"@litert/mime": "^2.0.0",
|
|
29
29
|
"@litert/redis": "^3.2.1",
|
|
30
30
|
"@litert/websocket": "^0.2.8",
|
|
31
31
|
"@radix-ui/react-checkbox": "^1.3.11",
|
|
@@ -39,30 +39,31 @@
|
|
|
39
39
|
"class-variance-authority": "^0.7.1",
|
|
40
40
|
"clsx": "^2.1.1",
|
|
41
41
|
"ejs": "^6.0.1",
|
|
42
|
-
"esbuild": "^0.28.
|
|
42
|
+
"esbuild": "^0.28.2",
|
|
43
43
|
"jszip": "^3.10.1",
|
|
44
|
-
"mysql2": "^3.
|
|
44
|
+
"mysql2": "^3.24.2",
|
|
45
45
|
"node-cron": "^4.6.0",
|
|
46
|
-
"openai": "^7.
|
|
47
|
-
"pg": "^8.
|
|
46
|
+
"openai": "^7.8.0",
|
|
47
|
+
"pg": "^8.23.0",
|
|
48
48
|
"react": "^19.2.8",
|
|
49
49
|
"react-dom": "^19.2.8",
|
|
50
|
-
"react-router-dom": "^7.18.
|
|
50
|
+
"react-router-dom": "^7.18.2",
|
|
51
51
|
"ssh2": "^1.17.0",
|
|
52
52
|
"svg-captcha": "^1.4.0",
|
|
53
53
|
"tailwind-merge": "^3.6.0",
|
|
54
|
-
"tencentcloud-sdk-nodejs": "^4.1.
|
|
55
|
-
"undici": "^8.
|
|
54
|
+
"tencentcloud-sdk-nodejs": "^4.1.303",
|
|
55
|
+
"undici": "^8.10.0",
|
|
56
|
+
"valibot": "^1.4.2"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@litert/eslint-plugin-rules": "^0.3.1",
|
|
59
60
|
"@types/ejs": "^3.1.5",
|
|
60
|
-
"@types/node": "^26.
|
|
61
|
-
"@types/pg": "^8.
|
|
62
|
-
"@types/react": "^19.2.
|
|
63
|
-
"@types/react-dom": "^19.2.
|
|
61
|
+
"@types/node": "^26.4.0",
|
|
62
|
+
"@types/pg": "^8.23.1",
|
|
63
|
+
"@types/react": "^19.2.18",
|
|
64
|
+
"@types/react-dom": "^19.2.5",
|
|
64
65
|
"typedoc": "^0.28.20",
|
|
65
|
-
"typedoc-plugin-markdown": "^4.
|
|
66
|
+
"typedoc-plugin-markdown": "^4.13.0",
|
|
66
67
|
"typescript": "^6.0.3"
|
|
67
68
|
}
|
|
68
69
|
}
|
package/sys/ctr.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project: Kebab, User: JianSuoQiYue
|
|
3
|
+
* Date: 2020-3-14 17:24:38
|
|
4
|
+
* Last: 2020-3-30 15:31:40, 2022-07-22 16:59:00, 2022-09-12 23:51:56, 2022-09-23 15:53:58, 2022-12-29 01:18:08, 2023-2-28 20:07:57, 2023-12-27 18:39:35, 2024-3-1 19:38:53, 2024-4-9 16:03:58, 2025-2-12 18:55:44, 2025-6-12 16:56:08, 2026-8-22
|
|
5
|
+
*/
|
|
1
6
|
import * as http from 'http';
|
|
2
7
|
import * as http2 from 'http2';
|
|
8
|
+
import type * as v from 'valibot';
|
|
3
9
|
import * as kebab from '#kebab/index.js';
|
|
4
10
|
import * as lSession from '#kebab/lib/session.js';
|
|
5
11
|
import * as lDb from '#kebab/lib/db.js';
|
|
@@ -9,6 +15,49 @@ import * as lWs from '#kebab/lib/ws.js';
|
|
|
9
15
|
* --- 清除已经加载的 data 与语言包文件缓存 ---
|
|
10
16
|
*/
|
|
11
17
|
export declare function clearLocaleData(): void;
|
|
18
|
+
/** --- Valibot 校验失败时可直接返回给客户端的内容或生成函数 --- */
|
|
19
|
+
export type TValibotResponse<TIssue extends v.BaseIssue<unknown>> = kebab.Json[] | ((issues: [TIssue, ...TIssue[]]) => kebab.Json[]);
|
|
20
|
+
/** --- 在 issue 路径完整后,根据 Kebab 生成的语言键翻译消息 --- */
|
|
21
|
+
export type TValibotTranslate = (key: string, issue: v.BaseIssue<unknown>) => string;
|
|
22
|
+
/** --- Valibot 同步校验选项 --- */
|
|
23
|
+
export interface IValibotOptions<TIssue extends v.BaseIssue<unknown>> {
|
|
24
|
+
/** --- 传给 Valibot 的校验配置 --- */
|
|
25
|
+
'config'?: v.Config<TIssue>;
|
|
26
|
+
/** --- 校验失败时返回给客户端的内容,默认使用首个 issue 的消息 --- */
|
|
27
|
+
'response'?: TValibotResponse<TIssue>;
|
|
28
|
+
/** --- 使用完整 issue 路径翻译消息 --- */
|
|
29
|
+
'translate'?: TValibotTranslate;
|
|
30
|
+
}
|
|
31
|
+
/** --- XSRF 校验失败的问题描述 --- */
|
|
32
|
+
export interface IValibotXsrfIssue extends v.BaseIssue<unknown> {
|
|
33
|
+
readonly kind: 'validation';
|
|
34
|
+
readonly type: 'xsrf';
|
|
35
|
+
}
|
|
36
|
+
/** --- 带 XSRF 检测的 Valibot 同步校验选项 --- */
|
|
37
|
+
export interface IValibotXOptions<TIssue extends v.BaseIssue<unknown>> {
|
|
38
|
+
/** --- 传给 Valibot 的校验配置 --- */
|
|
39
|
+
'config'?: v.Config<TIssue>;
|
|
40
|
+
/** --- 校验失败时返回给客户端的内容,回调同时可能收到 XSRF issue --- */
|
|
41
|
+
'response'?: TValibotResponse<TIssue | IValibotXsrfIssue>;
|
|
42
|
+
/** --- 使用完整 issue 路径翻译消息,包括 XSRF issue --- */
|
|
43
|
+
'translate'?: TValibotTranslate;
|
|
44
|
+
/** --- 是否忽略 XSRF 检测 --- */
|
|
45
|
+
'ignoreXsrf'?: boolean;
|
|
46
|
+
}
|
|
47
|
+
/** --- Valibot 校验结果:仅成功分支提供 schema 推导后的 output --- */
|
|
48
|
+
export type TValibotResult<TSchema extends v.GenericSchema, TIssue extends v.BaseIssue<unknown> = v.InferIssue<TSchema>> = {
|
|
49
|
+
readonly typed: true;
|
|
50
|
+
readonly success: true;
|
|
51
|
+
readonly output: v.InferOutput<TSchema>;
|
|
52
|
+
readonly issues: undefined;
|
|
53
|
+
readonly response: undefined;
|
|
54
|
+
} | {
|
|
55
|
+
readonly typed: boolean;
|
|
56
|
+
readonly success: false;
|
|
57
|
+
readonly output: undefined;
|
|
58
|
+
readonly issues: [TIssue, ...TIssue[]];
|
|
59
|
+
readonly response: kebab.Json[];
|
|
60
|
+
};
|
|
12
61
|
export declare class Ctr {
|
|
13
62
|
/** --- 路由参数序列数组 --- */
|
|
14
63
|
protected _param: string[];
|
|
@@ -200,6 +249,42 @@ export declare class Ctr {
|
|
|
200
249
|
* @param msgSuffix 可选的消息后缀
|
|
201
250
|
*/
|
|
202
251
|
private _setCheckError;
|
|
252
|
+
/**
|
|
253
|
+
* --- 生成 Valibot 校验失败结果 ---
|
|
254
|
+
* @param issues Valibot 问题列表
|
|
255
|
+
* @param response 自定义客户端返回值或生成函数
|
|
256
|
+
* @returns 校验失败结果
|
|
257
|
+
*/
|
|
258
|
+
private _getValibotFailure;
|
|
259
|
+
/**
|
|
260
|
+
* --- 根据完整 issue 路径生成 Kebab 语言包键 ---
|
|
261
|
+
* @param issue Valibot 校验问题
|
|
262
|
+
* @returns 语言包键
|
|
263
|
+
*/
|
|
264
|
+
private _getValibotLocaleKey;
|
|
265
|
+
/**
|
|
266
|
+
* --- 在 Valibot 完成父级路径组装后翻译问题消息 ---
|
|
267
|
+
* @param issues Valibot 问题列表
|
|
268
|
+
* @param translate 消息翻译函数
|
|
269
|
+
* @returns 翻译后的问题列表
|
|
270
|
+
*/
|
|
271
|
+
private _translateValibotIssues;
|
|
272
|
+
/**
|
|
273
|
+
* --- 使用 Valibot schema 校验并解析输入,成功后 output 会自动推导类型 ---
|
|
274
|
+
* @param schema Valibot 同步 schema
|
|
275
|
+
* @param input 待校验的输入
|
|
276
|
+
* @param options Valibot 配置和自定义客户端返回值
|
|
277
|
+
* @returns 可判别的校验结果
|
|
278
|
+
*/
|
|
279
|
+
protected _valibot<const TSchema extends v.GenericSchema>(schema: TSchema, input: unknown, options?: IValibotOptions<v.InferIssue<TSchema>>): TValibotResult<TSchema>;
|
|
280
|
+
/**
|
|
281
|
+
* --- 使用 Valibot schema 校验并解析输入,同时检测 XSRF ---
|
|
282
|
+
* @param schema Valibot 同步 schema
|
|
283
|
+
* @param input 待校验的输入
|
|
284
|
+
* @param options Valibot 配置、自定义客户端返回值和 XSRF 选项
|
|
285
|
+
* @returns 可判别的校验结果
|
|
286
|
+
*/
|
|
287
|
+
protected _valibotx<const TSchema extends v.GenericSchema>(schema: TSchema, input: unknown, options?: IValibotXOptions<v.InferIssue<TSchema>>): TValibotResult<TSchema, v.InferIssue<TSchema> | IValibotXsrfIssue>;
|
|
203
288
|
/**
|
|
204
289
|
* --- 检测提交的数据类型 ---
|
|
205
290
|
* @param input 要校验的输入项
|
package/sys/ctr.js
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Project: Kebab, User: JianSuoQiYue
|
|
3
|
-
* Date: 2020-3-14 17:24:38
|
|
4
|
-
* Last: 2020-3-30 15:31:40, 2022-07-22 16:59:00, 2022-09-12 23:51:56, 2022-09-23 15:53:58, 2022-12-29 01:18:08, 2023-2-28 20:07:57, 2023-12-27 18:39:35, 2024-3-1 19:38:53, 2024-4-9 16:03:58, 2025-2-12 18:55:44, 2025-6-12 16:56:08, 2026-8-22
|
|
5
|
-
*/
|
|
6
1
|
import ejs from 'ejs';
|
|
7
2
|
import * as lCore from '#kebab/lib/core.js';
|
|
8
3
|
import * as lFs from '#kebab/lib/fs.js';
|
|
@@ -416,6 +411,123 @@ export class Ctr {
|
|
|
416
411
|
rtn[2] = lastVal[2];
|
|
417
412
|
}
|
|
418
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* --- 生成 Valibot 校验失败结果 ---
|
|
416
|
+
* @param issues Valibot 问题列表
|
|
417
|
+
* @param response 自定义客户端返回值或生成函数
|
|
418
|
+
* @returns 校验失败结果
|
|
419
|
+
*/
|
|
420
|
+
_getValibotFailure(issues, response, translate) {
|
|
421
|
+
/** --- 路径已完整且消息已翻译的问题列表 --- */
|
|
422
|
+
const translatedIssues = this._translateValibotIssues(issues, translate);
|
|
423
|
+
const clientResponse = typeof response === 'function' ? response(translatedIssues) : response;
|
|
424
|
+
return {
|
|
425
|
+
'typed': false,
|
|
426
|
+
'success': false,
|
|
427
|
+
'output': undefined,
|
|
428
|
+
'issues': translatedIssues,
|
|
429
|
+
'response': clientResponse ?? [0, translatedIssues[0].message],
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* --- 根据完整 issue 路径生成 Kebab 语言包键 ---
|
|
434
|
+
* @param issue Valibot 校验问题
|
|
435
|
+
* @returns 语言包键
|
|
436
|
+
*/
|
|
437
|
+
_getValibotLocaleKey(issue) {
|
|
438
|
+
if ((issue.type === 'strict_object') && (issue.expected === 'never')) {
|
|
439
|
+
return 'validation.strict_object';
|
|
440
|
+
}
|
|
441
|
+
const path = lCore.v.getDotPath(issue);
|
|
442
|
+
return path ? `validation.${path}.${issue.type}` : `validation.${issue.type}`;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* --- 在 Valibot 完成父级路径组装后翻译问题消息 ---
|
|
446
|
+
* @param issues Valibot 问题列表
|
|
447
|
+
* @param translate 消息翻译函数
|
|
448
|
+
* @returns 翻译后的问题列表
|
|
449
|
+
*/
|
|
450
|
+
_translateValibotIssues(issues, translate) {
|
|
451
|
+
if (!translate) {
|
|
452
|
+
return issues;
|
|
453
|
+
}
|
|
454
|
+
const translateIssue = (issue) => {
|
|
455
|
+
const key = this._getValibotLocaleKey(issue);
|
|
456
|
+
const message = translate(key, issue);
|
|
457
|
+
return {
|
|
458
|
+
...issue,
|
|
459
|
+
/** --- 语言包缺失时保留键名,并回退到 Valibot 原始英文原因 --- */
|
|
460
|
+
'message': message === `[LocaleError]${key}` ? `[${key}] ${issue.message}` : message,
|
|
461
|
+
};
|
|
462
|
+
};
|
|
463
|
+
return [translateIssue(issues[0]), ...issues.slice(1).map(translateIssue)];
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* --- 使用 Valibot schema 校验并解析输入,成功后 output 会自动推导类型 ---
|
|
467
|
+
* @param schema Valibot 同步 schema
|
|
468
|
+
* @param input 待校验的输入
|
|
469
|
+
* @param options Valibot 配置和自定义客户端返回值
|
|
470
|
+
* @returns 可判别的校验结果
|
|
471
|
+
*/
|
|
472
|
+
_valibot(schema, input, options) {
|
|
473
|
+
const result = lCore.v.safeParse(schema, input, options?.config);
|
|
474
|
+
if (!result.success) {
|
|
475
|
+
return this._getValibotFailure(result.issues, options?.response, options?.translate);
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
...result,
|
|
479
|
+
'response': undefined,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* --- 使用 Valibot schema 校验并解析输入,同时检测 XSRF ---
|
|
484
|
+
* @param schema Valibot 同步 schema
|
|
485
|
+
* @param input 待校验的输入
|
|
486
|
+
* @param options Valibot 配置、自定义客户端返回值和 XSRF 选项
|
|
487
|
+
* @returns 可判别的校验结果
|
|
488
|
+
*/
|
|
489
|
+
_valibotx(schema, input, options) {
|
|
490
|
+
/** --- 用户提交的 XSRF token --- */
|
|
491
|
+
let submittedXsrf;
|
|
492
|
+
/** --- 移除 XSRF 传输字段后交给业务 schema 的输入 --- */
|
|
493
|
+
let schemaInput = input;
|
|
494
|
+
if ((typeof input === 'object') && (input !== null) && !Array.isArray(input)) {
|
|
495
|
+
const inputRecord = input;
|
|
496
|
+
submittedXsrf = inputRecord['_xsrf'];
|
|
497
|
+
schemaInput = { ...inputRecord };
|
|
498
|
+
delete schemaInput['_xsrf'];
|
|
499
|
+
}
|
|
500
|
+
if (!options?.ignoreXsrf) {
|
|
501
|
+
/** --- Cookie 中应匹配的 XSRF token --- */
|
|
502
|
+
const expectedXsrf = this._cookie['XSRF-TOKEN'];
|
|
503
|
+
if (!expectedXsrf || (submittedXsrf !== expectedXsrf)) {
|
|
504
|
+
const issue = {
|
|
505
|
+
'kind': 'validation',
|
|
506
|
+
'type': 'xsrf',
|
|
507
|
+
'input': undefined,
|
|
508
|
+
'expected': 'valid XSRF token',
|
|
509
|
+
'received': submittedXsrf === undefined ? 'undefined' : typeof submittedXsrf,
|
|
510
|
+
'message': 'Bad request, no permission.',
|
|
511
|
+
'path': [{
|
|
512
|
+
'type': 'unknown',
|
|
513
|
+
'origin': 'value',
|
|
514
|
+
'input': undefined,
|
|
515
|
+
'key': '_xsrf',
|
|
516
|
+
'value': undefined,
|
|
517
|
+
}],
|
|
518
|
+
};
|
|
519
|
+
return this._getValibotFailure([issue], options?.response, options?.translate);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const result = lCore.v.safeParse(schema, schemaInput, options?.config);
|
|
523
|
+
if (!result.success) {
|
|
524
|
+
return this._getValibotFailure(result.issues, options?.response, options?.translate);
|
|
525
|
+
}
|
|
526
|
+
return {
|
|
527
|
+
...result,
|
|
528
|
+
'response': undefined,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
419
531
|
/**
|
|
420
532
|
* --- 检测提交的数据类型 ---
|
|
421
533
|
* @param input 要校验的输入项
|
package/sys/route.js
CHANGED
|
@@ -175,7 +175,7 @@ export async function run(data) {
|
|
|
175
175
|
path = '@';
|
|
176
176
|
}
|
|
177
177
|
/** --- 不做语言跳转、不进入动态处理流程的路径 --- */
|
|
178
|
-
const stcPaths = /^(stc\/.*|favicon.\w+?\??.*|apple[\w-]+?\.png\??.*|[\w-]+?\.txt
|
|
178
|
+
const stcPaths = /^(stc\/.*|favicon.\w+?\??.*|apple[\w-]+?\.png\??.*|[\w-]+?\.(?:txt|html|xml)\??.*)/;
|
|
179
179
|
// --- 检测是否托管语言页面 ---
|
|
180
180
|
const param = [];
|
|
181
181
|
if (config.lang && data.res && !stcPaths.test(path)) {
|
|
@@ -18,6 +18,8 @@ export default class extends sCtr.Ctr {
|
|
|
18
18
|
ctrCheckinput1(): Promise<kebab.Json[]>;
|
|
19
19
|
ctrCheckinputSchema(): string;
|
|
20
20
|
ctrCheckinputSchema1(): Promise<kebab.Json[]>;
|
|
21
|
+
ctrValibot(): Promise<kebab.Json[] | string>;
|
|
22
|
+
ctrValibot1(): Promise<kebab.Json[]>;
|
|
21
23
|
ctrLocale(): Promise<kebab.Json[] | string>;
|
|
22
24
|
ctrCachettl(): string;
|
|
23
25
|
ctrHttpcode(): string;
|
package/www/example/ctr/test.js
CHANGED
|
@@ -28,6 +28,15 @@ import * as sCtr from '#kebab/sys/ctr.js';
|
|
|
28
28
|
// --- mod ---
|
|
29
29
|
import mTest from '../mod/test.js';
|
|
30
30
|
import mTestData from '../mod/testdata.js';
|
|
31
|
+
/** --- Kebab 核心库提供的 Valibot 对象 --- */
|
|
32
|
+
const v = lCore.v;
|
|
33
|
+
/** --- Valibot 示例支持的语言 --- */
|
|
34
|
+
const valibotLocaleSchema = v.picklist(['en', 'sc', 'tc', 'ja']);
|
|
35
|
+
/** --- 模块级复用的 Valibot 输入规则,不绑定任何请求或语言 --- */
|
|
36
|
+
const valibotInputSchema = v.strictObject({
|
|
37
|
+
'title': v.pipe(v.string(), v.nonEmpty()),
|
|
38
|
+
'count': v.pipe(v.string(), v.toNumber(), v.integer()),
|
|
39
|
+
});
|
|
31
40
|
export default class extends sCtr.Ctr {
|
|
32
41
|
_internalUrl = '';
|
|
33
42
|
onLoad() {
|
|
@@ -114,6 +123,7 @@ export default class extends sCtr.Ctr {
|
|
|
114
123
|
'<br><br><b>Ctr:</b>',
|
|
115
124
|
`<br><br><a href="${this._config.const.urlBase}test/ctr-xsrf">View "test/ctr-xsrf"</a>`,
|
|
116
125
|
`<br><a href="${this._config.const.urlBase}test/ctr-checkinput">View "test/ctr-checkinput"</a> <a href="${this._config.const.urlBase}test/ctr-checkinput-schema">schema</a>`,
|
|
126
|
+
`<br><a href="${this._config.const.urlBase}test/ctr-valibot">View "test/ctr-valibot"</a>`,
|
|
117
127
|
`<br><a href="${this._config.const.urlBase}test/ctr-locale">View "test/ctr-locale"</a>`,
|
|
118
128
|
`<br><a href="${this._config.const.urlBase}test/ctr-cachettl">View "test/ctr-cachettl"</a>`,
|
|
119
129
|
`<br><a href="${this._config.const.urlBase}test/ctr-httpcode">View "test/ctr-httpcode"</a>`,
|
|
@@ -546,6 +556,88 @@ function post(p) {
|
|
|
546
556
|
}
|
|
547
557
|
return [1, { 'post': this._post }];
|
|
548
558
|
}
|
|
559
|
+
async ctrValibot() {
|
|
560
|
+
const locale = this._valibot(valibotLocaleSchema, this._get['lang'] ?? 'en', {
|
|
561
|
+
'response': [0, 'Wrong language.'],
|
|
562
|
+
});
|
|
563
|
+
if (!locale.success) {
|
|
564
|
+
return locale.response;
|
|
565
|
+
}
|
|
566
|
+
if (!await this._loadLocale(locale.output, 'test')) {
|
|
567
|
+
return [0, 'Could not load locale.'];
|
|
568
|
+
}
|
|
569
|
+
this._enabledXsrf();
|
|
570
|
+
const echo = [
|
|
571
|
+
'<b>Test _valibotx with inferred output and request locale</b><br><br>',
|
|
572
|
+
`<a href="${this._config.const.urlBase}test/ctr-valibot?lang=en">English</a> | ` +
|
|
573
|
+
`<a href="${this._config.const.urlBase}test/ctr-valibot?lang=sc">简体中文</a> | ` +
|
|
574
|
+
`<a href="${this._config.const.urlBase}test/ctr-valibot?lang=tc">繁體中文</a> | ` +
|
|
575
|
+
`<a href="${this._config.const.urlBase}test/ctr-valibot?lang=ja">日本語</a><br><br>`,
|
|
576
|
+
`<b>Current locale:</b> ${locale.output}<br><br>`,
|
|
577
|
+
`<pre>const v = lCore.v;
|
|
578
|
+
const valibotInputSchema = v.strictObject({
|
|
579
|
+
'title': v.pipe(v.string(), v.nonEmpty()),
|
|
580
|
+
'count': v.pipe(v.string(), v.toNumber(), v.integer()),
|
|
581
|
+
});</pre>`,
|
|
582
|
+
];
|
|
583
|
+
const posts = [
|
|
584
|
+
{ 'title': 'Kebab', 'count': '2' },
|
|
585
|
+
{ 'title': '', 'count': '2' },
|
|
586
|
+
{ 'title': 'Kebab', 'count': 'abc' },
|
|
587
|
+
{ 'title': 'Kebab', 'count': '1.5' },
|
|
588
|
+
{ 'title': 'Kebab', 'count': '2', 'extra': true },
|
|
589
|
+
];
|
|
590
|
+
for (const item of posts) {
|
|
591
|
+
const str = lText.stringifyJson(item).replace(/"/g, '"');
|
|
592
|
+
echo.push(`<input type="button" value="Post '${str}'" onclick="post('${str}')"><br>`);
|
|
593
|
+
}
|
|
594
|
+
echo.push(`<script>
|
|
595
|
+
function post(p) {
|
|
596
|
+
const data = JSON.parse(p);
|
|
597
|
+
data._xsrf = '${this._xsrf}';
|
|
598
|
+
document.getElementById('result').innerText = 'Waiting...';
|
|
599
|
+
fetch('${this._config.const.urlBase}test/ctr-valibot1?lang=${locale.output}', {
|
|
600
|
+
method: 'POST',
|
|
601
|
+
headers: {
|
|
602
|
+
'content-type': 'application/json'
|
|
603
|
+
},
|
|
604
|
+
body: JSON.stringify(data)
|
|
605
|
+
}).then(function(r) {
|
|
606
|
+
return r.text();
|
|
607
|
+
}).then(function(t) {
|
|
608
|
+
document.getElementById('result').innerText = t;
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
</script>
|
|
612
|
+
<br>Result:<pre id="result">Nothing.</pre>`);
|
|
613
|
+
return echo.join('') + this._getEnd();
|
|
614
|
+
}
|
|
615
|
+
async ctrValibot1() {
|
|
616
|
+
if (!await this._handleFormData()) {
|
|
617
|
+
return [0];
|
|
618
|
+
}
|
|
619
|
+
const locale = this._valibot(valibotLocaleSchema, this._get['lang'] ?? 'en', {
|
|
620
|
+
'response': [0, 'Wrong language.'],
|
|
621
|
+
});
|
|
622
|
+
if (!locale.success) {
|
|
623
|
+
return locale.response;
|
|
624
|
+
}
|
|
625
|
+
if (!await this._loadLocale(locale.output, 'test')) {
|
|
626
|
+
return [0, 'Could not load locale.'];
|
|
627
|
+
}
|
|
628
|
+
const parsed = this._valibotx(valibotInputSchema, this._post, {
|
|
629
|
+
'translate': key => this._l(key),
|
|
630
|
+
'response': issues => [0, issues[0].message, {
|
|
631
|
+
'issues': v.flatten(issues),
|
|
632
|
+
}],
|
|
633
|
+
});
|
|
634
|
+
if (!parsed.success) {
|
|
635
|
+
return parsed.response;
|
|
636
|
+
}
|
|
637
|
+
return [1, {
|
|
638
|
+
'post': parsed.output,
|
|
639
|
+
}];
|
|
640
|
+
}
|
|
549
641
|
async ctrLocale() {
|
|
550
642
|
const rtn = [];
|
|
551
643
|
if (!this._checkInput(this._get, {
|
|
@@ -4,5 +4,22 @@
|
|
|
4
4
|
"test": "The ? test ?.",
|
|
5
5
|
"test2": {
|
|
6
6
|
"test3": "test4"
|
|
7
|
+
},
|
|
8
|
+
"validation": {
|
|
9
|
+
"strict_object": "The submitted data must be an object and contain only supported fields.",
|
|
10
|
+
"_xsrf": {
|
|
11
|
+
"xsrf": "Bad request, no permission."
|
|
12
|
+
},
|
|
13
|
+
"title": {
|
|
14
|
+
"strict_object": "The title is required.",
|
|
15
|
+
"string": "The title must be a string.",
|
|
16
|
+
"non_empty": "Please enter a title."
|
|
17
|
+
},
|
|
18
|
+
"count": {
|
|
19
|
+
"strict_object": "The count is required.",
|
|
20
|
+
"string": "The count must be a string.",
|
|
21
|
+
"to_number": "The count must be a number.",
|
|
22
|
+
"integer": "The count must be an integer."
|
|
23
|
+
}
|
|
7
24
|
}
|
|
8
|
-
}
|
|
25
|
+
}
|
|
@@ -4,5 +4,22 @@
|
|
|
4
4
|
"test": "これ ? 試験 ?。",
|
|
5
5
|
"test2": {
|
|
6
6
|
"test3": "試験4"
|
|
7
|
+
},
|
|
8
|
+
"validation": {
|
|
9
|
+
"strict_object": "送信内容の形式が正しくないか、未対応のフィールドが含まれています。",
|
|
10
|
+
"_xsrf": {
|
|
11
|
+
"xsrf": "無効なリクエスト、または権限がありません。"
|
|
12
|
+
},
|
|
13
|
+
"title": {
|
|
14
|
+
"strict_object": "タイトルがありません。",
|
|
15
|
+
"string": "タイトルは文字列で指定してください。",
|
|
16
|
+
"non_empty": "タイトルを入力してください。"
|
|
17
|
+
},
|
|
18
|
+
"count": {
|
|
19
|
+
"strict_object": "数量がありません。",
|
|
20
|
+
"string": "数量は文字列で指定してください。",
|
|
21
|
+
"to_number": "数量は数値で指定してください。",
|
|
22
|
+
"integer": "数量は整数で指定してください。"
|
|
23
|
+
}
|
|
7
24
|
}
|
|
8
|
-
}
|
|
25
|
+
}
|
|
@@ -4,5 +4,22 @@
|
|
|
4
4
|
"test": "这个 ? 测试 ?。",
|
|
5
5
|
"test2": {
|
|
6
6
|
"test3": "测试4"
|
|
7
|
+
},
|
|
8
|
+
"validation": {
|
|
9
|
+
"strict_object": "提交内容格式不正确或包含不支持的字段。",
|
|
10
|
+
"_xsrf": {
|
|
11
|
+
"xsrf": "请求无效或无权限。"
|
|
12
|
+
},
|
|
13
|
+
"title": {
|
|
14
|
+
"strict_object": "缺少标题。",
|
|
15
|
+
"string": "标题必须是字符串。",
|
|
16
|
+
"non_empty": "请输入标题。"
|
|
17
|
+
},
|
|
18
|
+
"count": {
|
|
19
|
+
"strict_object": "缺少数量。",
|
|
20
|
+
"string": "数量必须是字符串。",
|
|
21
|
+
"to_number": "数量必须是数字。",
|
|
22
|
+
"integer": "数量必须是整数。"
|
|
23
|
+
}
|
|
7
24
|
}
|
|
8
|
-
}
|
|
25
|
+
}
|
|
@@ -4,5 +4,22 @@
|
|
|
4
4
|
"test": "這個 ? 測試 ?.",
|
|
5
5
|
"test2": {
|
|
6
6
|
"test3": "測試4"
|
|
7
|
+
},
|
|
8
|
+
"validation": {
|
|
9
|
+
"strict_object": "提交內容格式不正確或包含不支援的欄位。",
|
|
10
|
+
"_xsrf": {
|
|
11
|
+
"xsrf": "請求無效或沒有權限。"
|
|
12
|
+
},
|
|
13
|
+
"title": {
|
|
14
|
+
"strict_object": "缺少標題。",
|
|
15
|
+
"string": "標題必須是字串。",
|
|
16
|
+
"non_empty": "請輸入標題。"
|
|
17
|
+
},
|
|
18
|
+
"count": {
|
|
19
|
+
"strict_object": "缺少數量。",
|
|
20
|
+
"string": "數量必須是字串。",
|
|
21
|
+
"to_number": "數量必須是數字。",
|
|
22
|
+
"integer": "數量必須是整數。"
|
|
23
|
+
}
|
|
7
24
|
}
|
|
8
|
-
}
|
|
25
|
+
}
|