cogsbox-shape 0.5.211 → 0.5.213
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.
|
@@ -1,23 +1,11 @@
|
|
|
1
|
-
import { type ChainMethodContext } from "cogsbox-state";
|
|
2
1
|
import { z } from "zod";
|
|
3
2
|
/** Minimal shape of a createSchemaBox entry — matches journalSchemaBox.journalTechnical etc. */
|
|
4
3
|
export type ShapeRefineInfo = {
|
|
5
4
|
fieldToGroup: Record<string, number[]>;
|
|
6
5
|
groups: {
|
|
7
|
-
deps:
|
|
6
|
+
deps: string[] | null;
|
|
8
7
|
}[];
|
|
9
8
|
};
|
|
10
|
-
export type ShapeIssue = {
|
|
11
|
-
path: string[];
|
|
12
|
-
message: string;
|
|
13
|
-
code?: string;
|
|
14
|
-
};
|
|
15
|
-
export type ShapeErrorGroup = {
|
|
16
|
-
hasErrors: boolean;
|
|
17
|
-
message: string;
|
|
18
|
-
issues: ShapeIssue[];
|
|
19
|
-
};
|
|
20
|
-
export type ShapeErrorGroups = Record<string, ShapeErrorGroup>;
|
|
21
9
|
export type ShapeSchemaBoxEntry = {
|
|
22
10
|
/** Field-key → value map from DeriveStateType (not z.infer on a flattened client object). */
|
|
23
11
|
stateType: Record<string, unknown>;
|
|
@@ -49,6 +37,7 @@ type FormUpdateParams = {
|
|
|
49
37
|
path: string[];
|
|
50
38
|
event: {
|
|
51
39
|
activityType: string;
|
|
40
|
+
details?: Record<string, unknown>;
|
|
52
41
|
};
|
|
53
42
|
getState: () => unknown;
|
|
54
43
|
addZodErrors: (errors: Array<{
|
|
@@ -56,26 +45,12 @@ type FormUpdateParams = {
|
|
|
56
45
|
message: string;
|
|
57
46
|
code?: string;
|
|
58
47
|
}>) => void;
|
|
59
|
-
clearZodErrors
|
|
60
|
-
setErrorGroups?: (parentPath: string[], groups: ShapeErrorGroups) => void;
|
|
48
|
+
clearZodErrors?: (paths: string[][]) => void;
|
|
61
49
|
};
|
|
62
|
-
type MutableTuple<T> = T extends readonly [...infer TItems] ? TItems : never;
|
|
63
|
-
type ExtractShapeRefineGroupFields<TEntry> = TEntry extends {
|
|
64
|
-
refineInfo?: {
|
|
65
|
-
groups: readonly (infer TGroup)[];
|
|
66
|
-
};
|
|
67
|
-
} ? TGroup extends {
|
|
68
|
-
deps: infer TDeps;
|
|
69
|
-
} ? TDeps extends readonly string[] ? MutableTuple<TDeps> : never : never : never;
|
|
70
|
-
type ShapeRefineGroupFields<TBox extends ShapeSchemaBox> = [
|
|
71
|
-
ExtractShapeRefineGroupFields<TBox[keyof TBox]>
|
|
72
|
-
] extends [never] ? string[] : ExtractShapeRefineGroupFields<TBox[keyof TBox]>;
|
|
73
50
|
export declare function wireShapeValidationOptions(box: ShapeSchemaBox, params: TransformStateParams): void;
|
|
74
51
|
/** Cross-field refine errors only — field rules are handled by state via setOptions. */
|
|
75
52
|
export declare function validateShapeRefines(box: ShapeSchemaBox, params: FormUpdateParams): void;
|
|
76
53
|
export declare function createShapePlugin<const TBox extends ShapeSchemaBox>(box: TBox): import("cogsbox-state").CogsPluginBuilder<"shape", {
|
|
77
54
|
logs: boolean | undefined;
|
|
78
|
-
}, unknown, unknown, never, {
|
|
79
|
-
$errorGroups: import("cogsbox-state").ChainMethodDefinition<(_ctx: ChainMethodContext, ...fields: ShapeRefineGroupFields<TBox>) => ShapeErrorGroup>;
|
|
80
|
-
}, true, false, true, true, false, true, InferShapeBoxState<TBox>>;
|
|
55
|
+
}, unknown, unknown, never, {}, true, false, true, false, false, true, InferShapeBoxState<TBox>>;
|
|
81
56
|
export {};
|
|
@@ -1,19 +1,8 @@
|
|
|
1
|
-
import { createPluginContext } from "cogsbox-state";
|
|
1
|
+
import { createPluginContext, getGlobalStore } from "cogsbox-state";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
const emptyErrorGroup = {
|
|
4
|
-
hasErrors: false,
|
|
5
|
-
message: "",
|
|
6
|
-
issues: [],
|
|
7
|
-
};
|
|
8
3
|
function pathKey(path) {
|
|
9
4
|
return path.join("\0");
|
|
10
5
|
}
|
|
11
|
-
function errorGroupStorageKey(stateKey, parentPath) {
|
|
12
|
-
return [stateKey, ...parentPath].join("\0");
|
|
13
|
-
}
|
|
14
|
-
function groupKey(fields) {
|
|
15
|
-
return fields.join("+");
|
|
16
|
-
}
|
|
17
6
|
function resolveRelatedPaths(blurPath, relatedFields) {
|
|
18
7
|
const parent = blurPath.slice(0, -1);
|
|
19
8
|
return [...relatedFields].map((field) => [...parent, field]);
|
|
@@ -25,6 +14,100 @@ function mapZodIssues(issues) {
|
|
|
25
14
|
code: issue.code,
|
|
26
15
|
}));
|
|
27
16
|
}
|
|
17
|
+
function cloneStateForInputEvent(state, path, value) {
|
|
18
|
+
if (path.length === 0)
|
|
19
|
+
return value;
|
|
20
|
+
if (state === null || typeof state !== "object")
|
|
21
|
+
return state;
|
|
22
|
+
const root = Array.isArray(state)
|
|
23
|
+
? [...state]
|
|
24
|
+
: { ...state };
|
|
25
|
+
let cursor = root;
|
|
26
|
+
for (let index = 0; index < path.length - 1; index++) {
|
|
27
|
+
const segment = path[index];
|
|
28
|
+
if (Array.isArray(cursor)) {
|
|
29
|
+
const arrayIndex = Number(segment);
|
|
30
|
+
if (!Number.isInteger(arrayIndex))
|
|
31
|
+
return root;
|
|
32
|
+
const next = cursor[arrayIndex];
|
|
33
|
+
const cloned = next && typeof next === "object"
|
|
34
|
+
? Array.isArray(next)
|
|
35
|
+
? [...next]
|
|
36
|
+
: { ...next }
|
|
37
|
+
: {};
|
|
38
|
+
cursor[arrayIndex] = cloned;
|
|
39
|
+
cursor = cloned;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
const next = cursor[segment];
|
|
43
|
+
const cloned = next && typeof next === "object"
|
|
44
|
+
? Array.isArray(next)
|
|
45
|
+
? [...next]
|
|
46
|
+
: { ...next }
|
|
47
|
+
: {};
|
|
48
|
+
cursor[segment] = cloned;
|
|
49
|
+
cursor = cloned;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const leaf = path[path.length - 1];
|
|
53
|
+
if (Array.isArray(cursor)) {
|
|
54
|
+
const arrayIndex = Number(leaf);
|
|
55
|
+
if (Number.isInteger(arrayIndex))
|
|
56
|
+
cursor[arrayIndex] = value;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
cursor[leaf] = value;
|
|
60
|
+
}
|
|
61
|
+
return root;
|
|
62
|
+
}
|
|
63
|
+
function getStateForValidation(params) {
|
|
64
|
+
const state = params.getState();
|
|
65
|
+
if (params.event.activityType !== "input" ||
|
|
66
|
+
!("details" in params.event) ||
|
|
67
|
+
!params.event.details ||
|
|
68
|
+
typeof params.event.details !== "object" ||
|
|
69
|
+
!("value" in params.event.details)) {
|
|
70
|
+
return state;
|
|
71
|
+
}
|
|
72
|
+
return cloneStateForInputEvent(state, params.path, params.event.details.value);
|
|
73
|
+
}
|
|
74
|
+
function notifyValidationPaths(stateKey, paths) {
|
|
75
|
+
const store = getGlobalStore.getState();
|
|
76
|
+
for (const path of paths) {
|
|
77
|
+
store.notifyPathSubscribers([stateKey, ...path].join("."), {
|
|
78
|
+
type: "VALIDATION_UPDATE",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function clearValidationPaths(params, paths) {
|
|
83
|
+
if (paths.length === 0)
|
|
84
|
+
return;
|
|
85
|
+
if (params.clearZodErrors) {
|
|
86
|
+
params.clearZodErrors(paths);
|
|
87
|
+
notifyValidationPaths(params.stateKey, paths);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const store = getGlobalStore.getState();
|
|
91
|
+
for (const path of paths) {
|
|
92
|
+
const currentMeta = store.getShadowMetadata(params.stateKey, path) || {};
|
|
93
|
+
store.setShadowMetadata(params.stateKey, path, {
|
|
94
|
+
...currentMeta,
|
|
95
|
+
validation: {
|
|
96
|
+
status: "NOT_VALIDATED",
|
|
97
|
+
errors: [],
|
|
98
|
+
lastValidated: Date.now(),
|
|
99
|
+
validatedValue: undefined,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
notifyValidationPaths(params.stateKey, paths);
|
|
104
|
+
}
|
|
105
|
+
function addValidationIssues(params, issues) {
|
|
106
|
+
if (issues.length === 0)
|
|
107
|
+
return;
|
|
108
|
+
params.addZodErrors(issues);
|
|
109
|
+
notifyValidationPaths(params.stateKey, issues.map((issue) => issue.path));
|
|
110
|
+
}
|
|
28
111
|
function getRelatedFields(entry, field) {
|
|
29
112
|
const groupIndexes = entry.refineInfo?.fieldToGroup[field];
|
|
30
113
|
if (!groupIndexes?.length)
|
|
@@ -43,23 +126,6 @@ function issueMatchesRelatedFields(issue, relatedFields) {
|
|
|
43
126
|
const leaf = String(issue.path.at(-1) ?? "");
|
|
44
127
|
return relatedFields.has(leaf);
|
|
45
128
|
}
|
|
46
|
-
function buildShapeErrorGroups(entry, groupIndexes, fallbackField, issues) {
|
|
47
|
-
const groups = {};
|
|
48
|
-
for (const index of groupIndexes) {
|
|
49
|
-
const deps = entry.refineInfo?.groups[index]?.deps ?? [fallbackField];
|
|
50
|
-
const relatedFields = new Set(deps);
|
|
51
|
-
const mapped = mapZodIssues(issues.filter((issue) => issueMatchesRelatedFields(issue, relatedFields)));
|
|
52
|
-
groups[groupKey(deps)] =
|
|
53
|
-
mapped.length > 0
|
|
54
|
-
? {
|
|
55
|
-
hasErrors: true,
|
|
56
|
-
message: mapped[0]?.message ?? "",
|
|
57
|
-
issues: mapped,
|
|
58
|
-
}
|
|
59
|
-
: emptyErrorGroup;
|
|
60
|
-
}
|
|
61
|
-
return groups;
|
|
62
|
-
}
|
|
63
129
|
export function wireShapeValidationOptions(box, params) {
|
|
64
130
|
const entry = box[params.stateKey];
|
|
65
131
|
if (!entry)
|
|
@@ -73,8 +139,10 @@ export function wireShapeValidationOptions(box, params) {
|
|
|
73
139
|
}
|
|
74
140
|
/** Cross-field refine errors only — field rules are handled by state via setOptions. */
|
|
75
141
|
export function validateShapeRefines(box, params) {
|
|
76
|
-
if (params.event.activityType !== "blur"
|
|
142
|
+
if (params.event.activityType !== "blur" &&
|
|
143
|
+
params.event.activityType !== "input") {
|
|
77
144
|
return;
|
|
145
|
+
}
|
|
78
146
|
const entry = box[params.stateKey];
|
|
79
147
|
const clientSchema = entry?.validators?.client ?? entry?.schemas.client;
|
|
80
148
|
if (!entry || !clientSchema)
|
|
@@ -82,29 +150,21 @@ export function validateShapeRefines(box, params) {
|
|
|
82
150
|
const field = params.path.at(-1);
|
|
83
151
|
if (!field)
|
|
84
152
|
return;
|
|
85
|
-
const groupIndexes = entry.refineInfo?.fieldToGroup[field];
|
|
86
153
|
const relatedFields = getRelatedFields(entry, field);
|
|
87
154
|
if (!relatedFields)
|
|
88
155
|
return;
|
|
89
156
|
const relatedPaths = resolveRelatedPaths(params.path, relatedFields);
|
|
90
|
-
const result = clientSchema.safeParse(params
|
|
91
|
-
const parentPath = params.path.slice(0, -1);
|
|
157
|
+
const result = clientSchema.safeParse(getStateForValidation(params));
|
|
92
158
|
if (result.success) {
|
|
93
|
-
params
|
|
94
|
-
params.clearZodErrors(relatedPaths);
|
|
159
|
+
clearValidationPaths(params, relatedPaths);
|
|
95
160
|
return;
|
|
96
161
|
}
|
|
97
|
-
params.setErrorGroups?.(parentPath, buildShapeErrorGroups(entry, groupIndexes ?? [], field, result.error.issues));
|
|
98
162
|
const issues = result.error.issues.filter((issue) => issueMatchesRelatedFields(issue, relatedFields));
|
|
99
163
|
const mapped = mapZodIssues(issues);
|
|
100
164
|
const activeKeys = new Set(mapped.map((entry) => pathKey(entry.path)));
|
|
101
165
|
const stalePaths = relatedPaths.filter((targetPath) => !activeKeys.has(pathKey(targetPath)));
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
if (mapped.length > 0) {
|
|
106
|
-
params.addZodErrors(mapped);
|
|
107
|
-
}
|
|
166
|
+
clearValidationPaths(params, stalePaths);
|
|
167
|
+
addValidationIssues(params, mapped);
|
|
108
168
|
}
|
|
109
169
|
function buildInitialState(box) {
|
|
110
170
|
const state = {};
|
|
@@ -123,30 +183,15 @@ const { createPlugin } = createPluginContext({
|
|
|
123
183
|
}),
|
|
124
184
|
});
|
|
125
185
|
export function createShapePlugin(box) {
|
|
126
|
-
const errorGroupsByPath = new Map();
|
|
127
|
-
const setErrorGroups = (stateKey, parentPath, groups) => {
|
|
128
|
-
const key = errorGroupStorageKey(stateKey, parentPath);
|
|
129
|
-
errorGroupsByPath.set(key, {
|
|
130
|
-
...errorGroupsByPath.get(key),
|
|
131
|
-
...groups,
|
|
132
|
-
});
|
|
133
|
-
};
|
|
134
186
|
return createPlugin("shape")
|
|
135
187
|
.initialState(() => buildInitialState(box))
|
|
136
|
-
.transformState((params) =>
|
|
188
|
+
.transformState((params) => {
|
|
189
|
+
wireShapeValidationOptions(box, params);
|
|
190
|
+
})
|
|
137
191
|
.onFormUpdate((params) => {
|
|
138
192
|
if (params.options?.logs) {
|
|
139
193
|
console.log("[shape]", params.stateKey, params.path, params.event.activityType);
|
|
140
194
|
}
|
|
141
|
-
validateShapeRefines(box,
|
|
142
|
-
|
|
143
|
-
setErrorGroups: (parentPath, groups) => setErrorGroups(params.stateKey, parentPath, groups),
|
|
144
|
-
});
|
|
145
|
-
})
|
|
146
|
-
.methods(({ object }) => ({
|
|
147
|
-
$errorGroups: object((_ctx, ...fields) => {
|
|
148
|
-
const key = errorGroupStorageKey(_ctx.stateKey, _ctx.path);
|
|
149
|
-
return (errorGroupsByPath.get(key)?.[groupKey(fields)] ?? emptyErrorGroup);
|
|
150
|
-
}),
|
|
151
|
-
}));
|
|
195
|
+
validateShapeRefines(box, params);
|
|
196
|
+
});
|
|
152
197
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cogsbox-shape",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.213",
|
|
4
4
|
"description": "A TypeScript library for creating type-safe database schemas with Zod validation, SQL type definitions, and automatic client/server transformations. Unifies client, server, and database types through a single schema definition, with built-in support for relationships and serialization.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|