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 +1 -0
- package/dist/index.js +80 -1
- package/package.json +3 -3
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.
|
|
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.
|
|
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.
|
|
48
|
+
"bank20baht-ui": ">=0.5.0 <0.6.0"
|
|
49
49
|
},
|
|
50
50
|
"main": "./dist/index.js",
|
|
51
51
|
"types": "./dist/index.d.ts",
|