koatty_validation 1.6.1 → 1.6.3

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.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * @Author: richen
3
- * @Date: 2025-10-23 20:54:39
3
+ * @Date: 2026-01-28 12:40:10
4
4
  * @License: BSD (3-Clause)
5
5
  * @Copyright (c) - <richenlin(at)gmail.com>
6
6
  * @HomePage: https://koatty.org/
@@ -820,6 +820,209 @@ function plateNumber(value) {
820
820
  }
821
821
  }
822
822
 
823
+ /**
824
+ * Improved error handling mechanism
825
+ * @author richen
826
+ */
827
+ /**
828
+ * Error message internationalization
829
+ */
830
+ const ERROR_MESSAGES = {
831
+ zh: {
832
+ // Chinese localization validation
833
+ IsCnName: '必须是有效的中文姓名',
834
+ IsIdNumber: '必须是有效的身份证号码',
835
+ IsZipCode: '必须是有效的邮政编码',
836
+ IsMobile: '必须是有效的手机号码',
837
+ IsPlateNumber: '必须是有效的车牌号码',
838
+ // Basic validation
839
+ IsNotEmpty: '不能为空',
840
+ IsDate: '必须是有效的日期',
841
+ IsEmail: '必须是有效的邮箱地址',
842
+ IsIP: '必须是有效的IP地址',
843
+ IsPhoneNumber: '必须是有效的电话号码',
844
+ IsUrl: '必须是有效的URL地址',
845
+ IsHash: '必须是有效的哈希值',
846
+ // Comparison validation
847
+ Equals: '必须等于 {comparison}',
848
+ NotEquals: '不能等于 {comparison}',
849
+ Contains: '必须包含 {seed}',
850
+ IsIn: '必须是以下值之一: {possibleValues}',
851
+ IsNotIn: '不能是以下值之一: {possibleValues}',
852
+ Gt: '必须大于 {min}',
853
+ Gte: '必须大于或等于 {min}',
854
+ Lt: '必须小于 {max}',
855
+ Lte: '必须小于或等于 {max}',
856
+ // Common errors
857
+ invalidParameter: '参数 {field} 无效',
858
+ validationFailed: '验证失败',
859
+ },
860
+ en: {
861
+ // Chinese localization validators
862
+ IsCnName: 'must be a valid Chinese name',
863
+ IsIdNumber: 'must be a valid ID number',
864
+ IsZipCode: 'must be a valid zip code',
865
+ IsMobile: 'must be a valid mobile number',
866
+ IsPlateNumber: 'must be a valid plate number',
867
+ // Basic validators
868
+ IsNotEmpty: 'should not be empty',
869
+ IsDate: 'must be a valid date',
870
+ IsEmail: 'must be a valid email',
871
+ IsIP: 'must be a valid IP address',
872
+ IsPhoneNumber: 'must be a valid phone number',
873
+ IsUrl: 'must be a valid URL',
874
+ IsHash: 'must be a valid hash',
875
+ // Comparison validators
876
+ Equals: 'must equal to {comparison}',
877
+ NotEquals: 'should not equal to {comparison}',
878
+ Contains: 'must contain {seed}',
879
+ IsIn: 'must be one of the following values: {possibleValues}',
880
+ IsNotIn: 'should not be one of the following values: {possibleValues}',
881
+ Gt: 'must be greater than {min}',
882
+ Gte: 'must be greater than or equal to {min}',
883
+ Lt: 'must be less than {max}',
884
+ Lte: 'must be less than or equal to {max}',
885
+ // Common errors
886
+ invalidParameter: 'invalid parameter {field}',
887
+ validationFailed: 'validation failed',
888
+ }
889
+ };
890
+ /**
891
+ * Enhanced validation error class
892
+ */
893
+ class KoattyValidationError extends Error {
894
+ constructor(errors, message) {
895
+ const errorMessage = message || 'Validation failed';
896
+ super(errorMessage);
897
+ this.name = 'KoattyValidationError';
898
+ this.errors = errors;
899
+ this.statusCode = 400;
900
+ this.timestamp = new Date();
901
+ // Ensure correct prototype chain
902
+ Object.setPrototypeOf(this, KoattyValidationError.prototype);
903
+ }
904
+ /**
905
+ * Get the first error message
906
+ */
907
+ getFirstError() {
908
+ return this.errors[0];
909
+ }
910
+ /**
911
+ * Get errors for a specific field
912
+ */
913
+ getFieldErrors(field) {
914
+ return this.errors.filter(error => error.field === field);
915
+ }
916
+ /**
917
+ * Convert to JSON format
918
+ */
919
+ toJSON() {
920
+ return {
921
+ name: this.name,
922
+ message: this.message,
923
+ statusCode: this.statusCode,
924
+ timestamp: this.timestamp,
925
+ errors: this.errors
926
+ };
927
+ }
928
+ }
929
+ /**
930
+ * Error message formatter
931
+ */
932
+ class ErrorMessageFormatter {
933
+ constructor(language = 'zh') {
934
+ this.language = 'zh';
935
+ this.language = language;
936
+ }
937
+ /**
938
+ * Set language
939
+ */
940
+ setLanguage(language) {
941
+ this.language = language;
942
+ }
943
+ /**
944
+ * Format error message
945
+ */
946
+ formatMessage(constraint, field, value, context) {
947
+ const messages = ERROR_MESSAGES[this.language];
948
+ let template = messages[constraint] || messages.invalidParameter;
949
+ // Replace placeholders
950
+ template = template.replace('{field}', field);
951
+ // Prioritize values from context, then use passed value
952
+ if (context) {
953
+ Object.entries(context).forEach(([key, val]) => {
954
+ template = template.replace(`{${key}}`, this.formatValue(val));
955
+ });
956
+ }
957
+ // If there's still a {value} placeholder and value was passed, replace it
958
+ if (value !== undefined && template.includes('{value}')) {
959
+ template = template.replace('{value}', this.formatValue(value));
960
+ }
961
+ return template;
962
+ }
963
+ /**
964
+ * Format value for message display
965
+ * @private
966
+ */
967
+ formatValue(value) {
968
+ if (value === null)
969
+ return 'null';
970
+ if (value === undefined)
971
+ return 'undefined';
972
+ if (typeof value === 'number')
973
+ return String(value);
974
+ if (typeof value === 'string')
975
+ return `"${value}"`;
976
+ if (Array.isArray(value))
977
+ return `[${value.map(v => this.formatValue(v)).join(', ')}]`;
978
+ if (typeof value === 'object') {
979
+ try {
980
+ return JSON.stringify(value);
981
+ }
982
+ catch {
983
+ // Handle circular references
984
+ return '[Circular Reference]';
985
+ }
986
+ }
987
+ return String(value);
988
+ }
989
+ }
990
+ /**
991
+ * Global error message formatter instance
992
+ */
993
+ const errorFormatter = new ErrorMessageFormatter();
994
+ /**
995
+ * Set global language
996
+ */
997
+ function setValidationLanguage(language) {
998
+ errorFormatter.setLanguage(language);
999
+ }
1000
+ /**
1001
+ * Create validation error
1002
+ */
1003
+ function createValidationError(field, value, constraint, customMessage, context) {
1004
+ const message = customMessage || errorFormatter.formatMessage(constraint, field, value, context);
1005
+ return {
1006
+ field,
1007
+ value,
1008
+ constraint,
1009
+ message,
1010
+ context
1011
+ };
1012
+ }
1013
+ /**
1014
+ * Create validation errors in batch
1015
+ */
1016
+ function createValidationErrors(errors, separator = '; ') {
1017
+ const validationErrors = errors.map(error => createValidationError(error.field, error.value, error.constraint, error.message, error.context));
1018
+ // Generate combined message from all errors
1019
+ const combinedMessage = validationErrors
1020
+ .map(err => err.message)
1021
+ .filter(msg => msg && msg.trim())
1022
+ .join(separator) || 'Validation failed';
1023
+ return new KoattyValidationError(validationErrors, combinedMessage);
1024
+ }
1025
+
823
1026
  /*
824
1027
  * @Description:
825
1028
  * @Usage:
@@ -879,10 +1082,11 @@ class ValidateClass {
879
1082
  * @param {*} Clazz
880
1083
  * @param {*} data
881
1084
  * @param {boolean} [convert=false] auto convert parameters type
1085
+ * @param {ValidationOptions} [options] validation options (returnAllErrors, errorSeparator)
882
1086
  * @returns {Promise<any>}
883
1087
  * @memberof ValidateClass
884
1088
  */
885
- async valid(Clazz, data, convert = false) {
1089
+ async valid(Clazz, data, convert = false, options) {
886
1090
  let obj = {};
887
1091
  if (data instanceof Clazz) {
888
1092
  obj = data;
@@ -898,7 +1102,21 @@ class ValidateClass {
898
1102
  errors = await classValidator.validate(obj, { skipMissingProperties: true });
899
1103
  }
900
1104
  if (errors.length > 0) {
901
- throw new Error(Object.values(errors[0].constraints)[0]);
1105
+ // Check if user wants all errors or just the first one
1106
+ if (options === null || options === void 0 ? void 0 : options.returnAllErrors) {
1107
+ // Throw KoattyValidationError with all error details
1108
+ throw createValidationErrors(errors.map(e => ({
1109
+ field: e.property,
1110
+ value: e.value,
1111
+ constraint: Object.keys(e.constraints || {})[0] || 'unknown',
1112
+ message: Object.values(e.constraints || {})[0] || 'Validation failed',
1113
+ context: e.constraints
1114
+ })), options.errorSeparator || '; ');
1115
+ }
1116
+ else {
1117
+ // Default behavior (backward compatible): return only first error
1118
+ throw new Error(Object.values(errors[0].constraints)[0]);
1119
+ }
902
1120
  }
903
1121
  return obj;
904
1122
  }
@@ -1170,8 +1388,9 @@ function createValidationDecorator(options) {
1170
1388
  },
1171
1389
  defaultMessage(validationArguments) {
1172
1390
  const property = validationArguments.property;
1173
- return defaultMessage
1174
- ? defaultMessage.replace('$property', property)
1391
+ const customMessage = (validationOptions === null || validationOptions === void 0 ? void 0 : validationOptions.message) || defaultMessage;
1392
+ return customMessage
1393
+ ? customMessage.replace('$property', property)
1175
1394
  : `Invalid value for ${property}`;
1176
1395
  }
1177
1396
  }
@@ -1210,204 +1429,6 @@ function createParameterizedDecorator(name, validator, defaultMessage) {
1210
1429
  });
1211
1430
  }
1212
1431
 
1213
- /**
1214
- * Improved error handling mechanism
1215
- * @author richen
1216
- */
1217
- /**
1218
- * Error message internationalization
1219
- */
1220
- const ERROR_MESSAGES = {
1221
- zh: {
1222
- // Chinese localization validation
1223
- IsCnName: '必须是有效的中文姓名',
1224
- IsIdNumber: '必须是有效的身份证号码',
1225
- IsZipCode: '必须是有效的邮政编码',
1226
- IsMobile: '必须是有效的手机号码',
1227
- IsPlateNumber: '必须是有效的车牌号码',
1228
- // Basic validation
1229
- IsNotEmpty: '不能为空',
1230
- IsDate: '必须是有效的日期',
1231
- IsEmail: '必须是有效的邮箱地址',
1232
- IsIP: '必须是有效的IP地址',
1233
- IsPhoneNumber: '必须是有效的电话号码',
1234
- IsUrl: '必须是有效的URL地址',
1235
- IsHash: '必须是有效的哈希值',
1236
- // Comparison validation
1237
- Equals: '必须等于 {comparison}',
1238
- NotEquals: '不能等于 {comparison}',
1239
- Contains: '必须包含 {seed}',
1240
- IsIn: '必须是以下值之一: {possibleValues}',
1241
- IsNotIn: '不能是以下值之一: {possibleValues}',
1242
- Gt: '必须大于 {min}',
1243
- Gte: '必须大于或等于 {min}',
1244
- Lt: '必须小于 {max}',
1245
- Lte: '必须小于或等于 {max}',
1246
- // Common errors
1247
- invalidParameter: '参数 {field} 无效',
1248
- validationFailed: '验证失败',
1249
- },
1250
- en: {
1251
- // Chinese localization validators
1252
- IsCnName: 'must be a valid Chinese name',
1253
- IsIdNumber: 'must be a valid ID number',
1254
- IsZipCode: 'must be a valid zip code',
1255
- IsMobile: 'must be a valid mobile number',
1256
- IsPlateNumber: 'must be a valid plate number',
1257
- // Basic validators
1258
- IsNotEmpty: 'should not be empty',
1259
- IsDate: 'must be a valid date',
1260
- IsEmail: 'must be a valid email',
1261
- IsIP: 'must be a valid IP address',
1262
- IsPhoneNumber: 'must be a valid phone number',
1263
- IsUrl: 'must be a valid URL',
1264
- IsHash: 'must be a valid hash',
1265
- // Comparison validators
1266
- Equals: 'must equal to {comparison}',
1267
- NotEquals: 'should not equal to {comparison}',
1268
- Contains: 'must contain {seed}',
1269
- IsIn: 'must be one of the following values: {possibleValues}',
1270
- IsNotIn: 'should not be one of the following values: {possibleValues}',
1271
- Gt: 'must be greater than {min}',
1272
- Gte: 'must be greater than or equal to {min}',
1273
- Lt: 'must be less than {max}',
1274
- Lte: 'must be less than or equal to {max}',
1275
- // Common errors
1276
- invalidParameter: 'invalid parameter {field}',
1277
- validationFailed: 'validation failed',
1278
- }
1279
- };
1280
- /**
1281
- * Enhanced validation error class
1282
- */
1283
- class KoattyValidationError extends Error {
1284
- constructor(errors, message) {
1285
- const errorMessage = message || 'Validation failed';
1286
- super(errorMessage);
1287
- this.name = 'KoattyValidationError';
1288
- this.errors = errors;
1289
- this.statusCode = 400;
1290
- this.timestamp = new Date();
1291
- // Ensure correct prototype chain
1292
- Object.setPrototypeOf(this, KoattyValidationError.prototype);
1293
- }
1294
- /**
1295
- * Get the first error message
1296
- */
1297
- getFirstError() {
1298
- return this.errors[0];
1299
- }
1300
- /**
1301
- * Get errors for a specific field
1302
- */
1303
- getFieldErrors(field) {
1304
- return this.errors.filter(error => error.field === field);
1305
- }
1306
- /**
1307
- * Convert to JSON format
1308
- */
1309
- toJSON() {
1310
- return {
1311
- name: this.name,
1312
- message: this.message,
1313
- statusCode: this.statusCode,
1314
- timestamp: this.timestamp,
1315
- errors: this.errors
1316
- };
1317
- }
1318
- }
1319
- /**
1320
- * Error message formatter
1321
- */
1322
- class ErrorMessageFormatter {
1323
- constructor(language = 'zh') {
1324
- this.language = 'zh';
1325
- this.language = language;
1326
- }
1327
- /**
1328
- * Set language
1329
- */
1330
- setLanguage(language) {
1331
- this.language = language;
1332
- }
1333
- /**
1334
- * Format error message
1335
- */
1336
- formatMessage(constraint, field, value, context) {
1337
- const messages = ERROR_MESSAGES[this.language];
1338
- let template = messages[constraint] || messages.invalidParameter;
1339
- // Replace placeholders
1340
- template = template.replace('{field}', field);
1341
- // Prioritize values from context, then use passed value
1342
- if (context) {
1343
- Object.entries(context).forEach(([key, val]) => {
1344
- template = template.replace(`{${key}}`, this.formatValue(val));
1345
- });
1346
- }
1347
- // If there's still a {value} placeholder and value was passed, replace it
1348
- if (value !== undefined && template.includes('{value}')) {
1349
- template = template.replace('{value}', this.formatValue(value));
1350
- }
1351
- return template;
1352
- }
1353
- /**
1354
- * Format value for message display
1355
- * @private
1356
- */
1357
- formatValue(value) {
1358
- if (value === null)
1359
- return 'null';
1360
- if (value === undefined)
1361
- return 'undefined';
1362
- if (typeof value === 'number')
1363
- return String(value);
1364
- if (typeof value === 'string')
1365
- return `"${value}"`;
1366
- if (Array.isArray(value))
1367
- return `[${value.map(v => this.formatValue(v)).join(', ')}]`;
1368
- if (typeof value === 'object') {
1369
- try {
1370
- return JSON.stringify(value);
1371
- }
1372
- catch {
1373
- // Handle circular references
1374
- return '[Circular Reference]';
1375
- }
1376
- }
1377
- return String(value);
1378
- }
1379
- }
1380
- /**
1381
- * Global error message formatter instance
1382
- */
1383
- const errorFormatter = new ErrorMessageFormatter();
1384
- /**
1385
- * Set global language
1386
- */
1387
- function setValidationLanguage(language) {
1388
- errorFormatter.setLanguage(language);
1389
- }
1390
- /**
1391
- * Create validation error
1392
- */
1393
- function createValidationError(field, value, constraint, customMessage, context) {
1394
- const message = customMessage || errorFormatter.formatMessage(constraint, field, value, context);
1395
- return {
1396
- field,
1397
- value,
1398
- constraint,
1399
- message,
1400
- context
1401
- };
1402
- }
1403
- /**
1404
- * Create validation errors in batch
1405
- */
1406
- function createValidationErrors(errors) {
1407
- const validationErrors = errors.map(error => createValidationError(error.field, error.value, error.constraint, error.message, error.context));
1408
- return new KoattyValidationError(validationErrors);
1409
- }
1410
-
1411
1432
  /**
1412
1433
  * Refactored decorator definitions - Using factory functions to eliminate duplication
1413
1434
  * @author richen
@@ -1434,7 +1455,10 @@ const Lt = createParameterizedDecorator('Lt', (value, max) => helper__namespace.
1434
1455
  const Lte = createParameterizedDecorator('Lte', (value, max) => helper__namespace.toNumber(value) <= max, 'must be less than or equal to $constraint1');
1435
1456
  // 复杂验证装饰器(需要特殊处理)
1436
1457
  function IsEmail(options, validationOptions) {
1437
- return createParameterizedDecorator('IsEmail', (value) => classValidator.isEmail(value, options), 'must be a valid email')(validationOptions);
1458
+ // Handle case where options is actually ValidationOptions (message only)
1459
+ const actualOptions = (options === null || options === void 0 ? void 0 : options.message) ? {} : options;
1460
+ const actualValidationOptions = (options === null || options === void 0 ? void 0 : options.message) ? options : validationOptions;
1461
+ return createParameterizedDecorator('IsEmail', (value) => classValidator.isEmail(value, actualOptions), 'must be a valid email')(actualValidationOptions);
1438
1462
  }
1439
1463
  function IsIP(version, validationOptions) {
1440
1464
  return createParameterizedDecorator('IsIP', (value) => classValidator.isIP(value, version), 'must be a valid IP address')(validationOptions);