@coffer-org/sdk 1.1.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 +7 -0
- package/dist/color-names.d.ts.map +1 -0
- package/dist/color-names.js +463 -0
- package/dist/color-names.js.map +1 -0
- package/dist/condition.d.ts +38 -0
- package/dist/condition.d.ts.map +1 -0
- package/dist/condition.js +109 -0
- package/dist/condition.js.map +1 -0
- package/dist/countries.d.ts +8 -0
- package/dist/countries.d.ts.map +1 -0
- package/dist/countries.js +197 -0
- package/dist/countries.js.map +1 -0
- package/dist/currencies.d.ts +14 -0
- package/dist/currencies.d.ts.map +1 -0
- package/dist/currencies.js +39 -0
- package/dist/currencies.js.map +1 -0
- package/dist/extend.d.ts +46 -0
- package/dist/extend.d.ts.map +1 -0
- package/dist/extend.js +35 -0
- package/dist/extend.js.map +1 -0
- package/dist/field-helpers.d.ts +9 -0
- package/dist/field-helpers.d.ts.map +1 -0
- package/dist/field-helpers.js +30 -0
- package/dist/field-helpers.js.map +1 -0
- package/dist/field-presets.d.ts +278 -0
- package/dist/field-presets.d.ts.map +1 -0
- package/dist/field-presets.js +303 -0
- package/dist/field-presets.js.map +1 -0
- package/dist/fields.d.ts +817 -0
- package/dist/fields.d.ts.map +1 -0
- package/dist/fields.js +911 -0
- package/dist/fields.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/module.d.ts +164 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +244 -0
- package/dist/module.js.map +1 -0
- package/dist/package.d.ts +30 -0
- package/dist/package.d.ts.map +1 -0
- package/dist/package.js +31 -0
- package/dist/package.js.map +1 -0
- package/dist/plugin.d.ts +77 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +57 -0
- package/dist/plugin.js.map +1 -0
- package/dist/settings.d.ts +9 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +4 -0
- package/dist/settings.js.map +1 -0
- package/dist/units.d.ts +24 -0
- package/dist/units.d.ts.map +1 -0
- package/dist/units.js +125 -0
- package/dist/units.js.map +1 -0
- package/dist/vault.d.ts +18 -0
- package/dist/vault.d.ts.map +1 -0
- package/dist/vault.js +4 -0
- package/dist/vault.js.map +1 -0
- package/package.json +27 -0
package/dist/fields.js
ADDED
|
@@ -0,0 +1,911 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Система типів полів — серце платформи. Чистий, ізоморфний модуль.
|
|
3
|
+
* Один опис поля → три споживачі:
|
|
4
|
+
* 1. `zod` — валідація (однакова на сервері й клієнті)
|
|
5
|
+
* 2. `column` — тип колонки в БД
|
|
6
|
+
* 3. `prim`/`kind` — ключі для резолву рендера (див. web/render/registry)
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import ISO6391 from 'iso-639-1';
|
|
10
|
+
import { resolveUnits } from './units.js';
|
|
11
|
+
import { isCurrencyCode } from './currencies.js';
|
|
12
|
+
// Пресети — тонкі обгортки над примітивами нижче. Циклічний імпорт безпечний:
|
|
13
|
+
// преcети викликають фабрики лише в тілі функцій (фабрики — hoisted-декларації).
|
|
14
|
+
import { presets } from './field-presets.js';
|
|
15
|
+
export const isField = (x) => 'key' in x && !('el' in x);
|
|
16
|
+
export const isStatic = (x) => 'el' in x && x.el === 'static';
|
|
17
|
+
export const isGroup = (x) => 'el' in x && x.el === 'group';
|
|
18
|
+
export const isDivider = (x) => 'el' in x && x.el === 'divider';
|
|
19
|
+
export const isInfo = (x) => 'el' in x && x.el === 'info';
|
|
20
|
+
export const isButton = (x) => 'el' in x && x.el === 'button';
|
|
21
|
+
// ─── Layout-фабрики ──────────────────────────────────────────────────────────
|
|
22
|
+
/**
|
|
23
|
+
* Візуальна або storage-група полів (один рівень вкладеності — груп усередині груп нема).
|
|
24
|
+
* Без `key` → layout-група (не зберігається). З `key` → storage:
|
|
25
|
+
* - embedded (key, без multiple): зберігається як колонка або вкладений об'єкт.
|
|
26
|
+
* - collection (key + multiple): child-таблиця (масив рядків).
|
|
27
|
+
* Storage-група вимагає `label` і лише `FieldItem`-нащадків (зі storage key).
|
|
28
|
+
*/
|
|
29
|
+
export function group(o) {
|
|
30
|
+
if (o.key) {
|
|
31
|
+
if (!o.label)
|
|
32
|
+
throw new Error(`[field.group] storage-група '${o.key}' потребує label`);
|
|
33
|
+
for (const f of o.fields) {
|
|
34
|
+
if (!('key' in f))
|
|
35
|
+
throw new Error(`[field.group] storage-група '${o.key}': діти мусять бути FieldItem (мати key)`);
|
|
36
|
+
}
|
|
37
|
+
for (const k of o.unique ?? []) {
|
|
38
|
+
if (!o.fields.some((f) => 'key' in f && f.key === k))
|
|
39
|
+
throw new Error(`[field.group] '${o.key}': unique-ключ '${k}' не є сабфілдом`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
el: 'group',
|
|
44
|
+
key: o.key,
|
|
45
|
+
multiple: o.multiple,
|
|
46
|
+
required: o.required,
|
|
47
|
+
unique: o.unique,
|
|
48
|
+
label: o.label,
|
|
49
|
+
icon: o.icon,
|
|
50
|
+
fields: o.fields,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export const isStorageGroup = (g) => g.key != null;
|
|
54
|
+
export const isCollectionGroup = (g) => g.key != null && g.multiple === true;
|
|
55
|
+
export const isEmbeddedGroup = (g) => g.key != null && g.multiple !== true;
|
|
56
|
+
/** Горизонтальний розділювач. */
|
|
57
|
+
export function divider() {
|
|
58
|
+
return { el: 'divider' };
|
|
59
|
+
}
|
|
60
|
+
/** Довідковий markdown-блок за i18n-ключем. */
|
|
61
|
+
export function info(textKey) {
|
|
62
|
+
return { el: 'info', text: textKey };
|
|
63
|
+
}
|
|
64
|
+
/** Кнопка-дія: викликає обробник, зареєстрований у actionRegistry під ключем `value`. */
|
|
65
|
+
export function button(o) {
|
|
66
|
+
return { el: 'button', label: o.label, value: o.value, icon: o.icon, variant: o.variant };
|
|
67
|
+
}
|
|
68
|
+
/** Обгортає FieldMeta у FieldItem/StaticEl залежно від opts.key/opts.value. */
|
|
69
|
+
export function wrapKey(opts, meta) {
|
|
70
|
+
// Бренд (phantom) існує лише в типі — у runtime повертаємо чистий { key, type }.
|
|
71
|
+
let m = opts.span != null ? { ...meta, span: opts.span } : meta;
|
|
72
|
+
if (opts.noEditControl)
|
|
73
|
+
m = { ...m, hints: { ...m.hints, noEditControl: true } };
|
|
74
|
+
if (opts.key)
|
|
75
|
+
return { key: opts.key, type: m };
|
|
76
|
+
if ('value' in opts && opts.value !== undefined)
|
|
77
|
+
return { el: 'static', type: m, value: opts.value };
|
|
78
|
+
return m;
|
|
79
|
+
}
|
|
80
|
+
// ─── Константи ────────────────────────────────────────────────────────────────
|
|
81
|
+
/** Усі мови ISO 639-1 (native-назви як label). Джерело — пакет `iso-639-1`. */
|
|
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
|
+
}
|
|
130
|
+
function resolveDate(v) {
|
|
131
|
+
return v === 'today' ? new Date().toISOString().slice(0, 10) : v;
|
|
132
|
+
}
|
|
133
|
+
const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
134
|
+
// ─── Модифікатори ───────────────────────────────────────────────────────────
|
|
135
|
+
/**
|
|
136
|
+
* Обгортає поле у JSON-масив значень (storage: TEXT). Внутрішній zod — `base.zod`.
|
|
137
|
+
* Порядок модифікаторів: base → applyMultiple.
|
|
138
|
+
*
|
|
139
|
+
* Виняток: kind 'image'/'media' обробляє multiple сам у рендері (галерея) —
|
|
140
|
+
* MultipleField*-обгортки на вебі їх пропускають.
|
|
141
|
+
*/
|
|
142
|
+
/** Zod «JSON-масив значень inner» — спільне для applyMultiple і bindSelectSourceZod. */
|
|
143
|
+
function multipleZod(inner) {
|
|
144
|
+
return z.string(reqErr()).superRefine((val, ctx) => {
|
|
145
|
+
try {
|
|
146
|
+
const arr = JSON.parse(val);
|
|
147
|
+
if (!Array.isArray(arr)) {
|
|
148
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('array') });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
for (const item of arr) {
|
|
152
|
+
const r = inner.safeParse(item);
|
|
153
|
+
if (!r.success) {
|
|
154
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('array_item') });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
export function applyMultiple(base, multiple) {
|
|
165
|
+
if (!multiple)
|
|
166
|
+
return base;
|
|
167
|
+
const s = multipleZod(base.zod);
|
|
168
|
+
return { ...base, column: 'text', hints: { ...base.hints, multiple: true }, zod: optionalize(s, base.required) };
|
|
169
|
+
}
|
|
170
|
+
export function string(o) {
|
|
171
|
+
const required = o.required ?? false;
|
|
172
|
+
const cfg = o.config ?? {};
|
|
173
|
+
const min = cfg.min ?? (required ? 1 : 0);
|
|
174
|
+
let s = z.string(reqTypeErr());
|
|
175
|
+
if (min > 0)
|
|
176
|
+
s = s.min(min, { message: vmsg('min_length', { min }) });
|
|
177
|
+
if (cfg.max != null)
|
|
178
|
+
s = s.max(cfg.max, { message: vmsg('max_length', { max: cfg.max }) });
|
|
179
|
+
if (cfg.pattern)
|
|
180
|
+
s = s.regex(new RegExp(cfg.pattern), {
|
|
181
|
+
message: vmsg('pattern', cfg.messageKey ? { messageKey: cfg.messageKey } : undefined),
|
|
182
|
+
});
|
|
183
|
+
const base = {
|
|
184
|
+
kind: 'text',
|
|
185
|
+
label: o.label ?? '',
|
|
186
|
+
required,
|
|
187
|
+
prim: 'text',
|
|
188
|
+
column: 'text',
|
|
189
|
+
hints: {
|
|
190
|
+
minLength: cfg.min,
|
|
191
|
+
maxLength: cfg.max,
|
|
192
|
+
},
|
|
193
|
+
zod: optionalize(s, required),
|
|
194
|
+
};
|
|
195
|
+
const meta = applyMultiple(base, o.multiple ?? false);
|
|
196
|
+
return wrapKey(o, meta);
|
|
197
|
+
}
|
|
198
|
+
export function text(o) {
|
|
199
|
+
// Support legacy min/max at top-level (used internally by presets)
|
|
200
|
+
const mergedConfig = { ...o.config };
|
|
201
|
+
if (o.min != null && mergedConfig.min == null)
|
|
202
|
+
mergedConfig.min = o.min;
|
|
203
|
+
if (o.max != null && mergedConfig.max == null)
|
|
204
|
+
mergedConfig.max = o.max;
|
|
205
|
+
const meta = string({ ...o, key: undefined, value: undefined, config: mergedConfig });
|
|
206
|
+
const textMeta = { ...meta, prim: 'text', kind: 'textarea' };
|
|
207
|
+
return wrapKey(o, textMeta);
|
|
208
|
+
}
|
|
209
|
+
export function real(o) {
|
|
210
|
+
const required = o.required ?? false;
|
|
211
|
+
const cfg = o.config ?? {};
|
|
212
|
+
let s = z.coerce.number(typeErr());
|
|
213
|
+
if (cfg.min != null)
|
|
214
|
+
s = s.min(cfg.min, { message: vmsg('min', { min: cfg.min }) });
|
|
215
|
+
if (cfg.max != null)
|
|
216
|
+
s = s.max(cfg.max, { message: vmsg('max', { max: cfg.max }) });
|
|
217
|
+
const base = {
|
|
218
|
+
kind: 'number',
|
|
219
|
+
label: o.label ?? '',
|
|
220
|
+
required,
|
|
221
|
+
prim: 'number',
|
|
222
|
+
column: 'real',
|
|
223
|
+
hints: { min: cfg.min, max: cfg.max, step: cfg.step ?? 'any' },
|
|
224
|
+
zod: optionalize(s, required),
|
|
225
|
+
};
|
|
226
|
+
const meta = applyMultiple(base, o.multiple ?? false);
|
|
227
|
+
return wrapKey(o, meta);
|
|
228
|
+
}
|
|
229
|
+
export function int(o) {
|
|
230
|
+
const required = o.required ?? false;
|
|
231
|
+
const cfg = o.config ?? {};
|
|
232
|
+
let s = z.coerce.number(typeErr()).int({ message: vmsg('int') });
|
|
233
|
+
if (cfg.min != null)
|
|
234
|
+
s = s.min(cfg.min, { message: vmsg('min', { min: cfg.min }) });
|
|
235
|
+
if (cfg.max != null)
|
|
236
|
+
s = s.max(cfg.max, { message: vmsg('max', { max: cfg.max }) });
|
|
237
|
+
const base = {
|
|
238
|
+
kind: 'number',
|
|
239
|
+
label: o.label ?? '',
|
|
240
|
+
required,
|
|
241
|
+
prim: 'number',
|
|
242
|
+
column: 'integer',
|
|
243
|
+
hints: { min: cfg.min, max: cfg.max, step: cfg.step ?? 1 },
|
|
244
|
+
zod: optionalize(s, required),
|
|
245
|
+
};
|
|
246
|
+
const meta = applyMultiple(base, o.multiple ?? false);
|
|
247
|
+
return wrapKey(o, meta);
|
|
248
|
+
}
|
|
249
|
+
export function date(o) {
|
|
250
|
+
const required = o.required ?? false;
|
|
251
|
+
const granularity = o.config?.granularity ?? 'day';
|
|
252
|
+
let s;
|
|
253
|
+
if (granularity === 'year') {
|
|
254
|
+
// year/month: min/max порівнюються лексикографічно як рядки (без resolveDate, на відміну від day)
|
|
255
|
+
s = z
|
|
256
|
+
.string(reqErr())
|
|
257
|
+
.regex(/^\d{4}$/, { message: vmsg('date_format') })
|
|
258
|
+
.superRefine((val, ctx) => {
|
|
259
|
+
if (o.min && val < o.min)
|
|
260
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('date_min', { min: o.min }) });
|
|
261
|
+
if (o.max && val > o.max)
|
|
262
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('date_max', { max: o.max }) });
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
else if (granularity === 'month') {
|
|
266
|
+
s = z
|
|
267
|
+
.string(reqErr())
|
|
268
|
+
.regex(/^\d{4}-\d{2}$/, { message: vmsg('date_format') })
|
|
269
|
+
.superRefine((val, ctx) => {
|
|
270
|
+
if (o.min && val < o.min)
|
|
271
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('date_min', { min: o.min }) });
|
|
272
|
+
if (o.max && val > o.max)
|
|
273
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('date_max', { max: o.max }) });
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
s = z
|
|
278
|
+
.string(reqErr())
|
|
279
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/, { message: vmsg('date_format') })
|
|
280
|
+
.superRefine((val, ctx) => {
|
|
281
|
+
if (o.min && val < resolveDate(o.min))
|
|
282
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('date_min', { min: resolveDate(o.min) }) });
|
|
283
|
+
if (o.max && val > resolveDate(o.max))
|
|
284
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('date_max', { max: resolveDate(o.max) }) });
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
const base = {
|
|
288
|
+
kind: 'date',
|
|
289
|
+
label: o.label ?? '',
|
|
290
|
+
required,
|
|
291
|
+
prim: 'date',
|
|
292
|
+
column: granularity === 'day' ? 'date' : 'text',
|
|
293
|
+
hints: { min: o.min, max: o.max, granularity },
|
|
294
|
+
zod: optionalize(s, required),
|
|
295
|
+
};
|
|
296
|
+
const meta = applyMultiple(base, o.multiple ?? false);
|
|
297
|
+
return wrapKey(o, meta);
|
|
298
|
+
}
|
|
299
|
+
export function time(o) {
|
|
300
|
+
const required = o.required ?? false;
|
|
301
|
+
const granularity = o.config?.granularity ?? 'second';
|
|
302
|
+
const timeRegex = {
|
|
303
|
+
hour: /^([01]\d|2[0-3])$/,
|
|
304
|
+
minute: /^([01]\d|2[0-3]):[0-5]\d$/,
|
|
305
|
+
second: /^([01]\d|2[0-3]):[0-5]\d:[0-5]\d$/,
|
|
306
|
+
};
|
|
307
|
+
const s = z.string(reqErr()).regex(timeRegex[granularity], { message: vmsg('time_format') });
|
|
308
|
+
const base = {
|
|
309
|
+
kind: 'time',
|
|
310
|
+
label: o.label ?? '',
|
|
311
|
+
required,
|
|
312
|
+
prim: 'time',
|
|
313
|
+
column: 'time',
|
|
314
|
+
hints: { granularity },
|
|
315
|
+
zod: optionalize(s, required),
|
|
316
|
+
};
|
|
317
|
+
const meta = applyMultiple(base, o.multiple ?? false);
|
|
318
|
+
return wrapKey(o, meta);
|
|
319
|
+
}
|
|
320
|
+
export function datetime(o) {
|
|
321
|
+
const required = o.required ?? false;
|
|
322
|
+
const granularity = o.config?.granularity ?? 'minute';
|
|
323
|
+
const dtRegex = {
|
|
324
|
+
hour: /^\d{4}-\d{2}-\d{2}T([01]\d|2[0-3])$/,
|
|
325
|
+
minute: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/,
|
|
326
|
+
second: /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/,
|
|
327
|
+
};
|
|
328
|
+
const s = z.string(reqErr()).regex(dtRegex[granularity], { message: vmsg('datetime_format') });
|
|
329
|
+
const base = {
|
|
330
|
+
kind: 'datetime',
|
|
331
|
+
label: o.label ?? '',
|
|
332
|
+
required,
|
|
333
|
+
prim: 'datetime',
|
|
334
|
+
column: 'datetime',
|
|
335
|
+
hints: { granularity },
|
|
336
|
+
zod: optionalize(s, required),
|
|
337
|
+
};
|
|
338
|
+
const meta = applyMultiple(base, o.multiple ?? false);
|
|
339
|
+
return wrapKey(o, meta);
|
|
340
|
+
}
|
|
341
|
+
export function boolean(o) {
|
|
342
|
+
const s = z.preprocess((v) => (v === 'true' || v === 1 || v === '1' ? true : v === 'false' || v === 0 || v === '0' ? false : v), z.boolean().default(false));
|
|
343
|
+
const meta = {
|
|
344
|
+
kind: 'boolean',
|
|
345
|
+
label: o.label ?? '',
|
|
346
|
+
required: false,
|
|
347
|
+
prim: 'checkbox',
|
|
348
|
+
column: 'boolean',
|
|
349
|
+
hints: {},
|
|
350
|
+
zod: s,
|
|
351
|
+
};
|
|
352
|
+
return wrapKey(o, meta);
|
|
353
|
+
}
|
|
354
|
+
export function triState(o) {
|
|
355
|
+
const required = o.required ?? false;
|
|
356
|
+
const s = z.enum(['yes', 'no', 'unknown'], { error: () => vmsg('enum') });
|
|
357
|
+
const meta = {
|
|
358
|
+
kind: 'triState',
|
|
359
|
+
label: o.label ?? '',
|
|
360
|
+
required,
|
|
361
|
+
prim: 'triState',
|
|
362
|
+
column: 'text',
|
|
363
|
+
hints: {},
|
|
364
|
+
zod: optionalize(s, required),
|
|
365
|
+
};
|
|
366
|
+
return wrapKey(o, meta);
|
|
367
|
+
}
|
|
368
|
+
export function select(o) {
|
|
369
|
+
const required = o.required ?? false;
|
|
370
|
+
// ── статичне джерело: NamedOptionList (string) або inline OptionItem[] ──
|
|
371
|
+
const source = typeof o.options === 'string' ? o.options : null;
|
|
372
|
+
const inlineOpts = Array.isArray(o.options) ? o.options : [];
|
|
373
|
+
const s = inlineOpts.length > 0
|
|
374
|
+
? z.enum(inlineOpts.map((x) => x.value), { error: () => vmsg('enum') })
|
|
375
|
+
: z.string(reqErr());
|
|
376
|
+
const base = {
|
|
377
|
+
kind: 'select',
|
|
378
|
+
label: o.label ?? '',
|
|
379
|
+
required,
|
|
380
|
+
prim: 'select',
|
|
381
|
+
column: 'text',
|
|
382
|
+
hints: { source },
|
|
383
|
+
options: inlineOpts,
|
|
384
|
+
zod: optionalize(s, required),
|
|
385
|
+
};
|
|
386
|
+
const meta = applyMultiple(base, o.multiple ?? false);
|
|
387
|
+
return wrapKey(o, meta);
|
|
388
|
+
}
|
|
389
|
+
export function relation(o) {
|
|
390
|
+
const required = o.required ?? false;
|
|
391
|
+
const multi = o.multiple ?? false;
|
|
392
|
+
let s;
|
|
393
|
+
if (multi) {
|
|
394
|
+
s = z.string().superRefine((val, ctx) => {
|
|
395
|
+
try {
|
|
396
|
+
const arr = JSON.parse(val);
|
|
397
|
+
if (!Array.isArray(arr) || !arr.every((id) => uuidRe.test(id)))
|
|
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
|
+
}
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
s = z.string().regex(uuidRe, { message: vmsg('uuid') });
|
|
407
|
+
}
|
|
408
|
+
const meta = {
|
|
409
|
+
kind: 'relation',
|
|
410
|
+
label: o.label ?? '',
|
|
411
|
+
required,
|
|
412
|
+
prim: 'relation',
|
|
413
|
+
column: 'text',
|
|
414
|
+
hints: { multi, displayKey: o.displayKey ?? 'name' },
|
|
415
|
+
relation: { vault: o.vault, type: o.module },
|
|
416
|
+
zod: optionalize(s, required),
|
|
417
|
+
};
|
|
418
|
+
return wrapKey(o, meta);
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Compose-time: підв'язати enum-валідацію source-селекту до значень списку.
|
|
422
|
+
* Factory не знає вмісту optionList (списки збираються лише в composeRegistry),
|
|
423
|
+
* тому zod source-полів спочатку z.string(); реєстр викликає це для кожного
|
|
424
|
+
* такого поля, щоб сервер відхиляв значення поза списком.
|
|
425
|
+
* Відтворює повний ланцюг модифікаторів: enum → multiple.
|
|
426
|
+
*/
|
|
427
|
+
export function bindSelectSourceZod(field, values) {
|
|
428
|
+
if (!values.length)
|
|
429
|
+
return;
|
|
430
|
+
const enumS = z.enum(values, { error: () => vmsg('enum') });
|
|
431
|
+
// Точне відтворення ланцюга factory: кожен крок обгортається в optionalize,
|
|
432
|
+
// як у select → applyMultiple.
|
|
433
|
+
let s = optionalize(enumS, field.required);
|
|
434
|
+
if (field.hints['multiple'] === true)
|
|
435
|
+
s = optionalize(multipleZod(s), field.required);
|
|
436
|
+
field.zod = s;
|
|
437
|
+
}
|
|
438
|
+
export function json(o) {
|
|
439
|
+
const required = o.required ?? false;
|
|
440
|
+
const s = z.string().superRefine((val, ctx) => {
|
|
441
|
+
try {
|
|
442
|
+
JSON.parse(val);
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
const meta = {
|
|
449
|
+
kind: 'json',
|
|
450
|
+
label: o.label ?? '',
|
|
451
|
+
required,
|
|
452
|
+
prim: 'json',
|
|
453
|
+
column: 'text',
|
|
454
|
+
hints: {},
|
|
455
|
+
zod: optionalize(s, required),
|
|
456
|
+
};
|
|
457
|
+
return wrapKey(o, meta);
|
|
458
|
+
}
|
|
459
|
+
export function checklist(o) {
|
|
460
|
+
const required = o.required ?? false;
|
|
461
|
+
const itemSchema = z.object({ done: z.boolean(), text: z.string().min(1) });
|
|
462
|
+
const s = jsonRefined(z.array(itemSchema), 'checklist_structure');
|
|
463
|
+
const base = {
|
|
464
|
+
kind: 'checklist',
|
|
465
|
+
label: o.label ?? '',
|
|
466
|
+
required,
|
|
467
|
+
prim: 'checklist',
|
|
468
|
+
column: 'text',
|
|
469
|
+
hints: {},
|
|
470
|
+
zod: optionalize(s, required),
|
|
471
|
+
};
|
|
472
|
+
const meta = applyMultiple(base, o.multiple ?? false);
|
|
473
|
+
return wrapKey(o, meta);
|
|
474
|
+
}
|
|
475
|
+
export function measured(o) {
|
|
476
|
+
const required = o.required ?? false;
|
|
477
|
+
const cfg = o.config ?? {};
|
|
478
|
+
// Resolve units: string preset name or inline array
|
|
479
|
+
const unitsSpec = (typeof o.options === 'string' ? o.options : o.options);
|
|
480
|
+
const units = resolveUnits(unitsSpec);
|
|
481
|
+
let numSchema = z.number().finite();
|
|
482
|
+
if (cfg.step === 1)
|
|
483
|
+
numSchema = numSchema.int({ message: vmsg('int') }); // int via step:1 is not used, kept for compat
|
|
484
|
+
if (cfg.min != null)
|
|
485
|
+
numSchema = numSchema.min(cfg.min);
|
|
486
|
+
if (cfg.max != null)
|
|
487
|
+
numSchema = numSchema.max(cfg.max);
|
|
488
|
+
const rowSchema = z.object({
|
|
489
|
+
value: numSchema,
|
|
490
|
+
unit: z.string().refine((v) => units.some((u) => u.value === v), { message: vmsg('measured_unit') }),
|
|
491
|
+
});
|
|
492
|
+
const s = z.string().superRefine((val, ctx) => {
|
|
493
|
+
try {
|
|
494
|
+
const r = rowSchema.safeParse(JSON.parse(val));
|
|
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 {
|
|
511
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
const base = {
|
|
515
|
+
kind: 'measured',
|
|
516
|
+
label: o.label ?? '',
|
|
517
|
+
required,
|
|
518
|
+
prim: 'measured',
|
|
519
|
+
column: 'text',
|
|
520
|
+
hints: {
|
|
521
|
+
units: o.options, // рядок або масив — як є
|
|
522
|
+
...(cfg.min != null && { min: cfg.min }),
|
|
523
|
+
...(cfg.max != null && { max: cfg.max }),
|
|
524
|
+
...(cfg.step != null && { step: cfg.step }),
|
|
525
|
+
},
|
|
526
|
+
zod: optionalize(s, required),
|
|
527
|
+
};
|
|
528
|
+
const meta = applyMultiple(base, o.multiple ?? false);
|
|
529
|
+
return wrapKey(o, meta);
|
|
530
|
+
}
|
|
531
|
+
export function money(o) {
|
|
532
|
+
const required = o.required ?? false;
|
|
533
|
+
const cfg = o.config ?? {};
|
|
534
|
+
let numSchema = z.number().finite();
|
|
535
|
+
if (cfg.min != null)
|
|
536
|
+
numSchema = numSchema.min(cfg.min);
|
|
537
|
+
if (cfg.max != null)
|
|
538
|
+
numSchema = numSchema.max(cfg.max);
|
|
539
|
+
const rowSchema = z.object({
|
|
540
|
+
value: numSchema,
|
|
541
|
+
currency: z.string().refine(isCurrencyCode, { message: vmsg('money_currency') }),
|
|
542
|
+
});
|
|
543
|
+
const s = z.string().superRefine((val, ctx) => {
|
|
544
|
+
try {
|
|
545
|
+
const r = rowSchema.safeParse(JSON.parse(val));
|
|
546
|
+
if (!r.success)
|
|
547
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('money_structure') });
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
const base = {
|
|
554
|
+
kind: 'money',
|
|
555
|
+
label: o.label ?? '',
|
|
556
|
+
required,
|
|
557
|
+
prim: 'money',
|
|
558
|
+
column: 'text',
|
|
559
|
+
hints: {
|
|
560
|
+
...(cfg.defaultCurrency && { defaultCurrency: cfg.defaultCurrency }),
|
|
561
|
+
...(cfg.min != null && { min: cfg.min }),
|
|
562
|
+
...(cfg.max != null && { max: cfg.max }),
|
|
563
|
+
},
|
|
564
|
+
zod: optionalize(s, required),
|
|
565
|
+
};
|
|
566
|
+
const meta = applyMultiple(base, o.multiple ?? false);
|
|
567
|
+
return wrapKey(o, meta);
|
|
568
|
+
}
|
|
569
|
+
export function code(o) {
|
|
570
|
+
const required = o.required ?? false;
|
|
571
|
+
const cfg = o.config ?? {};
|
|
572
|
+
const rowSchema = z.object({ code: z.string(), lang: z.string() });
|
|
573
|
+
const s = z.string().superRefine((val, ctx) => {
|
|
574
|
+
try {
|
|
575
|
+
if (!rowSchema.safeParse(JSON.parse(val)).success)
|
|
576
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('code_structure') });
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
const base = {
|
|
583
|
+
kind: 'code',
|
|
584
|
+
label: o.label ?? '',
|
|
585
|
+
required,
|
|
586
|
+
prim: 'code',
|
|
587
|
+
column: 'text',
|
|
588
|
+
hints: { ...(cfg.defaultLang && { defaultLang: cfg.defaultLang }) },
|
|
589
|
+
zod: optionalize(s, required),
|
|
590
|
+
};
|
|
591
|
+
return wrapKey(o, base);
|
|
592
|
+
}
|
|
593
|
+
export function geo(o) {
|
|
594
|
+
const required = o.required ?? false;
|
|
595
|
+
const rowSchema = z.object({
|
|
596
|
+
lat: z.number().min(-90).max(90),
|
|
597
|
+
lng: z.number().min(-180).max(180),
|
|
598
|
+
label: z.string().optional(),
|
|
599
|
+
});
|
|
600
|
+
const s = z.string().superRefine((val, ctx) => {
|
|
601
|
+
try {
|
|
602
|
+
if (!rowSchema.safeParse(JSON.parse(val)).success)
|
|
603
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('geo_structure') });
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
const base = {
|
|
610
|
+
kind: 'geo',
|
|
611
|
+
label: o.label ?? '',
|
|
612
|
+
required,
|
|
613
|
+
prim: 'geo',
|
|
614
|
+
column: 'text',
|
|
615
|
+
hints: {},
|
|
616
|
+
zod: optionalize(s, required),
|
|
617
|
+
};
|
|
618
|
+
return wrapKey(o, base);
|
|
619
|
+
}
|
|
620
|
+
export function perWeekday(o) {
|
|
621
|
+
const required = o.required ?? false;
|
|
622
|
+
const cfg = o.config ?? {};
|
|
623
|
+
let num = z.number(typeErr('per_weekday'));
|
|
624
|
+
if (cfg.min != null)
|
|
625
|
+
num = num.min(cfg.min, { message: vmsg('per_weekday') });
|
|
626
|
+
if (cfg.max != null)
|
|
627
|
+
num = num.max(cfg.max, { message: vmsg('per_weekday') });
|
|
628
|
+
const arrSchema = z.array(num).length(7);
|
|
629
|
+
const s = z.string().superRefine((val, ctx) => {
|
|
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),
|
|
651
|
+
};
|
|
652
|
+
return wrapKey(o, base);
|
|
653
|
+
}
|
|
654
|
+
export function lookup(o) {
|
|
655
|
+
const meta = {
|
|
656
|
+
kind: 'lookup',
|
|
657
|
+
label: o.label ?? '',
|
|
658
|
+
required: false,
|
|
659
|
+
prim: 'lookup',
|
|
660
|
+
column: 'text',
|
|
661
|
+
virtual: true,
|
|
662
|
+
hints: { from: o.from, pick: o.pick, compareWith: o.compareWith ?? null },
|
|
663
|
+
zod: z.any().optional(),
|
|
664
|
+
};
|
|
665
|
+
return wrapKey(o, meta);
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Відновлює FieldMeta з клієнтського FieldClient (для рекурсивного рендеру
|
|
669
|
+
* сабфілдів групових полів через FieldInput/FieldValue). zod тут не потрібен —
|
|
670
|
+
* валідація відбувається на рівні схеми батьківського поля.
|
|
671
|
+
*/
|
|
672
|
+
export function clientToMeta(fc) {
|
|
673
|
+
return { ...fc, column: 'text', zod: z.any() };
|
|
674
|
+
}
|
|
675
|
+
export function period(o) {
|
|
676
|
+
const granularity = o.granularity ?? 'day';
|
|
677
|
+
const fromField = granularity === 'datetime'
|
|
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.
|
|
686
|
+
const required = o.required ?? false;
|
|
687
|
+
const multiple = o.multiple ?? false;
|
|
688
|
+
const subDefs = { from: fromField, until: untilField };
|
|
689
|
+
const rowShape = {};
|
|
690
|
+
for (const [key, fm] of Object.entries(subDefs))
|
|
691
|
+
rowShape[key] = fm.zod.optional();
|
|
692
|
+
const rowSchema = z.object(rowShape).passthrough();
|
|
693
|
+
let s = jsonRefined(multiple ? z.array(rowSchema) : rowSchema, 'row_structure');
|
|
694
|
+
s = s.superRefine((val, ctx) => {
|
|
695
|
+
if (val == null || typeof val !== 'string')
|
|
696
|
+
return;
|
|
697
|
+
try {
|
|
698
|
+
const parsed = JSON.parse(val);
|
|
699
|
+
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
700
|
+
for (const item of items) {
|
|
701
|
+
const r = item;
|
|
702
|
+
if (r?.from && r?.until && String(r.from) > String(r.until))
|
|
703
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('period_order') });
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
catch {
|
|
707
|
+
/* already handled by jsonRefined */
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
const subFields = Object.entries(subDefs).map(([key, fm]) => ({ key, ...toClient(fm) }));
|
|
711
|
+
const meta = {
|
|
712
|
+
kind: 'period',
|
|
713
|
+
label: o.label ?? '',
|
|
714
|
+
required,
|
|
715
|
+
prim: 'period',
|
|
716
|
+
column: 'text',
|
|
717
|
+
hints: { fields: subFields, rowMultiple: multiple, unique: [], granularity },
|
|
718
|
+
zod: optionalize(s, required),
|
|
719
|
+
};
|
|
720
|
+
return wrapKey(o, meta);
|
|
721
|
+
}
|
|
722
|
+
function makeFile(kind, o) {
|
|
723
|
+
const required = o.required ?? false;
|
|
724
|
+
const multiple = o.multiple ?? false;
|
|
725
|
+
const rowSchema = z.object({
|
|
726
|
+
name: z.string().min(1),
|
|
727
|
+
mime: z.string().optional(),
|
|
728
|
+
size: z.number().optional(),
|
|
729
|
+
});
|
|
730
|
+
// Schema accepts both single {name,...} and array [{name,...},...] stored as JSON.
|
|
731
|
+
// multiple=true expects an array; single accepts a single object or array.
|
|
732
|
+
const s = z.string().superRefine((val, ctx) => {
|
|
733
|
+
try {
|
|
734
|
+
const parsed = JSON.parse(val);
|
|
735
|
+
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
736
|
+
for (const it of items) {
|
|
737
|
+
if (!rowSchema.safeParse(it).success) {
|
|
738
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('file_structure') });
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
catch {
|
|
744
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
const meta = {
|
|
748
|
+
kind,
|
|
749
|
+
label: o.label ?? '',
|
|
750
|
+
required,
|
|
751
|
+
prim: 'file',
|
|
752
|
+
column: 'text',
|
|
753
|
+
hints: multiple ? { multiple: true } : {},
|
|
754
|
+
zod: optionalize(s, required),
|
|
755
|
+
};
|
|
756
|
+
return wrapKey(o, meta);
|
|
757
|
+
}
|
|
758
|
+
export function file(o) {
|
|
759
|
+
return makeFile('file', o);
|
|
760
|
+
}
|
|
761
|
+
export function document(o) {
|
|
762
|
+
return makeFile('document', o);
|
|
763
|
+
}
|
|
764
|
+
export function audio(o) {
|
|
765
|
+
return makeFile('audio', o);
|
|
766
|
+
}
|
|
767
|
+
export function video(o) {
|
|
768
|
+
return makeFile('video', o);
|
|
769
|
+
}
|
|
770
|
+
export function image(o) {
|
|
771
|
+
return makeFile('image', o);
|
|
772
|
+
}
|
|
773
|
+
export function media(o) {
|
|
774
|
+
return makeFile('media', o);
|
|
775
|
+
}
|
|
776
|
+
const kvItemSchema = z.object({ key: z.string().min(1), value: z.string() });
|
|
777
|
+
export function keyValue(o) {
|
|
778
|
+
const required = o.required ?? false;
|
|
779
|
+
const multiple = o.multiple ?? true;
|
|
780
|
+
const s = z.string().superRefine((val, ctx) => {
|
|
781
|
+
try {
|
|
782
|
+
const r = z.array(kvItemSchema).safeParse(JSON.parse(val));
|
|
783
|
+
if (!r.success)
|
|
784
|
+
return ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('structure') });
|
|
785
|
+
if (!multiple && r.data.length > 1)
|
|
786
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('kv_single') });
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
const meta = {
|
|
793
|
+
kind: 'keyValue',
|
|
794
|
+
label: o.label ?? '',
|
|
795
|
+
required,
|
|
796
|
+
prim: 'key-value',
|
|
797
|
+
column: 'text',
|
|
798
|
+
// kvMultiple (НЕ multiple): «кілька пар» — внутрішня семантика віджету,
|
|
799
|
+
// не applyMultiple-обгортка. Інакше display/form гейти multiple ловлять його помилково.
|
|
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);
|
|
809
|
+
}
|
|
810
|
+
function makeRange(kind, int, o) {
|
|
811
|
+
const required = o.required ?? false;
|
|
812
|
+
const cfg = o.config ?? {};
|
|
813
|
+
let n = z.number().finite();
|
|
814
|
+
if (int)
|
|
815
|
+
n = n.int({ message: vmsg('int') });
|
|
816
|
+
if (cfg.min != null)
|
|
817
|
+
n = n.min(cfg.min);
|
|
818
|
+
if (cfg.max != null)
|
|
819
|
+
n = n.max(cfg.max);
|
|
820
|
+
const rowSchema = z.object({ from: n, to: n });
|
|
821
|
+
const s = z.string().superRefine((val, ctx) => {
|
|
822
|
+
try {
|
|
823
|
+
const r = rowSchema.safeParse(JSON.parse(val));
|
|
824
|
+
if (!r.success) {
|
|
825
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('range_structure') });
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
if (r.data.from > r.data.to)
|
|
829
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('range_order') });
|
|
830
|
+
}
|
|
831
|
+
catch {
|
|
832
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vmsg('json') });
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
const base = {
|
|
836
|
+
kind,
|
|
837
|
+
label: o.label ?? '',
|
|
838
|
+
required,
|
|
839
|
+
prim: kind,
|
|
840
|
+
column: 'text',
|
|
841
|
+
hints: {
|
|
842
|
+
...(cfg.min != null && { min: cfg.min }),
|
|
843
|
+
...(cfg.max != null && { max: cfg.max }),
|
|
844
|
+
...(cfg.step != null && { step: cfg.step }),
|
|
845
|
+
},
|
|
846
|
+
zod: optionalize(s, required),
|
|
847
|
+
};
|
|
848
|
+
return wrapKey(o, base);
|
|
849
|
+
}
|
|
850
|
+
export function numberRange(o) {
|
|
851
|
+
return makeRange('numberRange', true, o);
|
|
852
|
+
}
|
|
853
|
+
export function realRange(o) {
|
|
854
|
+
return makeRange('realRange', false, o);
|
|
855
|
+
}
|
|
856
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
857
|
+
// f — єдина точка входу для визначення полів у типах
|
|
858
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
859
|
+
/**
|
|
860
|
+
* Базові примітиви (storage-aligned) + структурні типи. Пресети (тонкі обгортки
|
|
861
|
+
* над цими примітивами) додаються нижче з field-presets.ts і НЕ можуть перебивати
|
|
862
|
+
* жоден ключ звідси (guard у composeF).
|
|
863
|
+
*/
|
|
864
|
+
const PRIMITIVES = {
|
|
865
|
+
string,
|
|
866
|
+
text,
|
|
867
|
+
real,
|
|
868
|
+
int,
|
|
869
|
+
date,
|
|
870
|
+
time,
|
|
871
|
+
datetime,
|
|
872
|
+
boolean,
|
|
873
|
+
triState,
|
|
874
|
+
select,
|
|
875
|
+
relation,
|
|
876
|
+
json,
|
|
877
|
+
checklist,
|
|
878
|
+
measured,
|
|
879
|
+
money,
|
|
880
|
+
code,
|
|
881
|
+
geo,
|
|
882
|
+
lookup,
|
|
883
|
+
perWeekday,
|
|
884
|
+
period,
|
|
885
|
+
file,
|
|
886
|
+
document,
|
|
887
|
+
audio,
|
|
888
|
+
video,
|
|
889
|
+
image,
|
|
890
|
+
media,
|
|
891
|
+
keyValue,
|
|
892
|
+
numberRange,
|
|
893
|
+
realRange,
|
|
894
|
+
group,
|
|
895
|
+
divider,
|
|
896
|
+
info,
|
|
897
|
+
button,
|
|
898
|
+
};
|
|
899
|
+
/** Збирає `f`, гарантуючи, що жоден пресет не затіняє примітив. */
|
|
900
|
+
function composeF(presets) {
|
|
901
|
+
for (const k of Object.keys(presets))
|
|
902
|
+
if (k in PRIMITIVES)
|
|
903
|
+
throw new Error(`[f] пресет '${k}' перебиває примітив`);
|
|
904
|
+
return { ...PRIMITIVES, ...presets };
|
|
905
|
+
}
|
|
906
|
+
export const field = composeF(presets);
|
|
907
|
+
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 };
|
|
910
|
+
}
|
|
911
|
+
//# sourceMappingURL=fields.js.map
|