@ai-matrx/design-system 0.22.0 → 0.25.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/CHANGELOG.md +151 -0
- package/dist/data-table/cell-undo.cjs +98 -0
- package/dist/data-table/cell-undo.cjs.map +1 -0
- package/dist/data-table/cell-undo.d.cts +85 -0
- package/dist/data-table/cell-undo.d.ts +85 -0
- package/dist/data-table/cell-undo.js +78 -0
- package/dist/data-table/cell-undo.js.map +1 -0
- package/dist/data-table/copy-helpers.d.cts +6 -2
- package/dist/data-table/copy-helpers.d.ts +6 -2
- package/dist/data-table/coverage.cjs +116 -0
- package/dist/data-table/coverage.cjs.map +1 -0
- package/dist/data-table/coverage.d.cts +65 -0
- package/dist/data-table/coverage.d.ts +65 -0
- package/dist/data-table/coverage.js +96 -0
- package/dist/data-table/coverage.js.map +1 -0
- package/dist/data-table/fill.cjs +299 -0
- package/dist/data-table/fill.cjs.map +1 -0
- package/dist/data-table/fill.d.cts +129 -0
- package/dist/data-table/fill.d.ts +129 -0
- package/dist/data-table/fill.js +277 -0
- package/dist/data-table/fill.js.map +1 -0
- package/dist/data-table/filter-engine.d.cts +6 -2
- package/dist/data-table/filter-engine.d.ts +6 -2
- package/dist/data-table/grid-clipboard.cjs +165 -0
- package/dist/data-table/grid-clipboard.cjs.map +1 -0
- package/dist/data-table/grid-clipboard.d.cts +85 -0
- package/dist/data-table/grid-clipboard.d.ts +85 -0
- package/dist/data-table/grid-clipboard.js +145 -0
- package/dist/data-table/grid-clipboard.js.map +1 -0
- package/dist/data-table/grid-selection.cjs +258 -0
- package/dist/data-table/grid-selection.cjs.map +1 -0
- package/dist/data-table/grid-selection.d.cts +166 -0
- package/dist/data-table/grid-selection.d.ts +166 -0
- package/dist/data-table/grid-selection.js +238 -0
- package/dist/data-table/grid-selection.js.map +1 -0
- package/dist/data-table/grouping.cjs +302 -0
- package/dist/data-table/grouping.cjs.map +1 -0
- package/dist/data-table/grouping.d.cts +119 -0
- package/dist/data-table/grouping.d.ts +119 -0
- package/dist/data-table/grouping.js +282 -0
- package/dist/data-table/grouping.js.map +1 -0
- package/dist/data-table/host.d.cts +6 -2
- package/dist/data-table/host.d.ts +6 -2
- package/dist/data-table/index.cjs +2433 -246
- package/dist/data-table/index.cjs.map +1 -1
- package/dist/data-table/index.d.cts +11 -3
- package/dist/data-table/index.d.ts +11 -3
- package/dist/data-table/index.js +2425 -238
- package/dist/data-table/index.js.map +1 -1
- package/dist/data-table/infer-filter.d.cts +6 -2
- package/dist/data-table/infer-filter.d.ts +6 -2
- package/dist/data-table/layered-filters.d.cts +6 -2
- package/dist/data-table/layered-filters.d.ts +6 -2
- package/dist/data-table/query-control.d.cts +6 -2
- package/dist/data-table/query-control.d.ts +6 -2
- package/dist/data-table/spreadsheet.cjs +1014 -0
- package/dist/data-table/spreadsheet.cjs.map +1 -0
- package/dist/data-table/spreadsheet.d.cts +118 -0
- package/dist/data-table/spreadsheet.d.ts +118 -0
- package/dist/data-table/spreadsheet.js +994 -0
- package/dist/data-table/spreadsheet.js.map +1 -0
- package/dist/data-table/types.cjs.map +1 -1
- package/dist/data-table/types.d.cts +6 -2
- package/dist/data-table/types.d.ts +6 -2
- package/dist/data-table/url-state.d.cts +6 -2
- package/dist/data-table/url-state.d.ts +6 -2
- package/dist/data-table/validation.cjs +229 -0
- package/dist/data-table/validation.cjs.map +1 -0
- package/dist/data-table/validation.d.cts +125 -0
- package/dist/data-table/validation.d.ts +125 -0
- package/dist/data-table/validation.js +209 -0
- package/dist/data-table/validation.js.map +1 -0
- package/dist/data-table/virtual-window.cjs +203 -0
- package/dist/data-table/virtual-window.cjs.map +1 -0
- package/dist/data-table/virtual-window.d.cts +84 -0
- package/dist/data-table/virtual-window.d.ts +84 -0
- package/dist/data-table/virtual-window.js +183 -0
- package/dist/data-table/virtual-window.js.map +1 -0
- package/dist/data-table/xlsx.d.cts +6 -2
- package/dist/data-table/xlsx.d.ts +6 -2
- package/dist/field-formats/index.d.cts +2 -2
- package/dist/field-formats/index.d.ts +2 -2
- package/dist/formulas/index.cjs +1041 -0
- package/dist/formulas/index.cjs.map +1 -0
- package/dist/formulas/index.d.cts +389 -0
- package/dist/formulas/index.d.ts +389 -0
- package/dist/formulas/index.js +1019 -0
- package/dist/formulas/index.js.map +1 -0
- package/dist/{types-BdbvlF9-.d.cts → types-CJWU1Gae.d.cts} +1 -1
- package/dist/{types-BdbvlF9-.d.ts → types-CJWU1Gae.d.ts} +1 -1
- package/dist/{types-Sn249eoy.d.cts → types-CmQrOz-T.d.cts} +154 -2
- package/dist/{types-D-flTpK0.d.ts → types-nlLCMiqd.d.ts} +154 -2
- package/package.json +109 -2
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/data-table/validation.ts
|
|
21
|
+
var validation_exports = {};
|
|
22
|
+
__export(validation_exports, {
|
|
23
|
+
describeValidationRules: () => describeValidationRules,
|
|
24
|
+
hasValidationRules: () => hasValidationRules,
|
|
25
|
+
parseValidationRules: () => parseValidationRules,
|
|
26
|
+
serializeValidationRules: () => serializeValidationRules,
|
|
27
|
+
validateCellValue: () => validateCellValue
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(validation_exports);
|
|
30
|
+
var OK = { ok: true };
|
|
31
|
+
var STORED_KEYS = [
|
|
32
|
+
"min",
|
|
33
|
+
"max",
|
|
34
|
+
"minLength",
|
|
35
|
+
"maxLength",
|
|
36
|
+
"pattern",
|
|
37
|
+
"patternHint",
|
|
38
|
+
"allowedValues",
|
|
39
|
+
"unique"
|
|
40
|
+
];
|
|
41
|
+
function finiteNumber(raw) {
|
|
42
|
+
if (typeof raw === "number") return Number.isFinite(raw) ? raw : void 0;
|
|
43
|
+
if (typeof raw === "string" && raw.trim() !== "") {
|
|
44
|
+
const n = Number(raw);
|
|
45
|
+
return Number.isFinite(n) ? n : void 0;
|
|
46
|
+
}
|
|
47
|
+
return void 0;
|
|
48
|
+
}
|
|
49
|
+
function nonNegativeInteger(raw) {
|
|
50
|
+
const n = finiteNumber(raw);
|
|
51
|
+
if (n === void 0) return void 0;
|
|
52
|
+
const i = Math.trunc(n);
|
|
53
|
+
return i >= 0 ? i : void 0;
|
|
54
|
+
}
|
|
55
|
+
function parseValidationRules(raw) {
|
|
56
|
+
let source = raw;
|
|
57
|
+
if (typeof source === "string") {
|
|
58
|
+
const text = source.trim();
|
|
59
|
+
if (text === "") return {};
|
|
60
|
+
try {
|
|
61
|
+
source = JSON.parse(text);
|
|
62
|
+
} catch {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!source || typeof source !== "object" || Array.isArray(source)) return {};
|
|
67
|
+
const obj = source;
|
|
68
|
+
const out = {};
|
|
69
|
+
const min = finiteNumber(obj.min);
|
|
70
|
+
if (min !== void 0) out.min = min;
|
|
71
|
+
const max = finiteNumber(obj.max);
|
|
72
|
+
if (max !== void 0) out.max = max;
|
|
73
|
+
const minLength = nonNegativeInteger(obj.minLength);
|
|
74
|
+
if (minLength !== void 0) out.minLength = minLength;
|
|
75
|
+
const maxLength = nonNegativeInteger(obj.maxLength);
|
|
76
|
+
if (maxLength !== void 0) out.maxLength = maxLength;
|
|
77
|
+
if (typeof obj.pattern === "string" && obj.pattern.trim() !== "") {
|
|
78
|
+
out.pattern = obj.pattern;
|
|
79
|
+
if (typeof obj.patternHint === "string" && obj.patternHint.trim() !== "") {
|
|
80
|
+
out.patternHint = obj.patternHint.trim();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(obj.allowedValues)) {
|
|
84
|
+
const values = obj.allowedValues.filter(
|
|
85
|
+
(v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean"
|
|
86
|
+
).map((v) => String(v).trim()).filter((v) => v !== "");
|
|
87
|
+
const seen = /* @__PURE__ */ new Set();
|
|
88
|
+
const unique = values.filter((v) => {
|
|
89
|
+
const key = v.toLowerCase();
|
|
90
|
+
if (seen.has(key)) return false;
|
|
91
|
+
seen.add(key);
|
|
92
|
+
return true;
|
|
93
|
+
});
|
|
94
|
+
if (unique.length > 0) out.allowedValues = unique;
|
|
95
|
+
}
|
|
96
|
+
if (obj.unique === true) out.unique = true;
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
function hasValidationRules(rules) {
|
|
100
|
+
if (!rules) return false;
|
|
101
|
+
return STORED_KEYS.some((key) => rules[key] !== void 0);
|
|
102
|
+
}
|
|
103
|
+
function serializeValidationRules(rules) {
|
|
104
|
+
const out = {};
|
|
105
|
+
if (!rules) return out;
|
|
106
|
+
for (const key of STORED_KEYS) {
|
|
107
|
+
const value = rules[key];
|
|
108
|
+
if (value === void 0) continue;
|
|
109
|
+
if (key === "allowedValues" && (!Array.isArray(value) || value.length === 0)) continue;
|
|
110
|
+
if (key === "unique" && value !== true) continue;
|
|
111
|
+
if ((key === "pattern" || key === "patternHint") && String(value).trim() === "") continue;
|
|
112
|
+
out[key] = value;
|
|
113
|
+
}
|
|
114
|
+
if (out.patternHint !== void 0 && out.pattern === void 0) delete out.patternHint;
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
function describeValidationRules(rules) {
|
|
118
|
+
const out = [];
|
|
119
|
+
if (rules.min !== void 0 && rules.max !== void 0) {
|
|
120
|
+
out.push(`Between ${rules.min} and ${rules.max}`);
|
|
121
|
+
} else if (rules.min !== void 0) {
|
|
122
|
+
out.push(`At least ${rules.min}`);
|
|
123
|
+
} else if (rules.max !== void 0) {
|
|
124
|
+
out.push(`At most ${rules.max}`);
|
|
125
|
+
}
|
|
126
|
+
if (rules.minLength !== void 0 && rules.maxLength !== void 0) {
|
|
127
|
+
out.push(`${rules.minLength}\u2013${rules.maxLength} characters`);
|
|
128
|
+
} else if (rules.minLength !== void 0) {
|
|
129
|
+
out.push(`At least ${rules.minLength} characters`);
|
|
130
|
+
} else if (rules.maxLength !== void 0) {
|
|
131
|
+
out.push(`At most ${rules.maxLength} characters`);
|
|
132
|
+
}
|
|
133
|
+
if (rules.pattern) {
|
|
134
|
+
out.push(rules.patternHint ? `Pattern ${rules.patternHint}` : `Matches ${rules.pattern}`);
|
|
135
|
+
}
|
|
136
|
+
if (rules.allowedValues?.length) {
|
|
137
|
+
out.push(`One of: ${rules.allowedValues.join(", ")}`);
|
|
138
|
+
}
|
|
139
|
+
if (rules.unique) out.push("Unique across rows");
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
var CHOICE_FORMATS = /* @__PURE__ */ new Set(["choice", "multi_choice"]);
|
|
143
|
+
function isEmptyValue(value) {
|
|
144
|
+
if (value === null || value === void 0) return true;
|
|
145
|
+
if (typeof value === "string" && value.trim() === "") return true;
|
|
146
|
+
if (Array.isArray(value) && value.length === 0) return true;
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
function asText(value) {
|
|
150
|
+
if (typeof value === "string") return value;
|
|
151
|
+
if (Array.isArray(value)) return value.map((v) => String(v)).join(", ");
|
|
152
|
+
if (value !== null && typeof value === "object") {
|
|
153
|
+
try {
|
|
154
|
+
return JSON.stringify(value);
|
|
155
|
+
} catch {
|
|
156
|
+
return String(value);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return String(value);
|
|
160
|
+
}
|
|
161
|
+
function validateCellValue(args) {
|
|
162
|
+
const { rules, dataType, format, value, existingValues } = args;
|
|
163
|
+
if (!rules || !hasValidationRules(rules)) return OK;
|
|
164
|
+
if (isEmptyValue(value)) return OK;
|
|
165
|
+
const text = asText(value);
|
|
166
|
+
const numeric = finiteNumber(value);
|
|
167
|
+
if (numeric !== void 0) {
|
|
168
|
+
if (rules.min !== void 0 && numeric < rules.min) {
|
|
169
|
+
return { ok: false, reason: `Must be at least ${rules.min}` };
|
|
170
|
+
}
|
|
171
|
+
if (rules.max !== void 0 && numeric > rules.max) {
|
|
172
|
+
return { ok: false, reason: `Must be at most ${rules.max}` };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (rules.minLength !== void 0 && text.length < rules.minLength) {
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
reason: `Must be at least ${rules.minLength} character${rules.minLength === 1 ? "" : "s"} (this is ${text.length})`
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (rules.maxLength !== void 0 && text.length > rules.maxLength) {
|
|
182
|
+
return {
|
|
183
|
+
ok: false,
|
|
184
|
+
reason: `Must be at most ${rules.maxLength} character${rules.maxLength === 1 ? "" : "s"} (this is ${text.length})`
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (rules.pattern) {
|
|
188
|
+
let re = null;
|
|
189
|
+
try {
|
|
190
|
+
re = new RegExp(rules.pattern);
|
|
191
|
+
} catch {
|
|
192
|
+
re = null;
|
|
193
|
+
}
|
|
194
|
+
if (re && !re.test(text)) {
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
reason: rules.patternHint ? `Must match the pattern ${rules.patternHint}` : `Must match the pattern ${rules.pattern}`
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const isChoiceColumn = format ? CHOICE_FORMATS.has(format.id) : false;
|
|
202
|
+
if (rules.allowedValues?.length && !isChoiceColumn) {
|
|
203
|
+
const target = text.trim();
|
|
204
|
+
const hit = rules.allowedValues.some(
|
|
205
|
+
(v) => v === target || v.toLowerCase() === target.toLowerCase()
|
|
206
|
+
);
|
|
207
|
+
if (!hit) {
|
|
208
|
+
return {
|
|
209
|
+
ok: false,
|
|
210
|
+
reason: `Must be one of: ${rules.allowedValues.join(", ")}`
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (rules.unique && existingValues && existingValues.length > 0) {
|
|
215
|
+
const target = text.trim().toLowerCase();
|
|
216
|
+
const clash = existingValues.some(
|
|
217
|
+
(other) => !isEmptyValue(other) && asText(other).trim().toLowerCase() === target
|
|
218
|
+
);
|
|
219
|
+
if (clash) {
|
|
220
|
+
return {
|
|
221
|
+
ok: false,
|
|
222
|
+
reason: `Must be unique \u2014 another row already has "${text}"`
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
void dataType;
|
|
227
|
+
return OK;
|
|
228
|
+
}
|
|
229
|
+
//# sourceMappingURL=validation.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/data-table/validation.ts"],"sourcesContent":["/**\n * Column validation rules for user data tables — THE one rule model.\n *\n * A rule lives on `workbench.udt_dataset_fields.validation_rules` (jsonb) and\n * answers one question: may this value be written into this column? The\n * champions are Excel's data validation (a rule per column; an invalid entry is\n * refused WITH the reason) and Airtable (type-level only). We take Excel's\n * posture: the reason is the product.\n *\n * THREE LAWS this module exists to keep:\n *\n * 1. **`required` is NOT stored here.** The column already declares it as\n * `is_required`, and a second copy is a second truth. `ValidationRules.required`\n * is a read-only MIRROR for callers that want one object to reason about —\n * `parseValidationRules` never invents it and the editor never writes it.\n *\n * 2. **A rule judges a value the user MEANT to write; it never judges what is\n * already stored.** Existing values that violate a rule are legal, are never\n * rewritten, and render in the SAME amber the format system already uses for\n * a mismatch (`<FormattedFieldValue validationRules>`). Declaring a rule is\n * how a user FINDS the stray values in their data, exactly as declaring a\n * choice column's options is.\n *\n * 3. **An empty value is never a rule violation.** Emptiness is `is_required`'s\n * question, asked once, by the callers that own the whole row. A `min: 0`\n * rule that refused an empty cell would make every optional number column\n * mandatory by accident.\n *\n * `unique` is the one rule this module cannot answer alone: it is a statement\n * about the OTHER rows, so the caller passes `existingValues` (the loaded rows'\n * values for that column) when it holds them. Without them the rule is skipped\n * rather than guessed — and it is deliberately NOT enforced by the database\n * trigger, because a cross-row check inside a per-row BEFORE trigger is the\n * wrong shape (it cannot see concurrent inserts and it walks the table on every\n * write). See `migrations/udt_validation_rules_strict_enforcement.sql`.\n *\n * Moved here from the app (`@ai-matrx/design-system/data-table/validation`) so\n * every table in the product enforces and explains validation the same way.\n */\n\nimport type { FieldFormatConfig } from \"../field-formats/types\";\n\nexport type ValidationRules = {\n /**\n * MIRROR of the column's `is_required`. Never stored in `validation_rules`,\n * never written by the editor — present so a caller can carry one object.\n */\n required?: boolean;\n /** Smallest number accepted. Number-ish columns only. */\n min?: number;\n /** Largest number accepted. Number-ish columns only. */\n max?: number;\n /** Fewest characters accepted. Text-ish columns only. */\n minLength?: number;\n /** Most characters accepted. Text-ish columns only. */\n maxLength?: number;\n /**\n * A regular expression the value must match. Written in the dialect BOTH\n * JavaScript and Postgres understand, and **anchored by the author** — an\n * unanchored `\\d{3}` matches \"abc123def\" in both engines, which is almost\n * never what a person drawing a rule means.\n */\n pattern?: string;\n /** Plain-English shape shown when `pattern` refuses, e.g. `###-####`. */\n patternHint?: string;\n /**\n * The exact values accepted. Only for a column that is NOT a choice column —\n * a choice column already carries its options in its format, and a second\n * list would be a second vocabulary for the same column.\n */\n allowedValues?: string[];\n /** No two rows may carry the same value. Checked by the caller, never by the trigger. */\n unique?: boolean;\n};\n\nexport type ValidationOk = { ok: true };\nexport type ValidationFailure = { ok: false; reason: string };\nexport type ValidationResult = ValidationOk | ValidationFailure;\n\nconst OK: ValidationOk = { ok: true };\n\n/** Keys the editor may write. `required` is deliberately absent — law 1. */\nconst STORED_KEYS = [\n \"min\",\n \"max\",\n \"minLength\",\n \"maxLength\",\n \"pattern\",\n \"patternHint\",\n \"allowedValues\",\n \"unique\",\n] as const;\n\nfunction finiteNumber(raw: unknown): number | undefined {\n if (typeof raw === \"number\") return Number.isFinite(raw) ? raw : undefined;\n if (typeof raw === \"string\" && raw.trim() !== \"\") {\n const n = Number(raw);\n return Number.isFinite(n) ? n : undefined;\n }\n return undefined;\n}\n\nfunction nonNegativeInteger(raw: unknown): number | undefined {\n const n = finiteNumber(raw);\n if (n === undefined) return undefined;\n const i = Math.trunc(n);\n return i >= 0 ? i : undefined;\n}\n\n/**\n * Read whatever is stored on the column into the rule model. TOLERANT by\n * design: this reads a jsonb column that predates the feature, that an agent or\n * an import may have written, and that a future version may extend. Anything it\n * cannot understand is dropped, never thrown on — a column with a garbled rule\n * must still be editable.\n */\nexport function parseValidationRules(raw: unknown): ValidationRules {\n let source: unknown = raw;\n if (typeof source === \"string\") {\n const text = source.trim();\n if (text === \"\") return {};\n try {\n source = JSON.parse(text);\n } catch {\n return {};\n }\n }\n if (!source || typeof source !== \"object\" || Array.isArray(source)) return {};\n const obj = source as Record<string, unknown>;\n const out: ValidationRules = {};\n\n const min = finiteNumber(obj.min);\n if (min !== undefined) out.min = min;\n const max = finiteNumber(obj.max);\n if (max !== undefined) out.max = max;\n\n const minLength = nonNegativeInteger(obj.minLength);\n if (minLength !== undefined) out.minLength = minLength;\n const maxLength = nonNegativeInteger(obj.maxLength);\n if (maxLength !== undefined) out.maxLength = maxLength;\n\n if (typeof obj.pattern === \"string\" && obj.pattern.trim() !== \"\") {\n out.pattern = obj.pattern;\n if (typeof obj.patternHint === \"string\" && obj.patternHint.trim() !== \"\") {\n out.patternHint = obj.patternHint.trim();\n }\n }\n\n if (Array.isArray(obj.allowedValues)) {\n const values = obj.allowedValues\n .filter(\n (v): v is string | number | boolean =>\n typeof v === \"string\" || typeof v === \"number\" || typeof v === \"boolean\",\n )\n .map((v) => String(v).trim())\n .filter((v) => v !== \"\");\n // De-duplicate, keeping the author's order.\n const seen = new Set<string>();\n const unique = values.filter((v) => {\n const key = v.toLowerCase();\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n });\n if (unique.length > 0) out.allowedValues = unique;\n }\n\n if (obj.unique === true) out.unique = true;\n\n return out;\n}\n\n/** True when the column actually constrains anything. */\nexport function hasValidationRules(rules: ValidationRules | null | undefined): boolean {\n if (!rules) return false;\n return STORED_KEYS.some((key) => rules[key] !== undefined);\n}\n\n/**\n * The object to STORE. Strips `required` (law 1) and every empty key, so a\n * column whose rules were all cleared stores `{}` rather than a husk of nulls\n * — which also matters because `update_user_table_config` COALESCEs the column,\n * so `{}` is the only way to clear it.\n */\nexport function serializeValidationRules(\n rules: ValidationRules | null | undefined,\n): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n if (!rules) return out;\n for (const key of STORED_KEYS) {\n const value = rules[key];\n if (value === undefined) continue;\n if (key === \"allowedValues\" && (!Array.isArray(value) || value.length === 0)) continue;\n if (key === \"unique\" && value !== true) continue;\n if ((key === \"pattern\" || key === \"patternHint\") && String(value).trim() === \"\") continue;\n out[key] = value;\n }\n // A hint with no pattern says nothing; drop it rather than store a dangling half-rule.\n if (out.patternHint !== undefined && out.pattern === undefined) delete out.patternHint;\n return out;\n}\n\n/** Plain-English list of what a column requires — for tooltips and for agents. */\nexport function describeValidationRules(rules: ValidationRules): string[] {\n const out: string[] = [];\n if (rules.min !== undefined && rules.max !== undefined) {\n out.push(`Between ${rules.min} and ${rules.max}`);\n } else if (rules.min !== undefined) {\n out.push(`At least ${rules.min}`);\n } else if (rules.max !== undefined) {\n out.push(`At most ${rules.max}`);\n }\n if (rules.minLength !== undefined && rules.maxLength !== undefined) {\n out.push(`${rules.minLength}–${rules.maxLength} characters`);\n } else if (rules.minLength !== undefined) {\n out.push(`At least ${rules.minLength} characters`);\n } else if (rules.maxLength !== undefined) {\n out.push(`At most ${rules.maxLength} characters`);\n }\n if (rules.pattern) {\n out.push(rules.patternHint ? `Pattern ${rules.patternHint}` : `Matches ${rules.pattern}`);\n }\n if (rules.allowedValues?.length) {\n out.push(`One of: ${rules.allowedValues.join(\", \")}`);\n }\n if (rules.unique) out.push(\"Unique across rows\");\n return out;\n}\n\nconst CHOICE_FORMATS = new Set([\"choice\", \"multi_choice\"]);\n\nfunction isEmptyValue(value: unknown): boolean {\n if (value === null || value === undefined) return true;\n if (typeof value === \"string\" && value.trim() === \"\") return true;\n if (Array.isArray(value) && value.length === 0) return true;\n return false;\n}\n\n/** The value as the user reads it — the basis for every text rule. */\nfunction asText(value: unknown): string {\n if (typeof value === \"string\") return value;\n if (Array.isArray(value)) return value.map((v) => String(v)).join(\", \");\n if (value !== null && typeof value === \"object\") {\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n }\n return String(value);\n}\n\nexport type ValidateCellArgs = {\n rules: ValidationRules | null | undefined;\n /** The column's storage type (`string` | `number` | `integer` | …). */\n dataType: string;\n /** The column's declared display format, when it has one. */\n format?: FieldFormatConfig | null;\n /** The value the user is trying to write, already type-normalized. */\n value: unknown;\n /**\n * Every OTHER row's value for this column, when the caller holds them. Only\n * `unique` reads this; omit it and `unique` is skipped rather than guessed.\n */\n existingValues?: unknown[];\n};\n\n/**\n * Judge one value against one column's rules.\n *\n * Returns the FIRST reason it fails, in the order a person would check them\n * (range, length, shape, membership, uniqueness) — a wall of five complaints\n * about one cell is not more helpful than the first one.\n */\nexport function validateCellValue(args: ValidateCellArgs): ValidationResult {\n const { rules, dataType, format, value, existingValues } = args;\n if (!rules || !hasValidationRules(rules)) return OK;\n\n // Law 3 — emptiness belongs to `is_required`, not to a rule.\n if (isEmptyValue(value)) return OK;\n\n const text = asText(value);\n const numeric = finiteNumber(value);\n\n // ── range ───────────────────────────────────────────────────────────────\n // Applied only when a number can honestly be read out of the value. A\n // non-numeric value in a number column is a TYPE problem, already reported by\n // the format layer and by strict mode; saying \"Must be at least 0\" about the\n // word \"pending\" would be a second, worse answer to a question already asked.\n if (numeric !== undefined) {\n if (rules.min !== undefined && numeric < rules.min) {\n return { ok: false, reason: `Must be at least ${rules.min}` };\n }\n if (rules.max !== undefined && numeric > rules.max) {\n return { ok: false, reason: `Must be at most ${rules.max}` };\n }\n }\n\n // ── length ──────────────────────────────────────────────────────────────\n if (rules.minLength !== undefined && text.length < rules.minLength) {\n return {\n ok: false,\n reason: `Must be at least ${rules.minLength} character${rules.minLength === 1 ? \"\" : \"s\"} (this is ${text.length})`,\n };\n }\n if (rules.maxLength !== undefined && text.length > rules.maxLength) {\n return {\n ok: false,\n reason: `Must be at most ${rules.maxLength} character${rules.maxLength === 1 ? \"\" : \"s\"} (this is ${text.length})`,\n };\n }\n\n // ── shape ───────────────────────────────────────────────────────────────\n if (rules.pattern) {\n let re: RegExp | null = null;\n try {\n re = new RegExp(rules.pattern);\n } catch {\n // An unparseable pattern is the AUTHOR's defect, not this value's. It is\n // reported where it is written (the editor refuses to save one); here it\n // must never turn into a refusal the typist cannot act on.\n re = null;\n }\n if (re && !re.test(text)) {\n return {\n ok: false,\n reason: rules.patternHint\n ? `Must match the pattern ${rules.patternHint}`\n : `Must match the pattern ${rules.pattern}`,\n };\n }\n }\n\n // ── membership ──────────────────────────────────────────────────────────\n // Skipped on a choice column by construction: its options are its format's\n // job, they are offered in the picker, and an off-list value there is legal\n // and amber by design. A second list here would contradict that.\n const isChoiceColumn = format ? CHOICE_FORMATS.has(format.id) : false;\n if (rules.allowedValues?.length && !isChoiceColumn) {\n const target = text.trim();\n const hit = rules.allowedValues.some(\n (v) => v === target || v.toLowerCase() === target.toLowerCase(),\n );\n if (!hit) {\n return {\n ok: false,\n reason: `Must be one of: ${rules.allowedValues.join(\", \")}`,\n };\n }\n }\n\n // ── uniqueness ──────────────────────────────────────────────────────────\n if (rules.unique && existingValues && existingValues.length > 0) {\n const target = text.trim().toLowerCase();\n const clash = existingValues.some(\n (other) => !isEmptyValue(other) && asText(other).trim().toLowerCase() === target,\n );\n if (clash) {\n return {\n ok: false,\n reason: `Must be unique — another row already has \"${text}\"`,\n };\n }\n }\n\n // `dataType` is not read by any rule today: every rule above is answered by\n // the VALUE, and the storage type is already enforced by strict mode and by\n // the format layer. It stays in the signature because it is the one fact a\n // future rule (a date range, say) cannot be written without, and adding it\n // later would change every callsite.\n void dataType;\n\n return OK;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA+EA,IAAM,KAAmB,EAAE,IAAI,KAAK;AAGpC,IAAM,cAAc;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,SAAS,aAAa,KAAkC;AACtD,MAAI,OAAO,QAAQ,SAAU,QAAO,OAAO,SAAS,GAAG,IAAI,MAAM;AACjE,MAAI,OAAO,QAAQ,YAAY,IAAI,KAAK,MAAM,IAAI;AAChD,UAAM,IAAI,OAAO,GAAG;AACpB,WAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AAAA,EAClC;AACA,SAAO;AACT;AAEA,SAAS,mBAAmB,KAAkC;AAC5D,QAAM,IAAI,aAAa,GAAG;AAC1B,MAAI,MAAM,OAAW,QAAO;AAC5B,QAAM,IAAI,KAAK,MAAM,CAAC;AACtB,SAAO,KAAK,IAAI,IAAI;AACtB;AASO,SAAS,qBAAqB,KAA+B;AAClE,MAAI,SAAkB;AACtB,MAAI,OAAO,WAAW,UAAU;AAC9B,UAAM,OAAO,OAAO,KAAK;AACzB,QAAI,SAAS,GAAI,QAAO,CAAC;AACzB,QAAI;AACF,eAAS,KAAK,MAAM,IAAI;AAAA,IAC1B,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AACA,MAAI,CAAC,UAAU,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,EAAG,QAAO,CAAC;AAC5E,QAAM,MAAM;AACZ,QAAM,MAAuB,CAAC;AAE9B,QAAM,MAAM,aAAa,IAAI,GAAG;AAChC,MAAI,QAAQ,OAAW,KAAI,MAAM;AACjC,QAAM,MAAM,aAAa,IAAI,GAAG;AAChC,MAAI,QAAQ,OAAW,KAAI,MAAM;AAEjC,QAAM,YAAY,mBAAmB,IAAI,SAAS;AAClD,MAAI,cAAc,OAAW,KAAI,YAAY;AAC7C,QAAM,YAAY,mBAAmB,IAAI,SAAS;AAClD,MAAI,cAAc,OAAW,KAAI,YAAY;AAE7C,MAAI,OAAO,IAAI,YAAY,YAAY,IAAI,QAAQ,KAAK,MAAM,IAAI;AAChE,QAAI,UAAU,IAAI;AAClB,QAAI,OAAO,IAAI,gBAAgB,YAAY,IAAI,YAAY,KAAK,MAAM,IAAI;AACxE,UAAI,cAAc,IAAI,YAAY,KAAK;AAAA,IACzC;AAAA,EACF;AAEA,MAAI,MAAM,QAAQ,IAAI,aAAa,GAAG;AACpC,UAAM,SAAS,IAAI,cAChB;AAAA,MACC,CAAC,MACC,OAAO,MAAM,YAAY,OAAO,MAAM,YAAY,OAAO,MAAM;AAAA,IACnE,EACC,IAAI,CAAC,MAAM,OAAO,CAAC,EAAE,KAAK,CAAC,EAC3B,OAAO,CAAC,MAAM,MAAM,EAAE;AAEzB,UAAM,OAAO,oBAAI,IAAY;AAC7B,UAAM,SAAS,OAAO,OAAO,CAAC,MAAM;AAClC,YAAM,MAAM,EAAE,YAAY;AAC1B,UAAI,KAAK,IAAI,GAAG,EAAG,QAAO;AAC1B,WAAK,IAAI,GAAG;AACZ,aAAO;AAAA,IACT,CAAC;AACD,QAAI,OAAO,SAAS,EAAG,KAAI,gBAAgB;AAAA,EAC7C;AAEA,MAAI,IAAI,WAAW,KAAM,KAAI,SAAS;AAEtC,SAAO;AACT;AAGO,SAAS,mBAAmB,OAAoD;AACrF,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,YAAY,KAAK,CAAC,QAAQ,MAAM,GAAG,MAAM,MAAS;AAC3D;AAQO,SAAS,yBACd,OACyB;AACzB,QAAM,MAA+B,CAAC;AACtC,MAAI,CAAC,MAAO,QAAO;AACnB,aAAW,OAAO,aAAa;AAC7B,UAAM,QAAQ,MAAM,GAAG;AACvB,QAAI,UAAU,OAAW;AACzB,QAAI,QAAQ,oBAAoB,CAAC,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,GAAI;AAC9E,QAAI,QAAQ,YAAY,UAAU,KAAM;AACxC,SAAK,QAAQ,aAAa,QAAQ,kBAAkB,OAAO,KAAK,EAAE,KAAK,MAAM,GAAI;AACjF,QAAI,GAAG,IAAI;AAAA,EACb;AAEA,MAAI,IAAI,gBAAgB,UAAa,IAAI,YAAY,OAAW,QAAO,IAAI;AAC3E,SAAO;AACT;AAGO,SAAS,wBAAwB,OAAkC;AACxE,QAAM,MAAgB,CAAC;AACvB,MAAI,MAAM,QAAQ,UAAa,MAAM,QAAQ,QAAW;AACtD,QAAI,KAAK,WAAW,MAAM,GAAG,QAAQ,MAAM,GAAG,EAAE;AAAA,EAClD,WAAW,MAAM,QAAQ,QAAW;AAClC,QAAI,KAAK,YAAY,MAAM,GAAG,EAAE;AAAA,EAClC,WAAW,MAAM,QAAQ,QAAW;AAClC,QAAI,KAAK,WAAW,MAAM,GAAG,EAAE;AAAA,EACjC;AACA,MAAI,MAAM,cAAc,UAAa,MAAM,cAAc,QAAW;AAClE,QAAI,KAAK,GAAG,MAAM,SAAS,SAAI,MAAM,SAAS,aAAa;AAAA,EAC7D,WAAW,MAAM,cAAc,QAAW;AACxC,QAAI,KAAK,YAAY,MAAM,SAAS,aAAa;AAAA,EACnD,WAAW,MAAM,cAAc,QAAW;AACxC,QAAI,KAAK,WAAW,MAAM,SAAS,aAAa;AAAA,EAClD;AACA,MAAI,MAAM,SAAS;AACjB,QAAI,KAAK,MAAM,cAAc,WAAW,MAAM,WAAW,KAAK,WAAW,MAAM,OAAO,EAAE;AAAA,EAC1F;AACA,MAAI,MAAM,eAAe,QAAQ;AAC/B,QAAI,KAAK,WAAW,MAAM,cAAc,KAAK,IAAI,CAAC,EAAE;AAAA,EACtD;AACA,MAAI,MAAM,OAAQ,KAAI,KAAK,oBAAoB;AAC/C,SAAO;AACT;AAEA,IAAM,iBAAiB,oBAAI,IAAI,CAAC,UAAU,cAAc,CAAC;AAEzD,SAAS,aAAa,OAAyB;AAC7C,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,MAAI,OAAO,UAAU,YAAY,MAAM,KAAK,MAAM,GAAI,QAAO;AAC7D,MAAI,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,EAAG,QAAO;AACvD,SAAO;AACT;AAGA,SAAS,OAAO,OAAwB;AACtC,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,IAAI,CAAC,MAAM,OAAO,CAAC,CAAC,EAAE,KAAK,IAAI;AACtE,MAAI,UAAU,QAAQ,OAAO,UAAU,UAAU;AAC/C,QAAI;AACF,aAAO,KAAK,UAAU,KAAK;AAAA,IAC7B,QAAQ;AACN,aAAO,OAAO,KAAK;AAAA,IACrB;AAAA,EACF;AACA,SAAO,OAAO,KAAK;AACrB;AAwBO,SAAS,kBAAkB,MAA0C;AAC1E,QAAM,EAAE,OAAO,UAAU,QAAQ,OAAO,eAAe,IAAI;AAC3D,MAAI,CAAC,SAAS,CAAC,mBAAmB,KAAK,EAAG,QAAO;AAGjD,MAAI,aAAa,KAAK,EAAG,QAAO;AAEhC,QAAM,OAAO,OAAO,KAAK;AACzB,QAAM,UAAU,aAAa,KAAK;AAOlC,MAAI,YAAY,QAAW;AACzB,QAAI,MAAM,QAAQ,UAAa,UAAU,MAAM,KAAK;AAClD,aAAO,EAAE,IAAI,OAAO,QAAQ,oBAAoB,MAAM,GAAG,GAAG;AAAA,IAC9D;AACA,QAAI,MAAM,QAAQ,UAAa,UAAU,MAAM,KAAK;AAClD,aAAO,EAAE,IAAI,OAAO,QAAQ,mBAAmB,MAAM,GAAG,GAAG;AAAA,IAC7D;AAAA,EACF;AAGA,MAAI,MAAM,cAAc,UAAa,KAAK,SAAS,MAAM,WAAW;AAClE,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ,oBAAoB,MAAM,SAAS,aAAa,MAAM,cAAc,IAAI,KAAK,GAAG,aAAa,KAAK,MAAM;AAAA,IAClH;AAAA,EACF;AACA,MAAI,MAAM,cAAc,UAAa,KAAK,SAAS,MAAM,WAAW;AAClE,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ,mBAAmB,MAAM,SAAS,aAAa,MAAM,cAAc,IAAI,KAAK,GAAG,aAAa,KAAK,MAAM;AAAA,IACjH;AAAA,EACF;AAGA,MAAI,MAAM,SAAS;AACjB,QAAI,KAAoB;AACxB,QAAI;AACF,WAAK,IAAI,OAAO,MAAM,OAAO;AAAA,IAC/B,QAAQ;AAIN,WAAK;AAAA,IACP;AACA,QAAI,MAAM,CAAC,GAAG,KAAK,IAAI,GAAG;AACxB,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,QAAQ,MAAM,cACV,0BAA0B,MAAM,WAAW,KAC3C,0BAA0B,MAAM,OAAO;AAAA,MAC7C;AAAA,IACF;AAAA,EACF;AAMA,QAAM,iBAAiB,SAAS,eAAe,IAAI,OAAO,EAAE,IAAI;AAChE,MAAI,MAAM,eAAe,UAAU,CAAC,gBAAgB;AAClD,UAAM,SAAS,KAAK,KAAK;AACzB,UAAM,MAAM,MAAM,cAAc;AAAA,MAC9B,CAAC,MAAM,MAAM,UAAU,EAAE,YAAY,MAAM,OAAO,YAAY;AAAA,IAChE;AACA,QAAI,CAAC,KAAK;AACR,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,QAAQ,mBAAmB,MAAM,cAAc,KAAK,IAAI,CAAC;AAAA,MAC3D;AAAA,IACF;AAAA,EACF;AAGA,MAAI,MAAM,UAAU,kBAAkB,eAAe,SAAS,GAAG;AAC/D,UAAM,SAAS,KAAK,KAAK,EAAE,YAAY;AACvC,UAAM,QAAQ,eAAe;AAAA,MAC3B,CAAC,UAAU,CAAC,aAAa,KAAK,KAAK,OAAO,KAAK,EAAE,KAAK,EAAE,YAAY,MAAM;AAAA,IAC5E;AACA,QAAI,OAAO;AACT,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,QAAQ,kDAA6C,IAAI;AAAA,MAC3D;AAAA,IACF;AAAA,EACF;AAOA,OAAK;AAEL,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { F as FieldFormatConfig } from '../types-CJWU1Gae.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Column validation rules for user data tables — THE one rule model.
|
|
5
|
+
*
|
|
6
|
+
* A rule lives on `workbench.udt_dataset_fields.validation_rules` (jsonb) and
|
|
7
|
+
* answers one question: may this value be written into this column? The
|
|
8
|
+
* champions are Excel's data validation (a rule per column; an invalid entry is
|
|
9
|
+
* refused WITH the reason) and Airtable (type-level only). We take Excel's
|
|
10
|
+
* posture: the reason is the product.
|
|
11
|
+
*
|
|
12
|
+
* THREE LAWS this module exists to keep:
|
|
13
|
+
*
|
|
14
|
+
* 1. **`required` is NOT stored here.** The column already declares it as
|
|
15
|
+
* `is_required`, and a second copy is a second truth. `ValidationRules.required`
|
|
16
|
+
* is a read-only MIRROR for callers that want one object to reason about —
|
|
17
|
+
* `parseValidationRules` never invents it and the editor never writes it.
|
|
18
|
+
*
|
|
19
|
+
* 2. **A rule judges a value the user MEANT to write; it never judges what is
|
|
20
|
+
* already stored.** Existing values that violate a rule are legal, are never
|
|
21
|
+
* rewritten, and render in the SAME amber the format system already uses for
|
|
22
|
+
* a mismatch (`<FormattedFieldValue validationRules>`). Declaring a rule is
|
|
23
|
+
* how a user FINDS the stray values in their data, exactly as declaring a
|
|
24
|
+
* choice column's options is.
|
|
25
|
+
*
|
|
26
|
+
* 3. **An empty value is never a rule violation.** Emptiness is `is_required`'s
|
|
27
|
+
* question, asked once, by the callers that own the whole row. A `min: 0`
|
|
28
|
+
* rule that refused an empty cell would make every optional number column
|
|
29
|
+
* mandatory by accident.
|
|
30
|
+
*
|
|
31
|
+
* `unique` is the one rule this module cannot answer alone: it is a statement
|
|
32
|
+
* about the OTHER rows, so the caller passes `existingValues` (the loaded rows'
|
|
33
|
+
* values for that column) when it holds them. Without them the rule is skipped
|
|
34
|
+
* rather than guessed — and it is deliberately NOT enforced by the database
|
|
35
|
+
* trigger, because a cross-row check inside a per-row BEFORE trigger is the
|
|
36
|
+
* wrong shape (it cannot see concurrent inserts and it walks the table on every
|
|
37
|
+
* write). See `migrations/udt_validation_rules_strict_enforcement.sql`.
|
|
38
|
+
*
|
|
39
|
+
* Moved here from the app (`@ai-matrx/design-system/data-table/validation`) so
|
|
40
|
+
* every table in the product enforces and explains validation the same way.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
type ValidationRules = {
|
|
44
|
+
/**
|
|
45
|
+
* MIRROR of the column's `is_required`. Never stored in `validation_rules`,
|
|
46
|
+
* never written by the editor — present so a caller can carry one object.
|
|
47
|
+
*/
|
|
48
|
+
required?: boolean;
|
|
49
|
+
/** Smallest number accepted. Number-ish columns only. */
|
|
50
|
+
min?: number;
|
|
51
|
+
/** Largest number accepted. Number-ish columns only. */
|
|
52
|
+
max?: number;
|
|
53
|
+
/** Fewest characters accepted. Text-ish columns only. */
|
|
54
|
+
minLength?: number;
|
|
55
|
+
/** Most characters accepted. Text-ish columns only. */
|
|
56
|
+
maxLength?: number;
|
|
57
|
+
/**
|
|
58
|
+
* A regular expression the value must match. Written in the dialect BOTH
|
|
59
|
+
* JavaScript and Postgres understand, and **anchored by the author** — an
|
|
60
|
+
* unanchored `\d{3}` matches "abc123def" in both engines, which is almost
|
|
61
|
+
* never what a person drawing a rule means.
|
|
62
|
+
*/
|
|
63
|
+
pattern?: string;
|
|
64
|
+
/** Plain-English shape shown when `pattern` refuses, e.g. `###-####`. */
|
|
65
|
+
patternHint?: string;
|
|
66
|
+
/**
|
|
67
|
+
* The exact values accepted. Only for a column that is NOT a choice column —
|
|
68
|
+
* a choice column already carries its options in its format, and a second
|
|
69
|
+
* list would be a second vocabulary for the same column.
|
|
70
|
+
*/
|
|
71
|
+
allowedValues?: string[];
|
|
72
|
+
/** No two rows may carry the same value. Checked by the caller, never by the trigger. */
|
|
73
|
+
unique?: boolean;
|
|
74
|
+
};
|
|
75
|
+
type ValidationOk = {
|
|
76
|
+
ok: true;
|
|
77
|
+
};
|
|
78
|
+
type ValidationFailure = {
|
|
79
|
+
ok: false;
|
|
80
|
+
reason: string;
|
|
81
|
+
};
|
|
82
|
+
type ValidationResult = ValidationOk | ValidationFailure;
|
|
83
|
+
/**
|
|
84
|
+
* Read whatever is stored on the column into the rule model. TOLERANT by
|
|
85
|
+
* design: this reads a jsonb column that predates the feature, that an agent or
|
|
86
|
+
* an import may have written, and that a future version may extend. Anything it
|
|
87
|
+
* cannot understand is dropped, never thrown on — a column with a garbled rule
|
|
88
|
+
* must still be editable.
|
|
89
|
+
*/
|
|
90
|
+
declare function parseValidationRules(raw: unknown): ValidationRules;
|
|
91
|
+
/** True when the column actually constrains anything. */
|
|
92
|
+
declare function hasValidationRules(rules: ValidationRules | null | undefined): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* The object to STORE. Strips `required` (law 1) and every empty key, so a
|
|
95
|
+
* column whose rules were all cleared stores `{}` rather than a husk of nulls
|
|
96
|
+
* — which also matters because `update_user_table_config` COALESCEs the column,
|
|
97
|
+
* so `{}` is the only way to clear it.
|
|
98
|
+
*/
|
|
99
|
+
declare function serializeValidationRules(rules: ValidationRules | null | undefined): Record<string, unknown>;
|
|
100
|
+
/** Plain-English list of what a column requires — for tooltips and for agents. */
|
|
101
|
+
declare function describeValidationRules(rules: ValidationRules): string[];
|
|
102
|
+
type ValidateCellArgs = {
|
|
103
|
+
rules: ValidationRules | null | undefined;
|
|
104
|
+
/** The column's storage type (`string` | `number` | `integer` | …). */
|
|
105
|
+
dataType: string;
|
|
106
|
+
/** The column's declared display format, when it has one. */
|
|
107
|
+
format?: FieldFormatConfig | null;
|
|
108
|
+
/** The value the user is trying to write, already type-normalized. */
|
|
109
|
+
value: unknown;
|
|
110
|
+
/**
|
|
111
|
+
* Every OTHER row's value for this column, when the caller holds them. Only
|
|
112
|
+
* `unique` reads this; omit it and `unique` is skipped rather than guessed.
|
|
113
|
+
*/
|
|
114
|
+
existingValues?: unknown[];
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Judge one value against one column's rules.
|
|
118
|
+
*
|
|
119
|
+
* Returns the FIRST reason it fails, in the order a person would check them
|
|
120
|
+
* (range, length, shape, membership, uniqueness) — a wall of five complaints
|
|
121
|
+
* about one cell is not more helpful than the first one.
|
|
122
|
+
*/
|
|
123
|
+
declare function validateCellValue(args: ValidateCellArgs): ValidationResult;
|
|
124
|
+
|
|
125
|
+
export { type ValidateCellArgs, type ValidationFailure, type ValidationOk, type ValidationResult, type ValidationRules, describeValidationRules, hasValidationRules, parseValidationRules, serializeValidationRules, validateCellValue };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { F as FieldFormatConfig } from '../types-CJWU1Gae.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Column validation rules for user data tables — THE one rule model.
|
|
5
|
+
*
|
|
6
|
+
* A rule lives on `workbench.udt_dataset_fields.validation_rules` (jsonb) and
|
|
7
|
+
* answers one question: may this value be written into this column? The
|
|
8
|
+
* champions are Excel's data validation (a rule per column; an invalid entry is
|
|
9
|
+
* refused WITH the reason) and Airtable (type-level only). We take Excel's
|
|
10
|
+
* posture: the reason is the product.
|
|
11
|
+
*
|
|
12
|
+
* THREE LAWS this module exists to keep:
|
|
13
|
+
*
|
|
14
|
+
* 1. **`required` is NOT stored here.** The column already declares it as
|
|
15
|
+
* `is_required`, and a second copy is a second truth. `ValidationRules.required`
|
|
16
|
+
* is a read-only MIRROR for callers that want one object to reason about —
|
|
17
|
+
* `parseValidationRules` never invents it and the editor never writes it.
|
|
18
|
+
*
|
|
19
|
+
* 2. **A rule judges a value the user MEANT to write; it never judges what is
|
|
20
|
+
* already stored.** Existing values that violate a rule are legal, are never
|
|
21
|
+
* rewritten, and render in the SAME amber the format system already uses for
|
|
22
|
+
* a mismatch (`<FormattedFieldValue validationRules>`). Declaring a rule is
|
|
23
|
+
* how a user FINDS the stray values in their data, exactly as declaring a
|
|
24
|
+
* choice column's options is.
|
|
25
|
+
*
|
|
26
|
+
* 3. **An empty value is never a rule violation.** Emptiness is `is_required`'s
|
|
27
|
+
* question, asked once, by the callers that own the whole row. A `min: 0`
|
|
28
|
+
* rule that refused an empty cell would make every optional number column
|
|
29
|
+
* mandatory by accident.
|
|
30
|
+
*
|
|
31
|
+
* `unique` is the one rule this module cannot answer alone: it is a statement
|
|
32
|
+
* about the OTHER rows, so the caller passes `existingValues` (the loaded rows'
|
|
33
|
+
* values for that column) when it holds them. Without them the rule is skipped
|
|
34
|
+
* rather than guessed — and it is deliberately NOT enforced by the database
|
|
35
|
+
* trigger, because a cross-row check inside a per-row BEFORE trigger is the
|
|
36
|
+
* wrong shape (it cannot see concurrent inserts and it walks the table on every
|
|
37
|
+
* write). See `migrations/udt_validation_rules_strict_enforcement.sql`.
|
|
38
|
+
*
|
|
39
|
+
* Moved here from the app (`@ai-matrx/design-system/data-table/validation`) so
|
|
40
|
+
* every table in the product enforces and explains validation the same way.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
type ValidationRules = {
|
|
44
|
+
/**
|
|
45
|
+
* MIRROR of the column's `is_required`. Never stored in `validation_rules`,
|
|
46
|
+
* never written by the editor — present so a caller can carry one object.
|
|
47
|
+
*/
|
|
48
|
+
required?: boolean;
|
|
49
|
+
/** Smallest number accepted. Number-ish columns only. */
|
|
50
|
+
min?: number;
|
|
51
|
+
/** Largest number accepted. Number-ish columns only. */
|
|
52
|
+
max?: number;
|
|
53
|
+
/** Fewest characters accepted. Text-ish columns only. */
|
|
54
|
+
minLength?: number;
|
|
55
|
+
/** Most characters accepted. Text-ish columns only. */
|
|
56
|
+
maxLength?: number;
|
|
57
|
+
/**
|
|
58
|
+
* A regular expression the value must match. Written in the dialect BOTH
|
|
59
|
+
* JavaScript and Postgres understand, and **anchored by the author** — an
|
|
60
|
+
* unanchored `\d{3}` matches "abc123def" in both engines, which is almost
|
|
61
|
+
* never what a person drawing a rule means.
|
|
62
|
+
*/
|
|
63
|
+
pattern?: string;
|
|
64
|
+
/** Plain-English shape shown when `pattern` refuses, e.g. `###-####`. */
|
|
65
|
+
patternHint?: string;
|
|
66
|
+
/**
|
|
67
|
+
* The exact values accepted. Only for a column that is NOT a choice column —
|
|
68
|
+
* a choice column already carries its options in its format, and a second
|
|
69
|
+
* list would be a second vocabulary for the same column.
|
|
70
|
+
*/
|
|
71
|
+
allowedValues?: string[];
|
|
72
|
+
/** No two rows may carry the same value. Checked by the caller, never by the trigger. */
|
|
73
|
+
unique?: boolean;
|
|
74
|
+
};
|
|
75
|
+
type ValidationOk = {
|
|
76
|
+
ok: true;
|
|
77
|
+
};
|
|
78
|
+
type ValidationFailure = {
|
|
79
|
+
ok: false;
|
|
80
|
+
reason: string;
|
|
81
|
+
};
|
|
82
|
+
type ValidationResult = ValidationOk | ValidationFailure;
|
|
83
|
+
/**
|
|
84
|
+
* Read whatever is stored on the column into the rule model. TOLERANT by
|
|
85
|
+
* design: this reads a jsonb column that predates the feature, that an agent or
|
|
86
|
+
* an import may have written, and that a future version may extend. Anything it
|
|
87
|
+
* cannot understand is dropped, never thrown on — a column with a garbled rule
|
|
88
|
+
* must still be editable.
|
|
89
|
+
*/
|
|
90
|
+
declare function parseValidationRules(raw: unknown): ValidationRules;
|
|
91
|
+
/** True when the column actually constrains anything. */
|
|
92
|
+
declare function hasValidationRules(rules: ValidationRules | null | undefined): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* The object to STORE. Strips `required` (law 1) and every empty key, so a
|
|
95
|
+
* column whose rules were all cleared stores `{}` rather than a husk of nulls
|
|
96
|
+
* — which also matters because `update_user_table_config` COALESCEs the column,
|
|
97
|
+
* so `{}` is the only way to clear it.
|
|
98
|
+
*/
|
|
99
|
+
declare function serializeValidationRules(rules: ValidationRules | null | undefined): Record<string, unknown>;
|
|
100
|
+
/** Plain-English list of what a column requires — for tooltips and for agents. */
|
|
101
|
+
declare function describeValidationRules(rules: ValidationRules): string[];
|
|
102
|
+
type ValidateCellArgs = {
|
|
103
|
+
rules: ValidationRules | null | undefined;
|
|
104
|
+
/** The column's storage type (`string` | `number` | `integer` | …). */
|
|
105
|
+
dataType: string;
|
|
106
|
+
/** The column's declared display format, when it has one. */
|
|
107
|
+
format?: FieldFormatConfig | null;
|
|
108
|
+
/** The value the user is trying to write, already type-normalized. */
|
|
109
|
+
value: unknown;
|
|
110
|
+
/**
|
|
111
|
+
* Every OTHER row's value for this column, when the caller holds them. Only
|
|
112
|
+
* `unique` reads this; omit it and `unique` is skipped rather than guessed.
|
|
113
|
+
*/
|
|
114
|
+
existingValues?: unknown[];
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Judge one value against one column's rules.
|
|
118
|
+
*
|
|
119
|
+
* Returns the FIRST reason it fails, in the order a person would check them
|
|
120
|
+
* (range, length, shape, membership, uniqueness) — a wall of five complaints
|
|
121
|
+
* about one cell is not more helpful than the first one.
|
|
122
|
+
*/
|
|
123
|
+
declare function validateCellValue(args: ValidateCellArgs): ValidationResult;
|
|
124
|
+
|
|
125
|
+
export { type ValidateCellArgs, type ValidationFailure, type ValidationOk, type ValidationResult, type ValidationRules, describeValidationRules, hasValidationRules, parseValidationRules, serializeValidationRules, validateCellValue };
|