bank20baht-validator 0.0.1 → 0.0.3
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/dist/index.d.ts +6 -3
- package/dist/index.js +75 -15
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { approvalFields, defaultValue, evaluate, validateField, type FormDefinition, type NestedData, type Registry, type WorkflowDefinition, type WorkflowForm } from 'bank20baht-ui/engine';
|
|
1
|
+
import { approvalFields, defaultValue, evaluate, resolveViewFields, setLocale, validateField, type FormDefinition, type NestedData, type Registry, type WorkflowDefinition, type WorkflowForm } from 'bank20baht-ui/engine';
|
|
2
2
|
export type SchemaDefinition = FormDefinition | WorkflowDefinition;
|
|
3
3
|
/** One violation. `ref` is the field key for forms, `formName.key` for workflows. */
|
|
4
4
|
export interface ValidationIssue {
|
|
@@ -20,7 +20,9 @@ export declare function isFormDefinition(schema: unknown): schema is FormDefinit
|
|
|
20
20
|
/**
|
|
21
21
|
* Validate a standalone form submission — mirrors `<baht-form>.validate()`
|
|
22
22
|
* with no derived field states: every non-label field, missing values fall
|
|
23
|
-
* back to the field default before checking.
|
|
23
|
+
* back to the field default before checking. `form.views` are ignored here:
|
|
24
|
+
* views are client-side display only, the server always validates the full
|
|
25
|
+
* form (a view can never weaken a validation rule).
|
|
24
26
|
*/
|
|
25
27
|
export declare function validateFormData(form: FormDefinition, input: unknown, registry?: Registry): ValidationResult;
|
|
26
28
|
/**
|
|
@@ -44,5 +46,6 @@ export declare function validateSubmission(schema: SchemaDefinition, data: unkno
|
|
|
44
46
|
* render. Throws with a human-readable message on bad input.
|
|
45
47
|
*/
|
|
46
48
|
export declare function parseSchema(json: unknown): SchemaDefinition;
|
|
47
|
-
export { approvalFields, defaultValue, evaluate, validateField };
|
|
49
|
+
export { approvalFields, defaultValue, evaluate, resolveViewFields, setLocale, validateField };
|
|
48
50
|
export type { FormDefinition, NestedData, Registry, WorkflowDefinition, WorkflowForm };
|
|
51
|
+
export type { FormViewSpec, Locale, MessageCatalog } from 'bank20baht-ui/engine';
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// request that bypasses the frontend hits the exact same rules — no codegen,
|
|
8
8
|
// no recompile: swap the stored JSON and validation follows.
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
|
-
import { approvalFields, defaultValue, evaluate, validateField, } from 'bank20baht-ui/engine';
|
|
10
|
+
import { approvalFields, defaultValue, evaluate, resolveViewFields, setLocale, t, validateField, } from 'bank20baht-ui/engine';
|
|
11
11
|
export function isWorkflowDefinition(schema) {
|
|
12
12
|
return (isRecord(schema) &&
|
|
13
13
|
typeof schema['workflowId'] === 'string' &&
|
|
@@ -30,20 +30,23 @@ function sorted(fields) {
|
|
|
30
30
|
/**
|
|
31
31
|
* Validate a standalone form submission — mirrors `<baht-form>.validate()`
|
|
32
32
|
* with no derived field states: every non-label field, missing values fall
|
|
33
|
-
* back to the field default before checking.
|
|
33
|
+
* back to the field default before checking. `form.views` are ignored here:
|
|
34
|
+
* views are client-side display only, the server always validates the full
|
|
35
|
+
* form (a view can never weaken a validation rule).
|
|
34
36
|
*/
|
|
35
37
|
export function validateFormData(form, input, registry) {
|
|
36
38
|
if (!isRecord(input)) {
|
|
37
39
|
return { valid: false, issues: [{ ref: '$', message: 'data must be an object' }], data: {} };
|
|
38
40
|
}
|
|
39
41
|
const issues = [];
|
|
42
|
+
const fields = sorted(form.field_data).filter((f) => f.type !== 'label');
|
|
43
|
+
// canonical data first, so compare validators see every sibling
|
|
40
44
|
const data = {};
|
|
41
|
-
for (const field of
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
data[field.key]
|
|
46
|
-
for (const err of validateField(field, value, registry)) {
|
|
45
|
+
for (const field of fields) {
|
|
46
|
+
data[field.key] = field.key in input ? input[field.key] : defaultValue(field);
|
|
47
|
+
}
|
|
48
|
+
for (const field of fields) {
|
|
49
|
+
for (const err of validateField(field, data[field.key], registry, data)) {
|
|
47
50
|
issues.push({ ref: err.key, message: err.message });
|
|
48
51
|
}
|
|
49
52
|
}
|
|
@@ -87,8 +90,8 @@ export function validateWorkflowData(workflow, input, registry) {
|
|
|
87
90
|
continue; // hidden fields excluded (spec 02 §5)
|
|
88
91
|
const required = state ? state.required : !!field.required;
|
|
89
92
|
const descriptor = required === !!field.required ? field : { ...field, required };
|
|
90
|
-
const
|
|
91
|
-
for (const err of validateField(descriptor,
|
|
93
|
+
const formData = derived.data[form.formName] ?? {};
|
|
94
|
+
for (const err of validateField(descriptor, formData[field.key], registry, formData)) {
|
|
92
95
|
issues.push({ ref, message: err.message });
|
|
93
96
|
}
|
|
94
97
|
}
|
|
@@ -101,12 +104,12 @@ function approvalIssues(form, data) {
|
|
|
101
104
|
const values = data[form.formName] ?? {};
|
|
102
105
|
const decision = values['decision'];
|
|
103
106
|
if (decision !== 'approved' && decision !== 'rejected') {
|
|
104
|
-
issues.push({ ref: `${form.formName}.decision`, message: '
|
|
107
|
+
issues.push({ ref: `${form.formName}.decision`, message: t('decisionRequired') });
|
|
105
108
|
}
|
|
106
109
|
const mode = form.approval?.comment ?? 'optional';
|
|
107
110
|
const commentRequired = mode === 'required' || (mode === 'requireOnReject' && decision === 'rejected');
|
|
108
111
|
if (commentRequired && String(values['comment'] ?? '').trim() === '') {
|
|
109
|
-
issues.push({ ref: `${form.formName}.comment`, message: '
|
|
112
|
+
issues.push({ ref: `${form.formName}.comment`, message: t('commentRequired') });
|
|
110
113
|
}
|
|
111
114
|
return issues;
|
|
112
115
|
}
|
|
@@ -145,6 +148,11 @@ export function parseSchema(json) {
|
|
|
145
148
|
if (f.kind !== undefined && f.kind !== 'form' && f.kind !== 'approval') {
|
|
146
149
|
throw new Error(`invalid form kind ${JSON.stringify(f.kind)} on ${f.formName}`);
|
|
147
150
|
}
|
|
151
|
+
assertValidViews(f);
|
|
152
|
+
assertValidRepeaters(f);
|
|
153
|
+
if (f.view !== undefined && !f.views?.some((v) => v.name === f.view)) {
|
|
154
|
+
throw new Error(`step view ${JSON.stringify(f.view)} on ${f.formName} matches no view`);
|
|
155
|
+
}
|
|
148
156
|
}
|
|
149
157
|
const names = parsed.forms.map((f) => f.formName);
|
|
150
158
|
if (new Set(names).size !== names.length) {
|
|
@@ -152,10 +160,62 @@ export function parseSchema(json) {
|
|
|
152
160
|
}
|
|
153
161
|
return parsed;
|
|
154
162
|
}
|
|
155
|
-
if (isFormDefinition(parsed))
|
|
163
|
+
if (isFormDefinition(parsed)) {
|
|
164
|
+
assertValidViews(parsed);
|
|
165
|
+
assertValidRepeaters(parsed);
|
|
156
166
|
return parsed;
|
|
167
|
+
}
|
|
157
168
|
throw new Error('missing workflowId/forms/rules (workflow) or formId/formName/field_data (form)');
|
|
158
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* View integrity — views are client-side display projections, but a stored
|
|
172
|
+
* one must always resolve on the client: names unique and shaped right, every
|
|
173
|
+
* listed key present in field_data. Catches builder typos at store time
|
|
174
|
+
* instead of as a silently empty form.
|
|
175
|
+
*/
|
|
176
|
+
/**
|
|
177
|
+
* Repeater integrity (spec 01 §10): a repeater must carry sub-fields with
|
|
178
|
+
* unique keys, and repeaters never nest.
|
|
179
|
+
*/
|
|
180
|
+
function assertValidRepeaters(form) {
|
|
181
|
+
for (const field of form.field_data) {
|
|
182
|
+
if (field.type !== 'repeater')
|
|
183
|
+
continue;
|
|
184
|
+
if (!Array.isArray(field.fields) || field.fields.length === 0) {
|
|
185
|
+
throw new Error(`repeater ${JSON.stringify(field.key)} on ${form.formName} needs fields[]`);
|
|
186
|
+
}
|
|
187
|
+
const keys = field.fields.map((f) => f.key);
|
|
188
|
+
if (new Set(keys).size !== keys.length) {
|
|
189
|
+
throw new Error(`repeater ${JSON.stringify(field.key)} on ${form.formName} has duplicate sub-keys`);
|
|
190
|
+
}
|
|
191
|
+
if (field.fields.some((f) => f.type === 'repeater')) {
|
|
192
|
+
throw new Error(`repeater ${JSON.stringify(field.key)} on ${form.formName} nests a repeater`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function assertValidViews(form) {
|
|
197
|
+
if (form.views === undefined)
|
|
198
|
+
return;
|
|
199
|
+
if (!Array.isArray(form.views))
|
|
200
|
+
throw new Error(`views must be an array on ${form.formName}`);
|
|
201
|
+
const keys = new Set(form.field_data.map((f) => f.key));
|
|
202
|
+
const names = new Set();
|
|
203
|
+
for (const view of form.views) {
|
|
204
|
+
if (!isRecord(view) || typeof view.name !== 'string' || !Array.isArray(view.fields)) {
|
|
205
|
+
throw new Error(`invalid view on ${form.formName}: expected { name, fields[] }`);
|
|
206
|
+
}
|
|
207
|
+
if (names.has(view.name)) {
|
|
208
|
+
throw new Error(`view names must be unique on ${form.formName}: ${JSON.stringify(view.name)}`);
|
|
209
|
+
}
|
|
210
|
+
names.add(view.name);
|
|
211
|
+
for (const key of view.fields) {
|
|
212
|
+
if (typeof key !== 'string' || !keys.has(key)) {
|
|
213
|
+
throw new Error(`view ${JSON.stringify(view.name)} on ${form.formName} references unknown field ${JSON.stringify(key)}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
159
218
|
// Re-exported so servers can type stores and registries without a direct
|
|
160
|
-
// dependency on bank20baht-ui.
|
|
161
|
-
|
|
219
|
+
// dependency on bank20baht-ui. `setLocale` localizes the issues this package
|
|
220
|
+
// emits (same catalog the client renders).
|
|
221
|
+
export { approvalFields, defaultValue, evaluate, resolveViewFields, setLocale, validateField };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bank20baht-validator",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
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",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"test:watch": "vitest"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"bank20baht-ui": "^0.0.
|
|
42
|
+
"bank20baht-ui": "^0.0.8"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"typescript": "^5.8.3",
|