bank20baht-validator 0.1.3 → 0.1.4

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/README.md CHANGED
@@ -44,9 +44,16 @@ POST /api/submissions {"id":"todo-form","data":{"title":"ab"}}
44
44
  → 422 {"errors":[{"ref":"title","message":"Must be at least 3 characters"}]}
45
45
  ```
46
46
 
47
+ ## Compatibility
48
+
49
+ Peers on `bank20baht-ui` **0.2.x** (the `FieldType` union — including `password` since 0.2.2 —
50
+ comes from there). In a workspace that also builds `bank20baht-ui`, keep the peer range in
51
+ step with the core version or the validator's `tsc` build resolves a stale copy from npm.
52
+
47
53
  ## What it enforces
48
54
 
49
55
  - Every declarative validator of the field descriptors: `required`, `minLength`, `maxLength`, `min`, `max`, `pattern`, `email`, and `custom` (via a `Registry`).
56
+ - Value **shape** per field type before any validator runs — a `password`/`text` must be a string, a `number` a finite number, a `dropdown` one of its option keys — so `{"$gt":""}` from a raw API call is rejected, not treated as "non-empty".
50
57
  - **Workflow rules first** (`evaluate` from `bank20baht-ui/engine`): fields hidden by rules are excluded and smuggled values are cleared; `required` derived from rules is enforced; `setValue` results are computed.
51
58
  - Approval steps: a `decision` must be present, and the comment policy (`required` / `requireOnReject`) is honored.
52
59
 
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@
13
13
  // every value must have the shape the UI control could actually emit before
14
14
  // the authored validators run.
15
15
  // ---------------------------------------------------------------------------
16
- import { applyFlags, approvalFields, defaultValue, evaluate, getCatalog, resolveViewFields, setCatalog, setLocale, t, validateField, walkPath, } from 'bank20baht-ui/engine';
16
+ import { applyFlags, approvalFields, defaultValue, evaluate, getCatalog, parseDisabledEntry, parseIsoDate, resolveViewFields, setCatalog, setLocale, t, validateField, walkPath, } from 'bank20baht-ui/engine';
17
17
  /**
18
18
  * Run `fn` with the catalog switched to `locale`, restoring the previous
19
19
  * catalog after. The engine catalog is module-global; validation is fully
@@ -34,6 +34,7 @@ function withLocale(locale, fn) {
34
34
  }
35
35
  /** Keys that write through Object.prototype — never valid as field/form names. */
36
36
  const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
37
+ const DATE_MODES = new Set(['single', 'range', 'multiple']);
37
38
  const FIELD_TYPES = new Set([
38
39
  'text',
39
40
  'textarea',
@@ -89,8 +90,21 @@ function typeIssue(field, value) {
89
90
  case 'textarea':
90
91
  case 'email':
91
92
  case 'password':
92
- case 'date':
93
93
  return typeof value === 'string' ? null : t('pattern');
94
+ case 'date': {
95
+ // value shape follows dateMode; every day must be a real ISO calendar day
96
+ const isDay = (v) => parseIsoDate(v) !== null;
97
+ switch (field.dateMode ?? 'single') {
98
+ case 'range':
99
+ return Array.isArray(value) && value.length === 2 && value.every(isDay)
100
+ ? null
101
+ : t('pattern');
102
+ case 'multiple':
103
+ return Array.isArray(value) && value.every(isDay) ? null : t('pattern');
104
+ default:
105
+ return isDay(value) ? null : t('pattern');
106
+ }
107
+ }
94
108
  case 'number':
95
109
  return typeof value === 'number' && Number.isFinite(value) ? null : t('pattern');
96
110
  case 'checkbox':
@@ -477,6 +491,8 @@ function assertFieldShape(field, formName) {
477
491
  throw new Error(`invalid field key ${JSON.stringify(field.key)} on ${formName}`);
478
492
  }
479
493
  }
494
+ if (field.type === 'date')
495
+ assertDateProps(field, formName);
480
496
  if (field.options !== undefined) {
481
497
  if (!Array.isArray(field.options) ||
482
498
  field.options.some((o) => !isRecord(o) || typeof o.key !== 'string')) {
@@ -510,6 +526,32 @@ function assertFieldShape(field, formName) {
510
526
  }
511
527
  }
512
528
  }
529
+ /** Calendar constraints must be well-formed or the picker silently ignores them. */
530
+ function assertDateProps(field, formName) {
531
+ const where = `${formName}.${field.key}`;
532
+ if (field.dateMode !== undefined && !DATE_MODES.has(field.dateMode)) {
533
+ throw new Error(`invalid dateMode ${JSON.stringify(field.dateMode)} on ${where}`);
534
+ }
535
+ for (const bound of ['min', 'max']) {
536
+ const v = field[bound];
537
+ if (v !== undefined && parseIsoDate(v) === null) {
538
+ throw new Error(`${bound} on ${where} must be an ISO date (yyyy-mm-dd)`);
539
+ }
540
+ }
541
+ if (field.disabledDates !== undefined) {
542
+ const bad = !Array.isArray(field.disabledDates) ||
543
+ field.disabledDates.some((d) => typeof d !== 'string' || parseDisabledEntry(d) === null);
544
+ if (bad) {
545
+ throw new Error(`disabledDates on ${where} must be ISO days ("2026-04-13") or intervals ("2026-04-13/2026-04-15")`);
546
+ }
547
+ }
548
+ if (field.disabledWeekdays !== undefined) {
549
+ const bad = !Array.isArray(field.disabledWeekdays) ||
550
+ field.disabledWeekdays.some((d) => !Number.isInteger(d) || d < 0 || d > 6);
551
+ if (bad)
552
+ throw new Error(`disabledWeekdays on ${where} must be integers 0 (Sun) … 6 (Sat)`);
553
+ }
554
+ }
513
555
  function assertValidRules(wf, formNames, refs) {
514
556
  wf.rules.forEach((raw, i) => {
515
557
  if (!isRecord(raw))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bank20baht-validator",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "description": "Runtime validation for bahtui form/workflow JSON — the exact engine rules the client runs, on the server.",
6
6
  "license": "MIT",
@@ -45,7 +45,7 @@
45
45
  "vitest": "^3.2.4"
46
46
  },
47
47
  "peerDependencies": {
48
- "bank20baht-ui": ">=0.2.2 <0.3.0"
48
+ "bank20baht-ui": ">=0.2.6 <0.3.0"
49
49
  },
50
50
  "main": "./dist/index.js",
51
51
  "types": "./dist/index.d.ts",