@orkestrel/form 0.0.4 → 0.0.5
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 +13 -13
- package/dist/src/core/index.cjs +284 -338
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +255 -168
- package/dist/src/core/index.d.ts +255 -168
- package/dist/src/core/index.js +281 -340
- package/dist/src/core/index.js.map +1 -1
- package/package.json +6 -6
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. */
|
|
55
111
|
var PATTERN_LIMIT = 256;
|
|
56
|
-
/**
|
|
112
|
+
/** Caps the number of fields one schema may declare. */
|
|
57
113
|
var FIELD_LIMIT = 512;
|
|
58
|
-
/**
|
|
114
|
+
/** Caps the number of groups one schema may declare. */
|
|
59
115
|
var GROUP_LIMIT = 64;
|
|
60
|
-
/**
|
|
116
|
+
/** Caps the number of choices one `select` or `checkbox` field may offer. */
|
|
61
117
|
var CHOICE_LIMIT = 1024;
|
|
62
|
-
/**
|
|
118
|
+
/** Caps the number of entries one list-valued answer may hold. */
|
|
63
119
|
var LIST_LIMIT = 1024;
|
|
64
|
-
/**
|
|
120
|
+
/** Caps the length, in UTF-16 code units, of a schema, group, or field name. */
|
|
65
121
|
var NAME_LIMIT = 128;
|
|
66
|
-
/**
|
|
122
|
+
/** Caps the length, in UTF-16 code units, of any single retained string. */
|
|
67
123
|
var STRING_LIMIT = 65536;
|
|
68
|
-
/**
|
|
124
|
+
/** Caps the total length, in UTF-16 code units, of every string one schema retains. */
|
|
69
125
|
var TEXT_LIMIT = 1048576;
|
|
70
|
-
/**
|
|
126
|
+
/** Caps the total number of records, arrays, and leaves one schema retains. */
|
|
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,37 @@ 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.
|
|
125
193
|
*
|
|
126
194
|
* @param input - The value to inspect.
|
|
127
|
-
* @returns
|
|
195
|
+
* @returns True if the value is a field value; false otherwise.
|
|
128
196
|
*/
|
|
129
197
|
function isFieldValue(input) {
|
|
130
198
|
return (0, _orkestrel_contract.unionOf)(_orkestrel_contract.isString, _orkestrel_contract.isFiniteNumber, _orkestrel_contract.isBoolean, (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString))(input);
|
|
131
199
|
}
|
|
132
200
|
/**
|
|
133
|
-
*
|
|
201
|
+
* Determines whether an unknown value is one exact field choice record.
|
|
134
202
|
*
|
|
135
203
|
* @param input - The value to inspect.
|
|
136
|
-
* @returns
|
|
204
|
+
* @returns True if the value is a field choice; false otherwise.
|
|
137
205
|
*/
|
|
138
206
|
function isFieldChoice(input) {
|
|
139
207
|
const keys = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.isRecord)(input) && Reflect.ownKeys(input).every((key) => (0, _orkestrel_contract.isString)(key)));
|
|
@@ -146,10 +214,10 @@ function isFieldChoice(input) {
|
|
|
146
214
|
}, ["help", "disabled"])(input);
|
|
147
215
|
}
|
|
148
216
|
/**
|
|
149
|
-
*
|
|
217
|
+
* Determines whether an unknown value is one exact field rule record.
|
|
150
218
|
*
|
|
151
219
|
* @param input - The value to inspect.
|
|
152
|
-
* @returns
|
|
220
|
+
* @returns True if the value is a structurally valid field rule; false otherwise.
|
|
153
221
|
*/
|
|
154
222
|
function isFieldRule(input) {
|
|
155
223
|
const keys = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.isRecord)(input) && Reflect.ownKeys(input).every((key) => (0, _orkestrel_contract.isString)(key)));
|
|
@@ -168,7 +236,7 @@ function isFieldRule(input) {
|
|
|
168
236
|
}, true)(input);
|
|
169
237
|
}
|
|
170
238
|
/**
|
|
171
|
-
*
|
|
239
|
+
* Determines whether an unknown value is one exact discriminated form field.
|
|
172
240
|
*
|
|
173
241
|
* @remarks
|
|
174
242
|
* Metadata is admitted structurally as bounded JSON. An accessor-bearing metadata record is
|
|
@@ -176,120 +244,15 @@ function isFieldRule(input) {
|
|
|
176
244
|
* data properties only.
|
|
177
245
|
*
|
|
178
246
|
* @param input - The value to inspect.
|
|
179
|
-
* @returns
|
|
247
|
+
* @returns True if the value is a structurally valid form field; false otherwise.
|
|
180
248
|
*/
|
|
181
249
|
function isFormField(input) {
|
|
182
250
|
const outcome = (0, _orkestrel_contract.attempt)(() => {
|
|
183
251
|
if (!(0, _orkestrel_contract.isRecord)(input) || !Object.hasOwn(input, "control") || !Object.hasOwn(input, "name")) return false;
|
|
184
252
|
const control = input.control;
|
|
185
253
|
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;
|
|
254
|
+
const permitted = FIELD_KEYS[control];
|
|
255
|
+
if (!Reflect.ownKeys(input).every((key) => (0, _orkestrel_contract.isString)(key) && permitted.includes(key))) return false;
|
|
293
256
|
const name = input.name;
|
|
294
257
|
const hasLabel = Object.hasOwn(input, "label");
|
|
295
258
|
const label = hasLabel ? input.label : void 0;
|
|
@@ -370,10 +333,10 @@ function isFormField(input) {
|
|
|
370
333
|
return outcome.success && outcome.value;
|
|
371
334
|
}
|
|
372
335
|
/**
|
|
373
|
-
*
|
|
336
|
+
* Determines whether an unknown value is one exact form group record.
|
|
374
337
|
*
|
|
375
338
|
* @param input - The value to inspect.
|
|
376
|
-
* @returns
|
|
339
|
+
* @returns True if the value is a form group; false otherwise.
|
|
377
340
|
*/
|
|
378
341
|
function isFormGroup(input) {
|
|
379
342
|
const keys = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.isRecord)(input) && Reflect.ownKeys(input).every((key) => (0, _orkestrel_contract.isString)(key)));
|
|
@@ -385,10 +348,10 @@ function isFormGroup(input) {
|
|
|
385
348
|
}, ["help"])(input);
|
|
386
349
|
}
|
|
387
350
|
/**
|
|
388
|
-
*
|
|
351
|
+
* Determines whether an unknown value is one exact structural form schema.
|
|
389
352
|
*
|
|
390
353
|
* @param input - The value to inspect.
|
|
391
|
-
* @returns
|
|
354
|
+
* @returns True if the value is a structurally valid form schema; false otherwise.
|
|
392
355
|
*/
|
|
393
356
|
function isFormSchema(input) {
|
|
394
357
|
const keys = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.isRecord)(input) && Reflect.ownKeys(input).every((key) => (0, _orkestrel_contract.isString)(key)));
|
|
@@ -407,10 +370,10 @@ function isFormSchema(input) {
|
|
|
407
370
|
])(input);
|
|
408
371
|
}
|
|
409
372
|
/**
|
|
410
|
-
*
|
|
373
|
+
* Determines whether an unknown value is a record of field values.
|
|
411
374
|
*
|
|
412
375
|
* @param input - The value to inspect.
|
|
413
|
-
* @returns
|
|
376
|
+
* @returns True if the value is a form values record; false otherwise.
|
|
414
377
|
*/
|
|
415
378
|
function isFormValues(input) {
|
|
416
379
|
const outcome = (0, _orkestrel_contract.attempt)(() => {
|
|
@@ -420,10 +383,10 @@ function isFormValues(input) {
|
|
|
420
383
|
return outcome.success && outcome.value;
|
|
421
384
|
}
|
|
422
385
|
/**
|
|
423
|
-
*
|
|
386
|
+
* Determines whether an unknown value is one exact field error record.
|
|
424
387
|
*
|
|
425
388
|
* @param input - The value to inspect.
|
|
426
|
-
* @returns
|
|
389
|
+
* @returns True if the value is a field error; false otherwise.
|
|
427
390
|
*/
|
|
428
391
|
function isFieldError(input) {
|
|
429
392
|
const keys = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.isRecord)(input) && Reflect.ownKeys(input).every((key) => (0, _orkestrel_contract.isString)(key)));
|
|
@@ -431,7 +394,7 @@ function isFieldError(input) {
|
|
|
431
394
|
return (0, _orkestrel_contract.recordOf)({
|
|
432
395
|
field: _orkestrel_contract.isString,
|
|
433
396
|
message: _orkestrel_contract.isString,
|
|
434
|
-
rule: (0, _orkestrel_contract.
|
|
397
|
+
rule: (0, _orkestrel_contract.keyOf)(RULE_MESSAGES)
|
|
435
398
|
}, ["rule"])(input);
|
|
436
399
|
}
|
|
437
400
|
//#endregion
|
|
@@ -440,7 +403,7 @@ function cloneValue(value) {
|
|
|
440
403
|
return (0, _orkestrel_contract.isArray)(value) ? Object.freeze(value.slice()) : value;
|
|
441
404
|
}
|
|
442
405
|
/**
|
|
443
|
-
*
|
|
406
|
+
* Clones a field's choices into an owned frozen snapshot.
|
|
444
407
|
*
|
|
445
408
|
* @param choices - The choices to own.
|
|
446
409
|
* @returns A frozen list of frozen choice records.
|
|
@@ -449,7 +412,7 @@ function cloneChoices(choices) {
|
|
|
449
412
|
return Object.freeze(choices.map((choice) => Object.freeze({ ...choice })));
|
|
450
413
|
}
|
|
451
414
|
/**
|
|
452
|
-
*
|
|
415
|
+
* Clones one form field into an owned frozen snapshot.
|
|
453
416
|
*
|
|
454
417
|
* @param field - The field to own.
|
|
455
418
|
* @returns A frozen field with every nested collection owned.
|
|
@@ -500,7 +463,7 @@ function cloneFormField(field) {
|
|
|
500
463
|
}
|
|
501
464
|
}
|
|
502
465
|
/**
|
|
503
|
-
*
|
|
466
|
+
* Clones a form schema into an owned frozen snapshot.
|
|
504
467
|
*
|
|
505
468
|
* @param schema - The schema to own.
|
|
506
469
|
* @returns A frozen schema with every nested record and list owned.
|
|
@@ -516,11 +479,64 @@ function cloneFormSchema(schema) {
|
|
|
516
479
|
//#endregion
|
|
517
480
|
//#region src/core/helpers.ts
|
|
518
481
|
/**
|
|
519
|
-
*
|
|
482
|
+
* Writes one own enumerable data property onto a record.
|
|
483
|
+
*
|
|
484
|
+
* @param target - The record to write into.
|
|
485
|
+
* @param name - The property name to write.
|
|
486
|
+
* @param value - The value to store.
|
|
487
|
+
*
|
|
488
|
+
* @remarks
|
|
489
|
+
* Plain assignment runs an inherited setter, so writing a `__proto__` key that way reaches
|
|
490
|
+
* `Object.prototype` and leaves the record without the entry. Defining the property writes the
|
|
491
|
+
* record itself, whatever the prototype chain declares. The entry stays writable and configurable.
|
|
492
|
+
*
|
|
493
|
+
* @example
|
|
494
|
+
* ```ts
|
|
495
|
+
* const values: Record<string, number> = {}
|
|
496
|
+
* defineEntry(values, '__proto__', 1)
|
|
497
|
+
* Object.hasOwn(values, '__proto__') // true
|
|
498
|
+
* ```
|
|
499
|
+
*/
|
|
500
|
+
function defineEntry(target, name, value) {
|
|
501
|
+
Object.defineProperty(target, name, {
|
|
502
|
+
value,
|
|
503
|
+
enumerable: true,
|
|
504
|
+
configurable: true,
|
|
505
|
+
writable: true
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Writes one own enumerable data property that cannot be rewritten or removed.
|
|
510
|
+
*
|
|
511
|
+
* @param target - The record to write into.
|
|
512
|
+
* @param name - The property name to write.
|
|
513
|
+
* @param value - The value to store.
|
|
514
|
+
*
|
|
515
|
+
* @remarks
|
|
516
|
+
* The prototype-safe write of {@link defineEntry}, frozen: the entry is neither writable nor
|
|
517
|
+
* configurable, so the record a parser hands back cannot be edited through the key it just filled.
|
|
518
|
+
*
|
|
519
|
+
* @example
|
|
520
|
+
* ```ts
|
|
521
|
+
* const values: Record<string, number> = {}
|
|
522
|
+
* freezeEntry(values, '__proto__', 1)
|
|
523
|
+
* Object.getOwnPropertyDescriptor(values, '__proto__')?.writable // false
|
|
524
|
+
* ```
|
|
525
|
+
*/
|
|
526
|
+
function freezeEntry(target, name, value) {
|
|
527
|
+
Object.defineProperty(target, name, {
|
|
528
|
+
value,
|
|
529
|
+
enumerable: true,
|
|
530
|
+
configurable: false,
|
|
531
|
+
writable: false
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Checks whether a value has the shape required by one field control.
|
|
520
536
|
*
|
|
521
537
|
* @param field - The field that owns the value.
|
|
522
538
|
* @param value - The unknown value to inspect.
|
|
523
|
-
* @returns
|
|
539
|
+
* @returns True if the control can hold the value; false otherwise.
|
|
524
540
|
*/
|
|
525
541
|
function matchesField(field, value) {
|
|
526
542
|
if ((0, _orkestrel_contract.isString)(value) && value.length > 65536) return false;
|
|
@@ -549,7 +565,7 @@ function matchesField(field, value) {
|
|
|
549
565
|
}
|
|
550
566
|
}
|
|
551
567
|
/**
|
|
552
|
-
*
|
|
568
|
+
* Decides whether a raw binding value projects to an answered field.
|
|
553
569
|
*
|
|
554
570
|
* @remarks
|
|
555
571
|
* Bind with `fill(name, matchesAnswer(raw) ? raw : undefined)`. This projection treats an absent
|
|
@@ -558,13 +574,13 @@ function matchesField(field, value) {
|
|
|
558
574
|
* its `required` rule remains presence-only.
|
|
559
575
|
*
|
|
560
576
|
* @param value - The raw field value, or absence.
|
|
561
|
-
* @returns
|
|
577
|
+
* @returns True if the binding preserves the value as an answer; false otherwise.
|
|
562
578
|
*/
|
|
563
579
|
function matchesAnswer(value) {
|
|
564
580
|
return value !== void 0 && (!(0, _orkestrel_contract.isString)(value) || value.trim().length > 0);
|
|
565
581
|
}
|
|
566
582
|
/**
|
|
567
|
-
*
|
|
583
|
+
* Checks whether a named rule applies to one field control.
|
|
568
584
|
*
|
|
569
585
|
* @remarks
|
|
570
586
|
* The runtime control-membership check keeps this boundary total for JavaScript callers that
|
|
@@ -572,7 +588,7 @@ function matchesAnswer(value) {
|
|
|
572
588
|
*
|
|
573
589
|
* @param control - The field control to inspect.
|
|
574
590
|
* @param rule - The named rule to inspect.
|
|
575
|
-
* @returns
|
|
591
|
+
* @returns True if the control evaluates that rule; false otherwise.
|
|
576
592
|
*/
|
|
577
593
|
function appliesRule(control, rule) {
|
|
578
594
|
if (!FIELD_CONTROLS.some((candidate) => candidate === control)) return false;
|
|
@@ -590,23 +606,21 @@ function appliesRule(control, rule) {
|
|
|
590
606
|
return false;
|
|
591
607
|
}
|
|
592
608
|
/**
|
|
593
|
-
*
|
|
609
|
+
* Evaluates one field rule against its current value.
|
|
594
610
|
*
|
|
595
611
|
* @param field - The field and rule to evaluate.
|
|
596
612
|
* @param value - The current value, or absence.
|
|
597
613
|
* @param values - Every value available to a custom rule.
|
|
598
614
|
* @param messages - Optional rule-specific message replacements.
|
|
599
615
|
* @returns Every failure in rule order.
|
|
616
|
+
* @throws Thrown when a {@link FieldValidator} supplied through {@link FieldRule.custom} throws:
|
|
617
|
+
* its own value escapes unchanged, because this helper adds no boundary around it.
|
|
600
618
|
*/
|
|
601
619
|
function evaluateField(field, value, values, messages) {
|
|
602
620
|
const errors = [];
|
|
603
621
|
const rule = field.rule;
|
|
604
622
|
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
|
-
}));
|
|
623
|
+
if (rule?.required === true && appliesRule(field.control, "required")) errors.push(createFieldError(field, "required", void 0, messages));
|
|
610
624
|
}
|
|
611
625
|
if (rule === void 0) return Object.freeze(errors);
|
|
612
626
|
if (rule.minimum !== void 0 && appliesRule(field.control, "minimum")) {
|
|
@@ -628,11 +642,7 @@ function evaluateField(field, value, values, messages) {
|
|
|
628
642
|
case "checkbox":
|
|
629
643
|
case "file": failed = (0, _orkestrel_contract.isArray)(value) && (0, _orkestrel_contract.isFiniteNumber)(rule.minimum) && value.length < rule.minimum;
|
|
630
644
|
}
|
|
631
|
-
if (failed) errors.push(
|
|
632
|
-
field: field.name,
|
|
633
|
-
message: formatMessage("minimum", rule.minimum, messages),
|
|
634
|
-
rule: "minimum"
|
|
635
|
-
}));
|
|
645
|
+
if (failed) errors.push(createFieldError(field, "minimum", rule.minimum, messages));
|
|
636
646
|
}
|
|
637
647
|
if (rule.maximum !== void 0 && appliesRule(field.control, "maximum")) {
|
|
638
648
|
let failed = false;
|
|
@@ -653,63 +663,27 @@ function evaluateField(field, value, values, messages) {
|
|
|
653
663
|
case "checkbox":
|
|
654
664
|
case "file": failed = (0, _orkestrel_contract.isArray)(value) && (0, _orkestrel_contract.isFiniteNumber)(rule.maximum) && value.length > rule.maximum;
|
|
655
665
|
}
|
|
656
|
-
if (failed) errors.push(
|
|
657
|
-
field: field.name,
|
|
658
|
-
message: formatMessage("maximum", rule.maximum, messages),
|
|
659
|
-
rule: "maximum"
|
|
660
|
-
}));
|
|
666
|
+
if (failed) errors.push(createFieldError(field, "maximum", rule.maximum, messages));
|
|
661
667
|
}
|
|
662
668
|
if (rule.step !== void 0 && appliesRule(field.control, "step") && (0, _orkestrel_contract.isFiniteNumber)(value)) {
|
|
663
669
|
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
|
-
}));
|
|
670
|
+
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
671
|
}
|
|
670
672
|
if ((0, _orkestrel_contract.isString)(value)) {
|
|
671
673
|
if (rule.pattern !== void 0 && appliesRule(field.control, "pattern")) {
|
|
672
674
|
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
|
-
}));
|
|
675
|
+
if (pattern.length > 256) errors.push(createFieldError(field, "pattern", void 0, messages));
|
|
678
676
|
else {
|
|
679
677
|
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
|
-
}));
|
|
678
|
+
if (!outcome.success || !outcome.value) errors.push(createFieldError(field, "pattern", void 0, messages));
|
|
685
679
|
}
|
|
686
680
|
}
|
|
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
|
-
}));
|
|
681
|
+
if (rule.email === true && appliesRule(field.control, "email") && !EMAIL_PATTERN.test(value)) errors.push(createFieldError(field, "email", void 0, messages));
|
|
682
|
+
if (rule.url === true && appliesRule(field.control, "url") && !URL_PATTERN.test(value)) errors.push(createFieldError(field, "url", void 0, messages));
|
|
683
|
+
if (rule.alphanumeric === true && appliesRule(field.control, "alphanumeric") && !ALPHANUMERIC_PATTERN.test(value)) errors.push(createFieldError(field, "alphanumeric", void 0, messages));
|
|
684
|
+
if (rule.integer === true && appliesRule(field.control, "integer") && !INTEGER_PATTERN.test(value)) errors.push(createFieldError(field, "integer", void 0, messages));
|
|
707
685
|
}
|
|
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
|
-
}));
|
|
686
|
+
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
687
|
if (rule.custom !== void 0) {
|
|
714
688
|
const result = rule.custom(value, values);
|
|
715
689
|
if ((0, _orkestrel_contract.isString)(result)) errors.push(Object.freeze({
|
|
@@ -720,12 +694,14 @@ function evaluateField(field, value, values, messages) {
|
|
|
720
694
|
return Object.freeze(errors);
|
|
721
695
|
}
|
|
722
696
|
/**
|
|
723
|
-
*
|
|
697
|
+
* Evaluates every active field in schema order.
|
|
724
698
|
*
|
|
725
699
|
* @param schema - The form schema to evaluate.
|
|
726
700
|
* @param values - The values keyed by field name.
|
|
727
701
|
* @param options - Optional message replacements and the effective disabled field set.
|
|
728
702
|
* @returns Every field failure in schema and rule order.
|
|
703
|
+
* @throws Thrown when a {@link FieldValidator} supplied through {@link FieldRule.custom} throws:
|
|
704
|
+
* its own value escapes unchanged, because this helper adds no boundary around it.
|
|
729
705
|
*/
|
|
730
706
|
function evaluateForm(schema, values, options) {
|
|
731
707
|
const errors = [];
|
|
@@ -736,7 +712,7 @@ function evaluateForm(schema, values, options) {
|
|
|
736
712
|
return Object.freeze(errors);
|
|
737
713
|
}
|
|
738
714
|
/**
|
|
739
|
-
*
|
|
715
|
+
* Computes the values explicitly seeded by a schema.
|
|
740
716
|
*
|
|
741
717
|
* @param schema - The schema whose defaults to collect.
|
|
742
718
|
* @returns A value record containing only fields with defaults.
|
|
@@ -745,12 +721,7 @@ function computeDefaults(schema) {
|
|
|
745
721
|
const defaults = {};
|
|
746
722
|
for (const field of schema.fields) switch (field.control) {
|
|
747
723
|
case "checkbox":
|
|
748
|
-
if (field.default !== void 0)
|
|
749
|
-
value: cloneValue(field.default),
|
|
750
|
-
enumerable: true,
|
|
751
|
-
configurable: true,
|
|
752
|
-
writable: true
|
|
753
|
-
});
|
|
724
|
+
if (field.default !== void 0) defineEntry(defaults, field.name, cloneValue(field.default));
|
|
754
725
|
break;
|
|
755
726
|
case "password":
|
|
756
727
|
case "file": break;
|
|
@@ -762,35 +733,30 @@ function computeDefaults(schema) {
|
|
|
762
733
|
case "datetime":
|
|
763
734
|
case "color":
|
|
764
735
|
case "confirm":
|
|
765
|
-
case "select": if (field.default !== void 0)
|
|
766
|
-
value: field.default,
|
|
767
|
-
enumerable: true,
|
|
768
|
-
configurable: true,
|
|
769
|
-
writable: true
|
|
770
|
-
});
|
|
736
|
+
case "select": if (field.default !== void 0) defineEntry(defaults, field.name, field.default);
|
|
771
737
|
}
|
|
772
738
|
return Object.freeze(defaults);
|
|
773
739
|
}
|
|
774
740
|
/**
|
|
775
|
-
*
|
|
741
|
+
* Compares two field values by scalar identity or ordered list content.
|
|
776
742
|
*
|
|
777
743
|
* @param a - The first field value.
|
|
778
744
|
* @param b - The second field value.
|
|
779
|
-
* @returns
|
|
745
|
+
* @returns True if both values contain the same answer; false otherwise.
|
|
780
746
|
*/
|
|
781
747
|
function matchesValue(a, b) {
|
|
782
748
|
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
749
|
return a === b;
|
|
784
750
|
}
|
|
785
751
|
/**
|
|
786
|
-
*
|
|
752
|
+
* Extracts the names whose answers differ between two form value records.
|
|
787
753
|
*
|
|
788
754
|
* @remarks
|
|
789
755
|
* Presence is compared in both directions before present values are compared through
|
|
790
756
|
* {@link matchesValue}. The returned set is a new snapshot, exposed as readonly because later
|
|
791
757
|
* changes to either input never alter its membership.
|
|
792
758
|
*
|
|
793
|
-
* @param current - The values
|
|
759
|
+
* @param current - The values the form holds.
|
|
794
760
|
* @param opened - The values held when the form opened.
|
|
795
761
|
* @returns A readonly snapshot of changed field names.
|
|
796
762
|
*/
|
|
@@ -809,17 +775,17 @@ function extractChanges(current, opened) {
|
|
|
809
775
|
return changed;
|
|
810
776
|
}
|
|
811
777
|
/**
|
|
812
|
-
*
|
|
778
|
+
* Compares two form value records by keys and value content.
|
|
813
779
|
*
|
|
814
780
|
* @param a - The first value record.
|
|
815
781
|
* @param b - The second value record.
|
|
816
|
-
* @returns
|
|
782
|
+
* @returns True if both records contain the same answers; false otherwise.
|
|
817
783
|
*/
|
|
818
784
|
function matchesValues(a, b) {
|
|
819
785
|
return extractChanges(a, b).size === 0;
|
|
820
786
|
}
|
|
821
787
|
/**
|
|
822
|
-
*
|
|
788
|
+
* Resolves and interpolates one rule message.
|
|
823
789
|
*
|
|
824
790
|
* @param rule - The rule whose message to resolve.
|
|
825
791
|
* @param limit - The optional operand substituted for `{limit}`.
|
|
@@ -831,7 +797,34 @@ function formatMessage(rule, limit, messages) {
|
|
|
831
797
|
return limit === void 0 ? message : message.replaceAll("{limit}", String(limit));
|
|
832
798
|
}
|
|
833
799
|
/**
|
|
834
|
-
*
|
|
800
|
+
* Creates one named-rule failure against a field.
|
|
801
|
+
*
|
|
802
|
+
* @param field - The field the rule failed on.
|
|
803
|
+
* @param rule - The named rule that failed.
|
|
804
|
+
* @param limit - The rule's operand, substituted for `{limit}`, or absence when it carries none.
|
|
805
|
+
* @param messages - Optional rule-specific message replacements.
|
|
806
|
+
* @returns A frozen {@link FieldError} carrying the field's name, the resolved text, and the rule.
|
|
807
|
+
*
|
|
808
|
+
* @remarks
|
|
809
|
+
* A `custom` validator and the form's `invalidate` method both report a message of their own
|
|
810
|
+
* under no rule name, so neither builds its failure here.
|
|
811
|
+
*
|
|
812
|
+
* @example
|
|
813
|
+
* ```ts
|
|
814
|
+
* const field: FormField = { control: 'text', name: 'nickname', rule: { minimum: 3 } }
|
|
815
|
+
* const error = createFieldError(field, 'minimum', 3)
|
|
816
|
+
* error.message // 'Must be at least 3'
|
|
817
|
+
* ```
|
|
818
|
+
*/
|
|
819
|
+
function createFieldError(field, rule, limit, messages) {
|
|
820
|
+
return Object.freeze({
|
|
821
|
+
field: field.name,
|
|
822
|
+
message: formatMessage(rule, limit, messages),
|
|
823
|
+
rule
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Projects a schema into JSON while removing custom validators and absent values.
|
|
835
828
|
*
|
|
836
829
|
* @param schema - The schema to project.
|
|
837
830
|
* @returns A deep JSON copy of the serializable schema.
|
|
@@ -933,7 +926,7 @@ function serializeForm(schema) {
|
|
|
933
926
|
return (0, _orkestrel_contract.cloneJSONRecord)(output);
|
|
934
927
|
}
|
|
935
928
|
/**
|
|
936
|
-
*
|
|
929
|
+
* Selects referenced groups in first-reference field order.
|
|
937
930
|
*
|
|
938
931
|
* @param schema - The schema whose group references to resolve.
|
|
939
932
|
* @returns The referenced schema groups without duplicates.
|
|
@@ -948,7 +941,7 @@ function extractGroups(schema) {
|
|
|
948
941
|
return Object.freeze(groups);
|
|
949
942
|
}
|
|
950
943
|
/**
|
|
951
|
-
*
|
|
944
|
+
* Audits a structurally valid schema for domain invariants.
|
|
952
945
|
*
|
|
953
946
|
* @param schema - The form schema to audit.
|
|
954
947
|
* @returns Human-readable invariant violations, or an empty list when the schema is sound.
|
|
@@ -1123,7 +1116,7 @@ function auditSchema(schema) {
|
|
|
1123
1116
|
//#endregion
|
|
1124
1117
|
//#region src/core/parsers.ts
|
|
1125
1118
|
/**
|
|
1126
|
-
*
|
|
1119
|
+
* Parses unknown wire data into an owned, semantically sound form schema.
|
|
1127
1120
|
*
|
|
1128
1121
|
* @param input - The unknown schema value to parse.
|
|
1129
1122
|
* @returns An owned schema with custom rules removed, or `undefined` on refusal.
|
|
@@ -1134,12 +1127,7 @@ function parseForm(input) {
|
|
|
1134
1127
|
const schema = {};
|
|
1135
1128
|
for (const key of Reflect.ownKeys(input)) {
|
|
1136
1129
|
if (!(0, _orkestrel_contract.isString)(key)) return void 0;
|
|
1137
|
-
|
|
1138
|
-
value: input[key],
|
|
1139
|
-
enumerable: true,
|
|
1140
|
-
configurable: true,
|
|
1141
|
-
writable: true
|
|
1142
|
-
});
|
|
1130
|
+
defineEntry(schema, key, input[key]);
|
|
1143
1131
|
}
|
|
1144
1132
|
const fields = schema.fields;
|
|
1145
1133
|
if (!(0, _orkestrel_contract.isArray)(fields)) return void 0;
|
|
@@ -1155,24 +1143,14 @@ function parseForm(input) {
|
|
|
1155
1143
|
const copy = {};
|
|
1156
1144
|
for (const key of Reflect.ownKeys(field)) {
|
|
1157
1145
|
if (!(0, _orkestrel_contract.isString)(key)) return void 0;
|
|
1158
|
-
|
|
1159
|
-
value: field[key],
|
|
1160
|
-
enumerable: true,
|
|
1161
|
-
configurable: true,
|
|
1162
|
-
writable: true
|
|
1163
|
-
});
|
|
1146
|
+
defineEntry(copy, key, field[key]);
|
|
1164
1147
|
}
|
|
1165
1148
|
const rule = copy.rule;
|
|
1166
1149
|
if ((0, _orkestrel_contract.isRecord)(rule)) {
|
|
1167
1150
|
const projected = {};
|
|
1168
1151
|
for (const key of Reflect.ownKeys(rule)) {
|
|
1169
1152
|
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
|
-
});
|
|
1153
|
+
if (key !== "custom") defineEntry(projected, key, rule[key]);
|
|
1176
1154
|
}
|
|
1177
1155
|
copy.rule = projected;
|
|
1178
1156
|
}
|
|
@@ -1186,7 +1164,7 @@ function parseForm(input) {
|
|
|
1186
1164
|
return outcome.success ? outcome.value : void 0;
|
|
1187
1165
|
}
|
|
1188
1166
|
/**
|
|
1189
|
-
*
|
|
1167
|
+
* Parses one answer against its field control.
|
|
1190
1168
|
*
|
|
1191
1169
|
* @param field - The field that defines the accepted value.
|
|
1192
1170
|
* @param input - The unknown value to parse.
|
|
@@ -1208,7 +1186,7 @@ function parseValue(field, input) {
|
|
|
1208
1186
|
return outcome.success ? outcome.value : void 0;
|
|
1209
1187
|
}
|
|
1210
1188
|
/**
|
|
1211
|
-
*
|
|
1189
|
+
* Parses a strict answer record against the fields declared by a schema.
|
|
1212
1190
|
*
|
|
1213
1191
|
* @param schema - The schema that owns the accepted field names and controls.
|
|
1214
1192
|
* @param input - The unknown answer record to parse.
|
|
@@ -1224,12 +1202,7 @@ function parseValues(schema, input) {
|
|
|
1224
1202
|
if (field === void 0) return void 0;
|
|
1225
1203
|
const value = parseValue(field, input[key]);
|
|
1226
1204
|
if (value === void 0) return void 0;
|
|
1227
|
-
|
|
1228
|
-
value,
|
|
1229
|
-
enumerable: true,
|
|
1230
|
-
configurable: false,
|
|
1231
|
-
writable: false
|
|
1232
|
-
});
|
|
1205
|
+
freezeEntry(values, key, value);
|
|
1233
1206
|
}
|
|
1234
1207
|
return Object.freeze(values);
|
|
1235
1208
|
});
|
|
@@ -1238,7 +1211,7 @@ function parseValues(schema, input) {
|
|
|
1238
1211
|
//#endregion
|
|
1239
1212
|
//#region src/core/Form.ts
|
|
1240
1213
|
/**
|
|
1241
|
-
*
|
|
1214
|
+
* Represents a form: a schema, the answers given against it, and the errors they carry.
|
|
1242
1215
|
*
|
|
1243
1216
|
* @remarks
|
|
1244
1217
|
* The form owns its schema, so a later edit to the schema the caller passed changes nothing here.
|
|
@@ -1278,7 +1251,7 @@ var Form = class {
|
|
|
1278
1251
|
#evaluation = 0;
|
|
1279
1252
|
#pending = false;
|
|
1280
1253
|
/**
|
|
1281
|
-
*
|
|
1254
|
+
* Opens a form against a schema.
|
|
1282
1255
|
*
|
|
1283
1256
|
* @param schema - The form to ask. It is copied, and the copy is what the form asks.
|
|
1284
1257
|
* @param options - The form's settings.
|
|
@@ -1287,93 +1260,76 @@ var Form = class {
|
|
|
1287
1260
|
* value is one its field's control cannot hold.
|
|
1288
1261
|
*/
|
|
1289
1262
|
constructor(schema, options) {
|
|
1290
|
-
const
|
|
1291
|
-
if (
|
|
1292
|
-
|
|
1263
|
+
const owned = (0, _orkestrel_contract.attempt)(() => cloneFormSchema(schema));
|
|
1264
|
+
if (!owned.success && isFormError(owned.error)) throw owned.error;
|
|
1265
|
+
const copy = owned.success && isFormSchema(owned.value) ? owned.value : void 0;
|
|
1266
|
+
const problems = copy === void 0 ? ["The schema is not a form schema"] : auditSchema(copy);
|
|
1267
|
+
if (copy === void 0 || problems.length > 0) throw new FormError("SCHEMA", `The form schema is unusable: ${problems.join("; ")}`, { problems: [...problems] });
|
|
1268
|
+
this.#schema = copy;
|
|
1293
1269
|
this.#messages = options?.messages === void 0 ? void 0 : Object.freeze({ ...options.messages });
|
|
1294
1270
|
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
|
-
});
|
|
1271
|
+
for (const [name, value] of Object.entries(computeDefaults(this.#schema))) defineEntry(baseline, name, value);
|
|
1301
1272
|
for (const [name, value] of Object.entries(options?.values ?? {})) {
|
|
1302
1273
|
const field = this.#requireField(name);
|
|
1303
1274
|
if (!matchesField(field, value)) throw new FormError("CONTROL", `The ${field.control} field "${name}" cannot hold that value`, {
|
|
1304
1275
|
field: name,
|
|
1305
1276
|
control: field.control
|
|
1306
1277
|
});
|
|
1307
|
-
|
|
1308
|
-
value: cloneValue(value),
|
|
1309
|
-
enumerable: true,
|
|
1310
|
-
configurable: true,
|
|
1311
|
-
writable: true
|
|
1312
|
-
});
|
|
1278
|
+
defineEntry(baseline, name, cloneValue(value));
|
|
1313
1279
|
}
|
|
1314
1280
|
this.#baseline = Object.freeze(baseline);
|
|
1315
1281
|
this.#values = {};
|
|
1316
|
-
for (const [name, value] of Object.entries(baseline))
|
|
1317
|
-
value,
|
|
1318
|
-
enumerable: true,
|
|
1319
|
-
configurable: true,
|
|
1320
|
-
writable: true
|
|
1321
|
-
});
|
|
1282
|
+
for (const [name, value] of Object.entries(baseline)) defineEntry(this.#values, name, value);
|
|
1322
1283
|
this.#resolvers.promise.catch(() => void 0);
|
|
1323
1284
|
this.#emitter = new _orkestrel_emitter.Emitter(options);
|
|
1324
1285
|
this.#evaluate();
|
|
1325
1286
|
}
|
|
1326
|
-
/**
|
|
1287
|
+
/** Holds the form's event emitter. */
|
|
1327
1288
|
get emitter() {
|
|
1328
1289
|
return this.#emitter;
|
|
1329
1290
|
}
|
|
1330
|
-
/**
|
|
1291
|
+
/** Holds the schema this form asks, owned and frozen. */
|
|
1331
1292
|
get schema() {
|
|
1332
1293
|
return this.#schema;
|
|
1333
1294
|
}
|
|
1334
|
-
/**
|
|
1295
|
+
/** Reports the answers the form holds. */
|
|
1335
1296
|
get values() {
|
|
1336
1297
|
const values = {};
|
|
1337
|
-
for (const name of Object.
|
|
1338
|
-
value: this.#values[name],
|
|
1339
|
-
enumerable: true,
|
|
1340
|
-
configurable: true,
|
|
1341
|
-
writable: true
|
|
1342
|
-
});
|
|
1298
|
+
for (const [name, value] of Object.entries(this.#values)) defineEntry(values, name, value);
|
|
1343
1299
|
return Object.freeze(values);
|
|
1344
1300
|
}
|
|
1345
|
-
/**
|
|
1301
|
+
/** Holds the answers the form opened with. */
|
|
1346
1302
|
get baseline() {
|
|
1347
1303
|
return this.#baseline;
|
|
1348
1304
|
}
|
|
1349
|
-
/**
|
|
1305
|
+
/** Holds every error the last completed evaluation produced. */
|
|
1350
1306
|
get errors() {
|
|
1351
1307
|
return this.#errors;
|
|
1352
1308
|
}
|
|
1353
|
-
/**
|
|
1309
|
+
/** Lists the names of the fields somebody has visited. */
|
|
1354
1310
|
get touched() {
|
|
1355
1311
|
return new Set(this.#touched);
|
|
1356
1312
|
}
|
|
1357
|
-
/**
|
|
1313
|
+
/** Lists the names of the fields that are out of the form. */
|
|
1358
1314
|
get disabled() {
|
|
1359
1315
|
const disabled = /* @__PURE__ */ new Set();
|
|
1360
1316
|
for (const field of this.#schema.fields) if ((this.#disabled.get(field.name) ?? field.disabled === true) === true) disabled.add(field.name);
|
|
1361
1317
|
return disabled;
|
|
1362
1318
|
}
|
|
1363
|
-
/**
|
|
1319
|
+
/** Reports where the form sits in its life. */
|
|
1364
1320
|
get status() {
|
|
1365
1321
|
return this.#status;
|
|
1366
1322
|
}
|
|
1367
|
-
/**
|
|
1323
|
+
/** Reports whether the last completed evaluation found no error. */
|
|
1368
1324
|
get valid() {
|
|
1369
1325
|
return this.#errors.length === 0;
|
|
1370
1326
|
}
|
|
1371
|
-
/**
|
|
1327
|
+
/** Reports whether any answer has moved since the form opened. */
|
|
1372
1328
|
get dirty() {
|
|
1373
1329
|
return !matchesValues(this.values, this.#baseline);
|
|
1374
1330
|
}
|
|
1375
1331
|
/**
|
|
1376
|
-
*
|
|
1332
|
+
* Holds the answers after the form settles.
|
|
1377
1333
|
*
|
|
1378
1334
|
* @remarks
|
|
1379
1335
|
* It resolves with the submitted values on the first valid submit, and rejects with a
|
|
@@ -1383,7 +1339,7 @@ var Form = class {
|
|
|
1383
1339
|
return this.#resolvers.promise;
|
|
1384
1340
|
}
|
|
1385
1341
|
/**
|
|
1386
|
-
*
|
|
1342
|
+
* Finds one field by name.
|
|
1387
1343
|
*
|
|
1388
1344
|
* @param name - The field's name.
|
|
1389
1345
|
* @returns The field, or `undefined` when the schema declares no such name.
|
|
@@ -1392,7 +1348,7 @@ var Form = class {
|
|
|
1392
1348
|
return this.#schema.fields.find((field) => field.name === name);
|
|
1393
1349
|
}
|
|
1394
1350
|
/**
|
|
1395
|
-
*
|
|
1351
|
+
* Answers one field or several.
|
|
1396
1352
|
*
|
|
1397
1353
|
* @param input - One field's name, or the answers to write keyed by field name.
|
|
1398
1354
|
* @param value - The answer to write when `input` names one field.
|
|
@@ -1415,12 +1371,7 @@ var Form = class {
|
|
|
1415
1371
|
for (const [name, answer] of entries) {
|
|
1416
1372
|
if (this.#differs(name, answer)) moved.push(name);
|
|
1417
1373
|
if (answer === void 0) delete this.#values[name];
|
|
1418
|
-
else
|
|
1419
|
-
value: cloneValue(answer),
|
|
1420
|
-
enumerable: true,
|
|
1421
|
-
configurable: true,
|
|
1422
|
-
writable: true
|
|
1423
|
-
});
|
|
1374
|
+
else defineEntry(this.#values, name, cloneValue(answer));
|
|
1424
1375
|
this.#invalidations.delete(name);
|
|
1425
1376
|
}
|
|
1426
1377
|
for (const name of moved) {
|
|
@@ -1431,7 +1382,7 @@ var Form = class {
|
|
|
1431
1382
|
});
|
|
1432
1383
|
}
|
|
1433
1384
|
/**
|
|
1434
|
-
*
|
|
1385
|
+
* Records that somebody has visited a field.
|
|
1435
1386
|
*
|
|
1436
1387
|
* @param name - The field's name.
|
|
1437
1388
|
* @throws A {@link FormError} coded `SETTLED` or `ABANDONED` when the form has ended, and
|
|
@@ -1443,7 +1394,7 @@ var Form = class {
|
|
|
1443
1394
|
this.#touched.add(name);
|
|
1444
1395
|
}
|
|
1445
1396
|
/**
|
|
1446
|
-
*
|
|
1397
|
+
* Fails a field from outside, for what the rules cannot see.
|
|
1447
1398
|
*
|
|
1448
1399
|
* @param name - The field's name.
|
|
1449
1400
|
* @param message - What to tell the person.
|
|
@@ -1462,7 +1413,7 @@ var Form = class {
|
|
|
1462
1413
|
});
|
|
1463
1414
|
}
|
|
1464
1415
|
/**
|
|
1465
|
-
*
|
|
1416
|
+
* Takes one or more fields out of the form.
|
|
1466
1417
|
*
|
|
1467
1418
|
* @param input - One field name, several names, or absence to select every field.
|
|
1468
1419
|
* @throws A {@link FormError} coded `SETTLED` or `ABANDONED` when the form has ended, and
|
|
@@ -1474,7 +1425,7 @@ var Form = class {
|
|
|
1474
1425
|
this.#change(input, true);
|
|
1475
1426
|
}
|
|
1476
1427
|
/**
|
|
1477
|
-
*
|
|
1428
|
+
* Puts one or more fields back into the form.
|
|
1478
1429
|
*
|
|
1479
1430
|
* @param input - One field name, several names, or absence to select every field.
|
|
1480
1431
|
* @throws A {@link FormError} coded `SETTLED` or `ABANDONED` when the form has ended, and
|
|
@@ -1486,7 +1437,7 @@ var Form = class {
|
|
|
1486
1437
|
this.#change(input, false);
|
|
1487
1438
|
}
|
|
1488
1439
|
/**
|
|
1489
|
-
*
|
|
1440
|
+
* Checks every answer and settles the form when they all pass.
|
|
1490
1441
|
*
|
|
1491
1442
|
* @returns The values on success, or every error that stopped them.
|
|
1492
1443
|
* @remarks
|
|
@@ -1539,7 +1490,7 @@ var Form = class {
|
|
|
1539
1490
|
});
|
|
1540
1491
|
}
|
|
1541
1492
|
/**
|
|
1542
|
-
*
|
|
1493
|
+
* Returns every answer to the ones the form opened with: the schema's defaults, overlaid with
|
|
1543
1494
|
* any seeded `values`. Reset the runtime disabled state to the schema's declarations.
|
|
1544
1495
|
*
|
|
1545
1496
|
* @throws A {@link FormError} coded `SETTLED` or `ABANDONED` when the form has ended.
|
|
@@ -1548,12 +1499,7 @@ var Form = class {
|
|
|
1548
1499
|
this.#gate();
|
|
1549
1500
|
this.#batch(() => {
|
|
1550
1501
|
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
|
-
});
|
|
1502
|
+
for (const [name, value] of Object.entries(this.#baseline)) defineEntry(this.#values, name, value);
|
|
1557
1503
|
this.#touched.clear();
|
|
1558
1504
|
this.#disabled.clear();
|
|
1559
1505
|
this.#invalidations.clear();
|
|
@@ -1563,7 +1509,7 @@ var Form = class {
|
|
|
1563
1509
|
});
|
|
1564
1510
|
}
|
|
1565
1511
|
/**
|
|
1566
|
-
*
|
|
1512
|
+
* Tears the form down, abandoning it when it has not settled.
|
|
1567
1513
|
*
|
|
1568
1514
|
* @remarks
|
|
1569
1515
|
* Destroying twice does nothing the second time. A settled form keeps its `settled` status and
|
|
@@ -1641,12 +1587,7 @@ var Form = class {
|
|
|
1641
1587
|
const disabled = this.disabled;
|
|
1642
1588
|
for (const field of this.#schema.fields) {
|
|
1643
1589
|
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
|
-
});
|
|
1590
|
+
if (!disabled.has(field.name) && value !== void 0) defineEntry(answers, field.name, value);
|
|
1650
1591
|
}
|
|
1651
1592
|
return Object.freeze(answers);
|
|
1652
1593
|
}
|
|
@@ -1671,7 +1612,7 @@ var Form = class {
|
|
|
1671
1612
|
//#endregion
|
|
1672
1613
|
//#region src/core/factories.ts
|
|
1673
1614
|
/**
|
|
1674
|
-
*
|
|
1615
|
+
* Opens a form against a schema.
|
|
1675
1616
|
*
|
|
1676
1617
|
* @param schema - The form to ask. It is copied, and the copy is what the form asks.
|
|
1677
1618
|
* @param options - The form's settings.
|
|
@@ -1706,7 +1647,9 @@ exports.COLOR_PATTERN = COLOR_PATTERN;
|
|
|
1706
1647
|
exports.DATETIME_PATTERN = DATETIME_PATTERN;
|
|
1707
1648
|
exports.DATE_PATTERN = DATE_PATTERN;
|
|
1708
1649
|
exports.EMAIL_PATTERN = EMAIL_PATTERN;
|
|
1650
|
+
exports.FIELD_BASE_KEYS = FIELD_BASE_KEYS;
|
|
1709
1651
|
exports.FIELD_CONTROLS = FIELD_CONTROLS;
|
|
1652
|
+
exports.FIELD_KEYS = FIELD_KEYS;
|
|
1710
1653
|
exports.FIELD_LIMIT = FIELD_LIMIT;
|
|
1711
1654
|
exports.FORM_STATUSES = FORM_STATUSES;
|
|
1712
1655
|
exports.Form = Form;
|
|
@@ -1729,12 +1672,15 @@ exports.cloneFormField = cloneFormField;
|
|
|
1729
1672
|
exports.cloneFormSchema = cloneFormSchema;
|
|
1730
1673
|
exports.cloneValue = cloneValue;
|
|
1731
1674
|
exports.computeDefaults = computeDefaults;
|
|
1675
|
+
exports.createFieldError = createFieldError;
|
|
1732
1676
|
exports.createForm = createForm;
|
|
1677
|
+
exports.defineEntry = defineEntry;
|
|
1733
1678
|
exports.evaluateField = evaluateField;
|
|
1734
1679
|
exports.evaluateForm = evaluateForm;
|
|
1735
1680
|
exports.extractChanges = extractChanges;
|
|
1736
1681
|
exports.extractGroups = extractGroups;
|
|
1737
1682
|
exports.formatMessage = formatMessage;
|
|
1683
|
+
exports.freezeEntry = freezeEntry;
|
|
1738
1684
|
exports.isFieldChoice = isFieldChoice;
|
|
1739
1685
|
exports.isFieldControl = isFieldControl;
|
|
1740
1686
|
exports.isFieldError = isFieldError;
|