bank20baht-validator 0.2.1 → 0.3.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.
package/README.md CHANGED
@@ -56,6 +56,7 @@ step with the core version or the validator's `tsc` build resolves a stale copy
56
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".
57
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.
58
58
  - Approval steps: a `decision` must be present, and the comment policy (`required` / `requireOnReject`) is honored.
59
+ - **Client `submit` scripts** (spec 01 §14): re-run headlessly with the same `registry.scripts` — `g.setValue()` lands in canonical data, `g.addErrorMessage()` becomes an issue on `$` (`form_name.$` in a workflow), returning `false` blocks. `load` / `change` scripts are UI-only and never run here, so a field a client script hides is still validated: keep submit-relevant logic in rules or validators. `parseSchema` rejects a `scripts[]` entry with an unknown `on`, no `handler`, or a `change` hook on a field that does not exist.
59
60
 
60
61
  ## API
61
62
 
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, parseDisabledEntry, parseIsoDate, resolveViewFields, setCatalog, setLocale, t, validateField, walkPath, normalizeLegacySchema, } from 'bank20baht-ui/engine';
16
+ import { applyFlags, approvalFields, createScriptContext, defaultValue, evaluate, hasScripts, runScripts, getCatalog, parseDisabledEntry, parseIsoDate, resolveViewFields, setCatalog, setLocale, t, validateField, walkPath, normalizeLegacySchema, } 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
@@ -39,6 +39,7 @@ const FIELD_TYPES = new Set([
39
39
  'text',
40
40
  'textarea',
41
41
  'number',
42
+ 'currency',
42
43
  'email',
43
44
  'password',
44
45
  'dropdown',
@@ -106,6 +107,7 @@ function typeIssue(field, value) {
106
107
  }
107
108
  }
108
109
  case 'number':
110
+ case 'currency':
109
111
  return typeof value === 'number' && Number.isFinite(value) ? null : t('pattern');
110
112
  case 'checkbox':
111
113
  case 'toggle':
@@ -204,8 +206,51 @@ function formDataResult(form, input, registry) {
204
206
  issues.push({ ref: err.key, message: err.message });
205
207
  }
206
208
  }
209
+ issues.push(...submitScriptIssues(form, data, registry));
207
210
  return { valid: issues.length === 0, issues, data };
208
211
  }
212
+ /**
213
+ * Re-run the form's `submit` client scripts (spec 01 §14) headlessly — the
214
+ * same gate `<baht-form>.validate()` applies after its validators. `load` and
215
+ * `change` scripts are UI-only and never run here. `g.setValue()` writes into
216
+ * canonical `data`; UI state/message calls are accepted and discarded, except
217
+ * `addErrorMessage()`, which becomes an issue on `$` (prefixed by the form name
218
+ * inside a workflow). A handler returning `false` with no message falls back to
219
+ * the catalog's `scriptBlocked`.
220
+ */
221
+ function submitScriptIssues(form, data, registry, refPrefix = '') {
222
+ if (!hasScripts(form, 'submit'))
223
+ return [];
224
+ const messages = [];
225
+ const g = createScriptContext(form, {
226
+ getData: () => data,
227
+ setValue: (key, value) => {
228
+ data[key] = value;
229
+ },
230
+ setState: () => { },
231
+ resetState: () => { },
232
+ setFieldMessage: () => { },
233
+ addFormMessage: (m) => {
234
+ messages.push(m);
235
+ },
236
+ clearFormMessages: () => {
237
+ messages.length = 0;
238
+ },
239
+ });
240
+ const ok = runScripts({
241
+ form,
242
+ registry: registry ?? {},
243
+ g,
244
+ on: 'submit',
245
+ detail: { isLoading: false, data: { ...data } },
246
+ });
247
+ const issues = messages
248
+ .filter((m) => m.type === 'error')
249
+ .map((m) => ({ ref: `${refPrefix}$`, message: m.message }));
250
+ if (!ok && issues.length === 0)
251
+ issues.push({ ref: `${refPrefix}$`, message: t('scriptBlocked') });
252
+ return issues;
253
+ }
209
254
  /**
210
255
  * Validate a whole workflow submission — mirrors what `<baht-workflow>`
211
256
  * enforces across its steps: rules are evaluated first (`evaluate`), invisible
@@ -297,6 +342,10 @@ function workflowDataResult(workflow, input, registry) {
297
342
  issues.push({ ref, message: err.message });
298
343
  }
299
344
  }
345
+ if (hasScripts(form, 'submit')) {
346
+ const formData = (derived.data[form.form_name] ??= {});
347
+ issues.push(...submitScriptIssues(form, formData, registry, `${form.form_name}.`));
348
+ }
300
349
  }
301
350
  return { valid: issues.length === 0, issues, data: derived.data };
302
351
  }
@@ -372,6 +421,7 @@ export function parseSchema(json) {
372
421
  assertValidFields(f);
373
422
  assertValidViews(f);
374
423
  assertValidRepeaters(f);
424
+ assertValidScripts(f);
375
425
  if (f.view !== undefined && !f.views?.some((v) => v.name === f.view)) {
376
426
  throw new Error(`step view ${JSON.stringify(f.view)} on ${f.form_name} matches no view`);
377
427
  }
@@ -422,6 +472,7 @@ export function parseSchema(json) {
422
472
  assertValidFields(parsed);
423
473
  assertValidViews(parsed);
424
474
  assertValidRepeaters(parsed);
475
+ assertValidScripts(parsed);
425
476
  return parsed;
426
477
  }
427
478
  throw new Error('missing workflow_id/forms/rules (workflow) or form_id/form_name/fields (form)');
@@ -683,6 +734,34 @@ function assertValidRepeaters(form) {
683
734
  }
684
735
  }
685
736
  }
737
+ const SCRIPT_EVENTS = new Set(['load', 'change', 'submit']);
738
+ /** `scripts[]` (spec 01 §14): known `on`, a handler name, and a real field for `change`. */
739
+ function assertValidScripts(form) {
740
+ if (form.scripts === undefined)
741
+ return;
742
+ if (!Array.isArray(form.scripts)) {
743
+ throw new Error(`scripts must be an array on ${form.form_name}`);
744
+ }
745
+ const keys = new Set(form.fields.map((f) => f.key));
746
+ for (const s of form.scripts) {
747
+ if (!isRecord(s))
748
+ throw new Error(`invalid script on ${form.form_name}: expected an object`);
749
+ if (typeof s.on !== 'string' || !SCRIPT_EVENTS.has(s.on)) {
750
+ throw new Error(`invalid script on=${JSON.stringify(s.on)} on ${form.form_name}`);
751
+ }
752
+ if (typeof s.handler !== 'string' || s.handler === '') {
753
+ throw new Error(`script on ${form.form_name} needs a handler name`);
754
+ }
755
+ if (s.on === 'change') {
756
+ if (typeof s.field !== 'string' || s.field === '') {
757
+ throw new Error(`change script "${s.handler}" on ${form.form_name} needs a field`);
758
+ }
759
+ if (!keys.has(s.field)) {
760
+ throw new Error(`change script "${s.handler}" on ${form.form_name} targets unknown field ${JSON.stringify(s.field)}`);
761
+ }
762
+ }
763
+ }
764
+ }
686
765
  function assertValidViews(form) {
687
766
  if (form.views === undefined)
688
767
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bank20baht-validator",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
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",
@@ -40,12 +40,12 @@
40
40
  "test:watch": "vitest"
41
41
  },
42
42
  "devDependencies": {
43
- "bank20baht-ui": "^0.4.0",
43
+ "bank20baht-ui": "^0.5.0",
44
44
  "typescript": "^5.8.3",
45
45
  "vitest": "^3.2.4"
46
46
  },
47
47
  "peerDependencies": {
48
- "bank20baht-ui": ">=0.3.0 <0.5.0"
48
+ "bank20baht-ui": ">=0.5.0 <0.6.0"
49
49
  },
50
50
  "main": "./dist/index.js",
51
51
  "types": "./dist/index.d.ts",