@orkestrel/form 0.0.4 → 0.0.6
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 +16 -14
- package/dist/src/core/index.cjs +314 -340
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +301 -181
- package/dist/src/core/index.d.ts +301 -181
- package/dist/src/core/index.js +311 -342
- package/dist/src/core/index.js.map +1 -1
- package/package.json +8 -9
package/dist/src/core/index.cjs
CHANGED
|
@@ -2,7 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
let _orkestrel_contract = require("@orkestrel/contract");
|
|
3
3
|
let _orkestrel_emitter = require("@orkestrel/emitter");
|
|
4
4
|
//#region src/core/constants.ts
|
|
5
|
-
/**
|
|
5
|
+
/** Lists every field control, in the order declared by the public contract. */
|
|
6
6
|
var FIELD_CONTROLS = Object.freeze([
|
|
7
7
|
"text",
|
|
8
8
|
"editor",
|
|
@@ -17,13 +17,69 @@ var FIELD_CONTROLS = Object.freeze([
|
|
|
17
17
|
"checkbox",
|
|
18
18
|
"file"
|
|
19
19
|
]);
|
|
20
|
-
/**
|
|
20
|
+
/** Lists the members every field declares, whatever its control. */
|
|
21
|
+
var FIELD_BASE_KEYS = Object.freeze([
|
|
22
|
+
"control",
|
|
23
|
+
"name",
|
|
24
|
+
"label",
|
|
25
|
+
"help",
|
|
26
|
+
"group",
|
|
27
|
+
"hidden",
|
|
28
|
+
"disabled",
|
|
29
|
+
"locked",
|
|
30
|
+
"rule",
|
|
31
|
+
"meta"
|
|
32
|
+
]);
|
|
33
|
+
/**
|
|
34
|
+
* Lists every member one field control permits, composed from {@link FIELD_BASE_KEYS} and the
|
|
35
|
+
* members the control's own interface adds.
|
|
36
|
+
*/
|
|
37
|
+
var FIELD_KEYS = Object.freeze({
|
|
38
|
+
text: Object.freeze([
|
|
39
|
+
...FIELD_BASE_KEYS,
|
|
40
|
+
"default",
|
|
41
|
+
"placeholder"
|
|
42
|
+
]),
|
|
43
|
+
editor: Object.freeze([
|
|
44
|
+
...FIELD_BASE_KEYS,
|
|
45
|
+
"default",
|
|
46
|
+
"placeholder"
|
|
47
|
+
]),
|
|
48
|
+
password: Object.freeze([...FIELD_BASE_KEYS, "mask"]),
|
|
49
|
+
number: Object.freeze([
|
|
50
|
+
...FIELD_BASE_KEYS,
|
|
51
|
+
"default",
|
|
52
|
+
"placeholder"
|
|
53
|
+
]),
|
|
54
|
+
date: Object.freeze([...FIELD_BASE_KEYS, "default"]),
|
|
55
|
+
time: Object.freeze([...FIELD_BASE_KEYS, "default"]),
|
|
56
|
+
datetime: Object.freeze([...FIELD_BASE_KEYS, "default"]),
|
|
57
|
+
color: Object.freeze([...FIELD_BASE_KEYS, "default"]),
|
|
58
|
+
confirm: Object.freeze([...FIELD_BASE_KEYS, "default"]),
|
|
59
|
+
select: Object.freeze([
|
|
60
|
+
...FIELD_BASE_KEYS,
|
|
61
|
+
"choices",
|
|
62
|
+
"default",
|
|
63
|
+
"open"
|
|
64
|
+
]),
|
|
65
|
+
checkbox: Object.freeze([
|
|
66
|
+
...FIELD_BASE_KEYS,
|
|
67
|
+
"choices",
|
|
68
|
+
"default"
|
|
69
|
+
]),
|
|
70
|
+
file: Object.freeze([
|
|
71
|
+
...FIELD_BASE_KEYS,
|
|
72
|
+
"accept",
|
|
73
|
+
"multiple"
|
|
74
|
+
])
|
|
75
|
+
});
|
|
76
|
+
/** Lists every form lifecycle status. */
|
|
21
77
|
var FORM_STATUSES = Object.freeze([
|
|
22
78
|
"editing",
|
|
23
79
|
"settled",
|
|
24
80
|
"abandoned"
|
|
25
81
|
]);
|
|
26
|
-
/**
|
|
82
|
+
/** Holds the default failure copy for every named field rule. */
|
|
27
83
|
var RULE_MESSAGES = Object.freeze({
|
|
28
84
|
required: "This field is required",
|
|
29
85
|
minimum: "Must be at least {limit}",
|
|
@@ -35,50 +91,62 @@ var RULE_MESSAGES = Object.freeze({
|
|
|
35
91
|
integer: "Must be an integer",
|
|
36
92
|
alphanumeric: "Must contain only letters and numbers"
|
|
37
93
|
});
|
|
38
|
-
/**
|
|
94
|
+
/** Matches a practical whole-address email shape. */
|
|
39
95
|
var EMAIL_PATTERN = Object.freeze(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
|
|
40
|
-
/**
|
|
96
|
+
/** Matches an absolute HTTP or HTTPS URL shape. */
|
|
41
97
|
var URL_PATTERN = Object.freeze(/^https?:\/\/[^\s]+$/);
|
|
42
|
-
/**
|
|
98
|
+
/** Matches one or more ASCII letters or digits. */
|
|
43
99
|
var ALPHANUMERIC_PATTERN = Object.freeze(/^[A-Za-z0-9]+$/);
|
|
44
|
-
/**
|
|
100
|
+
/** Matches a signed or unsigned base-ten integer string. */
|
|
45
101
|
var INTEGER_PATTERN = Object.freeze(/^[+-]?\d+$/);
|
|
46
|
-
/**
|
|
102
|
+
/** Matches a six-digit hexadecimal color string. */
|
|
47
103
|
var COLOR_PATTERN = Object.freeze(/^#[0-9A-Fa-f]{6}$/);
|
|
48
|
-
/**
|
|
104
|
+
/** Matches an ISO calendar date string in `YYYY-MM-DD` form. */
|
|
49
105
|
var DATE_PATTERN = Object.freeze(/^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/);
|
|
50
|
-
/**
|
|
106
|
+
/** Matches a 24-hour time string with optional seconds. */
|
|
51
107
|
var TIME_PATTERN = Object.freeze(/^(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d)?$/);
|
|
52
|
-
/**
|
|
108
|
+
/** Matches an ISO local date and time string with optional seconds. */
|
|
53
109
|
var DATETIME_PATTERN = Object.freeze(/^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d)?$/);
|
|
54
|
-
/**
|
|
110
|
+
/** Caps the accepted source length for an authored regular expression, at 256. */
|
|
55
111
|
var PATTERN_LIMIT = 256;
|
|
56
|
-
/**
|
|
112
|
+
/** Caps the number of fields one schema may declare, at 512. */
|
|
57
113
|
var FIELD_LIMIT = 512;
|
|
58
|
-
/**
|
|
114
|
+
/** Caps the number of groups one schema may declare, at 64. */
|
|
59
115
|
var GROUP_LIMIT = 64;
|
|
60
|
-
/**
|
|
116
|
+
/** Caps the number of choices one `select` or `checkbox` field may offer, at 1024. */
|
|
61
117
|
var CHOICE_LIMIT = 1024;
|
|
62
|
-
/**
|
|
118
|
+
/** Caps the number of entries one list-valued answer may hold, at 1024. */
|
|
63
119
|
var LIST_LIMIT = 1024;
|
|
64
|
-
/**
|
|
120
|
+
/** Caps the length, in UTF-16 code units, of a schema, group, or field name, at 128. */
|
|
65
121
|
var NAME_LIMIT = 128;
|
|
66
|
-
/**
|
|
122
|
+
/** Caps the length, in UTF-16 code units, of any single retained string, at 65536. */
|
|
67
123
|
var STRING_LIMIT = 65536;
|
|
68
|
-
/**
|
|
124
|
+
/** Caps the total length, in UTF-16 code units, of every string one schema retains, at 1048576. */
|
|
69
125
|
var TEXT_LIMIT = 1048576;
|
|
70
|
-
/**
|
|
126
|
+
/** Caps the total number of records, arrays, and leaves one schema retains, at 16384. */
|
|
71
127
|
var NODE_LIMIT = 16384;
|
|
72
128
|
//#endregion
|
|
73
129
|
//#region src/core/errors.ts
|
|
74
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* Represents an error raised by the form domain.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```ts
|
|
135
|
+
* const error = new FormError('FIELD', 'The schema declares no field named "nickname"', {
|
|
136
|
+
* field: 'nickname',
|
|
137
|
+
* })
|
|
138
|
+
*
|
|
139
|
+
* error.code // 'FIELD'
|
|
140
|
+
* error.context // { field: 'nickname' }
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
75
143
|
var FormError = class extends Error {
|
|
76
|
-
/**
|
|
144
|
+
/** Holds the machine-readable reason for this failure. */
|
|
77
145
|
code;
|
|
78
|
-
/**
|
|
146
|
+
/** Holds structured values that locate or explain this failure. */
|
|
79
147
|
context;
|
|
80
148
|
/**
|
|
81
|
-
*
|
|
149
|
+
* Creates a form error.
|
|
82
150
|
*
|
|
83
151
|
* @param code - The machine-readable reason.
|
|
84
152
|
* @param message - The human-readable failure text.
|
|
@@ -92,10 +160,10 @@ var FormError = class extends Error {
|
|
|
92
160
|
}
|
|
93
161
|
};
|
|
94
162
|
/**
|
|
95
|
-
*
|
|
163
|
+
* Determines whether an unknown value is a form error.
|
|
96
164
|
*
|
|
97
165
|
* @param input - The value to inspect.
|
|
98
|
-
* @returns
|
|
166
|
+
* @returns True if the value is a {@link FormError} instance; false otherwise.
|
|
99
167
|
*/
|
|
100
168
|
function isFormError(input) {
|
|
101
169
|
return input instanceof FormError;
|
|
@@ -103,37 +171,41 @@ function isFormError(input) {
|
|
|
103
171
|
//#endregion
|
|
104
172
|
//#region src/core/validators.ts
|
|
105
173
|
/**
|
|
106
|
-
*
|
|
174
|
+
* Determines whether an unknown value is a declared field control.
|
|
107
175
|
*
|
|
108
176
|
* @param input - The value to inspect.
|
|
109
|
-
* @returns
|
|
177
|
+
* @returns True if the value is a field control; false otherwise.
|
|
110
178
|
*/
|
|
111
179
|
function isFieldControl(input) {
|
|
112
180
|
return FIELD_CONTROLS.some((control) => control === input);
|
|
113
181
|
}
|
|
114
182
|
/**
|
|
115
|
-
*
|
|
183
|
+
* Determines whether an unknown value is a form lifecycle status.
|
|
116
184
|
*
|
|
117
185
|
* @param input - The value to inspect.
|
|
118
|
-
* @returns
|
|
186
|
+
* @returns True if the value is a form status; false otherwise.
|
|
119
187
|
*/
|
|
120
188
|
function isFormStatus(input) {
|
|
121
189
|
return FORM_STATUSES.some((status) => status === input);
|
|
122
190
|
}
|
|
123
191
|
/**
|
|
124
|
-
*
|
|
192
|
+
* Determines whether an unknown value has a form field value shape.
|
|
193
|
+
*
|
|
194
|
+
* @remarks
|
|
195
|
+
* A string, a finite number, a boolean, and a list of strings each qualify. A number that is not
|
|
196
|
+
* finite does not, so `NaN` and `Infinity` are refused.
|
|
125
197
|
*
|
|
126
198
|
* @param input - The value to inspect.
|
|
127
|
-
* @returns
|
|
199
|
+
* @returns True if the value is a field value; false otherwise.
|
|
128
200
|
*/
|
|
129
201
|
function isFieldValue(input) {
|
|
130
202
|
return (0, _orkestrel_contract.unionOf)(_orkestrel_contract.isString, _orkestrel_contract.isFiniteNumber, _orkestrel_contract.isBoolean, (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString))(input);
|
|
131
203
|
}
|
|
132
204
|
/**
|
|
133
|
-
*
|
|
205
|
+
* Determines whether an unknown value is one exact field choice record.
|
|
134
206
|
*
|
|
135
207
|
* @param input - The value to inspect.
|
|
136
|
-
* @returns
|
|
208
|
+
* @returns True if the value is a field choice; false otherwise.
|
|
137
209
|
*/
|
|
138
210
|
function isFieldChoice(input) {
|
|
139
211
|
const keys = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.isRecord)(input) && Reflect.ownKeys(input).every((key) => (0, _orkestrel_contract.isString)(key)));
|
|
@@ -146,10 +218,10 @@ function isFieldChoice(input) {
|
|
|
146
218
|
}, ["help", "disabled"])(input);
|
|
147
219
|
}
|
|
148
220
|
/**
|
|
149
|
-
*
|
|
221
|
+
* Determines whether an unknown value is one exact field rule record.
|
|
150
222
|
*
|
|
151
223
|
* @param input - The value to inspect.
|
|
152
|
-
* @returns
|
|
224
|
+
* @returns True if the value is a structurally valid field rule; false otherwise.
|
|
153
225
|
*/
|
|
154
226
|
function isFieldRule(input) {
|
|
155
227
|
const keys = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.isRecord)(input) && Reflect.ownKeys(input).every((key) => (0, _orkestrel_contract.isString)(key)));
|
|
@@ -168,7 +240,7 @@ function isFieldRule(input) {
|
|
|
168
240
|
}, true)(input);
|
|
169
241
|
}
|
|
170
242
|
/**
|
|
171
|
-
*
|
|
243
|
+
* Determines whether an unknown value is one exact discriminated form field.
|
|
172
244
|
*
|
|
173
245
|
* @remarks
|
|
174
246
|
* Metadata is admitted structurally as bounded JSON. An accessor-bearing metadata record is
|
|
@@ -176,120 +248,15 @@ function isFieldRule(input) {
|
|
|
176
248
|
* data properties only.
|
|
177
249
|
*
|
|
178
250
|
* @param input - The value to inspect.
|
|
179
|
-
* @returns
|
|
251
|
+
* @returns True if the value is a structurally valid form field; false otherwise.
|
|
180
252
|
*/
|
|
181
253
|
function isFormField(input) {
|
|
182
254
|
const outcome = (0, _orkestrel_contract.attempt)(() => {
|
|
183
255
|
if (!(0, _orkestrel_contract.isRecord)(input) || !Object.hasOwn(input, "control") || !Object.hasOwn(input, "name")) return false;
|
|
184
256
|
const control = input.control;
|
|
185
257
|
if (!isFieldControl(control)) return false;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
switch (control) {
|
|
189
|
-
case "text":
|
|
190
|
-
case "editor": return [
|
|
191
|
-
"control",
|
|
192
|
-
"name",
|
|
193
|
-
"label",
|
|
194
|
-
"help",
|
|
195
|
-
"group",
|
|
196
|
-
"hidden",
|
|
197
|
-
"disabled",
|
|
198
|
-
"locked",
|
|
199
|
-
"rule",
|
|
200
|
-
"meta",
|
|
201
|
-
"default",
|
|
202
|
-
"placeholder"
|
|
203
|
-
].includes(key);
|
|
204
|
-
case "password": return [
|
|
205
|
-
"control",
|
|
206
|
-
"name",
|
|
207
|
-
"label",
|
|
208
|
-
"help",
|
|
209
|
-
"group",
|
|
210
|
-
"hidden",
|
|
211
|
-
"disabled",
|
|
212
|
-
"locked",
|
|
213
|
-
"rule",
|
|
214
|
-
"meta",
|
|
215
|
-
"mask"
|
|
216
|
-
].includes(key);
|
|
217
|
-
case "number": return [
|
|
218
|
-
"control",
|
|
219
|
-
"name",
|
|
220
|
-
"label",
|
|
221
|
-
"help",
|
|
222
|
-
"group",
|
|
223
|
-
"hidden",
|
|
224
|
-
"disabled",
|
|
225
|
-
"locked",
|
|
226
|
-
"rule",
|
|
227
|
-
"meta",
|
|
228
|
-
"default",
|
|
229
|
-
"placeholder"
|
|
230
|
-
].includes(key);
|
|
231
|
-
case "date":
|
|
232
|
-
case "time":
|
|
233
|
-
case "datetime":
|
|
234
|
-
case "color":
|
|
235
|
-
case "confirm": return [
|
|
236
|
-
"control",
|
|
237
|
-
"name",
|
|
238
|
-
"label",
|
|
239
|
-
"help",
|
|
240
|
-
"group",
|
|
241
|
-
"hidden",
|
|
242
|
-
"disabled",
|
|
243
|
-
"locked",
|
|
244
|
-
"rule",
|
|
245
|
-
"meta",
|
|
246
|
-
"default"
|
|
247
|
-
].includes(key);
|
|
248
|
-
case "select": return [
|
|
249
|
-
"control",
|
|
250
|
-
"name",
|
|
251
|
-
"label",
|
|
252
|
-
"help",
|
|
253
|
-
"group",
|
|
254
|
-
"hidden",
|
|
255
|
-
"disabled",
|
|
256
|
-
"locked",
|
|
257
|
-
"rule",
|
|
258
|
-
"meta",
|
|
259
|
-
"choices",
|
|
260
|
-
"default",
|
|
261
|
-
"open"
|
|
262
|
-
].includes(key);
|
|
263
|
-
case "checkbox": return [
|
|
264
|
-
"control",
|
|
265
|
-
"name",
|
|
266
|
-
"label",
|
|
267
|
-
"help",
|
|
268
|
-
"group",
|
|
269
|
-
"hidden",
|
|
270
|
-
"disabled",
|
|
271
|
-
"locked",
|
|
272
|
-
"rule",
|
|
273
|
-
"meta",
|
|
274
|
-
"choices",
|
|
275
|
-
"default"
|
|
276
|
-
].includes(key);
|
|
277
|
-
case "file": return [
|
|
278
|
-
"control",
|
|
279
|
-
"name",
|
|
280
|
-
"label",
|
|
281
|
-
"help",
|
|
282
|
-
"group",
|
|
283
|
-
"hidden",
|
|
284
|
-
"disabled",
|
|
285
|
-
"locked",
|
|
286
|
-
"rule",
|
|
287
|
-
"meta",
|
|
288
|
-
"accept",
|
|
289
|
-
"multiple"
|
|
290
|
-
].includes(key);
|
|
291
|
-
}
|
|
292
|
-
})) return false;
|
|
258
|
+
const permitted = FIELD_KEYS[control];
|
|
259
|
+
if (!Reflect.ownKeys(input).every((key) => (0, _orkestrel_contract.isString)(key) && permitted.includes(key))) return false;
|
|
293
260
|
const name = input.name;
|
|
294
261
|
const hasLabel = Object.hasOwn(input, "label");
|
|
295
262
|
const label = hasLabel ? input.label : void 0;
|
|
@@ -370,10 +337,10 @@ function isFormField(input) {
|
|
|
370
337
|
return outcome.success && outcome.value;
|
|
371
338
|
}
|
|
372
339
|
/**
|
|
373
|
-
*
|
|
340
|
+
* Determines whether an unknown value is one exact form group record.
|
|
374
341
|
*
|
|
375
342
|
* @param input - The value to inspect.
|
|
376
|
-
* @returns
|
|
343
|
+
* @returns True if the value is a form group; false otherwise.
|
|
377
344
|
*/
|
|
378
345
|
function isFormGroup(input) {
|
|
379
346
|
const keys = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.isRecord)(input) && Reflect.ownKeys(input).every((key) => (0, _orkestrel_contract.isString)(key)));
|
|
@@ -385,10 +352,14 @@ function isFormGroup(input) {
|
|
|
385
352
|
}, ["help"])(input);
|
|
386
353
|
}
|
|
387
354
|
/**
|
|
388
|
-
*
|
|
355
|
+
* Determines whether an unknown value is one exact structural form schema.
|
|
356
|
+
*
|
|
357
|
+
* @remarks
|
|
358
|
+
* Structure alone is read. Domain soundness — a duplicate name, a bound no answer satisfies, a
|
|
359
|
+
* breached budget — is {@link auditSchema}'s question.
|
|
389
360
|
*
|
|
390
361
|
* @param input - The value to inspect.
|
|
391
|
-
* @returns
|
|
362
|
+
* @returns True if the value is a structurally valid form schema; false otherwise.
|
|
392
363
|
*/
|
|
393
364
|
function isFormSchema(input) {
|
|
394
365
|
const keys = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.isRecord)(input) && Reflect.ownKeys(input).every((key) => (0, _orkestrel_contract.isString)(key)));
|
|
@@ -407,10 +378,10 @@ function isFormSchema(input) {
|
|
|
407
378
|
])(input);
|
|
408
379
|
}
|
|
409
380
|
/**
|
|
410
|
-
*
|
|
381
|
+
* Determines whether an unknown value is a record of field values.
|
|
411
382
|
*
|
|
412
383
|
* @param input - The value to inspect.
|
|
413
|
-
* @returns
|
|
384
|
+
* @returns True if the value is a form values record; false otherwise.
|
|
414
385
|
*/
|
|
415
386
|
function isFormValues(input) {
|
|
416
387
|
const outcome = (0, _orkestrel_contract.attempt)(() => {
|
|
@@ -420,10 +391,10 @@ function isFormValues(input) {
|
|
|
420
391
|
return outcome.success && outcome.value;
|
|
421
392
|
}
|
|
422
393
|
/**
|
|
423
|
-
*
|
|
394
|
+
* Determines whether an unknown value is one exact field error record.
|
|
424
395
|
*
|
|
425
396
|
* @param input - The value to inspect.
|
|
426
|
-
* @returns
|
|
397
|
+
* @returns True if the value is a field error; false otherwise.
|
|
427
398
|
*/
|
|
428
399
|
function isFieldError(input) {
|
|
429
400
|
const keys = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.isRecord)(input) && Reflect.ownKeys(input).every((key) => (0, _orkestrel_contract.isString)(key)));
|
|
@@ -431,7 +402,7 @@ function isFieldError(input) {
|
|
|
431
402
|
return (0, _orkestrel_contract.recordOf)({
|
|
432
403
|
field: _orkestrel_contract.isString,
|
|
433
404
|
message: _orkestrel_contract.isString,
|
|
434
|
-
rule: (0, _orkestrel_contract.
|
|
405
|
+
rule: (0, _orkestrel_contract.keyOf)(RULE_MESSAGES)
|
|
435
406
|
}, ["rule"])(input);
|
|
436
407
|
}
|
|
437
408
|
//#endregion
|
|
@@ -440,7 +411,7 @@ function cloneValue(value) {
|
|
|
440
411
|
return (0, _orkestrel_contract.isArray)(value) ? Object.freeze(value.slice()) : value;
|
|
441
412
|
}
|
|
442
413
|
/**
|
|
443
|
-
*
|
|
414
|
+
* Clones a field's choices into an owned frozen snapshot.
|
|
444
415
|
*
|
|
445
416
|
* @param choices - The choices to own.
|
|
446
417
|
* @returns A frozen list of frozen choice records.
|
|
@@ -449,7 +420,7 @@ function cloneChoices(choices) {
|
|
|
449
420
|
return Object.freeze(choices.map((choice) => Object.freeze({ ...choice })));
|
|
450
421
|
}
|
|
451
422
|
/**
|
|
452
|
-
*
|
|
423
|
+
* Clones one form field into an owned frozen snapshot.
|
|
453
424
|
*
|
|
454
425
|
* @param field - The field to own.
|
|
455
426
|
* @returns A frozen field with every nested collection owned.
|
|
@@ -500,7 +471,7 @@ function cloneFormField(field) {
|
|
|
500
471
|
}
|
|
501
472
|
}
|
|
502
473
|
/**
|
|
503
|
-
*
|
|
474
|
+
* Clones a form schema into an owned frozen snapshot.
|
|
504
475
|
*
|
|
505
476
|
* @param schema - The schema to own.
|
|
506
477
|
* @returns A frozen schema with every nested record and list owned.
|
|
@@ -516,11 +487,68 @@ function cloneFormSchema(schema) {
|
|
|
516
487
|
//#endregion
|
|
517
488
|
//#region src/core/helpers.ts
|
|
518
489
|
/**
|
|
519
|
-
*
|
|
490
|
+
* Writes one own enumerable data property onto a record.
|
|
491
|
+
*
|
|
492
|
+
* @param target - The record to write into.
|
|
493
|
+
* @param name - The property name to write.
|
|
494
|
+
* @param value - The value to store.
|
|
495
|
+
*
|
|
496
|
+
* @remarks
|
|
497
|
+
* Plain assignment runs an inherited setter, so writing a `__proto__` key that way reaches
|
|
498
|
+
* `Object.prototype` and leaves the record without the entry. Defining the property writes the
|
|
499
|
+
* record itself, whatever the prototype chain declares. The entry stays writable and configurable.
|
|
500
|
+
*
|
|
501
|
+
* @example
|
|
502
|
+
* ```ts
|
|
503
|
+
* const values: Record<string, number> = {}
|
|
504
|
+
* defineEntry(values, '__proto__', 1)
|
|
505
|
+
* Object.hasOwn(values, '__proto__') // true
|
|
506
|
+
* ```
|
|
507
|
+
*/
|
|
508
|
+
function defineEntry(target, name, value) {
|
|
509
|
+
Object.defineProperty(target, name, {
|
|
510
|
+
value,
|
|
511
|
+
enumerable: true,
|
|
512
|
+
configurable: true,
|
|
513
|
+
writable: true
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Writes one own enumerable data property that cannot be rewritten or removed.
|
|
518
|
+
*
|
|
519
|
+
* @param target - The record to write into.
|
|
520
|
+
* @param name - The property name to write.
|
|
521
|
+
* @param value - The value to store.
|
|
522
|
+
*
|
|
523
|
+
* @remarks
|
|
524
|
+
* The prototype-safe write of {@link defineEntry}, frozen: the entry is neither writable nor
|
|
525
|
+
* configurable, so the record a parser hands back cannot be edited through the key it filled.
|
|
526
|
+
*
|
|
527
|
+
* @example
|
|
528
|
+
* ```ts
|
|
529
|
+
* const values: Record<string, number> = {}
|
|
530
|
+
* freezeEntry(values, '__proto__', 1)
|
|
531
|
+
* Object.getOwnPropertyDescriptor(values, '__proto__')?.writable // false
|
|
532
|
+
* ```
|
|
533
|
+
*/
|
|
534
|
+
function freezeEntry(target, name, value) {
|
|
535
|
+
Object.defineProperty(target, name, {
|
|
536
|
+
value,
|
|
537
|
+
enumerable: true,
|
|
538
|
+
configurable: false,
|
|
539
|
+
writable: false
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Checks whether a value has the shape required by one field control.
|
|
544
|
+
*
|
|
545
|
+
* @remarks
|
|
546
|
+
* Every write and every seeded value passes through this gate, and it reads `STRING_LIMIT` and
|
|
547
|
+
* `LIST_LIMIT` before it consults the control, so no regular expression sees an over-long value.
|
|
520
548
|
*
|
|
521
549
|
* @param field - The field that owns the value.
|
|
522
550
|
* @param value - The unknown value to inspect.
|
|
523
|
-
* @returns
|
|
551
|
+
* @returns True if the control can hold the value; false otherwise.
|
|
524
552
|
*/
|
|
525
553
|
function matchesField(field, value) {
|
|
526
554
|
if ((0, _orkestrel_contract.isString)(value) && value.length > 65536) return false;
|
|
@@ -549,7 +577,7 @@ function matchesField(field, value) {
|
|
|
549
577
|
}
|
|
550
578
|
}
|
|
551
579
|
/**
|
|
552
|
-
*
|
|
580
|
+
* Decides whether a raw binding value projects to an answered field.
|
|
553
581
|
*
|
|
554
582
|
* @remarks
|
|
555
583
|
* Bind with `fill(name, matchesAnswer(raw) ? raw : undefined)`. This projection treats an absent
|
|
@@ -558,13 +586,13 @@ function matchesField(field, value) {
|
|
|
558
586
|
* its `required` rule remains presence-only.
|
|
559
587
|
*
|
|
560
588
|
* @param value - The raw field value, or absence.
|
|
561
|
-
* @returns
|
|
589
|
+
* @returns True if the binding preserves the value as an answer; false otherwise.
|
|
562
590
|
*/
|
|
563
591
|
function matchesAnswer(value) {
|
|
564
592
|
return value !== void 0 && (!(0, _orkestrel_contract.isString)(value) || value.trim().length > 0);
|
|
565
593
|
}
|
|
566
594
|
/**
|
|
567
|
-
*
|
|
595
|
+
* Checks whether a named rule applies to one field control.
|
|
568
596
|
*
|
|
569
597
|
* @remarks
|
|
570
598
|
* The runtime control-membership check keeps this boundary total for JavaScript callers that
|
|
@@ -572,7 +600,7 @@ function matchesAnswer(value) {
|
|
|
572
600
|
*
|
|
573
601
|
* @param control - The field control to inspect.
|
|
574
602
|
* @param rule - The named rule to inspect.
|
|
575
|
-
* @returns
|
|
603
|
+
* @returns True if the control evaluates that rule; false otherwise.
|
|
576
604
|
*/
|
|
577
605
|
function appliesRule(control, rule) {
|
|
578
606
|
if (!FIELD_CONTROLS.some((candidate) => candidate === control)) return false;
|
|
@@ -590,23 +618,21 @@ function appliesRule(control, rule) {
|
|
|
590
618
|
return false;
|
|
591
619
|
}
|
|
592
620
|
/**
|
|
593
|
-
*
|
|
621
|
+
* Evaluates one field rule against its current value.
|
|
594
622
|
*
|
|
595
623
|
* @param field - The field and rule to evaluate.
|
|
596
624
|
* @param value - The current value, or absence.
|
|
597
625
|
* @param values - Every value available to a custom rule.
|
|
598
626
|
* @param messages - Optional rule-specific message replacements.
|
|
599
627
|
* @returns Every failure in rule order.
|
|
628
|
+
* @throws Thrown when a {@link FieldValidator} supplied through {@link FieldRule.custom} throws:
|
|
629
|
+
* its own value escapes unchanged, because this helper adds no boundary around it.
|
|
600
630
|
*/
|
|
601
631
|
function evaluateField(field, value, values, messages) {
|
|
602
632
|
const errors = [];
|
|
603
633
|
const rule = field.rule;
|
|
604
634
|
if (value === void 0) {
|
|
605
|
-
if (rule?.required === true && appliesRule(field.control, "required")) errors.push(
|
|
606
|
-
field: field.name,
|
|
607
|
-
message: formatMessage("required", void 0, messages),
|
|
608
|
-
rule: "required"
|
|
609
|
-
}));
|
|
635
|
+
if (rule?.required === true && appliesRule(field.control, "required")) errors.push(createFieldError(field, "required", void 0, messages));
|
|
610
636
|
}
|
|
611
637
|
if (rule === void 0) return Object.freeze(errors);
|
|
612
638
|
if (rule.minimum !== void 0 && appliesRule(field.control, "minimum")) {
|
|
@@ -628,11 +654,7 @@ function evaluateField(field, value, values, messages) {
|
|
|
628
654
|
case "checkbox":
|
|
629
655
|
case "file": failed = (0, _orkestrel_contract.isArray)(value) && (0, _orkestrel_contract.isFiniteNumber)(rule.minimum) && value.length < rule.minimum;
|
|
630
656
|
}
|
|
631
|
-
if (failed) errors.push(
|
|
632
|
-
field: field.name,
|
|
633
|
-
message: formatMessage("minimum", rule.minimum, messages),
|
|
634
|
-
rule: "minimum"
|
|
635
|
-
}));
|
|
657
|
+
if (failed) errors.push(createFieldError(field, "minimum", rule.minimum, messages));
|
|
636
658
|
}
|
|
637
659
|
if (rule.maximum !== void 0 && appliesRule(field.control, "maximum")) {
|
|
638
660
|
let failed = false;
|
|
@@ -653,63 +675,27 @@ function evaluateField(field, value, values, messages) {
|
|
|
653
675
|
case "checkbox":
|
|
654
676
|
case "file": failed = (0, _orkestrel_contract.isArray)(value) && (0, _orkestrel_contract.isFiniteNumber)(rule.maximum) && value.length > rule.maximum;
|
|
655
677
|
}
|
|
656
|
-
if (failed) errors.push(
|
|
657
|
-
field: field.name,
|
|
658
|
-
message: formatMessage("maximum", rule.maximum, messages),
|
|
659
|
-
rule: "maximum"
|
|
660
|
-
}));
|
|
678
|
+
if (failed) errors.push(createFieldError(field, "maximum", rule.maximum, messages));
|
|
661
679
|
}
|
|
662
680
|
if (rule.step !== void 0 && appliesRule(field.control, "step") && (0, _orkestrel_contract.isFiniteNumber)(value)) {
|
|
663
681
|
const multiple = (value - ((0, _orkestrel_contract.isFiniteNumber)(rule.minimum) ? rule.minimum : 0)) / rule.step;
|
|
664
|
-
if (!(0, _orkestrel_contract.isFiniteNumber)(rule.step) || rule.step === 0 || !(0, _orkestrel_contract.isFiniteNumber)(multiple) || Math.abs(multiple - Math.round(multiple)) > 1e-9) errors.push(
|
|
665
|
-
field: field.name,
|
|
666
|
-
message: formatMessage("step", rule.step, messages),
|
|
667
|
-
rule: "step"
|
|
668
|
-
}));
|
|
682
|
+
if (!(0, _orkestrel_contract.isFiniteNumber)(rule.step) || rule.step === 0 || !(0, _orkestrel_contract.isFiniteNumber)(multiple) || Math.abs(multiple - Math.round(multiple)) > 1e-9) errors.push(createFieldError(field, "step", rule.step, messages));
|
|
669
683
|
}
|
|
670
684
|
if ((0, _orkestrel_contract.isString)(value)) {
|
|
671
685
|
if (rule.pattern !== void 0 && appliesRule(field.control, "pattern")) {
|
|
672
686
|
const pattern = rule.pattern;
|
|
673
|
-
if (pattern.length > 256) errors.push(
|
|
674
|
-
field: field.name,
|
|
675
|
-
message: formatMessage("pattern", void 0, messages),
|
|
676
|
-
rule: "pattern"
|
|
677
|
-
}));
|
|
687
|
+
if (pattern.length > 256) errors.push(createFieldError(field, "pattern", void 0, messages));
|
|
678
688
|
else {
|
|
679
689
|
const outcome = (0, _orkestrel_contract.attempt)(() => new RegExp(pattern).test(value));
|
|
680
|
-
if (!outcome.success || !outcome.value) errors.push(
|
|
681
|
-
field: field.name,
|
|
682
|
-
message: formatMessage("pattern", void 0, messages),
|
|
683
|
-
rule: "pattern"
|
|
684
|
-
}));
|
|
690
|
+
if (!outcome.success || !outcome.value) errors.push(createFieldError(field, "pattern", void 0, messages));
|
|
685
691
|
}
|
|
686
692
|
}
|
|
687
|
-
if (rule.email === true && appliesRule(field.control, "email") && !EMAIL_PATTERN.test(value)) errors.push(
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
}));
|
|
692
|
-
if (rule.url === true && appliesRule(field.control, "url") && !URL_PATTERN.test(value)) errors.push(Object.freeze({
|
|
693
|
-
field: field.name,
|
|
694
|
-
message: formatMessage("url", void 0, messages),
|
|
695
|
-
rule: "url"
|
|
696
|
-
}));
|
|
697
|
-
if (rule.alphanumeric === true && appliesRule(field.control, "alphanumeric") && !ALPHANUMERIC_PATTERN.test(value)) errors.push(Object.freeze({
|
|
698
|
-
field: field.name,
|
|
699
|
-
message: formatMessage("alphanumeric", void 0, messages),
|
|
700
|
-
rule: "alphanumeric"
|
|
701
|
-
}));
|
|
702
|
-
if (rule.integer === true && appliesRule(field.control, "integer") && !INTEGER_PATTERN.test(value)) errors.push(Object.freeze({
|
|
703
|
-
field: field.name,
|
|
704
|
-
message: formatMessage("integer", void 0, messages),
|
|
705
|
-
rule: "integer"
|
|
706
|
-
}));
|
|
693
|
+
if (rule.email === true && appliesRule(field.control, "email") && !EMAIL_PATTERN.test(value)) errors.push(createFieldError(field, "email", void 0, messages));
|
|
694
|
+
if (rule.url === true && appliesRule(field.control, "url") && !URL_PATTERN.test(value)) errors.push(createFieldError(field, "url", void 0, messages));
|
|
695
|
+
if (rule.alphanumeric === true && appliesRule(field.control, "alphanumeric") && !ALPHANUMERIC_PATTERN.test(value)) errors.push(createFieldError(field, "alphanumeric", void 0, messages));
|
|
696
|
+
if (rule.integer === true && appliesRule(field.control, "integer") && !INTEGER_PATTERN.test(value)) errors.push(createFieldError(field, "integer", void 0, messages));
|
|
707
697
|
}
|
|
708
|
-
if (value !== void 0 && field.control === "number" && rule.integer === true && appliesRule(field.control, "integer") && !(0, _orkestrel_contract.isInteger)(value)) errors.push(
|
|
709
|
-
field: field.name,
|
|
710
|
-
message: formatMessage("integer", void 0, messages),
|
|
711
|
-
rule: "integer"
|
|
712
|
-
}));
|
|
698
|
+
if (value !== void 0 && field.control === "number" && rule.integer === true && appliesRule(field.control, "integer") && !(0, _orkestrel_contract.isInteger)(value)) errors.push(createFieldError(field, "integer", void 0, messages));
|
|
713
699
|
if (rule.custom !== void 0) {
|
|
714
700
|
const result = rule.custom(value, values);
|
|
715
701
|
if ((0, _orkestrel_contract.isString)(result)) errors.push(Object.freeze({
|
|
@@ -720,12 +706,14 @@ function evaluateField(field, value, values, messages) {
|
|
|
720
706
|
return Object.freeze(errors);
|
|
721
707
|
}
|
|
722
708
|
/**
|
|
723
|
-
*
|
|
709
|
+
* Evaluates every active field in schema order.
|
|
724
710
|
*
|
|
725
711
|
* @param schema - The form schema to evaluate.
|
|
726
712
|
* @param values - The values keyed by field name.
|
|
727
713
|
* @param options - Optional message replacements and the effective disabled field set.
|
|
728
714
|
* @returns Every field failure in schema and rule order.
|
|
715
|
+
* @throws Thrown when a {@link FieldValidator} supplied through {@link FieldRule.custom} throws:
|
|
716
|
+
* its own value escapes unchanged, because this helper adds no boundary around it.
|
|
729
717
|
*/
|
|
730
718
|
function evaluateForm(schema, values, options) {
|
|
731
719
|
const errors = [];
|
|
@@ -736,7 +724,11 @@ function evaluateForm(schema, values, options) {
|
|
|
736
724
|
return Object.freeze(errors);
|
|
737
725
|
}
|
|
738
726
|
/**
|
|
739
|
-
*
|
|
727
|
+
* Computes the values explicitly seeded by a schema.
|
|
728
|
+
*
|
|
729
|
+
* @remarks
|
|
730
|
+
* `password` and `file` declare no default, so a field of either control never appears in the
|
|
731
|
+
* result.
|
|
740
732
|
*
|
|
741
733
|
* @param schema - The schema whose defaults to collect.
|
|
742
734
|
* @returns A value record containing only fields with defaults.
|
|
@@ -745,12 +737,7 @@ function computeDefaults(schema) {
|
|
|
745
737
|
const defaults = {};
|
|
746
738
|
for (const field of schema.fields) switch (field.control) {
|
|
747
739
|
case "checkbox":
|
|
748
|
-
if (field.default !== void 0)
|
|
749
|
-
value: cloneValue(field.default),
|
|
750
|
-
enumerable: true,
|
|
751
|
-
configurable: true,
|
|
752
|
-
writable: true
|
|
753
|
-
});
|
|
740
|
+
if (field.default !== void 0) defineEntry(defaults, field.name, cloneValue(field.default));
|
|
754
741
|
break;
|
|
755
742
|
case "password":
|
|
756
743
|
case "file": break;
|
|
@@ -762,35 +749,30 @@ function computeDefaults(schema) {
|
|
|
762
749
|
case "datetime":
|
|
763
750
|
case "color":
|
|
764
751
|
case "confirm":
|
|
765
|
-
case "select": if (field.default !== void 0)
|
|
766
|
-
value: field.default,
|
|
767
|
-
enumerable: true,
|
|
768
|
-
configurable: true,
|
|
769
|
-
writable: true
|
|
770
|
-
});
|
|
752
|
+
case "select": if (field.default !== void 0) defineEntry(defaults, field.name, field.default);
|
|
771
753
|
}
|
|
772
754
|
return Object.freeze(defaults);
|
|
773
755
|
}
|
|
774
756
|
/**
|
|
775
|
-
*
|
|
757
|
+
* Compares two field values by scalar identity or ordered list content.
|
|
776
758
|
*
|
|
777
759
|
* @param a - The first field value.
|
|
778
760
|
* @param b - The second field value.
|
|
779
|
-
* @returns
|
|
761
|
+
* @returns True if both values contain the same answer; false otherwise.
|
|
780
762
|
*/
|
|
781
763
|
function matchesValue(a, b) {
|
|
782
764
|
if ((0, _orkestrel_contract.isArray)(a) || (0, _orkestrel_contract.isArray)(b)) return (0, _orkestrel_contract.isArray)(a) && (0, _orkestrel_contract.isArray)(b) && a.length === b.length && a.every((entry, index) => entry === b[index]);
|
|
783
765
|
return a === b;
|
|
784
766
|
}
|
|
785
767
|
/**
|
|
786
|
-
*
|
|
768
|
+
* Extracts the names whose answers differ between two form value records.
|
|
787
769
|
*
|
|
788
770
|
* @remarks
|
|
789
771
|
* Presence is compared in both directions before present values are compared through
|
|
790
772
|
* {@link matchesValue}. The returned set is a new snapshot, exposed as readonly because later
|
|
791
773
|
* changes to either input never alter its membership.
|
|
792
774
|
*
|
|
793
|
-
* @param current - The values
|
|
775
|
+
* @param current - The values the form holds.
|
|
794
776
|
* @param opened - The values held when the form opened.
|
|
795
777
|
* @returns A readonly snapshot of changed field names.
|
|
796
778
|
*/
|
|
@@ -809,17 +791,21 @@ function extractChanges(current, opened) {
|
|
|
809
791
|
return changed;
|
|
810
792
|
}
|
|
811
793
|
/**
|
|
812
|
-
*
|
|
794
|
+
* Compares two form value records by keys and value content.
|
|
813
795
|
*
|
|
814
796
|
* @param a - The first value record.
|
|
815
797
|
* @param b - The second value record.
|
|
816
|
-
* @returns
|
|
798
|
+
* @returns True if both records contain the same answers; false otherwise.
|
|
817
799
|
*/
|
|
818
800
|
function matchesValues(a, b) {
|
|
819
801
|
return extractChanges(a, b).size === 0;
|
|
820
802
|
}
|
|
821
803
|
/**
|
|
822
|
-
*
|
|
804
|
+
* Resolves and interpolates one rule message.
|
|
805
|
+
*
|
|
806
|
+
* @remarks
|
|
807
|
+
* A replacement in `messages` is read first and {@link RULE_MESSAGES} supplies the copy otherwise,
|
|
808
|
+
* and `{limit}` in whichever text wins is replaced with the rule's operand.
|
|
823
809
|
*
|
|
824
810
|
* @param rule - The rule whose message to resolve.
|
|
825
811
|
* @param limit - The optional operand substituted for `{limit}`.
|
|
@@ -831,7 +817,34 @@ function formatMessage(rule, limit, messages) {
|
|
|
831
817
|
return limit === void 0 ? message : message.replaceAll("{limit}", String(limit));
|
|
832
818
|
}
|
|
833
819
|
/**
|
|
834
|
-
*
|
|
820
|
+
* Creates one named-rule failure against a field.
|
|
821
|
+
*
|
|
822
|
+
* @param field - The field the rule failed on.
|
|
823
|
+
* @param rule - The named rule that failed.
|
|
824
|
+
* @param limit - The rule's operand, substituted for `{limit}`, or absence when it carries none.
|
|
825
|
+
* @param messages - Optional rule-specific message replacements.
|
|
826
|
+
* @returns A frozen {@link FieldError} carrying the field's name, the resolved text, and the rule.
|
|
827
|
+
*
|
|
828
|
+
* @remarks
|
|
829
|
+
* A `custom` validator and the form's `invalidate` method both report a message of their own
|
|
830
|
+
* under no rule name, so neither builds its failure here.
|
|
831
|
+
*
|
|
832
|
+
* @example
|
|
833
|
+
* ```ts
|
|
834
|
+
* const field: FormField = { control: 'text', name: 'nickname', rule: { minimum: 3 } }
|
|
835
|
+
* const error = createFieldError(field, 'minimum', 3)
|
|
836
|
+
* error.message // 'Must be at least 3'
|
|
837
|
+
* ```
|
|
838
|
+
*/
|
|
839
|
+
function createFieldError(field, rule, limit, messages) {
|
|
840
|
+
return Object.freeze({
|
|
841
|
+
field: field.name,
|
|
842
|
+
message: formatMessage(rule, limit, messages),
|
|
843
|
+
rule
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Projects a schema into JSON while removing custom validators and absent values.
|
|
835
848
|
*
|
|
836
849
|
* @param schema - The schema to project.
|
|
837
850
|
* @returns A deep JSON copy of the serializable schema.
|
|
@@ -933,7 +946,7 @@ function serializeForm(schema) {
|
|
|
933
946
|
return (0, _orkestrel_contract.cloneJSONRecord)(output);
|
|
934
947
|
}
|
|
935
948
|
/**
|
|
936
|
-
*
|
|
949
|
+
* Selects referenced groups in first-reference field order.
|
|
937
950
|
*
|
|
938
951
|
* @param schema - The schema whose group references to resolve.
|
|
939
952
|
* @returns The referenced schema groups without duplicates.
|
|
@@ -948,7 +961,7 @@ function extractGroups(schema) {
|
|
|
948
961
|
return Object.freeze(groups);
|
|
949
962
|
}
|
|
950
963
|
/**
|
|
951
|
-
*
|
|
964
|
+
* Audits a structurally valid schema for domain invariants.
|
|
952
965
|
*
|
|
953
966
|
* @param schema - The form schema to audit.
|
|
954
967
|
* @returns Human-readable invariant violations, or an empty list when the schema is sound.
|
|
@@ -1123,7 +1136,7 @@ function auditSchema(schema) {
|
|
|
1123
1136
|
//#endregion
|
|
1124
1137
|
//#region src/core/parsers.ts
|
|
1125
1138
|
/**
|
|
1126
|
-
*
|
|
1139
|
+
* Parses unknown wire data into an owned, semantically sound form schema.
|
|
1127
1140
|
*
|
|
1128
1141
|
* @param input - The unknown schema value to parse.
|
|
1129
1142
|
* @returns An owned schema with custom rules removed, or `undefined` on refusal.
|
|
@@ -1134,12 +1147,7 @@ function parseForm(input) {
|
|
|
1134
1147
|
const schema = {};
|
|
1135
1148
|
for (const key of Reflect.ownKeys(input)) {
|
|
1136
1149
|
if (!(0, _orkestrel_contract.isString)(key)) return void 0;
|
|
1137
|
-
|
|
1138
|
-
value: input[key],
|
|
1139
|
-
enumerable: true,
|
|
1140
|
-
configurable: true,
|
|
1141
|
-
writable: true
|
|
1142
|
-
});
|
|
1150
|
+
defineEntry(schema, key, input[key]);
|
|
1143
1151
|
}
|
|
1144
1152
|
const fields = schema.fields;
|
|
1145
1153
|
if (!(0, _orkestrel_contract.isArray)(fields)) return void 0;
|
|
@@ -1155,24 +1163,14 @@ function parseForm(input) {
|
|
|
1155
1163
|
const copy = {};
|
|
1156
1164
|
for (const key of Reflect.ownKeys(field)) {
|
|
1157
1165
|
if (!(0, _orkestrel_contract.isString)(key)) return void 0;
|
|
1158
|
-
|
|
1159
|
-
value: field[key],
|
|
1160
|
-
enumerable: true,
|
|
1161
|
-
configurable: true,
|
|
1162
|
-
writable: true
|
|
1163
|
-
});
|
|
1166
|
+
defineEntry(copy, key, field[key]);
|
|
1164
1167
|
}
|
|
1165
1168
|
const rule = copy.rule;
|
|
1166
1169
|
if ((0, _orkestrel_contract.isRecord)(rule)) {
|
|
1167
1170
|
const projected = {};
|
|
1168
1171
|
for (const key of Reflect.ownKeys(rule)) {
|
|
1169
1172
|
if (!(0, _orkestrel_contract.isString)(key)) return void 0;
|
|
1170
|
-
if (key !== "custom")
|
|
1171
|
-
value: rule[key],
|
|
1172
|
-
enumerable: true,
|
|
1173
|
-
configurable: true,
|
|
1174
|
-
writable: true
|
|
1175
|
-
});
|
|
1173
|
+
if (key !== "custom") defineEntry(projected, key, rule[key]);
|
|
1176
1174
|
}
|
|
1177
1175
|
copy.rule = projected;
|
|
1178
1176
|
}
|
|
@@ -1186,7 +1184,11 @@ function parseForm(input) {
|
|
|
1186
1184
|
return outcome.success ? outcome.value : void 0;
|
|
1187
1185
|
}
|
|
1188
1186
|
/**
|
|
1189
|
-
*
|
|
1187
|
+
* Parses one answer against its field control.
|
|
1188
|
+
*
|
|
1189
|
+
* @remarks
|
|
1190
|
+
* A numeric string coerces to a number for a `number` field, and `'true'` and `'false'` coerce to
|
|
1191
|
+
* a boolean for a `confirm` field. Every other value must already have its control's shape.
|
|
1190
1192
|
*
|
|
1191
1193
|
* @param field - The field that defines the accepted value.
|
|
1192
1194
|
* @param input - The unknown value to parse.
|
|
@@ -1208,7 +1210,7 @@ function parseValue(field, input) {
|
|
|
1208
1210
|
return outcome.success ? outcome.value : void 0;
|
|
1209
1211
|
}
|
|
1210
1212
|
/**
|
|
1211
|
-
*
|
|
1213
|
+
* Parses a strict answer record against the fields declared by a schema.
|
|
1212
1214
|
*
|
|
1213
1215
|
* @param schema - The schema that owns the accepted field names and controls.
|
|
1214
1216
|
* @param input - The unknown answer record to parse.
|
|
@@ -1224,12 +1226,7 @@ function parseValues(schema, input) {
|
|
|
1224
1226
|
if (field === void 0) return void 0;
|
|
1225
1227
|
const value = parseValue(field, input[key]);
|
|
1226
1228
|
if (value === void 0) return void 0;
|
|
1227
|
-
|
|
1228
|
-
value,
|
|
1229
|
-
enumerable: true,
|
|
1230
|
-
configurable: false,
|
|
1231
|
-
writable: false
|
|
1232
|
-
});
|
|
1229
|
+
freezeEntry(values, key, value);
|
|
1233
1230
|
}
|
|
1234
1231
|
return Object.freeze(values);
|
|
1235
1232
|
});
|
|
@@ -1238,7 +1235,8 @@ function parseValues(schema, input) {
|
|
|
1238
1235
|
//#endregion
|
|
1239
1236
|
//#region src/core/Form.ts
|
|
1240
1237
|
/**
|
|
1241
|
-
*
|
|
1238
|
+
* Implements `FormInterface` exactly, over an owned schema, the answers given against it, and the
|
|
1239
|
+
* errors they carry.
|
|
1242
1240
|
*
|
|
1243
1241
|
* @remarks
|
|
1244
1242
|
* The form owns its schema, so a later edit to the schema the caller passed changes nothing here.
|
|
@@ -1278,7 +1276,7 @@ var Form = class {
|
|
|
1278
1276
|
#evaluation = 0;
|
|
1279
1277
|
#pending = false;
|
|
1280
1278
|
/**
|
|
1281
|
-
*
|
|
1279
|
+
* Opens a form against a schema.
|
|
1282
1280
|
*
|
|
1283
1281
|
* @param schema - The form to ask. It is copied, and the copy is what the form asks.
|
|
1284
1282
|
* @param options - The form's settings.
|
|
@@ -1287,93 +1285,76 @@ var Form = class {
|
|
|
1287
1285
|
* value is one its field's control cannot hold.
|
|
1288
1286
|
*/
|
|
1289
1287
|
constructor(schema, options) {
|
|
1290
|
-
const
|
|
1291
|
-
if (
|
|
1292
|
-
|
|
1288
|
+
const owned = (0, _orkestrel_contract.attempt)(() => cloneFormSchema(schema));
|
|
1289
|
+
if (!owned.success && isFormError(owned.error)) throw owned.error;
|
|
1290
|
+
const copy = owned.success && isFormSchema(owned.value) ? owned.value : void 0;
|
|
1291
|
+
const problems = copy === void 0 ? ["The schema is not a form schema"] : auditSchema(copy);
|
|
1292
|
+
if (copy === void 0 || problems.length > 0) throw new FormError("SCHEMA", `The form schema is unusable: ${problems.join("; ")}`, { problems: [...problems] });
|
|
1293
|
+
this.#schema = copy;
|
|
1293
1294
|
this.#messages = options?.messages === void 0 ? void 0 : Object.freeze({ ...options.messages });
|
|
1294
1295
|
const baseline = {};
|
|
1295
|
-
for (const [name, value] of Object.entries(computeDefaults(this.#schema)))
|
|
1296
|
-
value,
|
|
1297
|
-
enumerable: true,
|
|
1298
|
-
configurable: true,
|
|
1299
|
-
writable: true
|
|
1300
|
-
});
|
|
1296
|
+
for (const [name, value] of Object.entries(computeDefaults(this.#schema))) defineEntry(baseline, name, value);
|
|
1301
1297
|
for (const [name, value] of Object.entries(options?.values ?? {})) {
|
|
1302
1298
|
const field = this.#requireField(name);
|
|
1303
1299
|
if (!matchesField(field, value)) throw new FormError("CONTROL", `The ${field.control} field "${name}" cannot hold that value`, {
|
|
1304
1300
|
field: name,
|
|
1305
1301
|
control: field.control
|
|
1306
1302
|
});
|
|
1307
|
-
|
|
1308
|
-
value: cloneValue(value),
|
|
1309
|
-
enumerable: true,
|
|
1310
|
-
configurable: true,
|
|
1311
|
-
writable: true
|
|
1312
|
-
});
|
|
1303
|
+
defineEntry(baseline, name, cloneValue(value));
|
|
1313
1304
|
}
|
|
1314
1305
|
this.#baseline = Object.freeze(baseline);
|
|
1315
1306
|
this.#values = {};
|
|
1316
|
-
for (const [name, value] of Object.entries(baseline))
|
|
1317
|
-
value,
|
|
1318
|
-
enumerable: true,
|
|
1319
|
-
configurable: true,
|
|
1320
|
-
writable: true
|
|
1321
|
-
});
|
|
1307
|
+
for (const [name, value] of Object.entries(baseline)) defineEntry(this.#values, name, value);
|
|
1322
1308
|
this.#resolvers.promise.catch(() => void 0);
|
|
1323
1309
|
this.#emitter = new _orkestrel_emitter.Emitter(options);
|
|
1324
1310
|
this.#evaluate();
|
|
1325
1311
|
}
|
|
1326
|
-
/**
|
|
1312
|
+
/** Holds the form's event emitter. */
|
|
1327
1313
|
get emitter() {
|
|
1328
1314
|
return this.#emitter;
|
|
1329
1315
|
}
|
|
1330
|
-
/**
|
|
1316
|
+
/** Holds the schema this form asks, owned and frozen. */
|
|
1331
1317
|
get schema() {
|
|
1332
1318
|
return this.#schema;
|
|
1333
1319
|
}
|
|
1334
|
-
/**
|
|
1320
|
+
/** Reports the answers the form holds. */
|
|
1335
1321
|
get values() {
|
|
1336
1322
|
const values = {};
|
|
1337
|
-
for (const name of Object.
|
|
1338
|
-
value: this.#values[name],
|
|
1339
|
-
enumerable: true,
|
|
1340
|
-
configurable: true,
|
|
1341
|
-
writable: true
|
|
1342
|
-
});
|
|
1323
|
+
for (const [name, value] of Object.entries(this.#values)) defineEntry(values, name, value);
|
|
1343
1324
|
return Object.freeze(values);
|
|
1344
1325
|
}
|
|
1345
|
-
/**
|
|
1326
|
+
/** Holds the answers the form opened with. */
|
|
1346
1327
|
get baseline() {
|
|
1347
1328
|
return this.#baseline;
|
|
1348
1329
|
}
|
|
1349
|
-
/**
|
|
1330
|
+
/** Holds every error the last completed evaluation produced. */
|
|
1350
1331
|
get errors() {
|
|
1351
1332
|
return this.#errors;
|
|
1352
1333
|
}
|
|
1353
|
-
/**
|
|
1334
|
+
/** Lists the names of the fields somebody has visited. */
|
|
1354
1335
|
get touched() {
|
|
1355
1336
|
return new Set(this.#touched);
|
|
1356
1337
|
}
|
|
1357
|
-
/**
|
|
1338
|
+
/** Lists the names of the fields that are out of the form. */
|
|
1358
1339
|
get disabled() {
|
|
1359
1340
|
const disabled = /* @__PURE__ */ new Set();
|
|
1360
1341
|
for (const field of this.#schema.fields) if ((this.#disabled.get(field.name) ?? field.disabled === true) === true) disabled.add(field.name);
|
|
1361
1342
|
return disabled;
|
|
1362
1343
|
}
|
|
1363
|
-
/**
|
|
1344
|
+
/** Reports where the form sits in its life. */
|
|
1364
1345
|
get status() {
|
|
1365
1346
|
return this.#status;
|
|
1366
1347
|
}
|
|
1367
|
-
/**
|
|
1348
|
+
/** Reports whether the last completed evaluation found no error. */
|
|
1368
1349
|
get valid() {
|
|
1369
1350
|
return this.#errors.length === 0;
|
|
1370
1351
|
}
|
|
1371
|
-
/**
|
|
1352
|
+
/** Reports whether any answer has moved since the form opened. */
|
|
1372
1353
|
get dirty() {
|
|
1373
1354
|
return !matchesValues(this.values, this.#baseline);
|
|
1374
1355
|
}
|
|
1375
1356
|
/**
|
|
1376
|
-
*
|
|
1357
|
+
* Holds the answers after the form settles.
|
|
1377
1358
|
*
|
|
1378
1359
|
* @remarks
|
|
1379
1360
|
* It resolves with the submitted values on the first valid submit, and rejects with a
|
|
@@ -1383,7 +1364,7 @@ var Form = class {
|
|
|
1383
1364
|
return this.#resolvers.promise;
|
|
1384
1365
|
}
|
|
1385
1366
|
/**
|
|
1386
|
-
*
|
|
1367
|
+
* Finds one field by name.
|
|
1387
1368
|
*
|
|
1388
1369
|
* @param name - The field's name.
|
|
1389
1370
|
* @returns The field, or `undefined` when the schema declares no such name.
|
|
@@ -1392,7 +1373,7 @@ var Form = class {
|
|
|
1392
1373
|
return this.#schema.fields.find((field) => field.name === name);
|
|
1393
1374
|
}
|
|
1394
1375
|
/**
|
|
1395
|
-
*
|
|
1376
|
+
* Answers one field or several.
|
|
1396
1377
|
*
|
|
1397
1378
|
* @param input - One field's name, or the answers to write keyed by field name.
|
|
1398
1379
|
* @param value - The answer to write when `input` names one field.
|
|
@@ -1415,12 +1396,7 @@ var Form = class {
|
|
|
1415
1396
|
for (const [name, answer] of entries) {
|
|
1416
1397
|
if (this.#differs(name, answer)) moved.push(name);
|
|
1417
1398
|
if (answer === void 0) delete this.#values[name];
|
|
1418
|
-
else
|
|
1419
|
-
value: cloneValue(answer),
|
|
1420
|
-
enumerable: true,
|
|
1421
|
-
configurable: true,
|
|
1422
|
-
writable: true
|
|
1423
|
-
});
|
|
1399
|
+
else defineEntry(this.#values, name, cloneValue(answer));
|
|
1424
1400
|
this.#invalidations.delete(name);
|
|
1425
1401
|
}
|
|
1426
1402
|
for (const name of moved) {
|
|
@@ -1431,7 +1407,7 @@ var Form = class {
|
|
|
1431
1407
|
});
|
|
1432
1408
|
}
|
|
1433
1409
|
/**
|
|
1434
|
-
*
|
|
1410
|
+
* Records that somebody has visited a field.
|
|
1435
1411
|
*
|
|
1436
1412
|
* @param name - The field's name.
|
|
1437
1413
|
* @throws A {@link FormError} coded `SETTLED` or `ABANDONED` when the form has ended, and
|
|
@@ -1443,7 +1419,7 @@ var Form = class {
|
|
|
1443
1419
|
this.#touched.add(name);
|
|
1444
1420
|
}
|
|
1445
1421
|
/**
|
|
1446
|
-
*
|
|
1422
|
+
* Fails a field from outside, for what the rules cannot see.
|
|
1447
1423
|
*
|
|
1448
1424
|
* @param name - The field's name.
|
|
1449
1425
|
* @param message - What to tell the person.
|
|
@@ -1462,7 +1438,7 @@ var Form = class {
|
|
|
1462
1438
|
});
|
|
1463
1439
|
}
|
|
1464
1440
|
/**
|
|
1465
|
-
*
|
|
1441
|
+
* Takes one or more fields out of the form.
|
|
1466
1442
|
*
|
|
1467
1443
|
* @param input - One field name, several names, or absence to select every field.
|
|
1468
1444
|
* @throws A {@link FormError} coded `SETTLED` or `ABANDONED` when the form has ended, and
|
|
@@ -1474,7 +1450,7 @@ var Form = class {
|
|
|
1474
1450
|
this.#change(input, true);
|
|
1475
1451
|
}
|
|
1476
1452
|
/**
|
|
1477
|
-
*
|
|
1453
|
+
* Puts one or more fields back into the form.
|
|
1478
1454
|
*
|
|
1479
1455
|
* @param input - One field name, several names, or absence to select every field.
|
|
1480
1456
|
* @throws A {@link FormError} coded `SETTLED` or `ABANDONED` when the form has ended, and
|
|
@@ -1486,7 +1462,7 @@ var Form = class {
|
|
|
1486
1462
|
this.#change(input, false);
|
|
1487
1463
|
}
|
|
1488
1464
|
/**
|
|
1489
|
-
*
|
|
1465
|
+
* Checks every answer and settles the form when they all pass.
|
|
1490
1466
|
*
|
|
1491
1467
|
* @returns The values on success, or every error that stopped them.
|
|
1492
1468
|
* @remarks
|
|
@@ -1539,7 +1515,7 @@ var Form = class {
|
|
|
1539
1515
|
});
|
|
1540
1516
|
}
|
|
1541
1517
|
/**
|
|
1542
|
-
*
|
|
1518
|
+
* Returns every answer to the ones the form opened with: the schema's defaults, overlaid with
|
|
1543
1519
|
* any seeded `values`. Reset the runtime disabled state to the schema's declarations.
|
|
1544
1520
|
*
|
|
1545
1521
|
* @throws A {@link FormError} coded `SETTLED` or `ABANDONED` when the form has ended.
|
|
@@ -1548,12 +1524,7 @@ var Form = class {
|
|
|
1548
1524
|
this.#gate();
|
|
1549
1525
|
this.#batch(() => {
|
|
1550
1526
|
for (const name of Object.keys(this.#values)) delete this.#values[name];
|
|
1551
|
-
for (const [name, value] of Object.entries(this.#baseline))
|
|
1552
|
-
value,
|
|
1553
|
-
enumerable: true,
|
|
1554
|
-
configurable: true,
|
|
1555
|
-
writable: true
|
|
1556
|
-
});
|
|
1527
|
+
for (const [name, value] of Object.entries(this.#baseline)) defineEntry(this.#values, name, value);
|
|
1557
1528
|
this.#touched.clear();
|
|
1558
1529
|
this.#disabled.clear();
|
|
1559
1530
|
this.#invalidations.clear();
|
|
@@ -1563,7 +1534,7 @@ var Form = class {
|
|
|
1563
1534
|
});
|
|
1564
1535
|
}
|
|
1565
1536
|
/**
|
|
1566
|
-
*
|
|
1537
|
+
* Tears the form down, abandoning it when it has not settled.
|
|
1567
1538
|
*
|
|
1568
1539
|
* @remarks
|
|
1569
1540
|
* Destroying twice does nothing the second time. A settled form keeps its `settled` status and
|
|
@@ -1641,12 +1612,7 @@ var Form = class {
|
|
|
1641
1612
|
const disabled = this.disabled;
|
|
1642
1613
|
for (const field of this.#schema.fields) {
|
|
1643
1614
|
const value = Object.hasOwn(this.#values, field.name) ? this.#values[field.name] : void 0;
|
|
1644
|
-
if (!disabled.has(field.name) && value !== void 0)
|
|
1645
|
-
value,
|
|
1646
|
-
enumerable: true,
|
|
1647
|
-
configurable: true,
|
|
1648
|
-
writable: true
|
|
1649
|
-
});
|
|
1615
|
+
if (!disabled.has(field.name) && value !== void 0) defineEntry(answers, field.name, value);
|
|
1650
1616
|
}
|
|
1651
1617
|
return Object.freeze(answers);
|
|
1652
1618
|
}
|
|
@@ -1671,7 +1637,7 @@ var Form = class {
|
|
|
1671
1637
|
//#endregion
|
|
1672
1638
|
//#region src/core/factories.ts
|
|
1673
1639
|
/**
|
|
1674
|
-
*
|
|
1640
|
+
* Opens a form against a schema.
|
|
1675
1641
|
*
|
|
1676
1642
|
* @param schema - The form to ask. It is copied, and the copy is what the form asks.
|
|
1677
1643
|
* @param options - The form's settings.
|
|
@@ -1682,8 +1648,10 @@ var Form = class {
|
|
|
1682
1648
|
* @throws A {@link FormError} coded `SCHEMA` when the schema is malformed, `FIELD` when
|
|
1683
1649
|
* `options.values` names a field the schema does not declare, and `CONTROL` when a seeded value
|
|
1684
1650
|
* is one its field's control cannot hold.
|
|
1685
|
-
* @example
|
|
1651
|
+
* @example Open a form, answer it, and settle it
|
|
1686
1652
|
* ```ts
|
|
1653
|
+
* import { createForm } from '@orkestrel/form'
|
|
1654
|
+
*
|
|
1687
1655
|
* const form = createForm({
|
|
1688
1656
|
* label: 'Sign up',
|
|
1689
1657
|
* fields: [
|
|
@@ -1693,7 +1661,8 @@ var Form = class {
|
|
|
1693
1661
|
* })
|
|
1694
1662
|
*
|
|
1695
1663
|
* form.fill({ email: 'ada@example.com', terms: true })
|
|
1696
|
-
* form.submit() // { success: true, value: { email: 'ada@example.com', terms: true } }
|
|
1664
|
+
* const result = form.submit() // { success: true, value: { email: 'ada@example.com', terms: true } }
|
|
1665
|
+
* const answers = await form.answer // { email: 'ada@example.com', terms: true }
|
|
1697
1666
|
* ```
|
|
1698
1667
|
*/
|
|
1699
1668
|
function createForm(schema, options) {
|
|
@@ -1706,7 +1675,9 @@ exports.COLOR_PATTERN = COLOR_PATTERN;
|
|
|
1706
1675
|
exports.DATETIME_PATTERN = DATETIME_PATTERN;
|
|
1707
1676
|
exports.DATE_PATTERN = DATE_PATTERN;
|
|
1708
1677
|
exports.EMAIL_PATTERN = EMAIL_PATTERN;
|
|
1678
|
+
exports.FIELD_BASE_KEYS = FIELD_BASE_KEYS;
|
|
1709
1679
|
exports.FIELD_CONTROLS = FIELD_CONTROLS;
|
|
1680
|
+
exports.FIELD_KEYS = FIELD_KEYS;
|
|
1710
1681
|
exports.FIELD_LIMIT = FIELD_LIMIT;
|
|
1711
1682
|
exports.FORM_STATUSES = FORM_STATUSES;
|
|
1712
1683
|
exports.Form = Form;
|
|
@@ -1729,12 +1700,15 @@ exports.cloneFormField = cloneFormField;
|
|
|
1729
1700
|
exports.cloneFormSchema = cloneFormSchema;
|
|
1730
1701
|
exports.cloneValue = cloneValue;
|
|
1731
1702
|
exports.computeDefaults = computeDefaults;
|
|
1703
|
+
exports.createFieldError = createFieldError;
|
|
1732
1704
|
exports.createForm = createForm;
|
|
1705
|
+
exports.defineEntry = defineEntry;
|
|
1733
1706
|
exports.evaluateField = evaluateField;
|
|
1734
1707
|
exports.evaluateForm = evaluateForm;
|
|
1735
1708
|
exports.extractChanges = extractChanges;
|
|
1736
1709
|
exports.extractGroups = extractGroups;
|
|
1737
1710
|
exports.formatMessage = formatMessage;
|
|
1711
|
+
exports.freezeEntry = freezeEntry;
|
|
1738
1712
|
exports.isFieldChoice = isFieldChoice;
|
|
1739
1713
|
exports.isFieldControl = isFieldControl;
|
|
1740
1714
|
exports.isFieldError = isFieldError;
|