@coffer-org/sdk 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/color-names.d.ts +0 -1
- package/dist/color-names.js +0 -1
- package/dist/condition.d.ts +3 -19
- package/dist/condition.js +50 -69
- package/dist/countries.d.ts +0 -2
- package/dist/countries.js +0 -1
- package/dist/currencies.d.ts +0 -10
- package/dist/currencies.js +0 -10
- package/dist/extend.d.ts +3 -22
- package/dist/extend.js +2 -6
- package/dist/field-helpers.d.ts +0 -7
- package/dist/field-helpers.js +0 -16
- package/dist/field-presets.d.ts +55 -87
- package/dist/field-presets.js +116 -108
- package/dist/fields/constants.d.ts +36 -0
- package/dist/fields/constants.js +18 -0
- package/dist/fields/normalize.d.ts +84 -0
- package/dist/fields/normalize.js +47 -0
- package/dist/fields/url.d.ts +13 -0
- package/dist/fields/url.js +49 -0
- package/dist/fields/validation.d.ts +30 -0
- package/dist/fields/validation.js +57 -0
- package/dist/fields.d.ts +222 -332
- package/dist/fields.js +435 -351
- package/dist/index.d.ts +14 -16
- package/dist/index.js +14 -16
- package/dist/module.d.ts +13 -103
- package/dist/module.js +51 -83
- package/dist/package.d.ts +3 -20
- package/dist/package.js +14 -19
- package/dist/plugin.d.ts +5 -45
- package/dist/plugin.js +5 -19
- package/dist/settings.d.ts +1 -2
- package/dist/settings.js +0 -1
- package/dist/units.d.ts +0 -1
- package/dist/units.js +0 -5
- package/dist/vault.d.ts +3 -8
- package/dist/vault.js +4 -1
- package/package.json +5 -1
- package/dist/color-names.d.ts.map +0 -1
- package/dist/color-names.js.map +0 -1
- package/dist/condition.d.ts.map +0 -1
- package/dist/condition.js.map +0 -1
- package/dist/countries.d.ts.map +0 -1
- package/dist/countries.js.map +0 -1
- package/dist/currencies.d.ts.map +0 -1
- package/dist/currencies.js.map +0 -1
- package/dist/extend.d.ts.map +0 -1
- package/dist/extend.js.map +0 -1
- package/dist/field-helpers.d.ts.map +0 -1
- package/dist/field-helpers.js.map +0 -1
- package/dist/field-presets.d.ts.map +0 -1
- package/dist/field-presets.js.map +0 -1
- package/dist/fields.d.ts.map +0 -1
- package/dist/fields.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/module.d.ts.map +0 -1
- package/dist/module.js.map +0 -1
- package/dist/package.d.ts.map +0 -1
- package/dist/package.js.map +0 -1
- package/dist/plugin.d.ts.map +0 -1
- package/dist/plugin.js.map +0 -1
- package/dist/settings.d.ts.map +0 -1
- package/dist/settings.js.map +0 -1
- package/dist/units.d.ts.map +0 -1
- package/dist/units.js.map +0 -1
- package/dist/vault.d.ts.map +0 -1
- package/dist/vault.js.map +0 -1
package/dist/fields.js
CHANGED
|
@@ -1,42 +1,31 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Система типів полів — серце платформи. Чистий, ізоморфний модуль.
|
|
3
|
-
* Один опис поля → три споживачі:
|
|
4
|
-
* 1. `zod` — валідація (однакова на сервері й клієнті)
|
|
5
|
-
* 2. `column` — тип колонки в БД
|
|
6
|
-
* 3. `prim`/`kind` — ключі для резолву рендера (див. web/render/registry)
|
|
7
|
-
*/
|
|
8
1
|
import { z } from 'zod';
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import {
|
|
2
|
+
import { resolveUnits } from "./units.js";
|
|
3
|
+
import { isCurrencyCode } from "./currencies.js";
|
|
4
|
+
import { vmsg, reqErr, typeErr, reqTypeErr, jsonRefined, optionalize, jsonValue } from "./fields/validation.js";
|
|
5
|
+
export { vmsg, reqErr, typeErr, reqTypeErr, jsonRefined, optionalize, jsonValue, decodeVmsg, validateField } from "./fields/validation.js";
|
|
6
|
+
export { normalizeOpts } from "./fields/normalize.js";
|
|
7
|
+
import { normalizeOpts } from "./fields/normalize.js";
|
|
8
|
+
import { presets } from "./field-presets.js";
|
|
15
9
|
export const isField = (x) => 'key' in x && !('el' in x);
|
|
16
10
|
export const isStatic = (x) => 'el' in x && x.el === 'static';
|
|
17
11
|
export const isGroup = (x) => 'el' in x && x.el === 'group';
|
|
18
12
|
export const isDivider = (x) => 'el' in x && x.el === 'divider';
|
|
19
13
|
export const isInfo = (x) => 'el' in x && x.el === 'info';
|
|
20
14
|
export const isButton = (x) => 'el' in x && x.el === 'button';
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Візуальна або storage-група полів (один рівень вкладеності — груп усередині груп нема).
|
|
24
|
-
* Без `key` → layout-група (не зберігається). З `key` → storage:
|
|
25
|
-
* - embedded (key, без multiple): зберігається як колонка або вкладений об'єкт.
|
|
26
|
-
* - collection (key + multiple): child-таблиця (масив рядків).
|
|
27
|
-
* Storage-група вимагає `label` і лише `FieldItem`-нащадків (зі storage key).
|
|
28
|
-
*/
|
|
15
|
+
export const isBreak = (x) => 'el' in x && x.el === 'break';
|
|
29
16
|
export function group(o) {
|
|
17
|
+
const r = o.rules ?? {};
|
|
18
|
+
const v = o.view ?? {};
|
|
30
19
|
if (o.key) {
|
|
31
20
|
if (!o.label)
|
|
32
|
-
throw new Error(`[field.group] storage
|
|
21
|
+
throw new Error(`[field.group] storage group '${o.key}' requires a label`);
|
|
33
22
|
for (const f of o.fields) {
|
|
34
23
|
if (!('key' in f))
|
|
35
|
-
throw new Error(`[field.group] storage
|
|
24
|
+
throw new Error(`[field.group] storage group '${o.key}': children must be FieldItem (have a key)`);
|
|
36
25
|
}
|
|
37
|
-
for (const k of
|
|
26
|
+
for (const k of r.unique ?? []) {
|
|
38
27
|
if (!o.fields.some((f) => 'key' in f && f.key === k))
|
|
39
|
-
throw new Error(`[field.group] '${o.key}': unique
|
|
28
|
+
throw new Error(`[field.group] '${o.key}': unique key '${k}' is not a subfield`);
|
|
40
29
|
}
|
|
41
30
|
}
|
|
42
31
|
return {
|
|
@@ -44,120 +33,143 @@ export function group(o) {
|
|
|
44
33
|
key: o.key,
|
|
45
34
|
multiple: o.multiple,
|
|
46
35
|
required: o.required,
|
|
47
|
-
unique:
|
|
36
|
+
unique: r.unique,
|
|
48
37
|
label: o.label,
|
|
49
|
-
icon:
|
|
38
|
+
icon: v.icon,
|
|
39
|
+
display: v.display ?? 'wrap',
|
|
40
|
+
kind: v.kind,
|
|
41
|
+
fixed: r.fixed,
|
|
50
42
|
fields: o.fields,
|
|
51
43
|
};
|
|
52
44
|
}
|
|
45
|
+
export function row(o) {
|
|
46
|
+
return group({ label: o.label, fields: o.fields, view: { ...o.view, display: 'scroll' } });
|
|
47
|
+
}
|
|
48
|
+
export function table(o) {
|
|
49
|
+
return group({
|
|
50
|
+
key: o.key,
|
|
51
|
+
label: o.label,
|
|
52
|
+
fields: o.fields,
|
|
53
|
+
multiple: true,
|
|
54
|
+
required: o.required,
|
|
55
|
+
rules: o.rules,
|
|
56
|
+
view: { ...o.view, display: 'table' },
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export function sheet(o) {
|
|
60
|
+
const flat = [];
|
|
61
|
+
o.fields.forEach((rowFields, i) => {
|
|
62
|
+
if (i > 0)
|
|
63
|
+
flat.push(brk());
|
|
64
|
+
flat.push(...rowFields);
|
|
65
|
+
});
|
|
66
|
+
return group({ label: o.label, fields: flat, view: { ...o.view, display: 'sheet' } });
|
|
67
|
+
}
|
|
68
|
+
export function url(o) {
|
|
69
|
+
return group({
|
|
70
|
+
key: o.key,
|
|
71
|
+
label: o.label ?? o.key,
|
|
72
|
+
required: o.required,
|
|
73
|
+
view: { kind: 'url' },
|
|
74
|
+
fields: [
|
|
75
|
+
string({ key: 'scheme' }),
|
|
76
|
+
string({ key: 'username' }),
|
|
77
|
+
string({ key: 'password' }),
|
|
78
|
+
string({ key: 'host' }),
|
|
79
|
+
int({ key: 'port' }),
|
|
80
|
+
string({ key: 'path' }),
|
|
81
|
+
string({ key: 'query' }),
|
|
82
|
+
string({ key: 'hash' }),
|
|
83
|
+
],
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
export function keyed(o) {
|
|
87
|
+
if (o.fixed && o.by.type.prim !== 'select')
|
|
88
|
+
throw new Error(`[field.keyed] fixed requires by=select (enumerated), got '${o.by.type.prim}'`);
|
|
89
|
+
const make = (o.container ?? group);
|
|
90
|
+
const g = make({ label: o.label, fields: [o.by, ...o.fields], view: o.view });
|
|
91
|
+
return {
|
|
92
|
+
...g,
|
|
93
|
+
key: o.key,
|
|
94
|
+
multiple: true,
|
|
95
|
+
required: o.required,
|
|
96
|
+
unique: o.unique ?? [o.by.key],
|
|
97
|
+
fixed: o.fixed,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
53
100
|
export const isStorageGroup = (g) => g.key != null;
|
|
54
101
|
export const isCollectionGroup = (g) => g.key != null && g.multiple === true;
|
|
55
102
|
export const isEmbeddedGroup = (g) => g.key != null && g.multiple !== true;
|
|
56
|
-
/** Горизонтальний розділювач. */
|
|
57
103
|
export function divider() {
|
|
58
104
|
return { el: 'divider' };
|
|
59
105
|
}
|
|
60
|
-
|
|
106
|
+
export function brk() {
|
|
107
|
+
return { el: 'break' };
|
|
108
|
+
}
|
|
61
109
|
export function info(textKey) {
|
|
62
110
|
return { el: 'info', text: textKey };
|
|
63
111
|
}
|
|
64
|
-
/** Кнопка-дія: викликає обробник, зареєстрований у actionRegistry під ключем `value`. */
|
|
65
112
|
export function button(o) {
|
|
66
113
|
return { el: 'button', label: o.label, value: o.value, icon: o.icon, variant: o.variant };
|
|
67
114
|
}
|
|
68
|
-
/** Обгортає FieldMeta у FieldItem/StaticEl залежно від opts.key/opts.value. */
|
|
69
115
|
export function wrapKey(opts, meta) {
|
|
70
|
-
|
|
71
|
-
let m = opts.span != null ? { ...meta, span: opts.span } : meta;
|
|
116
|
+
let m = meta;
|
|
72
117
|
if (opts.noEditControl)
|
|
73
118
|
m = { ...m, hints: { ...m.hints, noEditControl: true } };
|
|
119
|
+
if (opts.default !== undefined)
|
|
120
|
+
m = { ...m, default: opts.default };
|
|
121
|
+
if (opts.hidden !== undefined)
|
|
122
|
+
m = { ...m, hidden: opts.hidden };
|
|
74
123
|
if (opts.key)
|
|
75
124
|
return { key: opts.key, type: m };
|
|
76
125
|
if ('value' in opts && opts.value !== undefined)
|
|
77
126
|
return { el: 'static', type: m, value: opts.value };
|
|
78
127
|
return m;
|
|
79
128
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
export const LANGUAGES = ISO6391.getAllCodes()
|
|
83
|
-
.map((code) => ({ code, name: ISO6391.getNativeName(code) }))
|
|
84
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
85
|
-
/** Стать — спільний optionList 'sex' (реєструє plugin-core). i18n: core.sex.*. */
|
|
86
|
-
export const SEX_OPTIONS = [
|
|
87
|
-
{ value: 'M', title: 'core.sex.M' },
|
|
88
|
-
{ value: 'F', title: 'core.sex.F' },
|
|
89
|
-
{ value: 'other', title: 'core.sex.other' },
|
|
90
|
-
];
|
|
91
|
-
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
92
|
-
/** Структуроване повідомлення для zod: JSON {code, params}. Декодує mutate.ts. */
|
|
93
|
-
export function vmsg(code, params) {
|
|
94
|
-
return JSON.stringify(params ? { code, params } : { code });
|
|
95
|
-
}
|
|
96
|
-
/** v4 error-map: повідомлення для відсутнього значення (колишній required_error). */
|
|
97
|
-
export function reqErr(code = 'required') {
|
|
98
|
-
return { error: (iss) => (iss.input === undefined ? vmsg(code) : undefined) };
|
|
99
|
-
}
|
|
100
|
-
/** v4 error-map: повідомлення для невалідного типу (колишній invalid_type_error). */
|
|
101
|
-
export function typeErr(code = 'invalid_type') {
|
|
102
|
-
return { error: (iss) => (iss.code === 'invalid_type' ? vmsg(code) : undefined) };
|
|
103
|
-
}
|
|
104
|
-
/** v4 error-map: required + invalid_type разом (колишні required_error + invalid_type_error). */
|
|
105
|
-
export function reqTypeErr() {
|
|
106
|
-
return {
|
|
107
|
-
error: (iss) => iss.code === 'invalid_type' ? (iss.input === undefined ? vmsg('required') : vmsg('invalid_type')) : undefined,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Спільна фабрика для полів, що зберігають JSON-рядок і валідуються вкладеною
|
|
112
|
-
* zod-схемою: parse → inner.safeParse → при невдачі issue з кодом `code`,
|
|
113
|
-
* при невалідному JSON — код 'json'. Уникає дублювання try/parse/superRefine.
|
|
114
|
-
*/
|
|
115
|
-
export function jsonRefined(inner, code) {
|
|
116
|
-
return z.string(reqErr()).superRefine((val, ctx) => {
|
|
117
|
-
try {
|
|
118
|
-
const r = inner.safeParse(JSON.parse(val));
|
|
119
|
-
if (!r.success)
|
|
120
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg(code) });
|
|
121
|
-
}
|
|
122
|
-
catch {
|
|
123
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
export function optionalize(schema, required) {
|
|
128
|
-
return z.preprocess((v) => (v === '' || v === null ? undefined : v), required ? schema : schema.optional());
|
|
129
|
+
export function isJsonStored(field) {
|
|
130
|
+
return field.json === true || field.hints['multiple'] === true;
|
|
129
131
|
}
|
|
132
|
+
export { LANGUAGES, SEX_OPTIONS, WEEKDAY_OPTIONS } from "./fields/constants.js";
|
|
133
|
+
export { parseUrl, buildUrl, hrefOf } from "./fields/url.js";
|
|
130
134
|
function resolveDate(v) {
|
|
131
135
|
return v === 'today' ? new Date().toISOString().slice(0, 10) : v;
|
|
132
136
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
*/
|
|
142
|
-
/** Zod «JSON-масив значень inner» — спільне для applyMultiple і bindSelectSourceZod. */
|
|
137
|
+
function safeJsonParse(s) {
|
|
138
|
+
try {
|
|
139
|
+
return JSON.parse(s);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
143
145
|
function multipleZod(inner) {
|
|
144
|
-
return z.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
return z.unknown().superRefine((raw, ctx) => {
|
|
147
|
+
let arr;
|
|
148
|
+
if (Array.isArray(raw)) {
|
|
149
|
+
arr = raw;
|
|
150
|
+
}
|
|
151
|
+
else if (typeof raw === 'string') {
|
|
152
|
+
try {
|
|
153
|
+
arr = JSON.parse(raw);
|
|
150
154
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('array_item') });
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
155
|
+
catch {
|
|
156
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
157
|
+
return;
|
|
157
158
|
}
|
|
158
159
|
}
|
|
159
|
-
|
|
160
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('
|
|
160
|
+
else {
|
|
161
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('array') });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (!Array.isArray(arr)) {
|
|
165
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('array') });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
for (const item of arr) {
|
|
169
|
+
if (!inner.safeParse(item).success) {
|
|
170
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('array_item') });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
161
173
|
}
|
|
162
174
|
});
|
|
163
175
|
}
|
|
@@ -165,12 +177,13 @@ export function applyMultiple(base, multiple) {
|
|
|
165
177
|
if (!multiple)
|
|
166
178
|
return base;
|
|
167
179
|
const s = multipleZod(base.zod);
|
|
168
|
-
return { ...base, column: 'text', hints: { ...base.hints, multiple: true }, zod: optionalize(s, base.required) };
|
|
180
|
+
return { ...base, column: 'text', hints: { ...base.hints, multiple: true }, json: true, zod: optionalize(s, base.required) };
|
|
169
181
|
}
|
|
170
|
-
export function string(
|
|
182
|
+
export function string(raw) {
|
|
183
|
+
const o = normalizeOpts(raw);
|
|
171
184
|
const required = o.required ?? false;
|
|
172
|
-
const cfg = o.config ?? {};
|
|
173
|
-
const min = cfg.min ?? (required ? 1 : 0);
|
|
185
|
+
const cfg = (o.config ?? {});
|
|
186
|
+
const min = cfg.min ?? (o.required === true ? 1 : 0);
|
|
174
187
|
let s = z.string(reqTypeErr());
|
|
175
188
|
if (min > 0)
|
|
176
189
|
s = s.min(min, { message: vmsg('min_length', { min }) });
|
|
@@ -195,20 +208,50 @@ export function string(o) {
|
|
|
195
208
|
const meta = applyMultiple(base, o.multiple ?? false);
|
|
196
209
|
return wrapKey(o, meta);
|
|
197
210
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (o.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
211
|
+
function pathField(o, kind, exts) {
|
|
212
|
+
const required = o.required ?? false;
|
|
213
|
+
let s = z.string(reqTypeErr());
|
|
214
|
+
if (o.required === true)
|
|
215
|
+
s = s.min(1, { message: vmsg('min_length', { min: 1 }) });
|
|
216
|
+
const base = {
|
|
217
|
+
kind,
|
|
218
|
+
label: o.label ?? '',
|
|
219
|
+
required,
|
|
220
|
+
prim: 'text',
|
|
221
|
+
column: 'text',
|
|
222
|
+
hints: exts && exts.length ? { exts } : {},
|
|
223
|
+
zod: optionalize(s, required),
|
|
224
|
+
...(o.default !== undefined ? { default: o.default } : {}),
|
|
225
|
+
};
|
|
226
|
+
return wrapKey(o, base);
|
|
227
|
+
}
|
|
228
|
+
export function localDir(raw) {
|
|
229
|
+
return pathField(normalizeOpts(raw), 'localDir');
|
|
230
|
+
}
|
|
231
|
+
export function localFile(raw) {
|
|
232
|
+
const o = normalizeOpts(raw);
|
|
233
|
+
return pathField(o, 'localFile', o.exts);
|
|
234
|
+
}
|
|
235
|
+
export function text(raw) {
|
|
236
|
+
const o = normalizeOpts(raw);
|
|
237
|
+
const meta = string({ ...raw, key: undefined, value: undefined });
|
|
206
238
|
const textMeta = { ...meta, prim: 'text', kind: 'textarea' };
|
|
207
239
|
return wrapKey(o, textMeta);
|
|
208
240
|
}
|
|
209
|
-
export function
|
|
241
|
+
export function i18n(raw) {
|
|
242
|
+
const o = normalizeOpts(raw);
|
|
243
|
+
const meta = text({ ...raw, key: undefined, value: undefined });
|
|
244
|
+
return wrapKey(o, { ...meta, kind: 'i18n' });
|
|
245
|
+
}
|
|
246
|
+
export function smallText(raw) {
|
|
247
|
+
const o = normalizeOpts(raw);
|
|
248
|
+
const meta = text({ ...raw, key: undefined, value: undefined });
|
|
249
|
+
return wrapKey(o, { ...meta, kind: 'smalltext' });
|
|
250
|
+
}
|
|
251
|
+
export function real(raw) {
|
|
252
|
+
const o = normalizeOpts(raw);
|
|
210
253
|
const required = o.required ?? false;
|
|
211
|
-
const cfg = o.config ?? {};
|
|
254
|
+
const cfg = (o.config ?? {});
|
|
212
255
|
let s = z.coerce.number(typeErr());
|
|
213
256
|
if (cfg.min != null)
|
|
214
257
|
s = s.min(cfg.min, { message: vmsg('min', { min: cfg.min }) });
|
|
@@ -226,9 +269,10 @@ export function real(o) {
|
|
|
226
269
|
const meta = applyMultiple(base, o.multiple ?? false);
|
|
227
270
|
return wrapKey(o, meta);
|
|
228
271
|
}
|
|
229
|
-
export function int(
|
|
272
|
+
export function int(raw) {
|
|
273
|
+
const o = normalizeOpts(raw);
|
|
230
274
|
const required = o.required ?? false;
|
|
231
|
-
const cfg = o.config ?? {};
|
|
275
|
+
const cfg = (o.config ?? {});
|
|
232
276
|
let s = z.coerce.number(typeErr()).int({ message: vmsg('int') });
|
|
233
277
|
if (cfg.min != null)
|
|
234
278
|
s = s.min(cfg.min, { message: vmsg('min', { min: cfg.min }) });
|
|
@@ -246,12 +290,12 @@ export function int(o) {
|
|
|
246
290
|
const meta = applyMultiple(base, o.multiple ?? false);
|
|
247
291
|
return wrapKey(o, meta);
|
|
248
292
|
}
|
|
249
|
-
export function date(
|
|
293
|
+
export function date(raw) {
|
|
294
|
+
const o = normalizeOpts(raw);
|
|
250
295
|
const required = o.required ?? false;
|
|
251
296
|
const granularity = o.config?.granularity ?? 'day';
|
|
252
297
|
let s;
|
|
253
298
|
if (granularity === 'year') {
|
|
254
|
-
// year/month: min/max порівнюються лексикографічно як рядки (без resolveDate, на відміну від day)
|
|
255
299
|
s = z
|
|
256
300
|
.string(reqErr())
|
|
257
301
|
.regex(/^\d{4}$/, { message: vmsg('date_format') })
|
|
@@ -296,7 +340,8 @@ export function date(o) {
|
|
|
296
340
|
const meta = applyMultiple(base, o.multiple ?? false);
|
|
297
341
|
return wrapKey(o, meta);
|
|
298
342
|
}
|
|
299
|
-
export function time(
|
|
343
|
+
export function time(raw) {
|
|
344
|
+
const o = normalizeOpts(raw);
|
|
300
345
|
const required = o.required ?? false;
|
|
301
346
|
const granularity = o.config?.granularity ?? 'second';
|
|
302
347
|
const timeRegex = {
|
|
@@ -317,7 +362,8 @@ export function time(o) {
|
|
|
317
362
|
const meta = applyMultiple(base, o.multiple ?? false);
|
|
318
363
|
return wrapKey(o, meta);
|
|
319
364
|
}
|
|
320
|
-
export function datetime(
|
|
365
|
+
export function datetime(raw) {
|
|
366
|
+
const o = normalizeOpts(raw);
|
|
321
367
|
const required = o.required ?? false;
|
|
322
368
|
const granularity = o.config?.granularity ?? 'minute';
|
|
323
369
|
const dtRegex = {
|
|
@@ -338,8 +384,9 @@ export function datetime(o) {
|
|
|
338
384
|
const meta = applyMultiple(base, o.multiple ?? false);
|
|
339
385
|
return wrapKey(o, meta);
|
|
340
386
|
}
|
|
341
|
-
export function boolean(
|
|
342
|
-
const
|
|
387
|
+
export function boolean(raw) {
|
|
388
|
+
const o = normalizeOpts(raw);
|
|
389
|
+
const s = z.preprocess((v) => (v === 'true' || v === 1 || v === '1' ? true : v === 'false' || v === 0 || v === '0' ? false : v), z.boolean().default(false).nullable());
|
|
343
390
|
const meta = {
|
|
344
391
|
kind: 'boolean',
|
|
345
392
|
label: o.label ?? '',
|
|
@@ -351,7 +398,8 @@ export function boolean(o) {
|
|
|
351
398
|
};
|
|
352
399
|
return wrapKey(o, meta);
|
|
353
400
|
}
|
|
354
|
-
export function triState(
|
|
401
|
+
export function triState(raw) {
|
|
402
|
+
const o = normalizeOpts(raw);
|
|
355
403
|
const required = o.required ?? false;
|
|
356
404
|
const s = z.enum(['yes', 'no', 'unknown'], { error: () => vmsg('enum') });
|
|
357
405
|
const meta = {
|
|
@@ -365,9 +413,9 @@ export function triState(o) {
|
|
|
365
413
|
};
|
|
366
414
|
return wrapKey(o, meta);
|
|
367
415
|
}
|
|
368
|
-
export function select(
|
|
416
|
+
export function select(raw) {
|
|
417
|
+
const o = normalizeOpts(raw);
|
|
369
418
|
const required = o.required ?? false;
|
|
370
|
-
// ── статичне джерело: NamedOptionList (string) або inline OptionItem[] ──
|
|
371
419
|
const source = typeof o.options === 'string' ? o.options : null;
|
|
372
420
|
const inlineOpts = Array.isArray(o.options) ? o.options : [];
|
|
373
421
|
const s = inlineOpts.length > 0
|
|
@@ -386,63 +434,54 @@ export function select(o) {
|
|
|
386
434
|
const meta = applyMultiple(base, o.multiple ?? false);
|
|
387
435
|
return wrapKey(o, meta);
|
|
388
436
|
}
|
|
389
|
-
export function relation(
|
|
437
|
+
export function relation(raw) {
|
|
438
|
+
const o = normalizeOpts(raw);
|
|
390
439
|
const required = o.required ?? false;
|
|
391
440
|
const multi = o.multiple ?? false;
|
|
392
441
|
let s;
|
|
393
442
|
if (multi) {
|
|
394
|
-
s = z.
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('uuid_list') });
|
|
399
|
-
}
|
|
400
|
-
catch {
|
|
401
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('uuid_list') });
|
|
402
|
-
}
|
|
443
|
+
s = z.unknown().superRefine((raw, ctx) => {
|
|
444
|
+
const arr = jsonValue(raw);
|
|
445
|
+
if (!Array.isArray(arr) || !arr.every((id) => Number.isInteger(id) && id > 0))
|
|
446
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('int_list') });
|
|
403
447
|
});
|
|
404
448
|
}
|
|
405
449
|
else {
|
|
406
|
-
s = z.
|
|
450
|
+
s = z.coerce.number().int().positive({ message: vmsg('int') });
|
|
407
451
|
}
|
|
408
452
|
const meta = {
|
|
409
453
|
kind: 'relation',
|
|
410
454
|
label: o.label ?? '',
|
|
411
455
|
required,
|
|
412
456
|
prim: 'relation',
|
|
413
|
-
column: 'text',
|
|
457
|
+
column: multi ? 'text' : 'integer',
|
|
414
458
|
hints: { multi, displayKey: o.displayKey ?? 'name' },
|
|
415
|
-
relation: { vault:
|
|
459
|
+
relation: { vault: raw.options.vault, type: raw.options.module },
|
|
460
|
+
...(multi && { json: true }),
|
|
416
461
|
zod: optionalize(s, required),
|
|
417
462
|
};
|
|
418
463
|
return wrapKey(o, meta);
|
|
419
464
|
}
|
|
420
|
-
/**
|
|
421
|
-
* Compose-time: підв'язати enum-валідацію source-селекту до значень списку.
|
|
422
|
-
* Factory не знає вмісту optionList (списки збираються лише в composeRegistry),
|
|
423
|
-
* тому zod source-полів спочатку z.string(); реєстр викликає це для кожного
|
|
424
|
-
* такого поля, щоб сервер відхиляв значення поза списком.
|
|
425
|
-
* Відтворює повний ланцюг модифікаторів: enum → multiple.
|
|
426
|
-
*/
|
|
427
465
|
export function bindSelectSourceZod(field, values) {
|
|
428
466
|
if (!values.length)
|
|
429
467
|
return;
|
|
430
468
|
const enumS = z.enum(values, { error: () => vmsg('enum') });
|
|
431
|
-
// Точне відтворення ланцюга factory: кожен крок обгортається в optionalize,
|
|
432
|
-
// як у select → applyMultiple.
|
|
433
469
|
let s = optionalize(enumS, field.required);
|
|
434
470
|
if (field.hints['multiple'] === true)
|
|
435
471
|
s = optionalize(multipleZod(s), field.required);
|
|
436
472
|
field.zod = s;
|
|
437
473
|
}
|
|
438
|
-
export function json(
|
|
474
|
+
export function json(raw) {
|
|
475
|
+
const o = normalizeOpts(raw);
|
|
439
476
|
const required = o.required ?? false;
|
|
440
|
-
const s = z.
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
477
|
+
const s = z.unknown().superRefine((raw, ctx) => {
|
|
478
|
+
if (typeof raw === 'string') {
|
|
479
|
+
try {
|
|
480
|
+
JSON.parse(raw);
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
484
|
+
}
|
|
446
485
|
}
|
|
447
486
|
});
|
|
448
487
|
const meta = {
|
|
@@ -452,35 +491,58 @@ export function json(o) {
|
|
|
452
491
|
prim: 'json',
|
|
453
492
|
column: 'text',
|
|
454
493
|
hints: {},
|
|
494
|
+
json: true,
|
|
455
495
|
zod: optionalize(s, required),
|
|
456
496
|
};
|
|
457
497
|
return wrapKey(o, meta);
|
|
458
498
|
}
|
|
459
|
-
export function
|
|
499
|
+
export function check(raw) {
|
|
500
|
+
const o = normalizeOpts(raw);
|
|
460
501
|
const required = o.required ?? false;
|
|
461
|
-
const
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
502
|
+
const multiple = o.multiple ?? false;
|
|
503
|
+
const slots = o.slots && o.slots.length > 0 ? o.slots : [''];
|
|
504
|
+
if (multiple) {
|
|
505
|
+
const fields = [string({ key: 'text', label: 'core.fields.text' })];
|
|
506
|
+
slots.forEach((label, i) => fields.push(boolean({ key: `check${i}`, label: label || 'core.fields.check' })));
|
|
507
|
+
return group({ key: o.key, label: o.label ?? o.key, multiple: true, required: required, fields });
|
|
508
|
+
}
|
|
509
|
+
const shape = { text: z.string() };
|
|
510
|
+
const columns = { text: 'text' };
|
|
511
|
+
slots.forEach((_, i) => {
|
|
512
|
+
shape[`check${i}`] = z.boolean();
|
|
513
|
+
columns[`check${i}`] = 'boolean';
|
|
514
|
+
});
|
|
515
|
+
const rowSchema = z.object(shape).passthrough();
|
|
516
|
+
const s = z.unknown().superRefine((raw, ctx) => {
|
|
517
|
+
const parsed = typeof raw === 'string' ? safeJsonParse(raw) : raw;
|
|
518
|
+
if (parsed === undefined) {
|
|
519
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (!rowSchema.safeParse(parsed).success)
|
|
523
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('check_structure') });
|
|
524
|
+
});
|
|
525
|
+
const meta = {
|
|
526
|
+
kind: 'check',
|
|
465
527
|
label: o.label ?? '',
|
|
466
528
|
required,
|
|
467
|
-
prim: '
|
|
529
|
+
prim: 'check',
|
|
468
530
|
column: 'text',
|
|
469
|
-
hints: {},
|
|
531
|
+
hints: { slots },
|
|
532
|
+
columns,
|
|
470
533
|
zod: optionalize(s, required),
|
|
471
534
|
};
|
|
472
|
-
const meta = applyMultiple(base, o.multiple ?? false);
|
|
473
535
|
return wrapKey(o, meta);
|
|
474
536
|
}
|
|
475
|
-
export function measured(
|
|
537
|
+
export function measured(raw) {
|
|
538
|
+
const o = normalizeOpts(raw);
|
|
476
539
|
const required = o.required ?? false;
|
|
477
540
|
const cfg = o.config ?? {};
|
|
478
|
-
// Resolve units: string preset name or inline array
|
|
479
541
|
const unitsSpec = (typeof o.options === 'string' ? o.options : o.options);
|
|
480
542
|
const units = resolveUnits(unitsSpec);
|
|
481
543
|
let numSchema = z.number().finite();
|
|
482
544
|
if (cfg.step === 1)
|
|
483
|
-
numSchema = numSchema.int({ message: vmsg('int') });
|
|
545
|
+
numSchema = numSchema.int({ message: vmsg('int') });
|
|
484
546
|
if (cfg.min != null)
|
|
485
547
|
numSchema = numSchema.min(cfg.min);
|
|
486
548
|
if (cfg.max != null)
|
|
@@ -489,46 +551,47 @@ export function measured(o) {
|
|
|
489
551
|
value: numSchema,
|
|
490
552
|
unit: z.string().refine((v) => units.some((u) => u.value === v), { message: vmsg('measured_unit') }),
|
|
491
553
|
});
|
|
492
|
-
const s = z.
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
if (!r.success) {
|
|
496
|
-
const vmsgIssue = r.error.issues.find((iss) => {
|
|
497
|
-
try {
|
|
498
|
-
return typeof JSON.parse(iss.message).code === 'string';
|
|
499
|
-
}
|
|
500
|
-
catch {
|
|
501
|
-
return false;
|
|
502
|
-
}
|
|
503
|
-
});
|
|
504
|
-
ctx.addIssue({
|
|
505
|
-
code: z.ZodIssueCode.custom,
|
|
506
|
-
message: vmsgIssue ? vmsgIssue.message : vmsg('measured_structure'),
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
catch {
|
|
554
|
+
const s = z.unknown().superRefine((raw, ctx) => {
|
|
555
|
+
const parsed = jsonValue(raw);
|
|
556
|
+
if (typeof parsed === 'string') {
|
|
511
557
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const r = rowSchema.safeParse(parsed);
|
|
561
|
+
if (!r.success) {
|
|
562
|
+
const vmsgIssue = r.error.issues.find((iss) => {
|
|
563
|
+
try {
|
|
564
|
+
return typeof JSON.parse(iss.message).code === 'string';
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
ctx.addIssue({
|
|
571
|
+
code: z.ZodIssueCode.custom,
|
|
572
|
+
message: vmsgIssue ? vmsgIssue.message : vmsg('measured_structure'),
|
|
573
|
+
});
|
|
512
574
|
}
|
|
513
575
|
});
|
|
514
|
-
const
|
|
576
|
+
const meta = {
|
|
515
577
|
kind: 'measured',
|
|
516
578
|
label: o.label ?? '',
|
|
517
579
|
required,
|
|
518
580
|
prim: 'measured',
|
|
519
581
|
column: 'text',
|
|
520
582
|
hints: {
|
|
521
|
-
units: o.options,
|
|
583
|
+
units: o.options,
|
|
522
584
|
...(cfg.min != null && { min: cfg.min }),
|
|
523
585
|
...(cfg.max != null && { max: cfg.max }),
|
|
524
586
|
...(cfg.step != null && { step: cfg.step }),
|
|
525
587
|
},
|
|
588
|
+
columns: { value: 'real', unit: 'text' },
|
|
526
589
|
zod: optionalize(s, required),
|
|
527
590
|
};
|
|
528
|
-
const meta = applyMultiple(base, o.multiple ?? false);
|
|
529
591
|
return wrapKey(o, meta);
|
|
530
592
|
}
|
|
531
|
-
export function money(
|
|
593
|
+
export function money(raw) {
|
|
594
|
+
const o = normalizeOpts(raw);
|
|
532
595
|
const required = o.required ?? false;
|
|
533
596
|
const cfg = o.config ?? {};
|
|
534
597
|
let numSchema = z.number().finite();
|
|
@@ -540,17 +603,17 @@ export function money(o) {
|
|
|
540
603
|
value: numSchema,
|
|
541
604
|
currency: z.string().refine(isCurrencyCode, { message: vmsg('money_currency') }),
|
|
542
605
|
});
|
|
543
|
-
const s = z.
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
if (!r.success)
|
|
547
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('money_structure') });
|
|
548
|
-
}
|
|
549
|
-
catch {
|
|
606
|
+
const s = z.unknown().superRefine((raw, ctx) => {
|
|
607
|
+
const parsed = jsonValue(raw);
|
|
608
|
+
if (typeof parsed === 'string') {
|
|
550
609
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
610
|
+
return;
|
|
551
611
|
}
|
|
612
|
+
const r = rowSchema.safeParse(parsed);
|
|
613
|
+
if (!r.success)
|
|
614
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('money_structure') });
|
|
552
615
|
});
|
|
553
|
-
const
|
|
616
|
+
const meta = {
|
|
554
617
|
kind: 'money',
|
|
555
618
|
label: o.label ?? '',
|
|
556
619
|
required,
|
|
@@ -561,23 +624,24 @@ export function money(o) {
|
|
|
561
624
|
...(cfg.min != null && { min: cfg.min }),
|
|
562
625
|
...(cfg.max != null && { max: cfg.max }),
|
|
563
626
|
},
|
|
627
|
+
columns: { value: 'real', currency: 'text' },
|
|
564
628
|
zod: optionalize(s, required),
|
|
565
629
|
};
|
|
566
|
-
const meta = applyMultiple(base, o.multiple ?? false);
|
|
567
630
|
return wrapKey(o, meta);
|
|
568
631
|
}
|
|
569
|
-
export function code(
|
|
632
|
+
export function code(raw) {
|
|
633
|
+
const o = normalizeOpts(raw);
|
|
570
634
|
const required = o.required ?? false;
|
|
571
635
|
const cfg = o.config ?? {};
|
|
572
636
|
const rowSchema = z.object({ code: z.string(), lang: z.string() });
|
|
573
|
-
const s = z.
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('code_structure') });
|
|
577
|
-
}
|
|
578
|
-
catch {
|
|
637
|
+
const s = z.unknown().superRefine((raw, ctx) => {
|
|
638
|
+
const parsed = jsonValue(raw);
|
|
639
|
+
if (typeof parsed === 'string') {
|
|
579
640
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
641
|
+
return;
|
|
580
642
|
}
|
|
643
|
+
if (!rowSchema.safeParse(parsed).success)
|
|
644
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('code_structure') });
|
|
581
645
|
});
|
|
582
646
|
const base = {
|
|
583
647
|
kind: 'code',
|
|
@@ -586,25 +650,27 @@ export function code(o) {
|
|
|
586
650
|
prim: 'code',
|
|
587
651
|
column: 'text',
|
|
588
652
|
hints: { ...(cfg.defaultLang && { defaultLang: cfg.defaultLang }) },
|
|
653
|
+
columns: { code: 'text', lang: 'text' },
|
|
589
654
|
zod: optionalize(s, required),
|
|
590
655
|
};
|
|
591
656
|
return wrapKey(o, base);
|
|
592
657
|
}
|
|
593
|
-
export function geo(
|
|
658
|
+
export function geo(raw) {
|
|
659
|
+
const o = normalizeOpts(raw);
|
|
594
660
|
const required = o.required ?? false;
|
|
595
661
|
const rowSchema = z.object({
|
|
596
662
|
lat: z.number().min(-90).max(90),
|
|
597
663
|
lng: z.number().min(-180).max(180),
|
|
598
664
|
label: z.string().optional(),
|
|
599
665
|
});
|
|
600
|
-
const s = z.
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('geo_structure') });
|
|
604
|
-
}
|
|
605
|
-
catch {
|
|
666
|
+
const s = z.unknown().superRefine((raw, ctx) => {
|
|
667
|
+
const parsed = jsonValue(raw);
|
|
668
|
+
if (typeof parsed === 'string') {
|
|
606
669
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
670
|
+
return;
|
|
607
671
|
}
|
|
672
|
+
if (!rowSchema.safeParse(parsed).success)
|
|
673
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('geo_structure') });
|
|
608
674
|
});
|
|
609
675
|
const base = {
|
|
610
676
|
kind: 'geo',
|
|
@@ -613,45 +679,25 @@ export function geo(o) {
|
|
|
613
679
|
prim: 'geo',
|
|
614
680
|
column: 'text',
|
|
615
681
|
hints: {},
|
|
682
|
+
columns: { lat: 'real', lng: 'real', label: 'text' },
|
|
616
683
|
zod: optionalize(s, required),
|
|
617
684
|
};
|
|
618
685
|
return wrapKey(o, base);
|
|
619
686
|
}
|
|
620
687
|
export function perWeekday(o) {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
try {
|
|
631
|
-
const r = arrSchema.safeParse(JSON.parse(val));
|
|
632
|
-
if (!r.success)
|
|
633
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('per_weekday') });
|
|
634
|
-
}
|
|
635
|
-
catch {
|
|
636
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
637
|
-
}
|
|
638
|
-
});
|
|
639
|
-
const base = {
|
|
640
|
-
kind: 'perWeekday',
|
|
641
|
-
label: o.label ?? '',
|
|
642
|
-
required,
|
|
643
|
-
prim: 'per-weekday',
|
|
644
|
-
column: 'text',
|
|
645
|
-
hints: {
|
|
646
|
-
...(cfg.min != null && { min: cfg.min }),
|
|
647
|
-
...(cfg.max != null && { max: cfg.max }),
|
|
648
|
-
...(cfg.step != null && { step: cfg.step }),
|
|
649
|
-
},
|
|
650
|
-
zod: optionalize(s, required),
|
|
688
|
+
return {
|
|
689
|
+
...keyed({
|
|
690
|
+
key: o.key,
|
|
691
|
+
label: o.label,
|
|
692
|
+
by: select({ key: 'day', label: 'core.fields.weekday', options: 'weekdays' }),
|
|
693
|
+
fixed: true,
|
|
694
|
+
fields: [real({ key: 'value', label: 'core.fields.value', rules: o.rules })],
|
|
695
|
+
}),
|
|
696
|
+
kind: 'per-weekday',
|
|
651
697
|
};
|
|
652
|
-
return wrapKey(o, base);
|
|
653
698
|
}
|
|
654
|
-
export function lookup(
|
|
699
|
+
export function lookup(raw) {
|
|
700
|
+
const o = normalizeOpts(raw);
|
|
655
701
|
const meta = {
|
|
656
702
|
kind: 'lookup',
|
|
657
703
|
label: o.label ?? '',
|
|
@@ -664,62 +710,71 @@ export function lookup(o) {
|
|
|
664
710
|
};
|
|
665
711
|
return wrapKey(o, meta);
|
|
666
712
|
}
|
|
667
|
-
/**
|
|
668
|
-
* Відновлює FieldMeta з клієнтського FieldClient (для рекурсивного рендеру
|
|
669
|
-
* сабфілдів групових полів через FieldInput/FieldValue). zod тут не потрібен —
|
|
670
|
-
* валідація відбувається на рівні схеми батьківського поля.
|
|
671
|
-
*/
|
|
672
713
|
export function clientToMeta(fc) {
|
|
673
714
|
return { ...fc, column: 'text', zod: z.any() };
|
|
674
715
|
}
|
|
675
|
-
export function period(
|
|
676
|
-
const
|
|
677
|
-
const
|
|
678
|
-
? datetime({ label: 'core.period.from' })
|
|
679
|
-
: date({ label: 'core.period.from', config: { granularity: granularity === 'month' ? 'month' : 'day' } });
|
|
680
|
-
const untilField = granularity === 'datetime'
|
|
681
|
-
? datetime({ label: 'core.period.until' })
|
|
682
|
-
: date({ label: 'core.period.until', config: { granularity: granularity === 'month' ? 'month' : 'day' } });
|
|
683
|
-
// Самодостатній JSON-композит {from,until} (раніше будувався на field.row).
|
|
684
|
-
// Відчеплено від row, щоб row можна було видалити. Storage: JSON {from,until}
|
|
685
|
-
// (single) або [{from,until}] (multiple), як measured/keyValue.
|
|
716
|
+
export function period(raw) {
|
|
717
|
+
const o = normalizeOpts(raw);
|
|
718
|
+
const granularity = (o.config?.granularity ?? 'day');
|
|
686
719
|
const required = o.required ?? false;
|
|
687
720
|
const multiple = o.multiple ?? false;
|
|
688
|
-
const
|
|
721
|
+
const mkSub = (key, label) => granularity === 'datetime'
|
|
722
|
+
? datetime({ key, label })
|
|
723
|
+
: date({ key, label, rules: { granularity: granularity === 'month' ? 'month' : 'day' } });
|
|
724
|
+
if (multiple) {
|
|
725
|
+
return group({
|
|
726
|
+
key: o.key,
|
|
727
|
+
label: o.label ?? o.key,
|
|
728
|
+
multiple: true,
|
|
729
|
+
required: required,
|
|
730
|
+
fields: [mkSub('from', 'core.period.from'), mkSub('until', 'core.period.until')],
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
const fromField = mkSub('from', 'core.period.from');
|
|
734
|
+
const untilField = mkSub('until', 'core.period.until');
|
|
735
|
+
const subDefs = { from: fromField.type, until: untilField.type };
|
|
689
736
|
const rowShape = {};
|
|
690
737
|
for (const [key, fm] of Object.entries(subDefs))
|
|
691
738
|
rowShape[key] = fm.zod.optional();
|
|
692
|
-
|
|
693
|
-
let s = jsonRefined(multiple ? z.array(rowSchema) : rowSchema, 'row_structure');
|
|
739
|
+
let s = z.object(rowShape).passthrough();
|
|
694
740
|
s = s.superRefine((val, ctx) => {
|
|
695
|
-
if (val == null
|
|
741
|
+
if (val == null)
|
|
696
742
|
return;
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
743
|
+
let parsed = val;
|
|
744
|
+
if (typeof val === 'string') {
|
|
745
|
+
try {
|
|
746
|
+
parsed = JSON.parse(val);
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
return;
|
|
704
750
|
}
|
|
705
751
|
}
|
|
706
|
-
|
|
707
|
-
|
|
752
|
+
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
753
|
+
for (const item of items) {
|
|
754
|
+
const r = item;
|
|
755
|
+
if (r?.from && r?.until && String(r.from) > String(r.until))
|
|
756
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('period_order') });
|
|
708
757
|
}
|
|
709
758
|
});
|
|
710
|
-
const
|
|
759
|
+
const colType = granularity === 'datetime' ? 'datetime' : granularity === 'day' ? 'date' : 'text';
|
|
760
|
+
const subFields = [
|
|
761
|
+
{ key: 'from', ...toClient(fromField.type) },
|
|
762
|
+
{ key: 'until', ...toClient(untilField.type) },
|
|
763
|
+
];
|
|
711
764
|
const meta = {
|
|
712
765
|
kind: 'period',
|
|
713
766
|
label: o.label ?? '',
|
|
714
767
|
required,
|
|
715
768
|
prim: 'period',
|
|
716
769
|
column: 'text',
|
|
717
|
-
hints: { fields: subFields, rowMultiple:
|
|
770
|
+
hints: { fields: subFields, rowMultiple: false, unique: [], granularity },
|
|
771
|
+
columns: { from: colType, until: colType },
|
|
718
772
|
zod: optionalize(s, required),
|
|
719
773
|
};
|
|
720
774
|
return wrapKey(o, meta);
|
|
721
775
|
}
|
|
722
|
-
function makeFile(kind,
|
|
776
|
+
function makeFile(kind, raw) {
|
|
777
|
+
const o = normalizeOpts(raw);
|
|
723
778
|
const required = o.required ?? false;
|
|
724
779
|
const multiple = o.multiple ?? false;
|
|
725
780
|
const rowSchema = z.object({
|
|
@@ -727,21 +782,23 @@ function makeFile(kind, o) {
|
|
|
727
782
|
mime: z.string().optional(),
|
|
728
783
|
size: z.number().optional(),
|
|
729
784
|
});
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('file_structure') });
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
785
|
+
const s = z.unknown().superRefine((raw, ctx) => {
|
|
786
|
+
if (typeof raw === 'string') {
|
|
787
|
+
try {
|
|
788
|
+
JSON.parse(raw);
|
|
789
|
+
}
|
|
790
|
+
catch {
|
|
791
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
792
|
+
return;
|
|
741
793
|
}
|
|
742
794
|
}
|
|
743
|
-
|
|
744
|
-
|
|
795
|
+
const parsed = jsonValue(raw);
|
|
796
|
+
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
797
|
+
for (const it of items) {
|
|
798
|
+
if (!rowSchema.safeParse(it).success) {
|
|
799
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('file_structure') });
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
745
802
|
}
|
|
746
803
|
});
|
|
747
804
|
const meta = {
|
|
@@ -751,6 +808,7 @@ function makeFile(kind, o) {
|
|
|
751
808
|
prim: 'file',
|
|
752
809
|
column: 'text',
|
|
753
810
|
hints: multiple ? { multiple: true } : {},
|
|
811
|
+
json: true,
|
|
754
812
|
zod: optionalize(s, required),
|
|
755
813
|
};
|
|
756
814
|
return wrapKey(o, meta);
|
|
@@ -773,41 +831,26 @@ export function image(o) {
|
|
|
773
831
|
export function media(o) {
|
|
774
832
|
return makeFile('media', o);
|
|
775
833
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
}
|
|
788
|
-
catch {
|
|
789
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
790
|
-
}
|
|
791
|
-
});
|
|
792
|
-
const meta = {
|
|
793
|
-
kind: 'keyValue',
|
|
834
|
+
export function avatar(o) {
|
|
835
|
+
return makeFile('avatar', o);
|
|
836
|
+
}
|
|
837
|
+
export function cover(o) {
|
|
838
|
+
return makeFile('cover', o);
|
|
839
|
+
}
|
|
840
|
+
export function keyValue(raw) {
|
|
841
|
+
const o = normalizeOpts(raw);
|
|
842
|
+
const valueType = { ...(o.option ?? string({ label: '' })), label: o.valueLabel ?? '' };
|
|
843
|
+
return keyed({
|
|
844
|
+
key: o.key,
|
|
794
845
|
label: o.label ?? '',
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
hints: {
|
|
801
|
-
kvMultiple: multiple,
|
|
802
|
-
option: o.option ?? string({ label: '' }),
|
|
803
|
-
keyLabel: o.keyLabel,
|
|
804
|
-
valueLabel: o.valueLabel,
|
|
805
|
-
},
|
|
806
|
-
zod: optionalize(s, required),
|
|
807
|
-
};
|
|
808
|
-
return wrapKey(o, meta);
|
|
846
|
+
container: o.container ?? table,
|
|
847
|
+
by: string({ key: 'key', label: o.keyLabel ?? '' }),
|
|
848
|
+
fields: [{ key: 'value', type: valueType }],
|
|
849
|
+
required: o.required,
|
|
850
|
+
});
|
|
809
851
|
}
|
|
810
|
-
function makeRange(kind, int,
|
|
852
|
+
function makeRange(kind, int, raw) {
|
|
853
|
+
const o = normalizeOpts(raw);
|
|
811
854
|
const required = o.required ?? false;
|
|
812
855
|
const cfg = o.config ?? {};
|
|
813
856
|
let n = z.number().finite();
|
|
@@ -818,19 +861,23 @@ function makeRange(kind, int, o) {
|
|
|
818
861
|
if (cfg.max != null)
|
|
819
862
|
n = n.max(cfg.max);
|
|
820
863
|
const rowSchema = z.object({ from: n, to: n });
|
|
821
|
-
const s = z.
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
864
|
+
const s = z.unknown().superRefine((raw, ctx) => {
|
|
865
|
+
if (typeof raw === 'string') {
|
|
866
|
+
try {
|
|
867
|
+
JSON.parse(raw);
|
|
868
|
+
}
|
|
869
|
+
catch {
|
|
870
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
826
871
|
return;
|
|
827
872
|
}
|
|
828
|
-
if (r.data.from > r.data.to)
|
|
829
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('range_order') });
|
|
830
873
|
}
|
|
831
|
-
|
|
832
|
-
|
|
874
|
+
const r = rowSchema.safeParse(jsonValue(raw));
|
|
875
|
+
if (!r.success) {
|
|
876
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('range_structure') });
|
|
877
|
+
return;
|
|
833
878
|
}
|
|
879
|
+
if (r.data.from > r.data.to)
|
|
880
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('range_order') });
|
|
834
881
|
});
|
|
835
882
|
const base = {
|
|
836
883
|
kind,
|
|
@@ -843,6 +890,7 @@ function makeRange(kind, int, o) {
|
|
|
843
890
|
...(cfg.max != null && { max: cfg.max }),
|
|
844
891
|
...(cfg.step != null && { step: cfg.step }),
|
|
845
892
|
},
|
|
893
|
+
columns: int ? { from: 'integer', to: 'integer' } : { from: 'real', to: 'real' },
|
|
846
894
|
zod: optionalize(s, required),
|
|
847
895
|
};
|
|
848
896
|
return wrapKey(o, base);
|
|
@@ -853,17 +901,41 @@ export function numberRange(o) {
|
|
|
853
901
|
export function realRange(o) {
|
|
854
902
|
return makeRange('realRange', false, o);
|
|
855
903
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
904
|
+
export function embed(raw) {
|
|
905
|
+
const o = normalizeOpts(raw);
|
|
906
|
+
const required = o.required ?? false;
|
|
907
|
+
const multiple = o.multiple ?? false;
|
|
908
|
+
const recSchema = z.object({
|
|
909
|
+
url: z.string().min(1),
|
|
910
|
+
title: z.string().optional(),
|
|
911
|
+
description: z.string().optional(),
|
|
912
|
+
thumbnail: z.string().optional(),
|
|
913
|
+
provider: z.string().optional(),
|
|
914
|
+
type: z.enum(['video', 'rich', 'photo', 'link']).optional(),
|
|
915
|
+
html: z.string().optional(),
|
|
916
|
+
width: z.number().optional(),
|
|
917
|
+
height: z.number().optional(),
|
|
918
|
+
});
|
|
919
|
+
const s = jsonRefined(multiple ? z.array(recSchema) : z.union([recSchema, z.array(recSchema).max(1)]), 'embed_structure');
|
|
920
|
+
const meta = {
|
|
921
|
+
kind: 'embed',
|
|
922
|
+
label: o.label ?? '',
|
|
923
|
+
required,
|
|
924
|
+
prim: 'embed',
|
|
925
|
+
column: 'text',
|
|
926
|
+
hints: multiple ? { multiple: true } : {},
|
|
927
|
+
json: true,
|
|
928
|
+
zod: optionalize(s, required),
|
|
929
|
+
};
|
|
930
|
+
return wrapKey(o, meta);
|
|
931
|
+
}
|
|
864
932
|
const PRIMITIVES = {
|
|
865
933
|
string,
|
|
866
934
|
text,
|
|
935
|
+
i18n,
|
|
936
|
+
smallText,
|
|
937
|
+
localDir,
|
|
938
|
+
localFile,
|
|
867
939
|
real,
|
|
868
940
|
int,
|
|
869
941
|
date,
|
|
@@ -874,7 +946,7 @@ const PRIMITIVES = {
|
|
|
874
946
|
select,
|
|
875
947
|
relation,
|
|
876
948
|
json,
|
|
877
|
-
|
|
949
|
+
check,
|
|
878
950
|
measured,
|
|
879
951
|
money,
|
|
880
952
|
code,
|
|
@@ -888,24 +960,36 @@ const PRIMITIVES = {
|
|
|
888
960
|
video,
|
|
889
961
|
image,
|
|
890
962
|
media,
|
|
963
|
+
avatar,
|
|
964
|
+
cover,
|
|
965
|
+
embed,
|
|
891
966
|
keyValue,
|
|
892
967
|
numberRange,
|
|
893
968
|
realRange,
|
|
894
969
|
group,
|
|
970
|
+
row,
|
|
971
|
+
table,
|
|
972
|
+
sheet,
|
|
973
|
+
keyed,
|
|
974
|
+
url,
|
|
895
975
|
divider,
|
|
976
|
+
break: brk,
|
|
896
977
|
info,
|
|
897
978
|
button,
|
|
898
979
|
};
|
|
899
|
-
/** Збирає `f`, гарантуючи, що жоден пресет не затіняє примітив. */
|
|
900
980
|
function composeF(presets) {
|
|
901
981
|
for (const k of Object.keys(presets))
|
|
902
982
|
if (k in PRIMITIVES)
|
|
903
|
-
throw new Error(`[f]
|
|
983
|
+
throw new Error(`[f] preset '${k}' overrides a primitive`);
|
|
904
984
|
return { ...PRIMITIVES, ...presets };
|
|
905
985
|
}
|
|
906
|
-
|
|
986
|
+
let _composed;
|
|
987
|
+
const composedField = () => (_composed ??= composeF(presets));
|
|
988
|
+
export const field = new Proxy({}, {
|
|
989
|
+
get: (_t, k) => composedField()[k],
|
|
990
|
+
has: (_t, k) => k in composedField(),
|
|
991
|
+
});
|
|
907
992
|
export function toClient(field) {
|
|
908
|
-
const { kind, label, required, prim, hints, options, relation, virtual } = field;
|
|
909
|
-
return { kind, label, required, prim, hints, options, relation, virtual };
|
|
993
|
+
const { kind, label, required, prim, hints, options, relation, virtual, json, hidden } = field;
|
|
994
|
+
return { kind, label, required, prim, hints, options, relation, virtual, json, hidden };
|
|
910
995
|
}
|
|
911
|
-
//# sourceMappingURL=fields.js.map
|