cogsbox-shape 0.5.212 → 0.5.214

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.
@@ -37,6 +37,7 @@ type FormUpdateParams = {
37
37
  path: string[];
38
38
  event: {
39
39
  activityType: string;
40
+ details?: Record<string, unknown>;
40
41
  };
41
42
  getState: () => unknown;
42
43
  addZodErrors: (errors: Array<{
@@ -44,12 +45,29 @@ type FormUpdateParams = {
44
45
  message: string;
45
46
  code?: string;
46
47
  }>) => void;
47
- clearZodErrors: (paths: string[][]) => void;
48
+ clearZodErrors?: (paths: string[][]) => void;
49
+ };
50
+ type UpdateParams = {
51
+ stateKey: string;
52
+ update: {
53
+ path: string[];
54
+ updateType: string;
55
+ oldValue: unknown;
56
+ newValue: unknown;
57
+ };
58
+ getState: () => unknown;
59
+ addZodErrors: (errors: Array<{
60
+ path: string[];
61
+ message: string;
62
+ code?: string;
63
+ }>) => void;
64
+ clearZodErrors?: (paths: string[][]) => void;
48
65
  };
49
66
  export declare function wireShapeValidationOptions(box: ShapeSchemaBox, params: TransformStateParams): void;
50
67
  /** Cross-field refine errors only — field rules are handled by state via setOptions. */
51
68
  export declare function validateShapeRefines(box: ShapeSchemaBox, params: FormUpdateParams): void;
69
+ export declare function validateShapeRefinesOnUpdate(box: ShapeSchemaBox, params: UpdateParams): void;
52
70
  export declare function createShapePlugin<const TBox extends ShapeSchemaBox>(box: TBox): import("cogsbox-state").CogsPluginBuilder<"shape", {
53
71
  logs: boolean | undefined;
54
- }, unknown, unknown, never, {}, true, false, true, false, false, true, InferShapeBoxState<TBox>>;
72
+ }, unknown, unknown, never, {}, true, true, true, false, false, true, InferShapeBoxState<TBox>>;
55
73
  export {};
@@ -1,4 +1,4 @@
1
- import { createPluginContext } from "cogsbox-state";
1
+ import { createPluginContext, getGlobalStore } from "cogsbox-state";
2
2
  import { z } from "zod";
3
3
  function pathKey(path) {
4
4
  return path.join("\0");
@@ -14,6 +14,100 @@ function mapZodIssues(issues) {
14
14
  code: issue.code,
15
15
  }));
16
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
+ }
17
111
  function getRelatedFields(entry, field) {
18
112
  const groupIndexes = entry.refineInfo?.fieldToGroup[field];
19
113
  if (!groupIndexes?.length)
@@ -32,14 +126,90 @@ function issueMatchesRelatedFields(issue, relatedFields) {
32
126
  const leaf = String(issue.path.at(-1) ?? "");
33
127
  return relatedFields.has(leaf);
34
128
  }
35
- export function wireShapeValidationOptions(box, params) { }
129
+ function getChangedObjectFields(oldValue, newValue) {
130
+ if (oldValue === null ||
131
+ newValue === null ||
132
+ typeof oldValue !== "object" ||
133
+ typeof newValue !== "object" ||
134
+ Array.isArray(oldValue) ||
135
+ Array.isArray(newValue)) {
136
+ return null;
137
+ }
138
+ const fields = new Set();
139
+ const oldRecord = oldValue;
140
+ const newRecord = newValue;
141
+ for (const key of new Set([...Object.keys(oldRecord), ...Object.keys(newRecord)])) {
142
+ if (!Object.is(oldRecord[key], newRecord[key]))
143
+ fields.add(key);
144
+ }
145
+ return fields;
146
+ }
147
+ function resolveUpdateRefineTarget(entry, updatePath, oldValue, newValue) {
148
+ const changedObjectFields = getChangedObjectFields(oldValue, newValue);
149
+ if (changedObjectFields) {
150
+ const parentPath = updatePath;
151
+ const relatedFields = new Set();
152
+ for (const field of changedObjectFields) {
153
+ const groupFields = getRelatedFields(entry, field);
154
+ if (!groupFields)
155
+ continue;
156
+ for (const related of groupFields)
157
+ relatedFields.add(related);
158
+ }
159
+ if (relatedFields.size === 0)
160
+ return null;
161
+ return {
162
+ relatedFields,
163
+ relatedPaths: [...relatedFields].map((field) => [...parentPath, field]),
164
+ };
165
+ }
166
+ const field = updatePath.at(-1);
167
+ if (!field)
168
+ return null;
169
+ const relatedFields = getRelatedFields(entry, field);
170
+ if (!relatedFields)
171
+ return null;
172
+ return {
173
+ relatedFields,
174
+ relatedPaths: resolveRelatedPaths(updatePath, relatedFields),
175
+ };
176
+ }
177
+ function applyRefineValidation(box, params, target, state) {
178
+ const entry = box[params.stateKey];
179
+ const clientSchema = entry?.validators?.client ?? entry?.schemas.client;
180
+ if (!entry || !clientSchema)
181
+ return;
182
+ const result = clientSchema.safeParse(state);
183
+ if (result.success) {
184
+ clearValidationPaths(params, target.relatedPaths);
185
+ return;
186
+ }
187
+ const issues = result.error.issues.filter((issue) => issueMatchesRelatedFields(issue, target.relatedFields));
188
+ const mapped = mapZodIssues(issues);
189
+ const activeKeys = new Set(mapped.map((entry) => pathKey(entry.path)));
190
+ const stalePaths = target.relatedPaths.filter((targetPath) => !activeKeys.has(pathKey(targetPath)));
191
+ clearValidationPaths(params, stalePaths);
192
+ addValidationIssues(params, mapped);
193
+ }
194
+ export function wireShapeValidationOptions(box, params) {
195
+ const entry = box[params.stateKey];
196
+ if (!entry)
197
+ return;
198
+ params.setOptions({
199
+ validation: {
200
+ zodSchemaV4: entry.validators?.client ?? entry.schemas.client,
201
+ onBlur: "error",
202
+ },
203
+ });
204
+ }
36
205
  /** Cross-field refine errors only — field rules are handled by state via setOptions. */
37
206
  export function validateShapeRefines(box, params) {
38
- if (params.event.activityType !== "blur")
207
+ if (params.event.activityType !== "blur" &&
208
+ params.event.activityType !== "input") {
39
209
  return;
210
+ }
40
211
  const entry = box[params.stateKey];
41
- const clientSchema = entry?.validators?.client ?? entry?.schemas.client;
42
- if (!entry || !clientSchema)
212
+ if (!entry)
43
213
  return;
44
214
  const field = params.path.at(-1);
45
215
  if (!field)
@@ -47,22 +217,21 @@ export function validateShapeRefines(box, params) {
47
217
  const relatedFields = getRelatedFields(entry, field);
48
218
  if (!relatedFields)
49
219
  return;
50
- const relatedPaths = resolveRelatedPaths(params.path, relatedFields);
51
- const result = clientSchema.safeParse(params.getState());
52
- if (result.success) {
53
- params.clearZodErrors(relatedPaths);
220
+ applyRefineValidation(box, params, {
221
+ relatedFields,
222
+ relatedPaths: resolveRelatedPaths(params.path, relatedFields),
223
+ }, getStateForValidation(params));
224
+ }
225
+ export function validateShapeRefinesOnUpdate(box, params) {
226
+ if (params.update.updateType !== "update")
54
227
  return;
55
- }
56
- const issues = result.error.issues.filter((issue) => issueMatchesRelatedFields(issue, relatedFields));
57
- const mapped = mapZodIssues(issues);
58
- const activeKeys = new Set(mapped.map((entry) => pathKey(entry.path)));
59
- const stalePaths = relatedPaths.filter((targetPath) => !activeKeys.has(pathKey(targetPath)));
60
- if (stalePaths.length > 0) {
61
- params.clearZodErrors(stalePaths);
62
- }
63
- if (mapped.length > 0) {
64
- params.addZodErrors(mapped);
65
- }
228
+ const entry = box[params.stateKey];
229
+ if (!entry)
230
+ return;
231
+ const target = resolveUpdateRefineTarget(entry, params.update.path, params.update.oldValue, params.update.newValue);
232
+ if (!target)
233
+ return;
234
+ applyRefineValidation(box, params, target, params.getState());
66
235
  }
67
236
  function buildInitialState(box) {
68
237
  const state = {};
@@ -84,20 +253,15 @@ export function createShapePlugin(box) {
84
253
  return createPlugin("shape")
85
254
  .initialState(() => buildInitialState(box))
86
255
  .transformState((params) => {
87
- const entry = box[params.stateKey];
88
- if (!entry)
89
- return;
90
- params.setOptions({
91
- validation: {
92
- zodSchemaV4: entry.validators?.client ?? entry.schemas.client,
93
- onBlur: "error",
94
- },
95
- });
256
+ wireShapeValidationOptions(box, params);
96
257
  })
97
258
  .onFormUpdate((params) => {
98
259
  if (params.options?.logs) {
99
260
  console.log("[shape]", params.stateKey, params.path, params.event.activityType);
100
261
  }
101
262
  validateShapeRefines(box, params);
263
+ })
264
+ .onUpdate((params) => {
265
+ validateShapeRefinesOnUpdate(box, params);
102
266
  });
103
267
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cogsbox-shape",
3
- "version": "0.5.212",
3
+ "version": "0.5.214",
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",