koatty_validation 1.6.1 → 1.6.6

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/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * @Author: richen
3
- * @Date: 2025-10-23 20:54:39
3
+ * @Date: 2026-01-28 17:22:04
4
4
  * @License: BSD (3-Clause)
5
5
  * @Copyright (c) - <richenlin(at)gmail.com>
6
6
  * @HomePage: https://koatty.org/
@@ -799,6 +799,209 @@ function plateNumber(value) {
799
799
  }
800
800
  }
801
801
 
802
+ /**
803
+ * Improved error handling mechanism
804
+ * @author richen
805
+ */
806
+ /**
807
+ * Error message internationalization
808
+ */
809
+ const ERROR_MESSAGES = {
810
+ zh: {
811
+ // Chinese localization validation
812
+ IsCnName: '必须是有效的中文姓名',
813
+ IsIdNumber: '必须是有效的身份证号码',
814
+ IsZipCode: '必须是有效的邮政编码',
815
+ IsMobile: '必须是有效的手机号码',
816
+ IsPlateNumber: '必须是有效的车牌号码',
817
+ // Basic validation
818
+ IsNotEmpty: '不能为空',
819
+ IsDate: '必须是有效的日期',
820
+ IsEmail: '必须是有效的邮箱地址',
821
+ IsIP: '必须是有效的IP地址',
822
+ IsPhoneNumber: '必须是有效的电话号码',
823
+ IsUrl: '必须是有效的URL地址',
824
+ IsHash: '必须是有效的哈希值',
825
+ // Comparison validation
826
+ Equals: '必须等于 {comparison}',
827
+ NotEquals: '不能等于 {comparison}',
828
+ Contains: '必须包含 {seed}',
829
+ IsIn: '必须是以下值之一: {possibleValues}',
830
+ IsNotIn: '不能是以下值之一: {possibleValues}',
831
+ Gt: '必须大于 {min}',
832
+ Gte: '必须大于或等于 {min}',
833
+ Lt: '必须小于 {max}',
834
+ Lte: '必须小于或等于 {max}',
835
+ // Common errors
836
+ invalidParameter: '参数 {field} 无效',
837
+ validationFailed: '验证失败',
838
+ },
839
+ en: {
840
+ // Chinese localization validators
841
+ IsCnName: 'must be a valid Chinese name',
842
+ IsIdNumber: 'must be a valid ID number',
843
+ IsZipCode: 'must be a valid zip code',
844
+ IsMobile: 'must be a valid mobile number',
845
+ IsPlateNumber: 'must be a valid plate number',
846
+ // Basic validators
847
+ IsNotEmpty: 'should not be empty',
848
+ IsDate: 'must be a valid date',
849
+ IsEmail: 'must be a valid email',
850
+ IsIP: 'must be a valid IP address',
851
+ IsPhoneNumber: 'must be a valid phone number',
852
+ IsUrl: 'must be a valid URL',
853
+ IsHash: 'must be a valid hash',
854
+ // Comparison validators
855
+ Equals: 'must equal to {comparison}',
856
+ NotEquals: 'should not equal to {comparison}',
857
+ Contains: 'must contain {seed}',
858
+ IsIn: 'must be one of the following values: {possibleValues}',
859
+ IsNotIn: 'should not be one of the following values: {possibleValues}',
860
+ Gt: 'must be greater than {min}',
861
+ Gte: 'must be greater than or equal to {min}',
862
+ Lt: 'must be less than {max}',
863
+ Lte: 'must be less than or equal to {max}',
864
+ // Common errors
865
+ invalidParameter: 'invalid parameter {field}',
866
+ validationFailed: 'validation failed',
867
+ }
868
+ };
869
+ /**
870
+ * Enhanced validation error class
871
+ */
872
+ class KoattyValidationError extends Error {
873
+ constructor(errors, message) {
874
+ const errorMessage = message || 'Validation failed';
875
+ super(errorMessage);
876
+ this.name = 'KoattyValidationError';
877
+ this.errors = errors;
878
+ this.statusCode = 400;
879
+ this.timestamp = new Date();
880
+ // Ensure correct prototype chain
881
+ Object.setPrototypeOf(this, KoattyValidationError.prototype);
882
+ }
883
+ /**
884
+ * Get the first error message
885
+ */
886
+ getFirstError() {
887
+ return this.errors[0];
888
+ }
889
+ /**
890
+ * Get errors for a specific field
891
+ */
892
+ getFieldErrors(field) {
893
+ return this.errors.filter(error => error.field === field);
894
+ }
895
+ /**
896
+ * Convert to JSON format
897
+ */
898
+ toJSON() {
899
+ return {
900
+ name: this.name,
901
+ message: this.message,
902
+ statusCode: this.statusCode,
903
+ timestamp: this.timestamp,
904
+ errors: this.errors
905
+ };
906
+ }
907
+ }
908
+ /**
909
+ * Error message formatter
910
+ */
911
+ class ErrorMessageFormatter {
912
+ constructor(language = 'zh') {
913
+ this.language = 'zh';
914
+ this.language = language;
915
+ }
916
+ /**
917
+ * Set language
918
+ */
919
+ setLanguage(language) {
920
+ this.language = language;
921
+ }
922
+ /**
923
+ * Format error message
924
+ */
925
+ formatMessage(constraint, field, value, context) {
926
+ const messages = ERROR_MESSAGES[this.language];
927
+ let template = messages[constraint] || messages.invalidParameter;
928
+ // Replace placeholders
929
+ template = template.replace('{field}', field);
930
+ // Prioritize values from context, then use passed value
931
+ if (context) {
932
+ Object.entries(context).forEach(([key, val]) => {
933
+ template = template.replace(`{${key}}`, this.formatValue(val));
934
+ });
935
+ }
936
+ // If there's still a {value} placeholder and value was passed, replace it
937
+ if (value !== undefined && template.includes('{value}')) {
938
+ template = template.replace('{value}', this.formatValue(value));
939
+ }
940
+ return template;
941
+ }
942
+ /**
943
+ * Format value for message display
944
+ * @private
945
+ */
946
+ formatValue(value) {
947
+ if (value === null)
948
+ return 'null';
949
+ if (value === undefined)
950
+ return 'undefined';
951
+ if (typeof value === 'number')
952
+ return String(value);
953
+ if (typeof value === 'string')
954
+ return `"${value}"`;
955
+ if (Array.isArray(value))
956
+ return `[${value.map(v => this.formatValue(v)).join(', ')}]`;
957
+ if (typeof value === 'object') {
958
+ try {
959
+ return JSON.stringify(value);
960
+ }
961
+ catch {
962
+ // Handle circular references
963
+ return '[Circular Reference]';
964
+ }
965
+ }
966
+ return String(value);
967
+ }
968
+ }
969
+ /**
970
+ * Global error message formatter instance
971
+ */
972
+ const errorFormatter = new ErrorMessageFormatter();
973
+ /**
974
+ * Set global language
975
+ */
976
+ function setValidationLanguage(language) {
977
+ errorFormatter.setLanguage(language);
978
+ }
979
+ /**
980
+ * Create validation error
981
+ */
982
+ function createValidationError(field, value, constraint, customMessage, context) {
983
+ const message = customMessage || errorFormatter.formatMessage(constraint, field, value, context);
984
+ return {
985
+ field,
986
+ value,
987
+ constraint,
988
+ message,
989
+ context
990
+ };
991
+ }
992
+ /**
993
+ * Create validation errors in batch
994
+ */
995
+ function createValidationErrors(errors, separator = '; ') {
996
+ const validationErrors = errors.map(error => createValidationError(error.field, error.value, error.constraint, error.message, error.context));
997
+ // Generate combined message from all errors
998
+ const combinedMessage = validationErrors
999
+ .map(err => err.message)
1000
+ .filter(msg => msg && msg.trim())
1001
+ .join(separator) || 'Validation failed';
1002
+ return new KoattyValidationError(validationErrors, combinedMessage);
1003
+ }
1004
+
802
1005
  /*
803
1006
  * @Description:
804
1007
  * @Usage:
@@ -858,10 +1061,11 @@ class ValidateClass {
858
1061
  * @param {*} Clazz
859
1062
  * @param {*} data
860
1063
  * @param {boolean} [convert=false] auto convert parameters type
1064
+ * @param {ValidationOptions} [options] validation options (returnAllErrors, errorSeparator)
861
1065
  * @returns {Promise<any>}
862
1066
  * @memberof ValidateClass
863
1067
  */
864
- async valid(Clazz, data, convert = false) {
1068
+ async valid(Clazz, data, convert = false, options) {
865
1069
  let obj = {};
866
1070
  if (data instanceof Clazz) {
867
1071
  obj = data;
@@ -877,7 +1081,21 @@ class ValidateClass {
877
1081
  errors = await validate(obj, { skipMissingProperties: true });
878
1082
  }
879
1083
  if (errors.length > 0) {
880
- throw new Error(Object.values(errors[0].constraints)[0]);
1084
+ // Check if user wants all errors or just the first one
1085
+ if (options === null || options === void 0 ? void 0 : options.returnAllErrors) {
1086
+ // Throw KoattyValidationError with all error details
1087
+ throw createValidationErrors(errors.map(e => ({
1088
+ field: e.property,
1089
+ value: e.value,
1090
+ constraint: Object.keys(e.constraints || {})[0] || 'unknown',
1091
+ message: Object.values(e.constraints || {})[0] || 'Validation failed',
1092
+ context: e.constraints
1093
+ })), options.errorSeparator || '; ');
1094
+ }
1095
+ else {
1096
+ // Default behavior (backward compatible): return only first error
1097
+ throw new Error(Object.values(errors[0].constraints)[0]);
1098
+ }
881
1099
  }
882
1100
  return obj;
883
1101
  }
@@ -1149,8 +1367,9 @@ function createValidationDecorator(options) {
1149
1367
  },
1150
1368
  defaultMessage(validationArguments) {
1151
1369
  const property = validationArguments.property;
1152
- return defaultMessage
1153
- ? defaultMessage.replace('$property', property)
1370
+ const customMessage = (validationOptions === null || validationOptions === void 0 ? void 0 : validationOptions.message) || defaultMessage;
1371
+ return customMessage
1372
+ ? customMessage.replace('$property', property)
1154
1373
  : `Invalid value for ${property}`;
1155
1374
  }
1156
1375
  }
@@ -1189,204 +1408,6 @@ function createParameterizedDecorator(name, validator, defaultMessage) {
1189
1408
  });
1190
1409
  }
1191
1410
 
1192
- /**
1193
- * Improved error handling mechanism
1194
- * @author richen
1195
- */
1196
- /**
1197
- * Error message internationalization
1198
- */
1199
- const ERROR_MESSAGES = {
1200
- zh: {
1201
- // Chinese localization validation
1202
- IsCnName: '必须是有效的中文姓名',
1203
- IsIdNumber: '必须是有效的身份证号码',
1204
- IsZipCode: '必须是有效的邮政编码',
1205
- IsMobile: '必须是有效的手机号码',
1206
- IsPlateNumber: '必须是有效的车牌号码',
1207
- // Basic validation
1208
- IsNotEmpty: '不能为空',
1209
- IsDate: '必须是有效的日期',
1210
- IsEmail: '必须是有效的邮箱地址',
1211
- IsIP: '必须是有效的IP地址',
1212
- IsPhoneNumber: '必须是有效的电话号码',
1213
- IsUrl: '必须是有效的URL地址',
1214
- IsHash: '必须是有效的哈希值',
1215
- // Comparison validation
1216
- Equals: '必须等于 {comparison}',
1217
- NotEquals: '不能等于 {comparison}',
1218
- Contains: '必须包含 {seed}',
1219
- IsIn: '必须是以下值之一: {possibleValues}',
1220
- IsNotIn: '不能是以下值之一: {possibleValues}',
1221
- Gt: '必须大于 {min}',
1222
- Gte: '必须大于或等于 {min}',
1223
- Lt: '必须小于 {max}',
1224
- Lte: '必须小于或等于 {max}',
1225
- // Common errors
1226
- invalidParameter: '参数 {field} 无效',
1227
- validationFailed: '验证失败',
1228
- },
1229
- en: {
1230
- // Chinese localization validators
1231
- IsCnName: 'must be a valid Chinese name',
1232
- IsIdNumber: 'must be a valid ID number',
1233
- IsZipCode: 'must be a valid zip code',
1234
- IsMobile: 'must be a valid mobile number',
1235
- IsPlateNumber: 'must be a valid plate number',
1236
- // Basic validators
1237
- IsNotEmpty: 'should not be empty',
1238
- IsDate: 'must be a valid date',
1239
- IsEmail: 'must be a valid email',
1240
- IsIP: 'must be a valid IP address',
1241
- IsPhoneNumber: 'must be a valid phone number',
1242
- IsUrl: 'must be a valid URL',
1243
- IsHash: 'must be a valid hash',
1244
- // Comparison validators
1245
- Equals: 'must equal to {comparison}',
1246
- NotEquals: 'should not equal to {comparison}',
1247
- Contains: 'must contain {seed}',
1248
- IsIn: 'must be one of the following values: {possibleValues}',
1249
- IsNotIn: 'should not be one of the following values: {possibleValues}',
1250
- Gt: 'must be greater than {min}',
1251
- Gte: 'must be greater than or equal to {min}',
1252
- Lt: 'must be less than {max}',
1253
- Lte: 'must be less than or equal to {max}',
1254
- // Common errors
1255
- invalidParameter: 'invalid parameter {field}',
1256
- validationFailed: 'validation failed',
1257
- }
1258
- };
1259
- /**
1260
- * Enhanced validation error class
1261
- */
1262
- class KoattyValidationError extends Error {
1263
- constructor(errors, message) {
1264
- const errorMessage = message || 'Validation failed';
1265
- super(errorMessage);
1266
- this.name = 'KoattyValidationError';
1267
- this.errors = errors;
1268
- this.statusCode = 400;
1269
- this.timestamp = new Date();
1270
- // Ensure correct prototype chain
1271
- Object.setPrototypeOf(this, KoattyValidationError.prototype);
1272
- }
1273
- /**
1274
- * Get the first error message
1275
- */
1276
- getFirstError() {
1277
- return this.errors[0];
1278
- }
1279
- /**
1280
- * Get errors for a specific field
1281
- */
1282
- getFieldErrors(field) {
1283
- return this.errors.filter(error => error.field === field);
1284
- }
1285
- /**
1286
- * Convert to JSON format
1287
- */
1288
- toJSON() {
1289
- return {
1290
- name: this.name,
1291
- message: this.message,
1292
- statusCode: this.statusCode,
1293
- timestamp: this.timestamp,
1294
- errors: this.errors
1295
- };
1296
- }
1297
- }
1298
- /**
1299
- * Error message formatter
1300
- */
1301
- class ErrorMessageFormatter {
1302
- constructor(language = 'zh') {
1303
- this.language = 'zh';
1304
- this.language = language;
1305
- }
1306
- /**
1307
- * Set language
1308
- */
1309
- setLanguage(language) {
1310
- this.language = language;
1311
- }
1312
- /**
1313
- * Format error message
1314
- */
1315
- formatMessage(constraint, field, value, context) {
1316
- const messages = ERROR_MESSAGES[this.language];
1317
- let template = messages[constraint] || messages.invalidParameter;
1318
- // Replace placeholders
1319
- template = template.replace('{field}', field);
1320
- // Prioritize values from context, then use passed value
1321
- if (context) {
1322
- Object.entries(context).forEach(([key, val]) => {
1323
- template = template.replace(`{${key}}`, this.formatValue(val));
1324
- });
1325
- }
1326
- // If there's still a {value} placeholder and value was passed, replace it
1327
- if (value !== undefined && template.includes('{value}')) {
1328
- template = template.replace('{value}', this.formatValue(value));
1329
- }
1330
- return template;
1331
- }
1332
- /**
1333
- * Format value for message display
1334
- * @private
1335
- */
1336
- formatValue(value) {
1337
- if (value === null)
1338
- return 'null';
1339
- if (value === undefined)
1340
- return 'undefined';
1341
- if (typeof value === 'number')
1342
- return String(value);
1343
- if (typeof value === 'string')
1344
- return `"${value}"`;
1345
- if (Array.isArray(value))
1346
- return `[${value.map(v => this.formatValue(v)).join(', ')}]`;
1347
- if (typeof value === 'object') {
1348
- try {
1349
- return JSON.stringify(value);
1350
- }
1351
- catch {
1352
- // Handle circular references
1353
- return '[Circular Reference]';
1354
- }
1355
- }
1356
- return String(value);
1357
- }
1358
- }
1359
- /**
1360
- * Global error message formatter instance
1361
- */
1362
- const errorFormatter = new ErrorMessageFormatter();
1363
- /**
1364
- * Set global language
1365
- */
1366
- function setValidationLanguage(language) {
1367
- errorFormatter.setLanguage(language);
1368
- }
1369
- /**
1370
- * Create validation error
1371
- */
1372
- function createValidationError(field, value, constraint, customMessage, context) {
1373
- const message = customMessage || errorFormatter.formatMessage(constraint, field, value, context);
1374
- return {
1375
- field,
1376
- value,
1377
- constraint,
1378
- message,
1379
- context
1380
- };
1381
- }
1382
- /**
1383
- * Create validation errors in batch
1384
- */
1385
- function createValidationErrors(errors) {
1386
- const validationErrors = errors.map(error => createValidationError(error.field, error.value, error.constraint, error.message, error.context));
1387
- return new KoattyValidationError(validationErrors);
1388
- }
1389
-
1390
1411
  /**
1391
1412
  * Refactored decorator definitions - Using factory functions to eliminate duplication
1392
1413
  * @author richen
@@ -1413,7 +1434,10 @@ const Lt = createParameterizedDecorator('Lt', (value, max) => helper.toNumber(va
1413
1434
  const Lte = createParameterizedDecorator('Lte', (value, max) => helper.toNumber(value) <= max, 'must be less than or equal to $constraint1');
1414
1435
  // 复杂验证装饰器(需要特殊处理)
1415
1436
  function IsEmail(options, validationOptions) {
1416
- return createParameterizedDecorator('IsEmail', (value) => isEmail(value, options), 'must be a valid email')(validationOptions);
1437
+ // Handle case where options is actually ValidationOptions (message only)
1438
+ const actualOptions = (options === null || options === void 0 ? void 0 : options.message) ? {} : options;
1439
+ const actualValidationOptions = (options === null || options === void 0 ? void 0 : options.message) ? options : validationOptions;
1440
+ return createParameterizedDecorator('IsEmail', (value) => isEmail(value, actualOptions), 'must be a valid email')(actualValidationOptions);
1417
1441
  }
1418
1442
  function IsIP(version, validationOptions) {
1419
1443
  return createParameterizedDecorator('IsIP', (value) => isIP(value, version), 'must be a valid IP address')(validationOptions);
package/dist/package.json CHANGED
@@ -1,21 +1,17 @@
1
1
  {
2
2
  "name": "koatty_validation",
3
- "version": "1.6.1",
3
+ "version": "1.6.6",
4
4
  "description": "Validation Util for Koatty and ThinkORM.",
5
5
  "scripts": {
6
6
  "build": "npm run build:js && npm run build:dts && npm run build:doc && npm run build:cp",
7
7
  "build:cp": "node scripts/postBuild && copyfiles package.json LICENSE README.md dist/",
8
8
  "build:js": "del-cli --force dist && npx rollup --bundleConfigAsCjs -c .rollup.config.js",
9
9
  "build:doc": "del-cli --force docs/api && npx api-documenter markdown --input temp --output docs/api",
10
- "build:dts": "del-cli --force temp && npx tsc && npx api-extractor run --local --verbose",
10
+ "build:dts": "bash ../../scripts/build-dts.sh",
11
11
  "eslint": "eslint --ext .ts,.js ./",
12
12
  "lock": "npm i --package-lock-only",
13
13
  "prepublishOnly": "npm test && npm run build && git push --follow-tags origin",
14
14
  "prerelease": "npm test && npm run build",
15
- "release": "standard-version",
16
- "release:pre": "npm run release -- --prerelease",
17
- "release:major": "npm run release -- --release-as major",
18
- "release:minor": "npm run release -- --release-as minor",
19
15
  "test": "npm run eslint && jest --passWithNoTests"
20
16
  },
21
17
  "main": "./dist/index.js",
@@ -77,15 +73,15 @@
77
73
  "typescript": "^5.x.x"
78
74
  },
79
75
  "dependencies": {
80
- "class-validator": "^0.14.2",
81
- "koatty_container": "^1.x.x",
82
- "koatty_lib": "^1.x.x",
83
- "koatty_logger": "^2.x.x",
84
- "lru-cache": "^11.2.2"
76
+ "class-validator": "^0.14.3",
77
+ "koatty_container": "^1.17.4",
78
+ "koatty_lib": "^1.4.5",
79
+ "koatty_logger": "^2.3.4",
80
+ "lru-cache": "^11.2.5"
85
81
  },
86
82
  "peerDependencies": {
87
- "koatty_container": "^1.x.x",
88
- "koatty_lib": "^1.x.x",
89
- "koatty_logger": "^2.x.x"
83
+ "koatty_container": "^1.17.4",
84
+ "koatty_lib": "^1.4.5",
85
+ "koatty_logger": "^2.3.4"
90
86
  }
91
87
  }
package/package.json CHANGED
@@ -1,23 +1,7 @@
1
1
  {
2
2
  "name": "koatty_validation",
3
- "version": "1.6.1",
3
+ "version": "1.6.6",
4
4
  "description": "Validation Util for Koatty and ThinkORM.",
5
- "scripts": {
6
- "build": "npm run build:js && npm run build:dts && npm run build:doc && npm run build:cp",
7
- "build:cp": "node scripts/postBuild && copyfiles package.json LICENSE README.md dist/",
8
- "build:js": "del-cli --force dist && npx rollup --bundleConfigAsCjs -c .rollup.config.js",
9
- "build:doc": "del-cli --force docs/api && npx api-documenter markdown --input temp --output docs/api",
10
- "build:dts": "del-cli --force temp && npx tsc && npx api-extractor run --local --verbose",
11
- "eslint": "eslint --ext .ts,.js ./",
12
- "lock": "npm i --package-lock-only",
13
- "prepublishOnly": "npm test && npm run build && git push --follow-tags origin",
14
- "prerelease": "npm test && npm run build",
15
- "release": "standard-version",
16
- "release:pre": "npm run release -- --prerelease",
17
- "release:major": "npm run release -- --release-as major",
18
- "release:minor": "npm run release -- --release-as minor",
19
- "test": "npm run eslint && jest --passWithNoTests"
20
- },
21
5
  "main": "./dist/index.js",
22
6
  "exports": {
23
7
  "require": "./dist/index.js",
@@ -77,15 +61,26 @@
77
61
  "typescript": "^5.x.x"
78
62
  },
79
63
  "dependencies": {
80
- "class-validator": "^0.14.2",
81
- "koatty_container": "^1.x.x",
82
- "koatty_lib": "^1.x.x",
83
- "koatty_logger": "^2.x.x",
84
- "lru-cache": "^11.2.2"
64
+ "class-validator": "^0.14.3",
65
+ "koatty_container": "^1.17.4",
66
+ "koatty_lib": "^1.4.5",
67
+ "koatty_logger": "^2.3.4",
68
+ "lru-cache": "^11.2.5"
85
69
  },
86
70
  "peerDependencies": {
87
- "koatty_container": "^1.x.x",
88
- "koatty_lib": "^1.x.x",
89
- "koatty_logger": "^2.x.x"
71
+ "koatty_container": "^1.17.4",
72
+ "koatty_lib": "^1.4.5",
73
+ "koatty_logger": "^2.3.4"
74
+ },
75
+ "scripts": {
76
+ "build": "npm run build:js && npm run build:dts && npm run build:doc && npm run build:cp",
77
+ "build:cp": "node scripts/postBuild && copyfiles package.json LICENSE README.md dist/",
78
+ "build:js": "del-cli --force dist && npx rollup --bundleConfigAsCjs -c .rollup.config.js",
79
+ "build:doc": "del-cli --force docs/api && npx api-documenter markdown --input temp --output docs/api",
80
+ "build:dts": "bash ../../scripts/build-dts.sh",
81
+ "eslint": "eslint --ext .ts,.js ./",
82
+ "lock": "npm i --package-lock-only",
83
+ "prerelease": "npm test && npm run build",
84
+ "test": "npm run eslint && jest --passWithNoTests"
90
85
  }
91
- }
86
+ }