schema-dsl 1.2.5 → 2.0.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.
Files changed (243) hide show
  1. package/CHANGELOG.md +130 -238
  2. package/LICENSE +21 -21
  3. package/README.md +628 -2486
  4. package/dist/DslBuilder-BIgQOAXp.d.ts +343 -0
  5. package/dist/DslBuilder-CjHTucNQ.d.cts +343 -0
  6. package/dist/Validator-CllRdrY0.d.ts +192 -0
  7. package/dist/Validator-D6okG9tr.d.cts +192 -0
  8. package/dist/index.cjs +6640 -0
  9. package/dist/index.d.cts +1151 -0
  10. package/dist/index.d.ts +1151 -0
  11. package/dist/index.js +6574 -0
  12. package/dist/plugin-CIKtTMtS.d.cts +246 -0
  13. package/dist/plugin-CIKtTMtS.d.ts +246 -0
  14. package/dist/plugins/custom-format.cjs +3818 -0
  15. package/dist/plugins/custom-format.d.cts +12 -0
  16. package/dist/plugins/custom-format.d.ts +12 -0
  17. package/dist/plugins/custom-format.js +3788 -0
  18. package/dist/plugins/custom-type-example.cjs +3811 -0
  19. package/dist/plugins/custom-type-example.d.cts +8 -0
  20. package/dist/plugins/custom-type-example.d.ts +8 -0
  21. package/dist/plugins/custom-type-example.js +3781 -0
  22. package/dist/plugins/custom-validator.cjs +144 -0
  23. package/dist/plugins/custom-validator.d.cts +10 -0
  24. package/dist/plugins/custom-validator.d.ts +10 -0
  25. package/dist/plugins/custom-validator.js +119 -0
  26. package/docs/FEATURE-INDEX.md +553 -519
  27. package/docs/add-custom-locale.md +496 -483
  28. package/docs/add-keyword.md +24 -0
  29. package/docs/api-reference.md +1047 -805
  30. package/docs/api.md +13 -0
  31. package/docs/best-practices-project-structure.md +417 -408
  32. package/docs/best-practices.md +712 -672
  33. package/docs/cache-manager.md +344 -336
  34. package/docs/compile.md +45 -0
  35. package/docs/conditional-api.md +1307 -1278
  36. package/docs/custom-extensions-guide.md +339 -411
  37. package/docs/design-philosophy.md +606 -601
  38. package/docs/doc-index.md +324 -0
  39. package/docs/dsl-syntax.md +714 -664
  40. package/docs/dynamic-locale.md +608 -598
  41. package/docs/enum.md +482 -475
  42. package/docs/error-handling.md +1975 -1966
  43. package/docs/export-guide.md +501 -462
  44. package/docs/export-limitations.md +567 -551
  45. package/docs/faq.md +596 -577
  46. package/docs/frontend-i18n-guide.md +307 -293
  47. package/docs/i18n-user-guide.md +487 -474
  48. package/docs/i18n.md +476 -457
  49. package/docs/index.md +48 -0
  50. package/docs/json-schema-basics.md +40 -0
  51. package/docs/label-vs-description.md +271 -262
  52. package/docs/markdown-exporter.md +406 -397
  53. package/docs/mongodb-exporter.md +302 -295
  54. package/docs/multi-language.md +26 -0
  55. package/docs/multi-type-support.md +322 -329
  56. package/docs/mysql-exporter.md +280 -273
  57. package/docs/number-operators.md +449 -442
  58. package/docs/optional-marker-guide.md +326 -321
  59. package/docs/performance-guide.md +49 -0
  60. package/docs/plugin-system.md +381 -542
  61. package/docs/plugin-type-registration.md +34 -0
  62. package/docs/postgresql-exporter.md +311 -304
  63. package/docs/public/favicon.svg +5 -0
  64. package/docs/quick-start.md +435 -761
  65. package/docs/runtime-locale-support.md +532 -521
  66. package/docs/schema-helper.md +345 -340
  67. package/docs/schema-utils-advanced-issues.md +23 -0
  68. package/docs/schema-utils-best-practices.md +20 -0
  69. package/docs/schema-utils-chaining.md +150 -143
  70. package/docs/schema-utils.md +524 -490
  71. package/docs/security-checklist.md +20 -0
  72. package/docs/string-extensions.md +488 -480
  73. package/docs/troubleshooting.md +486 -471
  74. package/docs/type-converter.md +310 -319
  75. package/docs/type-reference.md +242 -219
  76. package/docs/typescript-guide.md +584 -573
  77. package/docs/union-type-guide.md +157 -147
  78. package/docs/union-types.md +284 -277
  79. package/docs/validate-async.md +491 -480
  80. package/docs/validate-batch.md +49 -0
  81. package/docs/validate-dsl-object-support.md +578 -573
  82. package/docs/validate.md +506 -486
  83. package/docs/validation-guide.md +502 -484
  84. package/docs/validator.md +39 -0
  85. package/package.json +131 -73
  86. package/plugins/custom-format.cjs +8 -0
  87. package/plugins/custom-type-example.cjs +8 -0
  88. package/plugins/custom-validator.cjs +8 -0
  89. package/src/adapters/DslAdapter.ts +111 -0
  90. package/src/adapters/index.ts +1 -0
  91. package/src/config/constants.ts +83 -0
  92. package/src/config/index.ts +2 -0
  93. package/src/config/patterns.ts +77 -0
  94. package/src/core/CacheManager.ts +169 -0
  95. package/src/core/ConditionalBuilder.ts +382 -0
  96. package/src/core/ConditionalRuntime.ts +28 -0
  97. package/src/core/ConditionalValidator.ts +255 -0
  98. package/src/core/DslBuilder.ts +687 -0
  99. package/src/core/ErrorCodes.ts +38 -0
  100. package/src/core/ErrorFormatter.ts +271 -0
  101. package/src/core/JSONSchemaCore.ts +65 -0
  102. package/src/core/Locale.ts +187 -0
  103. package/src/core/MessageTemplate.ts +42 -0
  104. package/src/core/ObjectDslBuilder.ts +64 -0
  105. package/src/core/PluginManager.ts +326 -0
  106. package/src/core/StringExtensions.ts +140 -0
  107. package/src/core/TemplateEngine.ts +44 -0
  108. package/src/core/Validator.ts +448 -0
  109. package/src/errors/I18nError.ts +159 -0
  110. package/src/errors/ValidationError.ts +105 -0
  111. package/src/exporters/BaseExporter.ts +60 -0
  112. package/src/exporters/MarkdownExporter.ts +305 -0
  113. package/src/exporters/MongoDBExporter.ts +126 -0
  114. package/src/exporters/MySQLExporter.ts +156 -0
  115. package/src/exporters/PostgreSQLExporter.ts +222 -0
  116. package/src/exporters/index.ts +18 -0
  117. package/src/index.ts +651 -0
  118. package/{lib/locales/en-US.js → src/locales/en-US.ts} +160 -176
  119. package/{lib/locales/es-ES.js → src/locales/es-ES.ts} +160 -113
  120. package/{lib/locales/fr-FR.js → src/locales/fr-FR.ts} +160 -113
  121. package/src/locales/index.ts +103 -0
  122. package/{lib/locales/ja-JP.js → src/locales/ja-JP.ts} +160 -118
  123. package/src/locales/types.ts +156 -0
  124. package/{lib/locales/zh-CN.js → src/locales/zh-CN.ts} +160 -177
  125. package/src/parser/ConstraintParser.ts +101 -0
  126. package/src/parser/DslParser.ts +470 -0
  127. package/src/parser/SchemaCompiler.ts +66 -0
  128. package/src/parser/TypeRegistry.ts +250 -0
  129. package/src/parser/index.ts +6 -0
  130. package/src/plugins/custom-format.ts +124 -0
  131. package/src/plugins/custom-type-example.ts +106 -0
  132. package/src/plugins/custom-validator.ts +138 -0
  133. package/src/types/conditional.ts +28 -0
  134. package/src/types/config.ts +59 -0
  135. package/src/types/dsl.ts +131 -0
  136. package/src/types/error.ts +60 -0
  137. package/src/types/index.ts +17 -0
  138. package/src/types/infer.ts +128 -0
  139. package/src/types/plugin.ts +58 -0
  140. package/src/types/safe-regex.d.ts +9 -0
  141. package/src/types/schema.ts +66 -0
  142. package/src/types/validate.ts +71 -0
  143. package/src/utils/SchemaHelper.ts +196 -0
  144. package/src/utils/SchemaUtils.ts +365 -0
  145. package/src/utils/TypeConverter.ts +215 -0
  146. package/src/utils/index.ts +10 -0
  147. package/src/validators/CustomKeywords.ts +477 -0
  148. package/.eslintignore +0 -11
  149. package/.eslintrc.json +0 -27
  150. package/CONTRIBUTING.md +0 -368
  151. package/STATUS.md +0 -491
  152. package/changelogs/v1.0.0.md +0 -328
  153. package/changelogs/v1.0.9.md +0 -367
  154. package/changelogs/v1.1.0.md +0 -389
  155. package/changelogs/v1.1.1.md +0 -308
  156. package/changelogs/v1.1.2.md +0 -183
  157. package/changelogs/v1.1.3.md +0 -161
  158. package/changelogs/v1.1.4.md +0 -432
  159. package/changelogs/v1.1.5.md +0 -493
  160. package/changelogs/v1.1.6.md +0 -211
  161. package/changelogs/v1.1.8.md +0 -376
  162. package/changelogs/v1.2.3.md +0 -124
  163. package/docs/INDEX.md +0 -252
  164. package/docs/issues-resolved-summary.md +0 -196
  165. package/docs/performance-benchmark-report.md +0 -179
  166. package/docs/performance-quick-reference.md +0 -123
  167. package/docs/user-questions-answered.md +0 -353
  168. package/docs/validation-rules-v1.0.2.md +0 -1608
  169. package/examples/README.md +0 -81
  170. package/examples/array-dsl-example.js +0 -227
  171. package/examples/conditional-example.js +0 -288
  172. package/examples/conditional-non-object.js +0 -129
  173. package/examples/conditional-validate-example.js +0 -321
  174. package/examples/custom-extension.js +0 -85
  175. package/examples/dsl-match-example.js +0 -74
  176. package/examples/dsl-style.js +0 -118
  177. package/examples/dynamic-locale-configuration.js +0 -348
  178. package/examples/dynamic-locale-example.js +0 -287
  179. package/examples/enum.examples.js +0 -324
  180. package/examples/export-demo.js +0 -130
  181. package/examples/express-integration.js +0 -376
  182. package/examples/i18n-error-handling-complete.js +0 -381
  183. package/examples/i18n-error-handling-quickstart.md +0 -0
  184. package/examples/i18n-error.examples.js +0 -181
  185. package/examples/i18n-full-demo.js +0 -301
  186. package/examples/i18n-memory-safety.examples.js +0 -268
  187. package/examples/markdown-export.js +0 -71
  188. package/examples/middleware-usage.js +0 -93
  189. package/examples/new-features-comparison.js +0 -315
  190. package/examples/password-reset/README.md +0 -153
  191. package/examples/password-reset/schema.js +0 -26
  192. package/examples/password-reset/test.js +0 -101
  193. package/examples/plugin-system.examples.js +0 -205
  194. package/examples/schema-utils-chaining.examples.js +0 -250
  195. package/examples/simple-example.js +0 -122
  196. package/examples/slug.examples.js +0 -179
  197. package/examples/string-extensions.js +0 -297
  198. package/examples/union-type-example.js +0 -127
  199. package/examples/union-types-example.js +0 -77
  200. package/examples/user-registration/README.md +0 -156
  201. package/examples/user-registration/routes.js +0 -92
  202. package/examples/user-registration/schema.js +0 -150
  203. package/examples/user-registration/server.js +0 -74
  204. package/index.d.ts +0 -3658
  205. package/index.js +0 -475
  206. package/index.mjs +0 -60
  207. package/lib/adapters/DslAdapter.js +0 -995
  208. package/lib/adapters/index.js +0 -20
  209. package/lib/config/constants.js +0 -286
  210. package/lib/config/patterns/common.js +0 -47
  211. package/lib/config/patterns/creditCard.js +0 -9
  212. package/lib/config/patterns/idCard.js +0 -9
  213. package/lib/config/patterns/index.js +0 -9
  214. package/lib/config/patterns/licensePlate.js +0 -4
  215. package/lib/config/patterns/passport.js +0 -4
  216. package/lib/config/patterns/phone.js +0 -9
  217. package/lib/config/patterns/postalCode.js +0 -5
  218. package/lib/core/CacheManager.js +0 -376
  219. package/lib/core/ConditionalBuilder.js +0 -503
  220. package/lib/core/DslBuilder.js +0 -1589
  221. package/lib/core/ErrorCodes.js +0 -233
  222. package/lib/core/ErrorFormatter.js +0 -445
  223. package/lib/core/JSONSchemaCore.js +0 -347
  224. package/lib/core/Locale.js +0 -130
  225. package/lib/core/MessageTemplate.js +0 -98
  226. package/lib/core/PluginManager.js +0 -448
  227. package/lib/core/StringExtensions.js +0 -240
  228. package/lib/core/Validator.js +0 -654
  229. package/lib/errors/I18nError.js +0 -328
  230. package/lib/errors/ValidationError.js +0 -191
  231. package/lib/exporters/MarkdownExporter.js +0 -420
  232. package/lib/exporters/MongoDBExporter.js +0 -162
  233. package/lib/exporters/MySQLExporter.js +0 -212
  234. package/lib/exporters/PostgreSQLExporter.js +0 -289
  235. package/lib/exporters/index.js +0 -24
  236. package/lib/locales/index.js +0 -8
  237. package/lib/utils/LRUCache.js +0 -174
  238. package/lib/utils/SchemaHelper.js +0 -240
  239. package/lib/utils/SchemaUtils.js +0 -445
  240. package/lib/utils/TypeConverter.js +0 -245
  241. package/lib/utils/index.js +0 -13
  242. package/lib/validators/CustomKeywords.js +0 -616
  243. package/lib/validators/index.js +0 -11
@@ -0,0 +1,477 @@
1
+ import type { Ajv, ErrorObject } from 'ajv'
2
+ import safeRegex from 'safe-regex'
3
+ import { Locale } from '../core/Locale.js'
4
+
5
+
6
+ // AJV DataValidateFunction compatible type
7
+ type ValidateFnWithErrors = ((schema: unknown, data: unknown, parentSchema?: unknown) => boolean) & {
8
+ errors?: Partial<ErrorObject>[]
9
+ }
10
+
11
+ /**
12
+ * CustomKeywords — AJV custom keyword registrar
13
+ *
14
+ * Fixes:
15
+ * CK-01: internally uses getMessageText() to obtain strings, avoiding v1 compat objects
16
+ * that serialized as "[object Object]"
17
+ * CK-02: regex keyword error messages use locale keys instead of concatenating raw messages
18
+ * CK-Y04: exactLength uses Unicode code-point counting ([...str].length) instead of
19
+ * str.length, correctly handling emoji and multi-byte characters
20
+ */
21
+ export class CustomKeywords {
22
+ /**
23
+ * Register all custom keywords on an AJV instance
24
+ */
25
+ static registerAll(ajv: Ajv): void {
26
+ CustomKeywords.registerRegexKeyword(ajv)
27
+ CustomKeywords.registerFunctionKeyword(ajv)
28
+ CustomKeywords.registerCustomValidatorsKeyword(ajv)
29
+ CustomKeywords.registerMetadataKeywords(ajv)
30
+ CustomKeywords.registerStringValidators(ajv)
31
+ CustomKeywords.registerNumberValidators(ajv)
32
+ CustomKeywords.registerObjectValidators(ajv)
33
+ CustomKeywords.registerArrayValidators(ajv)
34
+ CustomKeywords.registerDateValidators(ajv)
35
+ }
36
+
37
+ // ─── Metadata keywords ──────────────────────────────────────────────────
38
+
39
+ static registerMetadataKeywords(ajv: Ajv): void {
40
+ ajv.addKeyword({ keyword: '_label', metaSchema: { type: 'string' } })
41
+ ajv.addKeyword({ keyword: '_customMessages', metaSchema: { type: 'object' } })
42
+ ajv.addKeyword({ keyword: '_description', metaSchema: { type: 'string' } })
43
+ ajv.addKeyword({ keyword: '_whenConditions', metaSchema: { type: 'array' } })
44
+ ajv.addKeyword({ keyword: '_required', metaSchema: { type: 'boolean' } })
45
+ // Conditional schema marker: prevents AJV strict mode from throwing an unknown-keyword error
46
+ ajv.addKeyword({ keyword: '_isConditional', metaSchema: { type: 'boolean' } })
47
+ ajv.addKeyword({ keyword: '_runtimeOnlyConditional', metaSchema: { type: 'boolean' } })
48
+ ajv.addKeyword({ keyword: 'conditions' })
49
+ ajv.addKeyword({ keyword: '_evaluateCondition' })
50
+ }
51
+
52
+ // ─── _customValidators ──────────────────────────────────────────────────
53
+
54
+ static registerCustomValidatorsKeyword(ajv: Ajv): void {
55
+ const validate: ValidateFnWithErrors = (validators: unknown, data: unknown): boolean => {
56
+ if (!Array.isArray(validators)) return true
57
+
58
+ for (const validator of validators as unknown[]) {
59
+ if (typeof validator !== 'function') continue
60
+ try {
61
+ const result = (validator as (d: unknown) => unknown)(data)
62
+
63
+ if (result instanceof Promise) {
64
+ // BC-6: async validators are not supported in the synchronous AJV validate() path.
65
+ // Return an explicit error so callers know to use validateAsync() instead.
66
+ validate.errors = [{
67
+ keyword: '_customValidators',
68
+ message: 'Async validation not supported in sync validate(). Use validateAsync() instead.',
69
+ params: {},
70
+ }]
71
+ return false
72
+ }
73
+
74
+ if (result === false) {
75
+ const msg = Locale.getMessageText('CUSTOM_VALIDATION_FAILED')
76
+ validate.errors = [{ keyword: '_customValidators', message: msg, params: {} }]
77
+ return false
78
+ }
79
+ if (typeof result === 'string') {
80
+ validate.errors = [{ keyword: '_customValidators', message: result, params: {} }]
81
+ return false
82
+ }
83
+ if (result !== null && typeof result === 'object' && (result as Record<string, unknown>)['error']) {
84
+ const msg = String((result as Record<string, unknown>)['message'] ?? Locale.getMessageText('CUSTOM_VALIDATION_FAILED'))
85
+ validate.errors = [{ keyword: '_customValidators', message: msg, params: {} }]
86
+ return false
87
+ }
88
+ } catch (error) {
89
+ const msg = error instanceof Error ? error.message : String(error)
90
+ validate.errors = [{ keyword: '_customValidators', message: msg, params: {} }]
91
+ return false
92
+ }
93
+ }
94
+ return true
95
+ }
96
+
97
+ ajv.addKeyword({ keyword: '_customValidators', validate, errors: true })
98
+ }
99
+
100
+ // ─── regex ──────────────────────────────────────────────────────────────
101
+
102
+ // Detect potentially catastrophic patterns via a dedicated regex safety analyzer
103
+ private static _isUnsafePattern(pattern: string | RegExp): boolean {
104
+ return !safeRegex(pattern)
105
+ }
106
+
107
+ static registerRegexKeyword(ajv: Ajv): void {
108
+ const validate: ValidateFnWithErrors = (schema: unknown, data: unknown): boolean => {
109
+ const patternStr = String(schema)
110
+ try {
111
+ const regex = new RegExp(patternStr)
112
+ if (CustomKeywords._isUnsafePattern(regex)) {
113
+ validate.errors = [{
114
+ keyword: 'regex',
115
+ message: Locale.getMessageText('string.pattern'),
116
+ params: { pattern: patternStr, reason: 'unsafe regex pattern' },
117
+ }]
118
+ return false
119
+ }
120
+ if (regex.test(String(data))) return true
121
+ // CK-02 fix: use locale key instead of concatenating raw error message
122
+ validate.errors = [{
123
+ keyword: 'regex',
124
+ message: Locale.getMessageText('string.pattern'),
125
+ params: { pattern: schema },
126
+ }]
127
+ return false
128
+ } catch (error) {
129
+ // CK-02 fix: invalid regex also uses locale key
130
+ validate.errors = [{
131
+ keyword: 'regex',
132
+ message: Locale.getMessageText('string.pattern'),
133
+ params: { error: error instanceof Error ? error.message : String(error) },
134
+ }]
135
+ return false
136
+ }
137
+ }
138
+
139
+ ajv.addKeyword({ keyword: 'regex', type: 'string', schemaType: 'string', validate, errors: true })
140
+ }
141
+
142
+ // ─── validate (function validator) ──────────────────────────────────────
143
+
144
+ static registerFunctionKeyword(ajv: Ajv): void {
145
+ const validate: ValidateFnWithErrors = (schema: unknown, data: unknown): boolean => {
146
+ if (typeof schema !== 'function') {
147
+ validate.errors = [{
148
+ keyword: 'validate',
149
+ message: Locale.getMessageText('VALIDATE_MUST_BE_FUNCTION'),
150
+ params: {},
151
+ }]
152
+ return false
153
+ }
154
+
155
+ try {
156
+ const result = (schema as (d: unknown) => unknown)(data)
157
+ if (typeof result === 'boolean') return result
158
+ if (result !== null && typeof result === 'object') {
159
+ const res = result as Record<string, unknown>
160
+ if (typeof res['valid'] === 'boolean') {
161
+ if (!res['valid'] && res['message']) {
162
+ validate.errors = [{
163
+ keyword: 'validate',
164
+ message: String(res['message']),
165
+ params: {},
166
+ }]
167
+ }
168
+ return res['valid'] as boolean
169
+ }
170
+ }
171
+ return true
172
+ } catch (error) {
173
+ validate.errors = [{
174
+ keyword: 'validate',
175
+ message: error instanceof Error ? error.message : String(error),
176
+ params: {},
177
+ }]
178
+ return false
179
+ }
180
+ }
181
+
182
+ ajv.addKeyword({ keyword: 'validate', validate, errors: true })
183
+ }
184
+
185
+ // ─── String validators ───────────────────────────────────────────────────
186
+
187
+ static registerStringValidators(ajv: Ajv): void {
188
+ // exactLength — exact string length (CK-Y04 fix: Unicode code-point counting)
189
+ const exactLength: ValidateFnWithErrors = (schema: unknown, data: unknown): boolean => {
190
+ // CK-Y04: use spread iterator for counting — correctly handles emoji / multi-byte Unicode
191
+ const codePointLength = [...String(data)].length
192
+ if (codePointLength !== Number(schema)) {
193
+ exactLength.errors = [{
194
+ keyword: 'exactLength',
195
+ message: Locale.getMessageText('string.length'),
196
+ params: { limit: schema },
197
+ }]
198
+ return false
199
+ }
200
+ return true
201
+ }
202
+ ajv.addKeyword({ keyword: 'exactLength', type: 'string', schemaType: 'number', validate: exactLength, errors: true })
203
+
204
+ // alphanum
205
+ const alphanum: ValidateFnWithErrors = (schema: unknown, data: unknown): boolean => {
206
+ if (schema && !/^[a-zA-Z0-9]*$/.test(String(data))) {
207
+ alphanum.errors = [{ keyword: 'alphanum', message: Locale.getMessageText('string.alphanum'), params: {} }]
208
+ return false
209
+ }
210
+ return true
211
+ }
212
+ ajv.addKeyword({ keyword: 'alphanum', type: 'string', schemaType: 'boolean', validate: alphanum, errors: true })
213
+
214
+ // trim
215
+ const trim: ValidateFnWithErrors = (schema: unknown, data: unknown): boolean => {
216
+ const str = String(data)
217
+ if (schema && str !== str.trim()) {
218
+ trim.errors = [{ keyword: 'trim', message: Locale.getMessageText('string.trim'), params: {} }]
219
+ return false
220
+ }
221
+ return true
222
+ }
223
+ ajv.addKeyword({ keyword: 'trim', type: 'string', schemaType: 'boolean', validate: trim, errors: true })
224
+
225
+ // lowercase
226
+ const lowercase: ValidateFnWithErrors = (schema: unknown, data: unknown): boolean => {
227
+ const str = String(data)
228
+ if (schema && str !== str.toLowerCase()) {
229
+ lowercase.errors = [{ keyword: 'lowercase', message: Locale.getMessageText('string.lowercase'), params: {} }]
230
+ return false
231
+ }
232
+ return true
233
+ }
234
+ ajv.addKeyword({ keyword: 'lowercase', type: 'string', schemaType: 'boolean', validate: lowercase, errors: true })
235
+
236
+ // uppercase
237
+ const uppercase: ValidateFnWithErrors = (schema: unknown, data: unknown): boolean => {
238
+ const str = String(data)
239
+ if (schema && str !== str.toUpperCase()) {
240
+ uppercase.errors = [{ keyword: 'uppercase', message: Locale.getMessageText('string.uppercase'), params: {} }]
241
+ return false
242
+ }
243
+ return true
244
+ }
245
+ ajv.addKeyword({ keyword: 'uppercase', type: 'string', schemaType: 'boolean', validate: uppercase, errors: true })
246
+
247
+ // jsonString
248
+ const jsonString: ValidateFnWithErrors = (schema: unknown, data: unknown): boolean => {
249
+ if (schema) {
250
+ try {
251
+ JSON.parse(String(data))
252
+ } catch {
253
+ jsonString.errors = [{ keyword: 'jsonString', message: Locale.getMessageText('pattern.json'), params: {} }]
254
+ return false
255
+ }
256
+ }
257
+ return true
258
+ }
259
+ ajv.addKeyword({ keyword: 'jsonString', type: 'string', schemaType: 'boolean', validate: jsonString, errors: true })
260
+ }
261
+
262
+ // ─── Number validators ───────────────────────────────────────────────────
263
+
264
+ static registerNumberValidators(ajv: Ajv): void {
265
+ // precision — decimal place limit
266
+ const precision: ValidateFnWithErrors = (schema: unknown, data: unknown): boolean => {
267
+ const n = data as number
268
+ const limit = Number(schema)
269
+ const factor = Math.pow(10, limit)
270
+ const shifted = n * factor
271
+ // Epsilon-based check handles floating-point artifacts (e.g. 0.1+0.2 = 0.30000000000000004)
272
+ if (Math.abs(shifted - Math.round(shifted)) > 1e-10) {
273
+ precision.errors = [{ keyword: 'precision', message: Locale.getMessageText('number.precision'), params: { limit: schema } }]
274
+ return false
275
+ }
276
+ return true
277
+ }
278
+ ajv.addKeyword({ keyword: 'precision', type: 'number', schemaType: 'number', validate: precision, errors: true })
279
+
280
+ // port — port number validation (1-65535)
281
+ const port: ValidateFnWithErrors = (schema: unknown, data: unknown): boolean => {
282
+ const num = data as number
283
+ if (schema && (!Number.isInteger(num) || num < 1 || num > 65535)) {
284
+ port.errors = [{ keyword: 'port', message: Locale.getMessageText('number.port'), params: {} }]
285
+ return false
286
+ }
287
+ return true
288
+ }
289
+ ajv.addKeyword({ keyword: 'port', type: ['integer', 'number'], schemaType: 'boolean', validate: port, errors: true })
290
+ }
291
+
292
+ // ─── Object validators ──────────────────────────────────────────────────
293
+
294
+ static registerObjectValidators(ajv: Ajv): void {
295
+ // requiredAll — require all defined properties to be present
296
+ const requiredAll: ValidateFnWithErrors = (schema: unknown, data: unknown, parentSchema?: unknown): boolean => {
297
+ if (!schema) return true
298
+ const props = ((parentSchema as Record<string, unknown>)?.['properties'] as Record<string, unknown>) ?? {}
299
+ const missingKeys = Object.keys(props).filter(k => !(k in (data as Record<string, unknown>)))
300
+ if (missingKeys.length > 0) {
301
+ requiredAll.errors = [{
302
+ keyword: 'requiredAll',
303
+ message: Locale.getMessageText('object.missing'),
304
+ params: { missing: missingKeys },
305
+ }]
306
+ return false
307
+ }
308
+ return true
309
+ }
310
+ ajv.addKeyword({ keyword: 'requiredAll', type: 'object', schemaType: 'boolean', validate: requiredAll, errors: true })
311
+
312
+ // strictSchema — disallow extra properties
313
+ const strictSchema: ValidateFnWithErrors = (schema: unknown, data: unknown, parentSchema?: unknown): boolean => {
314
+ if (!schema) return true
315
+ const props = ((parentSchema as Record<string, unknown>)?.['properties'] as Record<string, unknown>) ?? {}
316
+ const allowedKeys = Object.keys(props)
317
+ const extraKeys = Object.keys(data as Record<string, unknown>).filter(k => !allowedKeys.includes(k))
318
+ if (extraKeys.length > 0) {
319
+ strictSchema.errors = [{
320
+ keyword: 'strictSchema',
321
+ message: Locale.getMessageText('object.schema'),
322
+ params: { extra: extraKeys },
323
+ }]
324
+ return false
325
+ }
326
+ return true
327
+ }
328
+ ajv.addKeyword({ keyword: 'strictSchema', type: 'object', schemaType: 'boolean', validate: strictSchema, errors: true })
329
+ }
330
+
331
+ // ─── Array validators ───────────────────────────────────────────────────
332
+
333
+ private static _deepEqual(a: unknown, b: unknown): boolean {
334
+ if (a === b) return true
335
+ if (a === null || b === null || typeof a !== typeof b) return false
336
+ if (typeof a !== 'object') return false
337
+ if (Array.isArray(a) !== Array.isArray(b)) return false
338
+ if (Array.isArray(a)) {
339
+ if ((a as unknown[]).length !== (b as unknown[]).length) return false
340
+ return (a as unknown[]).every((item, i) => CustomKeywords._deepEqual(item, (b as unknown[])[i]))
341
+ }
342
+ const aKeys = Object.keys(a as object).sort()
343
+ const bKeys = Object.keys(b as object).sort()
344
+ if (aKeys.length !== bKeys.length) return false
345
+ return aKeys.every(k =>
346
+ CustomKeywords._deepEqual((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k])
347
+ )
348
+ }
349
+
350
+ static registerArrayValidators(ajv: Ajv): void {
351
+ // noSparse — disallow sparse arrays
352
+ const noSparse: ValidateFnWithErrors = (schema: unknown, data: unknown): boolean => {
353
+ const arr = data as unknown[]
354
+ if (schema) {
355
+ for (let i = 0; i < arr.length; i++) {
356
+ if (!(i in arr)) {
357
+ noSparse.errors = [{
358
+ keyword: 'noSparse',
359
+ message: Locale.getMessageText('array.sparse'),
360
+ params: { index: i },
361
+ }]
362
+ return false
363
+ }
364
+ }
365
+ }
366
+ return true
367
+ }
368
+ ajv.addKeyword({ keyword: 'noSparse', type: 'array', schemaType: 'boolean', validate: noSparse, errors: true })
369
+
370
+ // includesRequired — must include specified elements
371
+ const includesRequired: ValidateFnWithErrors = (schema: unknown, data: unknown): boolean => {
372
+ if (!Array.isArray(schema) || schema.length === 0) return true
373
+ const arr = data as unknown[]
374
+ const missing = (schema as unknown[]).filter(required => {
375
+ return !arr.some(item => {
376
+ if (typeof required === 'object' && required !== null) {
377
+ return CustomKeywords._deepEqual(item, required)
378
+ }
379
+ return item === required
380
+ })
381
+ })
382
+ if (missing.length > 0) {
383
+ includesRequired.errors = [{
384
+ keyword: 'includesRequired',
385
+ message: Locale.getMessageText('array.includesRequired'),
386
+ params: { missing },
387
+ }]
388
+ return false
389
+ }
390
+ return true
391
+ }
392
+ ajv.addKeyword({ keyword: 'includesRequired', type: 'array', schemaType: 'array', validate: includesRequired, errors: true })
393
+ }
394
+
395
+ // ─── Date validators ────────────────────────────────────────────────────
396
+
397
+ static registerDateValidators(ajv: Ajv): void {
398
+ const DATE_FORMATS: Record<string, RegExp> = {
399
+ 'YYYY-MM-DD': /^\d{4}-\d{2}-\d{2}$/,
400
+ 'YYYY/MM/DD': /^\d{4}\/\d{2}\/\d{2}$/,
401
+ 'DD-MM-YYYY': /^\d{2}-\d{2}-\d{4}$/,
402
+ 'DD/MM/YYYY': /^\d{2}\/\d{2}\/\d{4}$/,
403
+ 'ISO8601': /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/,
404
+ }
405
+
406
+ // dateFormat
407
+ const dateFormat: ValidateFnWithErrors = (schema: unknown, data: unknown): boolean => {
408
+ const fmt = String(schema)
409
+ const pattern = DATE_FORMATS[fmt]
410
+ const str = String(data)
411
+ if (!pattern || !pattern.test(str)) {
412
+ dateFormat.errors = [{
413
+ keyword: 'dateFormat',
414
+ message: Locale.getMessageText('date.format'),
415
+ params: { format: schema },
416
+ }]
417
+ return false
418
+ }
419
+ // Calendar validity: extract components based on format and verify via Date
420
+ const sep = /[-/]/.exec(str)?.[0] ?? '-'
421
+ const parts = str.split(sep)
422
+ let y: number, m: number, dd: number
423
+ if (fmt === 'DD-MM-YYYY' || fmt === 'DD/MM/YYYY') {
424
+ [dd, m, y] = [parseInt(parts[0], 10), parseInt(parts[1], 10), parseInt(parts[2], 10)]
425
+ } else if (fmt === 'ISO8601') {
426
+ const d2 = new Date(str)
427
+ if (isNaN(d2.getTime())) {
428
+ dateFormat.errors = [{ keyword: 'dateFormat', message: Locale.getMessageText('date.format'), params: { format: schema } }]
429
+ return false
430
+ }
431
+ return true
432
+ } else {
433
+ [y, m, dd] = [parseInt(parts[0], 10), parseInt(parts[1], 10), parseInt(parts[2], 10)]
434
+ }
435
+ // Verify the date exists (e.g., reject 2024-13-99, 2024-02-31)
436
+ const probe = new Date(y, m - 1, dd)
437
+ if (probe.getFullYear() !== y || probe.getMonth() !== m - 1 || probe.getDate() !== dd) {
438
+ dateFormat.errors = [{ keyword: 'dateFormat', message: Locale.getMessageText('date.format'), params: { format: schema } }]
439
+ return false
440
+ }
441
+ return true
442
+ }
443
+ ajv.addKeyword({ keyword: 'dateFormat', type: 'string', schemaType: 'string', validate: dateFormat, errors: true })
444
+
445
+ // dateGreater
446
+ const dateGreater: ValidateFnWithErrors = (schema: unknown, data: unknown): boolean => {
447
+ const dataDate = new Date(String(data))
448
+ const compareDate = new Date(String(schema))
449
+ if (isNaN(dataDate.getTime()) || isNaN(compareDate.getTime()) || dataDate <= compareDate) {
450
+ dateGreater.errors = [{
451
+ keyword: 'dateGreater',
452
+ message: Locale.getMessageText('date.greater'),
453
+ params: { limit: schema },
454
+ }]
455
+ return false
456
+ }
457
+ return true
458
+ }
459
+ ajv.addKeyword({ keyword: 'dateGreater', type: 'string', schemaType: 'string', validate: dateGreater, errors: true })
460
+
461
+ // dateLess
462
+ const dateLess: ValidateFnWithErrors = (schema: unknown, data: unknown): boolean => {
463
+ const dataDate = new Date(String(data))
464
+ const compareDate = new Date(String(schema))
465
+ if (isNaN(dataDate.getTime()) || isNaN(compareDate.getTime()) || dataDate >= compareDate) {
466
+ dateLess.errors = [{
467
+ keyword: 'dateLess',
468
+ message: Locale.getMessageText('date.less'),
469
+ params: { limit: schema },
470
+ }]
471
+ return false
472
+ }
473
+ return true
474
+ }
475
+ ajv.addKeyword({ keyword: 'dateLess', type: 'string', schemaType: 'string', validate: dateLess, errors: true })
476
+ }
477
+ }
package/.eslintignore DELETED
@@ -1,11 +0,0 @@
1
- node_modules/
2
- coverage/
3
- .nyc_output/
4
- dist/
5
- build/
6
- *.log
7
- .DS_Store
8
- .temp/
9
- reports/
10
- *.d.ts
11
-
package/.eslintrc.json DELETED
@@ -1,27 +0,0 @@
1
- {
2
- "env": {
3
- "node": true,
4
- "es2021": true,
5
- "mocha": true
6
- },
7
- "extends": "eslint:recommended",
8
- "parserOptions": {
9
- "ecmaVersion": 12,
10
- "sourceType": "module"
11
- },
12
- "rules": {
13
- "semi": ["error", "always"],
14
- "quotes": ["error", "single"],
15
- "indent": ["error", 2],
16
- "linebreak-style": ["error", "windows"],
17
- "no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
18
- "no-console": "off",
19
- "comma-dangle": ["error", "never"],
20
- "object-curly-spacing": ["error", "always"],
21
- "arrow-spacing": "error",
22
- "keyword-spacing": "error",
23
- "space-before-blocks": "error",
24
- "space-infix-ops": "error"
25
- }
26
- }
27
-