schema-dsl 1.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/.eslintignore +10 -0
- package/.eslintrc.json +27 -0
- package/.github/CODE_OF_CONDUCT.md +45 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +57 -0
- package/.github/ISSUE_TEMPLATE/config.yml +11 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +45 -0
- package/.github/ISSUE_TEMPLATE/question.md +31 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +70 -0
- package/.github/SECURITY.md +184 -0
- package/.github/workflows/ci.yml +33 -0
- package/CHANGELOG.md +633 -0
- package/CONTRIBUTING.md +368 -0
- package/LICENSE +21 -0
- package/README.md +1184 -0
- package/STATUS.md +101 -0
- package/docs/FEATURE-INDEX.md +519 -0
- package/docs/INDEX.md +253 -0
- package/docs/api-reference.md +1096 -0
- package/docs/best-practices.md +672 -0
- package/docs/cache-manager.md +336 -0
- package/docs/design-philosophy.md +601 -0
- package/docs/dsl-syntax.md +653 -0
- package/docs/dynamic-locale.md +552 -0
- package/docs/error-handling.md +703 -0
- package/docs/export-guide.md +462 -0
- package/docs/export-limitations.md +551 -0
- package/docs/faq.md +577 -0
- package/docs/frontend-i18n-guide.md +290 -0
- package/docs/i18n-user-guide.md +476 -0
- package/docs/label-vs-description.md +262 -0
- package/docs/markdown-exporter.md +397 -0
- package/docs/mongodb-exporter.md +295 -0
- package/docs/multi-type-support.md +319 -0
- package/docs/mysql-exporter.md +273 -0
- package/docs/plugin-system.md +542 -0
- package/docs/postgresql-exporter.md +304 -0
- package/docs/quick-start.md +761 -0
- package/docs/schema-helper.md +340 -0
- package/docs/schema-utils-chaining.md +143 -0
- package/docs/schema-utils.md +490 -0
- package/docs/string-extensions.md +480 -0
- package/docs/troubleshooting.md +471 -0
- package/docs/type-converter.md +319 -0
- package/docs/type-reference.md +219 -0
- package/docs/validate-async.md +480 -0
- package/docs/validate.md +486 -0
- package/docs/validation-guide.md +484 -0
- package/examples/array-dsl-example.js +227 -0
- package/examples/custom-extension.js +85 -0
- package/examples/dsl-match-example.js +74 -0
- package/examples/dsl-style.js +118 -0
- package/examples/dynamic-locale-configuration.js +348 -0
- package/examples/dynamic-locale-example.js +287 -0
- package/examples/export-demo.js +130 -0
- package/examples/express-integration.js +376 -0
- package/examples/i18n-full-demo.js +310 -0
- package/examples/i18n-memory-safety.examples.js +268 -0
- package/examples/markdown-export.js +71 -0
- package/examples/middleware-usage.js +93 -0
- package/examples/new-features-comparison.js +315 -0
- package/examples/password-reset/README.md +153 -0
- package/examples/password-reset/schema.js +26 -0
- package/examples/password-reset/test.js +101 -0
- package/examples/plugin-system.examples.js +205 -0
- package/examples/schema-utils-chaining.examples.js +250 -0
- package/examples/simple-example.js +122 -0
- package/examples/string-extensions.js +297 -0
- package/examples/user-registration/README.md +156 -0
- package/examples/user-registration/routes.js +92 -0
- package/examples/user-registration/schema.js +150 -0
- package/examples/user-registration/server.js +74 -0
- package/index.d.ts +1999 -0
- package/index.js +282 -0
- package/index.mjs +30 -0
- package/lib/adapters/DslAdapter.js +699 -0
- package/lib/adapters/index.js +20 -0
- package/lib/config/constants.js +286 -0
- package/lib/config/patterns/creditCard.js +9 -0
- package/lib/config/patterns/idCard.js +9 -0
- package/lib/config/patterns/index.js +8 -0
- package/lib/config/patterns/licensePlate.js +4 -0
- package/lib/config/patterns/passport.js +4 -0
- package/lib/config/patterns/phone.js +9 -0
- package/lib/config/patterns/postalCode.js +5 -0
- package/lib/core/CacheManager.js +376 -0
- package/lib/core/DslBuilder.js +740 -0
- package/lib/core/ErrorCodes.js +233 -0
- package/lib/core/ErrorFormatter.js +342 -0
- package/lib/core/JSONSchemaCore.js +347 -0
- package/lib/core/Locale.js +119 -0
- package/lib/core/MessageTemplate.js +89 -0
- package/lib/core/PluginManager.js +448 -0
- package/lib/core/StringExtensions.js +209 -0
- package/lib/core/Validator.js +376 -0
- package/lib/errors/ValidationError.js +191 -0
- package/lib/exporters/MarkdownExporter.js +420 -0
- package/lib/exporters/MongoDBExporter.js +162 -0
- package/lib/exporters/MySQLExporter.js +212 -0
- package/lib/exporters/PostgreSQLExporter.js +289 -0
- package/lib/exporters/index.js +24 -0
- package/lib/locales/en-US.js +65 -0
- package/lib/locales/es-ES.js +66 -0
- package/lib/locales/fr-FR.js +66 -0
- package/lib/locales/index.js +8 -0
- package/lib/locales/ja-JP.js +66 -0
- package/lib/locales/zh-CN.js +93 -0
- package/lib/utils/LRUCache.js +174 -0
- package/lib/utils/SchemaHelper.js +240 -0
- package/lib/utils/SchemaUtils.js +445 -0
- package/lib/utils/TypeConverter.js +245 -0
- package/lib/utils/index.js +13 -0
- package/lib/validators/CustomKeywords.js +203 -0
- package/lib/validators/index.js +11 -0
- package/package.json +70 -0
- package/plugins/custom-format.js +101 -0
- package/plugins/custom-validator.js +200 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express 集成示例 - 异步验证与 Schema 链式调用
|
|
3
|
+
*
|
|
4
|
+
* 展示如何在 Express 中使用:
|
|
5
|
+
* 1. validateAsync 异步验证
|
|
6
|
+
* 2. ValidationError 错误处理
|
|
7
|
+
* 3. SchemaUtils 链式调用
|
|
8
|
+
* 4. 完整 CRUD 场景
|
|
9
|
+
*
|
|
10
|
+
* @version 2.1.0
|
|
11
|
+
* @date 2025-12-29
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const express = require('express');
|
|
15
|
+
const { dsl, validateAsync, ValidationError, SchemaUtils } = require('../index');
|
|
16
|
+
|
|
17
|
+
const app = express();
|
|
18
|
+
app.use(express.json());
|
|
19
|
+
|
|
20
|
+
// ===== 模拟数据库 =====
|
|
21
|
+
const db = {
|
|
22
|
+
users: [],
|
|
23
|
+
nextId: 1
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// ===== 定义完整 User Schema =====
|
|
27
|
+
const fullUserSchema = dsl({
|
|
28
|
+
id: 'objectId!',
|
|
29
|
+
name: 'string:1-50!',
|
|
30
|
+
email: 'email!',
|
|
31
|
+
password: 'string:8-32!',
|
|
32
|
+
age: 'integer:18-120',
|
|
33
|
+
role: 'admin|user|guest',
|
|
34
|
+
createdAt: 'date',
|
|
35
|
+
updatedAt: 'date'
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// ===== 派生各种 Schema =====
|
|
39
|
+
|
|
40
|
+
// POST /users - 创建用户 Schema(排除系统字段)
|
|
41
|
+
const createUserSchema = SchemaUtils.omit(fullUserSchema, ['id', 'createdAt', 'updatedAt']);
|
|
42
|
+
|
|
43
|
+
// GET /users/:id - 公开用户 Schema(移除敏感字段)
|
|
44
|
+
const publicUserSchema = SchemaUtils.omit(fullUserSchema, ['password']);
|
|
45
|
+
|
|
46
|
+
// PATCH /users/:id - 更新用户 Schema(部分验证)
|
|
47
|
+
const updateUserSchema = SchemaUtils
|
|
48
|
+
.pick(fullUserSchema, ['name', 'age'])
|
|
49
|
+
.partial();
|
|
50
|
+
|
|
51
|
+
// PUT /users/:id - 替换用户 Schema(排除系统字段)
|
|
52
|
+
const replaceUserSchema = SchemaUtils.omit(fullUserSchema, ['id', 'createdAt', 'updatedAt']);
|
|
53
|
+
|
|
54
|
+
// ===== 路由实现 =====
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* POST /users - 创建用户
|
|
58
|
+
*
|
|
59
|
+
* 使用 createUserSchema:
|
|
60
|
+
* - 排除系统字段(id, createdAt, updatedAt)
|
|
61
|
+
*/
|
|
62
|
+
app.post('/users', async (req, res, next) => {
|
|
63
|
+
try {
|
|
64
|
+
console.log('\n[POST /users] 创建用户');
|
|
65
|
+
console.log('请求体:', req.body);
|
|
66
|
+
|
|
67
|
+
// 使用 validateAsync 验证
|
|
68
|
+
const data = await validateAsync(createUserSchema, req.body);
|
|
69
|
+
|
|
70
|
+
console.log('验证通过,数据:', data);
|
|
71
|
+
|
|
72
|
+
// 保存到数据库
|
|
73
|
+
const user = {
|
|
74
|
+
id: String(db.nextId++),
|
|
75
|
+
...data,
|
|
76
|
+
createdAt: new Date().toISOString(),
|
|
77
|
+
updatedAt: new Date().toISOString()
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
db.users.push(user);
|
|
81
|
+
|
|
82
|
+
// 返回公开信息
|
|
83
|
+
const { validate } = require('../index');
|
|
84
|
+
const result = validate(publicUserSchema, user);
|
|
85
|
+
|
|
86
|
+
console.log('返回数据:', result.data);
|
|
87
|
+
|
|
88
|
+
res.status(201).json({
|
|
89
|
+
success: true,
|
|
90
|
+
user: result.data
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
} catch (error) {
|
|
94
|
+
next(error);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* GET /users - 获取所有用户
|
|
100
|
+
*
|
|
101
|
+
* 使用 publicUserSchema 自动移除敏感字段
|
|
102
|
+
*/
|
|
103
|
+
app.get('/users', (req, res) => {
|
|
104
|
+
console.log('\n[GET /users] 获取所有用户');
|
|
105
|
+
|
|
106
|
+
const { validate } = require('../index');
|
|
107
|
+
|
|
108
|
+
// 对每个用户应用 publicUserSchema
|
|
109
|
+
const publicUsers = db.users.map(user => {
|
|
110
|
+
const result = validate(publicUserSchema, user);
|
|
111
|
+
return result.data;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
console.log(`返回 ${publicUsers.length} 个用户`);
|
|
115
|
+
|
|
116
|
+
res.json({
|
|
117
|
+
success: true,
|
|
118
|
+
count: publicUsers.length,
|
|
119
|
+
users: publicUsers
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* GET /users/:id - 获取单个用户
|
|
125
|
+
*
|
|
126
|
+
* 使用 publicUserSchema 移除敏感字段
|
|
127
|
+
*/
|
|
128
|
+
app.get('/users/:id', (req, res) => {
|
|
129
|
+
console.log(`\n[GET /users/${req.params.id}] 获取用户`);
|
|
130
|
+
|
|
131
|
+
const user = db.users.find(u => u.id === req.params.id);
|
|
132
|
+
|
|
133
|
+
if (!user) {
|
|
134
|
+
console.log('用户不存在');
|
|
135
|
+
return res.status(404).json({
|
|
136
|
+
success: false,
|
|
137
|
+
error: '用户不存在'
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 使用 clean 模式自动移除敏感字段
|
|
142
|
+
const { validate } = require('../index');
|
|
143
|
+
const result = validate(publicUserSchema, user);
|
|
144
|
+
|
|
145
|
+
console.log('返回数据:', result.data);
|
|
146
|
+
|
|
147
|
+
res.json({
|
|
148
|
+
success: true,
|
|
149
|
+
user: result.data
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* PATCH /users/:id - 部分更新用户
|
|
155
|
+
*
|
|
156
|
+
* 使用 updateUserSchema:
|
|
157
|
+
* - 只验证 name 和 age
|
|
158
|
+
* - 部分验证(可选)
|
|
159
|
+
* - 宽松模式(允许额外字段)
|
|
160
|
+
*/
|
|
161
|
+
app.patch('/users/:id', async (req, res, next) => {
|
|
162
|
+
try {
|
|
163
|
+
console.log(`\n[PATCH /users/${req.params.id}] 部分更新用户`);
|
|
164
|
+
console.log('请求体:', req.body);
|
|
165
|
+
|
|
166
|
+
const user = db.users.find(u => u.id === req.params.id);
|
|
167
|
+
|
|
168
|
+
if (!user) {
|
|
169
|
+
console.log('用户不存在');
|
|
170
|
+
return res.status(404).json({
|
|
171
|
+
success: false,
|
|
172
|
+
error: '用户不存在'
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 验证部分数据
|
|
177
|
+
const data = await validateAsync(updateUserSchema, req.body);
|
|
178
|
+
|
|
179
|
+
console.log('验证通过,更新字段:', data);
|
|
180
|
+
|
|
181
|
+
// 更新用户
|
|
182
|
+
Object.assign(user, data, {
|
|
183
|
+
updatedAt: new Date().toISOString()
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// 返回公开信息
|
|
187
|
+
const { validate } = require('../index');
|
|
188
|
+
const result = validate(publicUserSchema, user);
|
|
189
|
+
|
|
190
|
+
console.log('返回数据:', result.data);
|
|
191
|
+
|
|
192
|
+
res.json({
|
|
193
|
+
success: true,
|
|
194
|
+
user: result.data
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
} catch (error) {
|
|
198
|
+
next(error);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* PUT /users/:id - 替换用户
|
|
204
|
+
*
|
|
205
|
+
* 使用 replaceUserSchema:
|
|
206
|
+
* - 排除系统字段
|
|
207
|
+
* - 严格模式(必填字段必须全部提供)
|
|
208
|
+
*/
|
|
209
|
+
app.put('/users/:id', async (req, res, next) => {
|
|
210
|
+
try {
|
|
211
|
+
console.log(`\n[PUT /users/${req.params.id}] 替换用户`);
|
|
212
|
+
console.log('请求体:', req.body);
|
|
213
|
+
|
|
214
|
+
const userIndex = db.users.findIndex(u => u.id === req.params.id);
|
|
215
|
+
|
|
216
|
+
if (userIndex === -1) {
|
|
217
|
+
console.log('用户不存在');
|
|
218
|
+
return res.status(404).json({
|
|
219
|
+
success: false,
|
|
220
|
+
error: '用户不存在'
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 验证完整数据
|
|
225
|
+
const data = await validateAsync(replaceUserSchema, req.body);
|
|
226
|
+
|
|
227
|
+
console.log('验证通过,替换用户:', data);
|
|
228
|
+
|
|
229
|
+
// 替换用户(保留 id 和 createdAt)
|
|
230
|
+
const oldUser = db.users[userIndex];
|
|
231
|
+
db.users[userIndex] = {
|
|
232
|
+
id: oldUser.id,
|
|
233
|
+
...data,
|
|
234
|
+
createdAt: oldUser.createdAt,
|
|
235
|
+
updatedAt: new Date().toISOString()
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// 返回公开信息
|
|
239
|
+
const { validate } = require('../index');
|
|
240
|
+
const result = validate(publicUserSchema, db.users[userIndex]);
|
|
241
|
+
|
|
242
|
+
console.log('返回数据:', result.data);
|
|
243
|
+
|
|
244
|
+
res.json({
|
|
245
|
+
success: true,
|
|
246
|
+
user: result.data
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
} catch (error) {
|
|
250
|
+
next(error);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* DELETE /users/:id - 删除用户
|
|
256
|
+
*/
|
|
257
|
+
app.delete('/users/:id', (req, res) => {
|
|
258
|
+
console.log(`\n[DELETE /users/${req.params.id}] 删除用户`);
|
|
259
|
+
|
|
260
|
+
const userIndex = db.users.findIndex(u => u.id === req.params.id);
|
|
261
|
+
|
|
262
|
+
if (userIndex === -1) {
|
|
263
|
+
console.log('用户不存在');
|
|
264
|
+
return res.status(404).json({
|
|
265
|
+
success: false,
|
|
266
|
+
error: '用户不存在'
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
db.users.splice(userIndex, 1);
|
|
271
|
+
|
|
272
|
+
console.log('删除成功');
|
|
273
|
+
|
|
274
|
+
res.json({
|
|
275
|
+
success: true,
|
|
276
|
+
message: '用户已删除'
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// ===== 全局错误处理中间件 =====
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* ValidationError 错误处理
|
|
284
|
+
*
|
|
285
|
+
* 自动捕获 validateAsync 抛出的 ValidationError
|
|
286
|
+
* 返回友好的错误信息
|
|
287
|
+
*/
|
|
288
|
+
app.use((error, req, res, next) => {
|
|
289
|
+
if (error instanceof ValidationError) {
|
|
290
|
+
console.log('\n[错误] ValidationError 被捕获');
|
|
291
|
+
console.log('错误数量:', error.getErrorCount());
|
|
292
|
+
console.log('字段错误:', error.getFieldErrors());
|
|
293
|
+
|
|
294
|
+
return res.status(error.statusCode).json(error.toJSON());
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// 其他错误
|
|
298
|
+
console.error('\n[错误] 服务器错误:', error);
|
|
299
|
+
res.status(500).json({
|
|
300
|
+
error: 'Internal Server Error',
|
|
301
|
+
message: error.message
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// ===== 启动服务器 =====
|
|
306
|
+
|
|
307
|
+
const PORT = 3000;
|
|
308
|
+
|
|
309
|
+
app.listen(PORT, () => {
|
|
310
|
+
console.log(`\n========================================`);
|
|
311
|
+
console.log(` Express 集成示例服务器已启动`);
|
|
312
|
+
console.log(` 监听端口: ${PORT}`);
|
|
313
|
+
console.log(`========================================\n`);
|
|
314
|
+
console.log(`可用的 API 端点:`);
|
|
315
|
+
console.log(` POST http://localhost:${PORT}/users - 创建用户`);
|
|
316
|
+
console.log(` GET http://localhost:${PORT}/users - 获取所有用户`);
|
|
317
|
+
console.log(` GET http://localhost:${PORT}/users/:id - 获取单个用户`);
|
|
318
|
+
console.log(` PATCH http://localhost:${PORT}/users/:id - 部分更新用户`);
|
|
319
|
+
console.log(` PUT http://localhost:${PORT}/users/:id - 替换用户`);
|
|
320
|
+
console.log(` DELETE http://localhost:${PORT}/users/:id - 删除用户`);
|
|
321
|
+
console.log(`\n========================================\n`);
|
|
322
|
+
|
|
323
|
+
// 打印测试命令
|
|
324
|
+
printTestCommands();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// ===== 测试命令 =====
|
|
328
|
+
|
|
329
|
+
function printTestCommands() {
|
|
330
|
+
console.log(`测试命令(使用 curl):\n`);
|
|
331
|
+
|
|
332
|
+
console.log(`# 1. 创建用户(成功)`);
|
|
333
|
+
console.log(`curl -X POST http://localhost:${PORT}/users \\`);
|
|
334
|
+
console.log(` -H "Content-Type: application/json" \\`);
|
|
335
|
+
console.log(` -d '{"name":"John Doe","email":"john@example.com","password":"password123","age":30,"role":"user"}'\n`);
|
|
336
|
+
|
|
337
|
+
console.log(`# 2. 创建用户(失败 - 缺少必填字段)`);
|
|
338
|
+
console.log(`curl -X POST http://localhost:${PORT}/users \\`);
|
|
339
|
+
console.log(` -H "Content-Type: application/json" \\`);
|
|
340
|
+
console.log(` -d '{"name":"Jane"}'\n`);
|
|
341
|
+
|
|
342
|
+
console.log(`# 3. 创建用户(失败 - 额外字段被拒绝)`);
|
|
343
|
+
console.log(`curl -X POST http://localhost:${PORT}/users \\`);
|
|
344
|
+
console.log(` -H "Content-Type: application/json" \\`);
|
|
345
|
+
console.log(` -d '{"name":"Bob","email":"bob@example.com","password":"password123","extraField":"not allowed"}'\n`);
|
|
346
|
+
|
|
347
|
+
console.log(`# 4. 获取所有用户`);
|
|
348
|
+
console.log(`curl http://localhost:${PORT}/users\n`);
|
|
349
|
+
|
|
350
|
+
console.log(`# 5. 获取单个用户(替换 ID)`);
|
|
351
|
+
console.log(`curl http://localhost:${PORT}/users/1\n`);
|
|
352
|
+
|
|
353
|
+
console.log(`# 6. 部分更新用户(成功 - 只更新 name)`);
|
|
354
|
+
console.log(`curl -X PATCH http://localhost:${PORT}/users/1 \\`);
|
|
355
|
+
console.log(` -H "Content-Type: application/json" \\`);
|
|
356
|
+
console.log(` -d '{"name":"John Updated"}'\n`);
|
|
357
|
+
|
|
358
|
+
console.log(`# 7. 部分更新用户(成功 - 允许额外字段)`);
|
|
359
|
+
console.log(`curl -X PATCH http://localhost:${PORT}/users/1 \\`);
|
|
360
|
+
console.log(` -H "Content-Type: application/json" \\`);
|
|
361
|
+
console.log(` -d '{"age":31,"extraField":"allowed"}'\n`);
|
|
362
|
+
|
|
363
|
+
console.log(`# 8. 替换用户(成功 - 必须提供所有必填字段)`);
|
|
364
|
+
console.log(`curl -X PUT http://localhost:${PORT}/users/1 \\`);
|
|
365
|
+
console.log(` -H "Content-Type: application/json" \\`);
|
|
366
|
+
console.log(` -d '{"name":"John Replaced","email":"john.new@example.com","password":"newpassword123","age":32}'\n`);
|
|
367
|
+
|
|
368
|
+
console.log(`# 9. 删除用户`);
|
|
369
|
+
console.log(`curl -X DELETE http://localhost:${PORT}/users/1\n`);
|
|
370
|
+
|
|
371
|
+
console.log(`========================================\n`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// 导出 app 用于测试
|
|
375
|
+
module.exports = app;
|
|
376
|
+
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 完整的多语言配置示例
|
|
3
|
+
*
|
|
4
|
+
* 演示如何使用 dsl.config() 配置用户自定义语言包
|
|
5
|
+
* 以及如何在 Express 应用中实现动态语言切换
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const express = require('express');
|
|
9
|
+
const { dsl, validate, Locale } = require('../index');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
console.log('========== 多语言配置完整示例 ==========\n');
|
|
13
|
+
|
|
14
|
+
// ========================================
|
|
15
|
+
// 步骤 1:应用启动时配置
|
|
16
|
+
// ========================================
|
|
17
|
+
console.log('【步骤 1】配置用户语言包和缓存\n');
|
|
18
|
+
|
|
19
|
+
dsl.config({
|
|
20
|
+
// 用户语言包配置
|
|
21
|
+
i18n: {
|
|
22
|
+
// 方式 A:从目录加载(推荐用于大型项目)
|
|
23
|
+
// localesPath: path.join(__dirname, 'i18n/labels'),
|
|
24
|
+
|
|
25
|
+
// 方式 B:直接传入对象(推荐用于小型项目)
|
|
26
|
+
locales: {
|
|
27
|
+
'zh-CN': {
|
|
28
|
+
// 字段标签
|
|
29
|
+
'username': '用户名',
|
|
30
|
+
'email': '邮箱地址',
|
|
31
|
+
'password': '密码',
|
|
32
|
+
'age': '年龄',
|
|
33
|
+
'phone': '手机号',
|
|
34
|
+
|
|
35
|
+
// 嵌套字段
|
|
36
|
+
'address.city': '城市',
|
|
37
|
+
'address.street': '街道',
|
|
38
|
+
|
|
39
|
+
// 自定义错误消息
|
|
40
|
+
'custom.invalidEmail': '邮箱格式不正确,请重新输入',
|
|
41
|
+
'custom.emailTaken': '该邮箱已被注册',
|
|
42
|
+
'custom.usernameTaken': '该用户名已被使用',
|
|
43
|
+
'custom.passwordWeak': '密码强度不够,请包含大小写字母和数字'
|
|
44
|
+
},
|
|
45
|
+
'en-US': {
|
|
46
|
+
// Field labels
|
|
47
|
+
'username': 'Username',
|
|
48
|
+
'email': 'Email Address',
|
|
49
|
+
'password': 'Password',
|
|
50
|
+
'age': 'Age',
|
|
51
|
+
'phone': 'Phone Number',
|
|
52
|
+
|
|
53
|
+
// Nested fields
|
|
54
|
+
'address.city': 'City',
|
|
55
|
+
'address.street': 'Street',
|
|
56
|
+
|
|
57
|
+
// Custom error messages
|
|
58
|
+
'custom.invalidEmail': 'Invalid email format, please try again',
|
|
59
|
+
'custom.emailTaken': 'This email is already registered',
|
|
60
|
+
'custom.usernameTaken': 'This username is already taken',
|
|
61
|
+
'custom.passwordWeak': 'Password is too weak, please include uppercase, lowercase and numbers'
|
|
62
|
+
},
|
|
63
|
+
'ja-JP': {
|
|
64
|
+
// フィールドラベル
|
|
65
|
+
'username': 'ユーザー名',
|
|
66
|
+
'email': 'メールアドレス',
|
|
67
|
+
'password': 'パスワード',
|
|
68
|
+
'age': '年齢',
|
|
69
|
+
'phone': '電話番号',
|
|
70
|
+
|
|
71
|
+
// ネストされたフィールド
|
|
72
|
+
'address.city': '都市',
|
|
73
|
+
'address.street': '通り',
|
|
74
|
+
|
|
75
|
+
// カスタムエラーメッセージ
|
|
76
|
+
'custom.invalidEmail': 'メール形式が正しくありません',
|
|
77
|
+
'custom.emailTaken': 'このメールは既に登録されています',
|
|
78
|
+
'custom.usernameTaken': 'このユーザー名は既に使用されています',
|
|
79
|
+
'custom.passwordWeak': 'パスワードが弱すぎます'
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// 缓存配置(可选,大型项目推荐)
|
|
85
|
+
cache: {
|
|
86
|
+
maxSize: 10000, // 大型项目:1万个 Schema
|
|
87
|
+
ttl: 7200000 // 2 小时
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
console.log('✅ 配置完成\n');
|
|
92
|
+
|
|
93
|
+
// ========================================
|
|
94
|
+
// 步骤 2:定义 Schema(使用 key 引用语言包)
|
|
95
|
+
// ========================================
|
|
96
|
+
console.log('【步骤 2】定义 Schema\n');
|
|
97
|
+
|
|
98
|
+
const userSchema = dsl({
|
|
99
|
+
username: 'string:3-32!'.label('username'),
|
|
100
|
+
email: 'email!'.label('email').messages({
|
|
101
|
+
'format': 'custom.invalidEmail'
|
|
102
|
+
}),
|
|
103
|
+
password: 'string:8-32!'.label('password'),
|
|
104
|
+
age: 'number:18-120'.label('age'),
|
|
105
|
+
phone: 'string'.label('phone'),
|
|
106
|
+
address: dsl({
|
|
107
|
+
city: 'string!'.label('address.city'),
|
|
108
|
+
street: 'string!'.label('address.street')
|
|
109
|
+
})
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
console.log('✅ Schema 定义完成\n');
|
|
113
|
+
|
|
114
|
+
// ========================================
|
|
115
|
+
// 步骤 3:测试不同语言的验证
|
|
116
|
+
// ========================================
|
|
117
|
+
console.log('【步骤 3】测试多语言验证\n');
|
|
118
|
+
|
|
119
|
+
const testData = {
|
|
120
|
+
username: 'ab',
|
|
121
|
+
email: 'invalid-email',
|
|
122
|
+
password: '123',
|
|
123
|
+
age: 15,
|
|
124
|
+
address: {}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// 中文
|
|
128
|
+
console.log('--- 中文验证 ---');
|
|
129
|
+
let result = validate(userSchema, testData, { locale: 'zh-CN' });
|
|
130
|
+
console.log('有效:', result.valid);
|
|
131
|
+
console.log('错误:');
|
|
132
|
+
result.errors.forEach(err => {
|
|
133
|
+
console.log(` - ${err.path}: ${err.message}`);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
console.log('\n--- 英文验证 ---');
|
|
137
|
+
result = validate(userSchema, testData, { locale: 'en-US' });
|
|
138
|
+
console.log('有效:', result.valid);
|
|
139
|
+
console.log('错误:');
|
|
140
|
+
result.errors.forEach(err => {
|
|
141
|
+
console.log(` - ${err.path}: ${err.message}`);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
console.log('\n--- 日文验证 ---');
|
|
145
|
+
result = validate(userSchema, testData, { locale: 'ja-JP' });
|
|
146
|
+
console.log('有效:', result.valid);
|
|
147
|
+
console.log('错误:');
|
|
148
|
+
result.errors.forEach(err => {
|
|
149
|
+
console.log(` - ${err.path}: ${err.message}`);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
console.log('\n========================================');
|
|
153
|
+
console.log('【步骤 4】Express 集成示例');
|
|
154
|
+
console.log('========================================\n');
|
|
155
|
+
|
|
156
|
+
// ========================================
|
|
157
|
+
// Express 应用示例
|
|
158
|
+
// ========================================
|
|
159
|
+
|
|
160
|
+
const app = express();
|
|
161
|
+
app.use(express.json());
|
|
162
|
+
|
|
163
|
+
// 中间件:提取语言参数
|
|
164
|
+
app.use((req, res, next) => {
|
|
165
|
+
// 优先级:URL 参数 > Accept-Language 头 > 默认
|
|
166
|
+
req.locale = req.query.lang ||
|
|
167
|
+
req.headers['accept-language'] ||
|
|
168
|
+
'zh-CN';
|
|
169
|
+
|
|
170
|
+
// 只取主语言代码
|
|
171
|
+
if (req.locale.includes(',')) {
|
|
172
|
+
req.locale = req.locale.split(',')[0].trim();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
next();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// API 路由:用户注册
|
|
179
|
+
app.post('/api/register', (req, res) => {
|
|
180
|
+
console.log(`\n[POST /api/register] 语言: ${req.locale}`);
|
|
181
|
+
|
|
182
|
+
// 使用全局 validate,传递 locale 参数
|
|
183
|
+
const result = validate(userSchema, req.body, {
|
|
184
|
+
locale: req.locale
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (!result.valid) {
|
|
188
|
+
return res.status(400).json({
|
|
189
|
+
success: false,
|
|
190
|
+
errors: result.errors
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 注册逻辑...
|
|
195
|
+
res.json({
|
|
196
|
+
success: true,
|
|
197
|
+
message: '注册成功'
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// 健康检查
|
|
202
|
+
app.get('/health', (req, res) => {
|
|
203
|
+
res.json({ status: 'ok' });
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// 启动服务器
|
|
207
|
+
const PORT = 3000;
|
|
208
|
+
const server = app.listen(PORT, () => {
|
|
209
|
+
console.log(`✅ Express 服务器启动成功`);
|
|
210
|
+
console.log(` 地址: http://localhost:${PORT}`);
|
|
211
|
+
console.log(`\n测试 API:`);
|
|
212
|
+
console.log(` curl -X POST http://localhost:${PORT}/api/register \\`);
|
|
213
|
+
console.log(` -H "Content-Type: application/json" \\`);
|
|
214
|
+
console.log(` -H "Accept-Language: zh-CN" \\`);
|
|
215
|
+
console.log(` -d '{"username":"ab","email":"bad"}'`);
|
|
216
|
+
console.log(`\n按 Ctrl+C 停止服务器\n`);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// 优雅关闭
|
|
220
|
+
process.on('SIGINT', () => {
|
|
221
|
+
console.log('\n\n正在关闭服务器...');
|
|
222
|
+
server.close(() => {
|
|
223
|
+
console.log('服务器已关闭');
|
|
224
|
+
process.exit(0);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// ========================================
|
|
229
|
+
// 前端集成示例(React)
|
|
230
|
+
// ========================================
|
|
231
|
+
|
|
232
|
+
console.log('========================================');
|
|
233
|
+
console.log('【步骤 5】前端集成示例(React)');
|
|
234
|
+
console.log('========================================\n');
|
|
235
|
+
|
|
236
|
+
const frontendExample = `
|
|
237
|
+
// src/api/validation.js
|
|
238
|
+
export async function validateUser(formData, locale = 'zh-CN') {
|
|
239
|
+
const response = await fetch('http://localhost:3000/api/register', {
|
|
240
|
+
method: 'POST',
|
|
241
|
+
headers: {
|
|
242
|
+
'Content-Type': 'application/json',
|
|
243
|
+
'Accept-Language': locale // ← 传递语言
|
|
244
|
+
},
|
|
245
|
+
body: JSON.stringify(formData)
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
return response.json();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/components/RegisterForm.jsx
|
|
252
|
+
import { useState } from 'react';
|
|
253
|
+
import { validateUser } from '../api/validation';
|
|
254
|
+
|
|
255
|
+
function RegisterForm() {
|
|
256
|
+
const [locale, setLocale] = useState('zh-CN');
|
|
257
|
+
const [errors, setErrors] = useState([]);
|
|
258
|
+
|
|
259
|
+
const handleSubmit = async (e) => {
|
|
260
|
+
e.preventDefault();
|
|
261
|
+
|
|
262
|
+
const formData = {
|
|
263
|
+
username: e.target.username.value,
|
|
264
|
+
email: e.target.email.value,
|
|
265
|
+
password: e.target.password.value,
|
|
266
|
+
age: Number(e.target.age.value)
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const result = await validateUser(formData, locale);
|
|
270
|
+
|
|
271
|
+
if (!result.success) {
|
|
272
|
+
setErrors(result.errors);
|
|
273
|
+
} else {
|
|
274
|
+
alert('注册成功!');
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<div>
|
|
280
|
+
{/* 语言切换 */}
|
|
281
|
+
<select value={locale} onChange={(e) => setLocale(e.target.value)}>
|
|
282
|
+
<option value="zh-CN">中文</option>
|
|
283
|
+
<option value="en-US">English</option>
|
|
284
|
+
<option value="ja-JP">日本語</option>
|
|
285
|
+
</select>
|
|
286
|
+
|
|
287
|
+
<form onSubmit={handleSubmit}>
|
|
288
|
+
<input name="username" placeholder="用户名" />
|
|
289
|
+
<input name="email" type="email" placeholder="邮箱" />
|
|
290
|
+
<input name="password" type="password" placeholder="密码" />
|
|
291
|
+
<input name="age" type="number" placeholder="年龄" />
|
|
292
|
+
<button type="submit">注册</button>
|
|
293
|
+
</form>
|
|
294
|
+
|
|
295
|
+
{/* 错误显示 */}
|
|
296
|
+
{errors.map(err => (
|
|
297
|
+
<div key={err.path} style={{ color: 'red' }}>
|
|
298
|
+
{err.message} {/* 已经是对应语言 */}
|
|
299
|
+
</div>
|
|
300
|
+
))}
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
`;
|
|
305
|
+
|
|
306
|
+
console.log('React 示例代码:');
|
|
307
|
+
console.log(frontendExample);
|
|
308
|
+
|
|
309
|
+
console.log('\n========== 示例完成 ==========');
|
|
310
|
+
|