schema-dsl 1.2.4 → 2.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.
Files changed (242) hide show
  1. package/CHANGELOG.md +87 -210
  2. package/README.md +391 -2249
  3. package/dist/DslBuilder-DQDN0ZxZ.d.cts +341 -0
  4. package/dist/DslBuilder-DkLaOo9Q.d.ts +341 -0
  5. package/dist/Validator-C7GsVQOH.d.cts +192 -0
  6. package/dist/Validator-hFWKGxir.d.ts +192 -0
  7. package/dist/index.cjs +6594 -0
  8. package/dist/index.d.cts +1145 -0
  9. package/dist/index.d.ts +1145 -0
  10. package/dist/index.js +6528 -0
  11. package/dist/plugin-CIKtTMtS.d.cts +246 -0
  12. package/dist/plugin-CIKtTMtS.d.ts +246 -0
  13. package/dist/plugins/custom-format.cjs +3802 -0
  14. package/dist/plugins/custom-format.d.cts +12 -0
  15. package/dist/plugins/custom-format.d.ts +12 -0
  16. package/dist/plugins/custom-format.js +3772 -0
  17. package/dist/plugins/custom-type-example.cjs +3795 -0
  18. package/dist/plugins/custom-type-example.d.cts +8 -0
  19. package/dist/plugins/custom-type-example.d.ts +8 -0
  20. package/dist/plugins/custom-type-example.js +3765 -0
  21. package/dist/plugins/custom-validator.cjs +146 -0
  22. package/dist/plugins/custom-validator.d.cts +10 -0
  23. package/dist/plugins/custom-validator.d.ts +10 -0
  24. package/dist/plugins/custom-validator.js +121 -0
  25. package/docs/FEATURE-INDEX.md +102 -68
  26. package/docs/add-custom-locale.md +48 -35
  27. package/docs/add-keyword.md +24 -0
  28. package/docs/api-reference.md +396 -154
  29. package/docs/api.md +13 -0
  30. package/docs/best-practices-project-structure.md +19 -10
  31. package/docs/best-practices.md +93 -53
  32. package/docs/cache-manager.md +23 -15
  33. package/docs/compile.md +45 -0
  34. package/docs/conditional-api.md +40 -11
  35. package/docs/custom-extensions-guide.md +80 -152
  36. package/docs/design-philosophy.md +76 -71
  37. package/docs/doc-index.md +324 -0
  38. package/docs/dsl-syntax.md +69 -19
  39. package/docs/dynamic-locale.md +24 -14
  40. package/docs/enum.md +12 -5
  41. package/docs/error-handling.md +53 -44
  42. package/docs/export-guide.md +47 -8
  43. package/docs/export-limitations.md +27 -11
  44. package/docs/faq.md +86 -67
  45. package/docs/frontend-i18n-guide.md +26 -12
  46. package/docs/i18n-user-guide.md +60 -47
  47. package/docs/i18n.md +51 -32
  48. package/docs/index.md +48 -0
  49. package/docs/json-schema-basics.md +40 -0
  50. package/docs/label-vs-description.md +12 -3
  51. package/docs/markdown-exporter.md +15 -6
  52. package/docs/mongodb-exporter.md +11 -4
  53. package/docs/multi-language.md +26 -0
  54. package/docs/multi-type-support.md +26 -33
  55. package/docs/mysql-exporter.md +9 -2
  56. package/docs/number-operators.md +12 -5
  57. package/docs/optional-marker-guide.md +28 -23
  58. package/docs/performance-guide.md +49 -0
  59. package/docs/plugin-system.md +205 -366
  60. package/docs/plugin-type-registration.md +34 -0
  61. package/docs/postgresql-exporter.md +9 -2
  62. package/docs/public/favicon.svg +5 -0
  63. package/docs/quick-start.md +37 -363
  64. package/docs/runtime-locale-support.md +20 -9
  65. package/docs/schema-helper.md +10 -5
  66. package/docs/schema-utils-advanced-issues.md +23 -0
  67. package/docs/schema-utils-best-practices.md +20 -0
  68. package/docs/schema-utils-chaining.md +7 -0
  69. package/docs/schema-utils.md +76 -42
  70. package/docs/security-checklist.md +20 -0
  71. package/docs/string-extensions.md +17 -9
  72. package/docs/troubleshooting.md +36 -21
  73. package/docs/type-converter.md +41 -50
  74. package/docs/type-reference.md +38 -15
  75. package/docs/typescript-guide.md +53 -42
  76. package/docs/union-type-guide.md +11 -1
  77. package/docs/union-types.md +10 -3
  78. package/docs/validate-async.md +36 -25
  79. package/docs/validate-batch.md +49 -0
  80. package/docs/validate-dsl-object-support.md +33 -28
  81. package/docs/validate.md +36 -16
  82. package/docs/validation-guide.md +25 -7
  83. package/docs/validator.md +39 -0
  84. package/package.json +85 -27
  85. package/plugins/custom-format.cjs +8 -0
  86. package/plugins/custom-type-example.cjs +8 -0
  87. package/plugins/custom-validator.cjs +8 -0
  88. package/src/adapters/DslAdapter.ts +111 -0
  89. package/src/adapters/index.ts +1 -0
  90. package/src/config/constants.ts +83 -0
  91. package/src/config/index.ts +2 -0
  92. package/src/config/patterns.ts +77 -0
  93. package/src/core/CacheManager.ts +159 -0
  94. package/src/core/ConditionalBuilder.ts +382 -0
  95. package/src/core/ConditionalRuntime.ts +28 -0
  96. package/src/core/ConditionalValidator.ts +255 -0
  97. package/src/core/DslBuilder.ts +677 -0
  98. package/src/core/ErrorCodes.ts +38 -0
  99. package/src/core/ErrorFormatter.ts +271 -0
  100. package/src/core/JSONSchemaCore.ts +65 -0
  101. package/src/core/Locale.ts +187 -0
  102. package/src/core/MessageTemplate.ts +42 -0
  103. package/src/core/ObjectDslBuilder.ts +64 -0
  104. package/src/core/PluginManager.ts +326 -0
  105. package/src/core/StringExtensions.ts +140 -0
  106. package/src/core/TemplateEngine.ts +44 -0
  107. package/src/core/Validator.ts +448 -0
  108. package/src/errors/I18nError.ts +159 -0
  109. package/src/errors/ValidationError.ts +105 -0
  110. package/src/exporters/BaseExporter.ts +60 -0
  111. package/src/exporters/MarkdownExporter.ts +305 -0
  112. package/src/exporters/MongoDBExporter.ts +126 -0
  113. package/src/exporters/MySQLExporter.ts +155 -0
  114. package/src/exporters/PostgreSQLExporter.ts +222 -0
  115. package/src/exporters/index.ts +18 -0
  116. package/src/index.ts +633 -0
  117. package/{lib/locales/en-US.js → src/locales/en-US.ts} +21 -37
  118. package/{lib/locales/es-ES.js → src/locales/es-ES.ts} +63 -16
  119. package/{lib/locales/fr-FR.js → src/locales/fr-FR.ts} +74 -27
  120. package/src/locales/index.ts +103 -0
  121. package/{lib/locales/ja-JP.js → src/locales/ja-JP.ts} +59 -17
  122. package/src/locales/types.ts +156 -0
  123. package/{lib/locales/zh-CN.js → src/locales/zh-CN.ts} +21 -38
  124. package/src/parser/ConstraintParser.ts +101 -0
  125. package/src/parser/DslParser.ts +470 -0
  126. package/src/parser/SchemaCompiler.ts +66 -0
  127. package/src/parser/TypeRegistry.ts +250 -0
  128. package/src/parser/index.ts +6 -0
  129. package/src/plugins/custom-format.ts +126 -0
  130. package/src/plugins/custom-type-example.ts +108 -0
  131. package/src/plugins/custom-validator.ts +140 -0
  132. package/src/types/conditional.ts +28 -0
  133. package/src/types/config.ts +59 -0
  134. package/src/types/dsl.ts +131 -0
  135. package/src/types/error.ts +60 -0
  136. package/src/types/index.ts +17 -0
  137. package/src/types/infer.ts +128 -0
  138. package/src/types/plugin.ts +58 -0
  139. package/src/types/safe-regex.d.ts +9 -0
  140. package/src/types/schema.ts +66 -0
  141. package/src/types/validate.ts +71 -0
  142. package/src/utils/SchemaHelper.ts +196 -0
  143. package/src/utils/SchemaUtils.ts +346 -0
  144. package/src/utils/TypeConverter.ts +215 -0
  145. package/src/utils/index.ts +10 -0
  146. package/src/validators/CustomKeywords.ts +477 -0
  147. package/.eslintignore +0 -11
  148. package/.eslintrc.json +0 -27
  149. package/CONTRIBUTING.md +0 -368
  150. package/STATUS.md +0 -491
  151. package/changelogs/v1.0.0.md +0 -328
  152. package/changelogs/v1.0.9.md +0 -367
  153. package/changelogs/v1.1.0.md +0 -389
  154. package/changelogs/v1.1.1.md +0 -308
  155. package/changelogs/v1.1.2.md +0 -183
  156. package/changelogs/v1.1.3.md +0 -161
  157. package/changelogs/v1.1.4.md +0 -432
  158. package/changelogs/v1.1.5.md +0 -493
  159. package/changelogs/v1.1.6.md +0 -211
  160. package/changelogs/v1.1.8.md +0 -376
  161. package/changelogs/v1.2.3.md +0 -124
  162. package/docs/INDEX.md +0 -252
  163. package/docs/issues-resolved-summary.md +0 -196
  164. package/docs/performance-benchmark-report.md +0 -179
  165. package/docs/performance-quick-reference.md +0 -123
  166. package/docs/user-questions-answered.md +0 -353
  167. package/docs/validation-rules-v1.0.2.md +0 -1608
  168. package/examples/README.md +0 -81
  169. package/examples/array-dsl-example.js +0 -227
  170. package/examples/conditional-example.js +0 -288
  171. package/examples/conditional-non-object.js +0 -129
  172. package/examples/conditional-validate-example.js +0 -321
  173. package/examples/custom-extension.js +0 -85
  174. package/examples/dsl-match-example.js +0 -74
  175. package/examples/dsl-style.js +0 -118
  176. package/examples/dynamic-locale-configuration.js +0 -348
  177. package/examples/dynamic-locale-example.js +0 -287
  178. package/examples/enum.examples.js +0 -324
  179. package/examples/export-demo.js +0 -130
  180. package/examples/express-integration.js +0 -376
  181. package/examples/i18n-error-handling-complete.js +0 -381
  182. package/examples/i18n-error-handling-quickstart.md +0 -0
  183. package/examples/i18n-error.examples.js +0 -181
  184. package/examples/i18n-full-demo.js +0 -301
  185. package/examples/i18n-memory-safety.examples.js +0 -268
  186. package/examples/markdown-export.js +0 -71
  187. package/examples/middleware-usage.js +0 -93
  188. package/examples/new-features-comparison.js +0 -315
  189. package/examples/password-reset/README.md +0 -153
  190. package/examples/password-reset/schema.js +0 -26
  191. package/examples/password-reset/test.js +0 -101
  192. package/examples/plugin-system.examples.js +0 -205
  193. package/examples/schema-utils-chaining.examples.js +0 -250
  194. package/examples/simple-example.js +0 -122
  195. package/examples/slug.examples.js +0 -179
  196. package/examples/string-extensions.js +0 -297
  197. package/examples/union-type-example.js +0 -127
  198. package/examples/union-types-example.js +0 -77
  199. package/examples/user-registration/README.md +0 -156
  200. package/examples/user-registration/routes.js +0 -92
  201. package/examples/user-registration/schema.js +0 -150
  202. package/examples/user-registration/server.js +0 -74
  203. package/index.d.ts +0 -3540
  204. package/index.js +0 -457
  205. package/index.mjs +0 -60
  206. package/lib/adapters/DslAdapter.js +0 -871
  207. package/lib/adapters/index.js +0 -20
  208. package/lib/config/constants.js +0 -286
  209. package/lib/config/patterns/common.js +0 -47
  210. package/lib/config/patterns/creditCard.js +0 -9
  211. package/lib/config/patterns/idCard.js +0 -9
  212. package/lib/config/patterns/index.js +0 -9
  213. package/lib/config/patterns/licensePlate.js +0 -4
  214. package/lib/config/patterns/passport.js +0 -4
  215. package/lib/config/patterns/phone.js +0 -9
  216. package/lib/config/patterns/postalCode.js +0 -5
  217. package/lib/core/CacheManager.js +0 -376
  218. package/lib/core/ConditionalBuilder.js +0 -503
  219. package/lib/core/DslBuilder.js +0 -1400
  220. package/lib/core/ErrorCodes.js +0 -233
  221. package/lib/core/ErrorFormatter.js +0 -445
  222. package/lib/core/JSONSchemaCore.js +0 -347
  223. package/lib/core/Locale.js +0 -130
  224. package/lib/core/MessageTemplate.js +0 -98
  225. package/lib/core/PluginManager.js +0 -448
  226. package/lib/core/StringExtensions.js +0 -240
  227. package/lib/core/Validator.js +0 -654
  228. package/lib/errors/I18nError.js +0 -328
  229. package/lib/errors/ValidationError.js +0 -191
  230. package/lib/exporters/MarkdownExporter.js +0 -420
  231. package/lib/exporters/MongoDBExporter.js +0 -162
  232. package/lib/exporters/MySQLExporter.js +0 -212
  233. package/lib/exporters/PostgreSQLExporter.js +0 -289
  234. package/lib/exporters/index.js +0 -24
  235. package/lib/locales/index.js +0 -8
  236. package/lib/utils/LRUCache.js +0 -174
  237. package/lib/utils/SchemaHelper.js +0 -240
  238. package/lib/utils/SchemaUtils.js +0 -445
  239. package/lib/utils/TypeConverter.js +0 -245
  240. package/lib/utils/index.js +0 -13
  241. package/lib/validators/CustomKeywords.js +0 -616
  242. 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
-