schema-dsl 1.1.0 → 1.1.1
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/CHANGELOG.md +131 -11
- package/README.md +244 -271
- package/STATUS.md +65 -3
- package/docs/conditional-api.md +257 -11
- package/examples/i18n-error.examples.js +181 -0
- package/index.d.ts +324 -1
- package/index.js +35 -2
- package/lib/core/ConditionalBuilder.js +115 -13
- package/lib/core/Validator.js +7 -3
- package/lib/errors/I18nError.js +222 -0
- package/lib/locales/en-US.js +25 -0
- package/lib/locales/zh-CN.js +25 -0
- package/package.json +1 -1
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* I18nError - 多语言错误工具类
|
|
3
|
+
*
|
|
4
|
+
* 提供统一的多语言错误抛出机制,支持:
|
|
5
|
+
* - 多语言 key 自动翻译
|
|
6
|
+
* - 参数插值(如 {{field}}, {{limit}})
|
|
7
|
+
* - 自定义错误代码
|
|
8
|
+
* - Express/Koa 集成
|
|
9
|
+
*
|
|
10
|
+
* @module lib/errors/I18nError
|
|
11
|
+
* @version 1.1.1
|
|
12
|
+
*
|
|
13
|
+
* @example 基础用法
|
|
14
|
+
* const { I18nError } = require('schema-dsl');
|
|
15
|
+
*
|
|
16
|
+
* // 抛出多语言错误
|
|
17
|
+
* throw I18nError.create('error.notFound', { resource: '账户' });
|
|
18
|
+
* // 中文: "找不到账户"
|
|
19
|
+
* // 英文: "Account not found"
|
|
20
|
+
*
|
|
21
|
+
* @example 业务代码中使用
|
|
22
|
+
* function getAccount(id) {
|
|
23
|
+
* const account = db.findAccount(id);
|
|
24
|
+
* if (!account) {
|
|
25
|
+
* throw I18nError.create('account.notFound', { accountId: id });
|
|
26
|
+
* }
|
|
27
|
+
* if (account.balance < 100) {
|
|
28
|
+
* throw I18nError.create('account.insufficientBalance', {
|
|
29
|
+
* balance: account.balance,
|
|
30
|
+
* required: 100
|
|
31
|
+
* });
|
|
32
|
+
* }
|
|
33
|
+
* return account;
|
|
34
|
+
* }
|
|
35
|
+
*
|
|
36
|
+
* @example Express 中间件
|
|
37
|
+
* app.use((error, req, res, next) => {
|
|
38
|
+
* if (error instanceof I18nError) {
|
|
39
|
+
* return res.status(error.statusCode).json(error.toJSON());
|
|
40
|
+
* }
|
|
41
|
+
* next(error);
|
|
42
|
+
* });
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
const Locale = require('../core/Locale');
|
|
46
|
+
const MessageTemplate = require('../core/MessageTemplate');
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 多语言错误类
|
|
50
|
+
*
|
|
51
|
+
* @class I18nError
|
|
52
|
+
* @extends Error
|
|
53
|
+
*
|
|
54
|
+
* @property {string} name - 错误名称(固定为 'I18nError')
|
|
55
|
+
* @property {string} message - 错误消息(已翻译)
|
|
56
|
+
* @property {string} code - 错误代码(多语言 key)
|
|
57
|
+
* @property {Object} params - 错误参数(用于插值)
|
|
58
|
+
* @property {number} statusCode - HTTP 状态码(默认 400)
|
|
59
|
+
* @property {string} locale - 使用的语言环境
|
|
60
|
+
*/
|
|
61
|
+
class I18nError extends Error {
|
|
62
|
+
/**
|
|
63
|
+
* 构造函数
|
|
64
|
+
* @param {string} code - 错误代码(多语言 key)
|
|
65
|
+
* @param {Object} params - 错误参数(用于插值)
|
|
66
|
+
* @param {number} statusCode - HTTP 状态码(默认 400)
|
|
67
|
+
* @param {string} locale - 语言环境(默认使用当前语言)
|
|
68
|
+
*/
|
|
69
|
+
constructor(code, params = {}, statusCode = 400, locale = null) {
|
|
70
|
+
// 获取翻译后的消息模板
|
|
71
|
+
const actualLocale = locale || Locale.getLocale();
|
|
72
|
+
const template = Locale.getMessage(code, {}, actualLocale);
|
|
73
|
+
|
|
74
|
+
// 使用 MessageTemplate 进行参数插值
|
|
75
|
+
const messageTemplate = new MessageTemplate(template);
|
|
76
|
+
const message = messageTemplate.render(params || {});
|
|
77
|
+
|
|
78
|
+
super(message);
|
|
79
|
+
|
|
80
|
+
this.name = 'I18nError';
|
|
81
|
+
this.code = code;
|
|
82
|
+
this.params = params || {};
|
|
83
|
+
this.statusCode = statusCode;
|
|
84
|
+
this.locale = actualLocale;
|
|
85
|
+
|
|
86
|
+
// 保持堆栈跟踪
|
|
87
|
+
if (Error.captureStackTrace) {
|
|
88
|
+
Error.captureStackTrace(this, I18nError);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 静态工厂方法 - 创建并抛出错误
|
|
94
|
+
*
|
|
95
|
+
* @param {string} code - 错误代码(多语言 key)
|
|
96
|
+
* @param {Object} params - 错误参数
|
|
97
|
+
* @param {number} statusCode - HTTP 状态码
|
|
98
|
+
* @returns {I18nError} 错误实例
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* // 创建错误(不抛出)
|
|
102
|
+
* const error = I18nError.create('error.notFound', { resource: '用户' });
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* // 直接抛出
|
|
106
|
+
* throw I18nError.create('error.notFound', { resource: '用户' });
|
|
107
|
+
*/
|
|
108
|
+
static create(code, params = {}, statusCode = 400) {
|
|
109
|
+
return new I18nError(code, params, statusCode);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 静态工厂方法 - 快速抛出错误
|
|
114
|
+
*
|
|
115
|
+
* @param {string} code - 错误代码(多语言 key)
|
|
116
|
+
* @param {Object} params - 错误参数
|
|
117
|
+
* @param {number} statusCode - HTTP 状态码
|
|
118
|
+
* @throws {I18nError} 直接抛出错误
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* I18nError.throw('error.notFound', { resource: '用户' });
|
|
122
|
+
* // 等同于:throw I18nError.create('error.notFound', { resource: '用户' });
|
|
123
|
+
*/
|
|
124
|
+
static throw(code, params = {}, statusCode = 400) {
|
|
125
|
+
throw new I18nError(code, params, statusCode);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 断言方法 - 条件不满足时抛错
|
|
130
|
+
*
|
|
131
|
+
* @param {boolean} condition - 条件表达式
|
|
132
|
+
* @param {string} code - 错误代码(多语言 key)
|
|
133
|
+
* @param {Object} params - 错误参数
|
|
134
|
+
* @param {number} statusCode - HTTP 状态码
|
|
135
|
+
* @throws {I18nError} 条件为 false 时抛出错误
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* I18nError.assert(account, 'account.notFound', { accountId: id });
|
|
139
|
+
* // 等同于:if (!account) throw I18nError.create('account.notFound', { accountId: id });
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* I18nError.assert(
|
|
143
|
+
* account.balance >= 100,
|
|
144
|
+
* 'account.insufficientBalance',
|
|
145
|
+
* { balance: account.balance, required: 100 }
|
|
146
|
+
* );
|
|
147
|
+
*/
|
|
148
|
+
static assert(condition, code, params = {}, statusCode = 400) {
|
|
149
|
+
if (!condition) {
|
|
150
|
+
throw new I18nError(code, params, statusCode);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 检查错误是否为指定代码
|
|
156
|
+
*
|
|
157
|
+
* @param {string} code - 错误代码
|
|
158
|
+
* @returns {boolean} 是否匹配
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* try {
|
|
162
|
+
* // ...
|
|
163
|
+
* } catch (error) {
|
|
164
|
+
* if (error instanceof I18nError && error.is('account.notFound')) {
|
|
165
|
+
* // 处理账户不存在的情况
|
|
166
|
+
* }
|
|
167
|
+
* }
|
|
168
|
+
*/
|
|
169
|
+
is(code) {
|
|
170
|
+
return this.code === code;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 转换为 JSON 格式(用于 API 响应)
|
|
175
|
+
*
|
|
176
|
+
* @returns {Object} JSON 对象
|
|
177
|
+
* @returns {string} return.error - 错误名称
|
|
178
|
+
* @returns {string} return.code - 错误代码
|
|
179
|
+
* @returns {string} return.message - 错误消息(已翻译)
|
|
180
|
+
* @returns {Object} return.params - 错误参数
|
|
181
|
+
* @returns {number} return.statusCode - 状态码
|
|
182
|
+
* @returns {string} return.locale - 语言环境
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* const json = error.toJSON();
|
|
186
|
+
* res.status(error.statusCode).json(json);
|
|
187
|
+
* // {
|
|
188
|
+
* // error: 'I18nError',
|
|
189
|
+
* // code: 'account.notFound',
|
|
190
|
+
* // message: '找不到账户',
|
|
191
|
+
* // params: { accountId: '123' },
|
|
192
|
+
* // statusCode: 404,
|
|
193
|
+
* // locale: 'zh-CN'
|
|
194
|
+
* // }
|
|
195
|
+
*/
|
|
196
|
+
toJSON() {
|
|
197
|
+
return {
|
|
198
|
+
error: this.name,
|
|
199
|
+
code: this.code,
|
|
200
|
+
message: this.message,
|
|
201
|
+
params: this.params,
|
|
202
|
+
statusCode: this.statusCode,
|
|
203
|
+
locale: this.locale
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 转换为字符串
|
|
209
|
+
*
|
|
210
|
+
* @returns {string} 格式化的错误信息
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* console.log(error.toString());
|
|
214
|
+
* // "I18nError [account.notFound]: 找不到账户"
|
|
215
|
+
*/
|
|
216
|
+
toString() {
|
|
217
|
+
return `${this.name} [${this.code}]: ${this.message}`;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = I18nError;
|
|
222
|
+
|
package/lib/locales/en-US.js
CHANGED
|
@@ -17,6 +17,31 @@ module.exports = {
|
|
|
17
17
|
'conditional.blocked': 'Account has been blocked',
|
|
18
18
|
'conditional.notAllowed': 'Registration not allowed',
|
|
19
19
|
|
|
20
|
+
// I18nError - General errors (v1.1.1)
|
|
21
|
+
'error.notFound': '{{#resource}} not found',
|
|
22
|
+
'error.forbidden': 'Access to {{#resource}} is forbidden',
|
|
23
|
+
'error.unauthorized': 'Unauthorized, please log in',
|
|
24
|
+
'error.invalid': 'Invalid {{#field}}',
|
|
25
|
+
'error.duplicate': '{{#resource}} already exists',
|
|
26
|
+
'error.conflict': 'Operation conflict: {{#reason}}',
|
|
27
|
+
|
|
28
|
+
// I18nError - Account related (v1.1.1)
|
|
29
|
+
'account.notFound': 'Account not found',
|
|
30
|
+
'account.inactive': 'Account is inactive',
|
|
31
|
+
'account.banned': 'Account has been banned',
|
|
32
|
+
'account.insufficientBalance': 'Insufficient balance, current: {{#balance}}, required: {{#required}}',
|
|
33
|
+
'account.insufficientCredits': 'Insufficient credits, current: {{#credits}}, required: {{#required}}',
|
|
34
|
+
|
|
35
|
+
// I18nError - User related (v1.1.1)
|
|
36
|
+
'user.notFound': 'User not found',
|
|
37
|
+
'user.notVerified': 'User is not verified',
|
|
38
|
+
'user.noPermission': 'No admin permission',
|
|
39
|
+
|
|
40
|
+
// I18nError - Order related (v1.1.1)
|
|
41
|
+
'order.notPaid': 'Order not paid',
|
|
42
|
+
'order.paymentMissing': 'Payment information missing',
|
|
43
|
+
'order.addressMissing': 'Shipping address missing',
|
|
44
|
+
|
|
20
45
|
// Formats
|
|
21
46
|
'format.email': '{{#label}} must be a valid email address',
|
|
22
47
|
'format.url': '{{#label}} must be a valid URL',
|
package/lib/locales/zh-CN.js
CHANGED
|
@@ -17,6 +17,31 @@ module.exports = {
|
|
|
17
17
|
'conditional.blocked': '账号已被封禁',
|
|
18
18
|
'conditional.notAllowed': '不允许注册',
|
|
19
19
|
|
|
20
|
+
// I18nError - 通用错误消息 (v1.1.1)
|
|
21
|
+
'error.notFound': '找不到{{#resource}}',
|
|
22
|
+
'error.forbidden': '没有权限访问{{#resource}}',
|
|
23
|
+
'error.unauthorized': '未授权,请先登录',
|
|
24
|
+
'error.invalid': '{{#field}}无效',
|
|
25
|
+
'error.duplicate': '{{#resource}}已存在',
|
|
26
|
+
'error.conflict': '操作冲突: {{#reason}}',
|
|
27
|
+
|
|
28
|
+
// I18nError - 账户相关 (v1.1.1)
|
|
29
|
+
'account.notFound': '账户不存在',
|
|
30
|
+
'account.inactive': '账户未激活',
|
|
31
|
+
'account.banned': '账户已被封禁',
|
|
32
|
+
'account.insufficientBalance': '余额不足,当前余额{{#balance}},需要{{#required}}',
|
|
33
|
+
'account.insufficientCredits': '积分不足,当前积分{{#credits}},需要{{#required}}',
|
|
34
|
+
|
|
35
|
+
// I18nError - 用户相关 (v1.1.1)
|
|
36
|
+
'user.notFound': '用户不存在',
|
|
37
|
+
'user.notVerified': '用户未验证',
|
|
38
|
+
'user.noPermission': '没有管理员权限',
|
|
39
|
+
|
|
40
|
+
// I18nError - 订单相关 (v1.1.1)
|
|
41
|
+
'order.notPaid': '订单未支付',
|
|
42
|
+
'order.paymentMissing': '缺少支付信息',
|
|
43
|
+
'order.addressMissing': '缺少收货地址',
|
|
44
|
+
|
|
20
45
|
// Formats
|
|
21
46
|
'format.email': '{{#label}}必须是有效的邮箱地址',
|
|
22
47
|
'format.url': '{{#label}}必须是有效的URL地址',
|