cogsbox-shape 0.5.186 → 0.5.188

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.
@@ -0,0 +1,307 @@
1
+ import { Kysely, sql } from "kysely";
2
+ import { buildWhereConditions, buildPkConditions } from "./where-builder.js";
3
+ import { RecordNotFoundError } from "./errors.js";
4
+ export class TableDB {
5
+ db;
6
+ meta;
7
+ transforms;
8
+ reconcile;
9
+ constructor(db, meta, transforms, reconcile) {
10
+ this.db = db;
11
+ this.meta = meta;
12
+ this.transforms = transforms;
13
+ this.reconcile = reconcile;
14
+ }
15
+ async findMany(opts) {
16
+ const qb = this.db;
17
+ let query = qb.selectFrom(this.meta.tableName).selectAll();
18
+ if (opts?.where) {
19
+ const conditions = buildWhereConditions(opts.where, this.meta);
20
+ if (conditions.length > 0) {
21
+ query = query.where(sql.join(conditions, sql ` AND `));
22
+ }
23
+ }
24
+ if (opts?.orderBy) {
25
+ for (const [col, dir] of Object.entries(opts.orderBy)) {
26
+ if (dir) {
27
+ const field = this.meta.dbFields.get(col);
28
+ const dbCol = field?.dbName ?? col;
29
+ query = query.orderBy(dbCol, dir);
30
+ }
31
+ }
32
+ }
33
+ const limit = opts?.limit ?? 100;
34
+ query = query.limit(limit);
35
+ if (opts?.offset !== undefined) {
36
+ query = query.offset(opts.offset);
37
+ }
38
+ const rows = (await query.execute());
39
+ return rows.map((r) => this.transforms.parseFromDb(r));
40
+ }
41
+ async findById(id) {
42
+ const pkValues = Array.isArray(id) ? id : [id];
43
+ const pkFields = this.meta.pkFields.length > 0
44
+ ? this.meta.pkFields
45
+ : Array.from(this.meta.dbFields.values()).map((f) => f.dbName);
46
+ const conditions = buildPkConditions(pkValues, pkFields);
47
+ const qb = this.db;
48
+ const rows = await qb
49
+ .selectFrom(this.meta.tableName)
50
+ .selectAll()
51
+ .where(sql.join(conditions, sql ` AND `))
52
+ .limit(1)
53
+ .execute();
54
+ const row = rows[0] ?? null;
55
+ if (!row)
56
+ return null;
57
+ return this.transforms.parseFromDb(row);
58
+ }
59
+ insert(data, ...args) {
60
+ const dbOnlyData = args[0];
61
+ return {
62
+ ids: () => this.insertIds(data, dbOnlyData),
63
+ full: async () => {
64
+ const ids = await this.insertIds(data, dbOnlyData);
65
+ return this.reconcileIds(data, ids);
66
+ },
67
+ };
68
+ }
69
+ async create(data, ...args) {
70
+ const dbOnlyData = args[0];
71
+ return this.insertIds(data, dbOnlyData);
72
+ }
73
+ async insertIds(data, dbOnlyData) {
74
+ const dbData = this.transforms.parseForDb(data);
75
+ const parsedDbOnlyData = this.parseDbOnlyData(dbOnlyData, {
76
+ requireRequired: true,
77
+ });
78
+ const clientPkClientKeys = this.meta.clientPkFields;
79
+ const pkDbNames = new Set(clientPkClientKeys.map((k) => {
80
+ const field = this.meta.dbFields.get(k);
81
+ return field?.dbName ?? k;
82
+ }));
83
+ const insertData = {};
84
+ for (const key of Object.keys(dbData)) {
85
+ if (!pkDbNames.has(key)) {
86
+ insertData[key] = dbData[key];
87
+ }
88
+ }
89
+ Object.assign(insertData, parsedDbOnlyData);
90
+ const qb = this.db;
91
+ const result = await qb
92
+ .insertInto(this.meta.tableName)
93
+ .values(insertData)
94
+ .execute();
95
+ const insertId = result[0]?.insertId;
96
+ if (insertId !== undefined && this.meta.pkFields.length > 0) {
97
+ const dbPkField = this.meta.pkFields[0];
98
+ return { [dbPkField]: Number(insertId) };
99
+ }
100
+ return {};
101
+ }
102
+ update(id, data, dbOnlyData) {
103
+ return {
104
+ ids: () => this.updateIds(id, data, dbOnlyData),
105
+ full: async () => {
106
+ const ids = await this.updateIds(id, data, dbOnlyData);
107
+ const idValue = this.firstPkValue(ids);
108
+ const row = await this.findById(idValue);
109
+ if (!row) {
110
+ throw new RecordNotFoundError(this.meta.tableName, idValue);
111
+ }
112
+ return row;
113
+ },
114
+ };
115
+ }
116
+ async updateIds(id, data, dbOnlyData) {
117
+ const pkValues = Array.isArray(id) ? id : [id];
118
+ const pkFields = this.meta.pkFields.length > 0
119
+ ? this.meta.pkFields
120
+ : Array.from(this.meta.dbFields.values()).map((f) => f.dbName);
121
+ const patchData = data;
122
+ const deriveKeys = this.affectedDbBackedDerives(patchData);
123
+ const missingDeps = this.missingDeriveDependencies(patchData, deriveKeys);
124
+ const fetchedDeps = missingDeps.length > 0
125
+ ? await this.fetchClientFieldsById(pkValues, pkFields, missingDeps)
126
+ : {};
127
+ const parseInput = { ...fetchedDeps, ...patchData };
128
+ const parsedDbData = this.transforms.parsePatchForDb(parseInput);
129
+ const dbData = this.pickDbPatchFields(parsedDbData, [
130
+ ...Object.keys(patchData),
131
+ ...deriveKeys,
132
+ ]);
133
+ Object.assign(dbData, this.parseDbOnlyData(dbOnlyData, { requireRequired: false }));
134
+ const conditions = buildPkConditions(pkValues, pkFields);
135
+ const qb = this.db;
136
+ const result = await qb
137
+ .updateTable(this.meta.tableName)
138
+ .set(dbData)
139
+ .where(sql.join(conditions, sql ` AND `))
140
+ .execute();
141
+ const numUpdated = result[0]?.numUpdatedRows ?? 0n;
142
+ if (Number(numUpdated) === 0) {
143
+ throw new RecordNotFoundError(this.meta.tableName, id);
144
+ }
145
+ const pkResult = {};
146
+ for (let i = 0; i < pkFields.length; i++) {
147
+ pkResult[pkFields[i]] = pkValues[i];
148
+ }
149
+ return pkResult;
150
+ }
151
+ affectedDbBackedDerives(patchData) {
152
+ const patchKeys = new Set(Object.keys(patchData));
153
+ const affected = [];
154
+ for (const [deriveKey, deps] of this.meta.deriveDependencies.entries()) {
155
+ if (!this.meta.dbFields.has(deriveKey))
156
+ continue;
157
+ if (deps.some((dep) => patchKeys.has(dep))) {
158
+ affected.push(deriveKey);
159
+ }
160
+ }
161
+ return affected;
162
+ }
163
+ missingDeriveDependencies(patchData, deriveKeys) {
164
+ const patchKeys = new Set(Object.keys(patchData));
165
+ const missing = new Set();
166
+ for (const deriveKey of deriveKeys) {
167
+ const deps = this.meta.deriveDependencies.get(deriveKey) ?? [];
168
+ for (const dep of deps) {
169
+ if (!patchKeys.has(dep))
170
+ missing.add(dep);
171
+ }
172
+ }
173
+ return Array.from(missing).filter((dep) => this.meta.dbFields.has(dep));
174
+ }
175
+ async fetchClientFieldsById(pkValues, pkFields, clientFields) {
176
+ const dbColumns = clientFields.map((clientKey) => {
177
+ const field = this.meta.dbFields.get(clientKey);
178
+ return field?.dbName ?? clientKey;
179
+ });
180
+ const conditions = buildPkConditions(pkValues, pkFields);
181
+ const qb = this.db;
182
+ const row = (await qb
183
+ .selectFrom(this.meta.tableName)
184
+ .select(dbColumns)
185
+ .where(sql.join(conditions, sql ` AND `))
186
+ .limit(1)
187
+ .executeTakeFirst());
188
+ if (!row) {
189
+ throw new RecordNotFoundError(this.meta.tableName, pkValues);
190
+ }
191
+ const result = {};
192
+ for (const clientKey of clientFields) {
193
+ const field = this.meta.dbFields.get(clientKey);
194
+ const dbName = field?.dbName ?? clientKey;
195
+ const value = row[dbName];
196
+ result[clientKey] = field?.toClient ? field.toClient(value) : value;
197
+ }
198
+ return result;
199
+ }
200
+ pickDbPatchFields(dbData, clientKeys) {
201
+ const picked = {};
202
+ for (const clientKey of clientKeys) {
203
+ const dbName = this.meta.clientToDbName.get(clientKey) ?? clientKey;
204
+ if (dbData[dbName] !== undefined) {
205
+ picked[dbName] = dbData[dbName];
206
+ }
207
+ }
208
+ return picked;
209
+ }
210
+ parseDbOnlyData(dbOnlyData, opts = { requireRequired: false }) {
211
+ if (opts.requireRequired) {
212
+ for (const requiredKey of this.meta.sqlOnlyRequiredClientFields) {
213
+ if (!dbOnlyData || dbOnlyData[requiredKey] === undefined) {
214
+ throw new Error(`Missing required sqlOnly field "${requiredKey}" for "${this.meta.tableName}".`);
215
+ }
216
+ }
217
+ }
218
+ if (!dbOnlyData)
219
+ return {};
220
+ const parsed = {};
221
+ for (const [clientKey, value] of Object.entries(dbOnlyData)) {
222
+ if (!this.meta.sqlOnlyClientFields.has(clientKey)) {
223
+ throw new Error(`Field "${clientKey}" is not a sqlOnly field on "${this.meta.tableName}".`);
224
+ }
225
+ const validator = this.meta.sqlOnlyValidators.get(clientKey);
226
+ const validValue = validator ? validator(value) : value;
227
+ const field = this.meta.dbFields.get(clientKey);
228
+ const dbName = field?.dbName ?? clientKey;
229
+ parsed[dbName] = field?.toDb ? field.toDb(validValue) : validValue;
230
+ }
231
+ return parsed;
232
+ }
233
+ reconcileIds(clientData, ids) {
234
+ if (this.reconcile) {
235
+ return this.reconcile(clientData).withServer(ids);
236
+ }
237
+ return this.reconcileFlatIds(clientData, ids);
238
+ }
239
+ reconcileFlatIds(clientData, ids) {
240
+ if (Array.isArray(clientData)) {
241
+ if (!Array.isArray(ids))
242
+ return clientData;
243
+ return clientData.map((item, index) => this.reconcileFlatIds(item, ids[index]));
244
+ }
245
+ if (typeof clientData !== "object" ||
246
+ clientData === null ||
247
+ typeof ids !== "object" ||
248
+ ids === null) {
249
+ return clientData;
250
+ }
251
+ return {
252
+ ...clientData,
253
+ ...this.mapIdsToClientFields(ids),
254
+ };
255
+ }
256
+ mapIdsToClientFields(ids) {
257
+ const mapped = {};
258
+ for (const [idKey, value] of Object.entries(ids)) {
259
+ const clientKey = this.clientKeyForDbField(idKey);
260
+ const field = this.meta.dbFields.get(clientKey);
261
+ mapped[clientKey] = field?.toClient ? field.toClient(value) : value;
262
+ }
263
+ return mapped;
264
+ }
265
+ clientKeyForDbField(dbField) {
266
+ for (const [clientKey, field] of this.meta.dbFields.entries()) {
267
+ if (field.dbName === dbField)
268
+ return clientKey;
269
+ }
270
+ return dbField;
271
+ }
272
+ firstPkValue(ids) {
273
+ const pkField = this.meta.pkFields[0];
274
+ if (pkField && ids[pkField] !== undefined) {
275
+ return ids[pkField];
276
+ }
277
+ return Object.values(ids)[0];
278
+ }
279
+ async delete(id) {
280
+ const pkValues = Array.isArray(id) ? id : [id];
281
+ const pkFields = this.meta.pkFields.length > 0
282
+ ? this.meta.pkFields
283
+ : Array.from(this.meta.dbFields.values()).map((f) => f.dbName);
284
+ const conditions = buildPkConditions(pkValues, pkFields);
285
+ const qb = this.db;
286
+ const result = await qb
287
+ .deleteFrom(this.meta.tableName)
288
+ .where(sql.join(conditions, sql ` AND `))
289
+ .execute();
290
+ const numDeleted = result[0]?.numDeletedRows ?? 0n;
291
+ return { deleted: Number(numDeleted) > 0 };
292
+ }
293
+ async count(where) {
294
+ const qb = this.db;
295
+ let query = qb
296
+ .selectFrom(this.meta.tableName)
297
+ .select(sql `count(*)`.as("count"));
298
+ if (where) {
299
+ const conditions = buildWhereConditions(where, this.meta);
300
+ if (conditions.length > 0) {
301
+ query = query.where(sql.join(conditions, sql ` AND `));
302
+ }
303
+ }
304
+ const row = (await query.execute());
305
+ return Number(row[0]?.count ?? 0);
306
+ }
307
+ }
@@ -0,0 +1,39 @@
1
+ export type WhereValue<T> = T | {
2
+ contains?: string;
3
+ startsWith?: string;
4
+ endsWith?: string;
5
+ gt?: T;
6
+ gte?: T;
7
+ lt?: T;
8
+ lte?: T;
9
+ in?: T[];
10
+ not?: T | Exclude<WhereValue<T>, T>;
11
+ };
12
+ export type WhereInput<T> = {
13
+ [K in keyof T]?: WhereValue<T[K]>;
14
+ };
15
+ export interface FindManyOpts<T> {
16
+ where?: WhereInput<Partial<T>>;
17
+ orderBy?: {
18
+ [K in keyof T]?: "asc" | "desc";
19
+ };
20
+ limit?: number;
21
+ offset?: number;
22
+ }
23
+ export interface ClientToDbField {
24
+ dbName: string;
25
+ toDb?: (val: any) => any;
26
+ toClient?: (val: any) => any;
27
+ }
28
+ export interface TableMeta {
29
+ tableName: string;
30
+ dbFields: Map<string, ClientToDbField>;
31
+ clientToDbName: Map<string, string>;
32
+ pkFields: string[];
33
+ clientPkFields: string[];
34
+ sqlOnlyFields: Set<string>;
35
+ sqlOnlyClientFields: Set<string>;
36
+ sqlOnlyRequiredClientFields: Set<string>;
37
+ sqlOnlyValidators: Map<string, (val: unknown) => unknown>;
38
+ deriveDependencies: Map<string, string[]>;
39
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import type { TableMeta } from "./types.js";
2
+ import { sql } from "kysely";
3
+ export declare function buildWhereConditions(filter: Record<string, unknown>, meta: TableMeta): ReturnType<typeof sql>[];
4
+ export declare function buildPkConditions(pkValues: unknown[], pkFields: string[]): ReturnType<typeof sql>[];
@@ -0,0 +1,71 @@
1
+ import { sql } from "kysely";
2
+ export function buildWhereConditions(filter, meta) {
3
+ const conditions = [];
4
+ for (const [clientKey, value] of Object.entries(filter)) {
5
+ if (value === undefined)
6
+ continue;
7
+ const field = meta.dbFields.get(clientKey);
8
+ const dbName = field?.dbName ?? clientKey;
9
+ const dbRef = sql.ref(dbName);
10
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
11
+ const op = value;
12
+ if ("contains" in op) {
13
+ const tv = field?.toDb ? field.toDb(op.contains) : op.contains;
14
+ conditions.push(sql `${dbRef} LIKE ${sql.val(`%${String(tv ?? "")}%`)}`);
15
+ }
16
+ else if ("startsWith" in op) {
17
+ const tv = field?.toDb ? field.toDb(op.startsWith) : op.startsWith;
18
+ conditions.push(sql `${dbRef} LIKE ${sql.val(`${String(tv ?? "")}%`)}`);
19
+ }
20
+ else if ("endsWith" in op) {
21
+ const tv = field?.toDb ? field.toDb(op.endsWith) : op.endsWith;
22
+ conditions.push(sql `${dbRef} LIKE ${sql.val(`%${String(tv ?? "")}`)}`);
23
+ }
24
+ else if ("gt" in op) {
25
+ const tv = field?.toDb ? field.toDb(op.gt) : op.gt;
26
+ conditions.push(sql `${dbRef} > ${sql.val(tv)}`);
27
+ }
28
+ else if ("gte" in op) {
29
+ const tv = field?.toDb ? field.toDb(op.gte) : op.gte;
30
+ conditions.push(sql `${dbRef} >= ${sql.val(tv)}`);
31
+ }
32
+ else if ("lt" in op) {
33
+ const tv = field?.toDb ? field.toDb(op.lt) : op.lt;
34
+ conditions.push(sql `${dbRef} < ${sql.val(tv)}`);
35
+ }
36
+ else if ("lte" in op) {
37
+ const tv = field?.toDb ? field.toDb(op.lte) : op.lte;
38
+ conditions.push(sql `${dbRef} <= ${sql.val(tv)}`);
39
+ }
40
+ else if ("in" in op && Array.isArray(op.in)) {
41
+ const items = op.in.map((v) => {
42
+ const tv = field?.toDb ? field.toDb(v) : v;
43
+ return sql.val(tv);
44
+ });
45
+ conditions.push(sql `${dbRef} IN (${sql.join(items, sql `, `)})`);
46
+ }
47
+ else if ("not" in op) {
48
+ const nv = op.not;
49
+ if (nv !== null && typeof nv === "object" && !Array.isArray(nv)) {
50
+ const no = nv;
51
+ if ("contains" in no) {
52
+ const tv = field?.toDb ? field.toDb(no.contains) : no.contains;
53
+ conditions.push(sql `${dbRef} NOT LIKE ${sql.val(`%${String(tv ?? "")}%`)}`);
54
+ }
55
+ }
56
+ else {
57
+ const tv = field?.toDb ? field.toDb(nv) : nv;
58
+ conditions.push(sql `${dbRef} != ${sql.val(tv)}`);
59
+ }
60
+ }
61
+ }
62
+ else {
63
+ const transformed = field?.toDb ? field.toDb(value) : value;
64
+ conditions.push(sql `${dbRef} = ${sql.val(transformed)}`);
65
+ }
66
+ }
67
+ return conditions;
68
+ }
69
+ export function buildPkConditions(pkValues, pkFields) {
70
+ return pkFields.map((f, i) => sql `${sql.ref(f)} = ${sql.val(pkValues[i])}`);
71
+ }
package/dist/schema.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import { z } from "zod";
2
+ export type DeepPartial<T> = T extends object ? {
3
+ [P in keyof T]?: DeepPartial<T[P]>;
4
+ } : T;
2
5
  type CurrentTimestampConfig = {
3
6
  default: "CURRENT_TIMESTAMP";
4
7
  defaultValue: Date;
@@ -234,6 +237,7 @@ export declare function createSchema<T extends {
234
237
  }, R extends Record<string, any> = {}, TActualSchema extends Omit<T & R, typeof SchemaWrapperBrand> = Omit<T & R, typeof SchemaWrapperBrand>>(schema: T, relations?: R): {
235
238
  pk: string[] | null;
236
239
  clientPk: string[] | null;
240
+ deriveDependencies: Record<string, string[]>;
237
241
  isClientRecord: (record: any) => boolean;
238
242
  sqlSchema: z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodSqlSchema">>>;
239
243
  clientInputSchema: z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodClientInputSchema">>>;
@@ -245,6 +249,7 @@ export declare function createSchema<T extends {
245
249
  toClient: (dbObject: Partial<z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodSqlSchema">>>>>) => z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodClientSchema">>>>;
246
250
  toDb: (clientObject: Partial<z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodClientSchema">>>>>) => z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodSqlSchema">>>>;
247
251
  parseForDb: (appData: z.input<z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodValidationSchema">>>>) => z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodSqlSchema">>>>;
252
+ parsePatchForDb: (patchData: Partial<z.input<z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodValidationSchema">>>>>) => Partial<z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodSqlSchema">>>>>;
248
253
  parseFromDb: (dbData: Partial<z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodSqlSchema">>>>>) => z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodClientSchema">>>>;
249
254
  };
250
255
  export type PlaceholderReference = {
@@ -309,6 +314,7 @@ type ResolvedRegistryWithSchemas<S extends Record<string, SchemaWithPlaceholders
309
314
  toClient: (dbObject: any) => any;
310
315
  toDb: (clientObject: any) => any;
311
316
  parseForDb: (appData: any) => any;
317
+ parsePatchForDb: (patchData: any) => any;
312
318
  parseFromDb: (dbData: any) => any;
313
319
  };
314
320
  pk: string[] | null;
@@ -411,8 +417,12 @@ export type DeriveViewResult<TTableName extends keyof TRegistry, TSelection, TRe
411
417
  toClient: TRegistry[TTableName]["transforms"]["toClient"];
412
418
  toDb: TRegistry[TTableName]["transforms"]["toDb"];
413
419
  parseForDb: (appData: z.input<z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "serverSchema">>>) => z.infer<z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "sqlSchema">>>;
420
+ parsePatchForDb: (patchData: Partial<z.input<z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "serverSchema">>>>) => Partial<z.infer<z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "sqlSchema">>>>;
414
421
  parseFromDb: (dbData: Partial<z.infer<z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "sqlSchema">>>>) => z.infer<z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "clientSchema">>>;
415
422
  };
423
+ reconcile: (clientData: z.infer<z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "clientSchema">>> | z.infer<z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "clientSchema">>>[]) => {
424
+ withServer: (serverData: DeepPartial<z.infer<z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "sqlSchema">>>> | DeepPartial<z.infer<z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "sqlSchema">>>>[]) => z.infer<z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "clientSchema">>> | z.infer<z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "clientSchema">>>[];
425
+ };
416
426
  defaults: () => DeriveViewDefaults<TTableName, TSelection, TRegistry>;
417
427
  defaultsDefinition: () => DeriveViewDefaultsDefinition<TTableName, TSelection, TRegistry>;
418
428
  pk: string[] | null;
@@ -454,19 +464,22 @@ type RegistryShape = Record<string, {
454
464
  serverSchema: z.ZodObject<any>;
455
465
  defaultValues: any;
456
466
  stateType: any;
467
+ deriveDependencies: Record<string, string[]>;
457
468
  };
458
469
  transforms: {
459
470
  toClient: (dbObject: any) => any;
460
471
  toDb: (clientObject: any) => any;
461
472
  parseForDb: (appData: any) => any;
473
+ parsePatchForDb: (patchData: any) => any;
462
474
  parseFromDb: (dbData: any) => any;
463
475
  };
464
476
  pk: string[] | null;
465
477
  clientPk: string[] | null;
478
+ deriveDependencies: Record<string, string[]>;
466
479
  isClientRecord: (record: any) => boolean;
467
480
  generateDefaults: () => any;
468
481
  }>;
469
- type CreateSchemaBoxReturn<S extends Record<string, SchemaWithPlaceholders>, R extends ResolutionMap<S>, Resolved extends RegistryShape = ResolvedRegistryWithSchemas<S, R> extends RegistryShape ? ResolvedRegistryWithSchemas<S, R> : RegistryShape> = {
482
+ type CreateSchemaBoxReturn<S extends Record<string, SchemaWithPlaceholders>, R extends ResolutionMap<S>, Resolved extends Record<string, any> = ResolvedRegistryWithSchemas<S, R>> = {
470
483
  [K in keyof Resolved]: {
471
484
  definition: Resolved[K]["rawSchema"];
472
485
  schemaKey: K;
@@ -480,6 +493,7 @@ type CreateSchemaBoxReturn<S extends Record<string, SchemaWithPlaceholders>, R e
480
493
  toClient: (dbData: z.infer<Resolved[K]["zodSchemas"]["sqlSchema"]>) => z.infer<Resolved[K]["zodSchemas"]["clientSchema"]>;
481
494
  toDb: (clientData: z.infer<Resolved[K]["zodSchemas"]["clientSchema"]>) => z.infer<Resolved[K]["zodSchemas"]["sqlSchema"]>;
482
495
  parseForDb: (appData: z.input<Resolved[K]["zodSchemas"]["serverSchema"]>) => z.infer<Resolved[K]["zodSchemas"]["sqlSchema"]>;
496
+ parsePatchForDb: (patchData: Partial<z.input<Resolved[K]["zodSchemas"]["serverSchema"]>>) => Partial<z.infer<Resolved[K]["zodSchemas"]["sqlSchema"]>>;
483
497
  parseFromDb: (dbData: Partial<z.infer<Resolved[K]["zodSchemas"]["sqlSchema"]>>) => z.infer<Resolved[K]["zodSchemas"]["clientSchema"]>;
484
498
  };
485
499
  defaults: Resolved[K]["zodSchemas"]["defaultValues"];
package/dist/schema.js CHANGED
@@ -638,9 +638,30 @@ export function createSchema(schema, relations) {
638
638
  const finalClientInputSchema = z.object(clientInputFields);
639
639
  const finalClientSchema = z.object(clientFields);
640
640
  const finalValidationSchema = z.object(serverFields);
641
+ const deriveDependencies = {};
642
+ if (derives) {
643
+ const trackingSeed = { ...defaultValues };
644
+ for (const key in derives) {
645
+ const accessed = new Set();
646
+ const trackingRow = new Proxy(trackingSeed, {
647
+ get(target, prop, receiver) {
648
+ if (typeof prop === "string" && prop !== key) {
649
+ accessed.add(prop);
650
+ }
651
+ return Reflect.get(target, prop, receiver);
652
+ },
653
+ });
654
+ try {
655
+ derives[key](trackingRow);
656
+ }
657
+ catch (e) { }
658
+ deriveDependencies[key] = Array.from(accessed);
659
+ }
660
+ }
641
661
  return {
642
662
  pk: pkKeys.length ? pkKeys : null,
643
663
  clientPk: clientPkKeys.length ? clientPkKeys : null,
664
+ deriveDependencies,
644
665
  isClientRecord,
645
666
  sqlSchema: finalSqlSchema,
646
667
  clientInputSchema: finalClientInputSchema,
@@ -655,6 +676,10 @@ export function createSchema(schema, relations) {
655
676
  const validData = finalValidationSchema.parse(appData);
656
677
  return toDb(validData);
657
678
  },
679
+ parsePatchForDb: (patchData) => {
680
+ const validPatch = finalValidationSchema.partial().parse(patchData);
681
+ return toDb(validPatch);
682
+ },
658
683
  parseFromDb: (dbData) => {
659
684
  const parsed = finalSqlSchema.parse(dbData);
660
685
  return toClient(parsed);
@@ -798,10 +823,12 @@ export function createSchemaBox(schemas, resolutions) {
798
823
  toClient: zodSchemas.toClient,
799
824
  toDb: zodSchemas.toDb,
800
825
  parseForDb: zodSchemas.parseForDb,
826
+ parsePatchForDb: zodSchemas.parsePatchForDb,
801
827
  parseFromDb: zodSchemas.parseFromDb,
802
828
  },
803
829
  pk: zodSchemas.pk,
804
830
  clientPk: zodSchemas.clientPk,
831
+ deriveDependencies: zodSchemas.deriveDependencies,
805
832
  isClientRecord: zodSchemas.isClientRecord,
806
833
  generateDefaults: zodSchemas.generateDefaults,
807
834
  };
@@ -877,6 +904,7 @@ export function createSchemaBox(schemas, resolutions) {
877
904
  toClient: entry.transforms.toClient,
878
905
  toDb: entry.transforms.toDb,
879
906
  parseForDb: entry.transforms.parseForDb,
907
+ parsePatchForDb: entry.transforms.parsePatchForDb,
880
908
  parseFromDb: entry.transforms.parseFromDb,
881
909
  },
882
910
  defaults: entry.generateDefaults(),
@@ -885,6 +913,7 @@ export function createSchemaBox(schemas, resolutions) {
885
913
  generateDefaults: entry.generateDefaults,
886
914
  pk: entry.pk,
887
915
  clientPk: entry.clientPk,
916
+ deriveDependencies: entry.deriveDependencies,
888
917
  isClientRecord: entry.isClientRecord,
889
918
  nav: createNavProxy(tableName, finalRegistry),
890
919
  createView: (selection) => {
@@ -946,6 +975,64 @@ export function createSchemaBox(schemas, resolutions) {
946
975
  };
947
976
  const viewToClient = (dbData) => deepToClient(dbData, selection, tableName);
948
977
  const viewToDb = (clientData) => deepToDb(clientData, selection, tableName);
978
+ const reconcile = (clientData) => {
979
+ return {
980
+ withServer: (serverData) => {
981
+ const parsedServerData = viewToClient(serverData);
982
+ const mergeTrees = (cNode, sNode, tableKey, sel) => {
983
+ if (sNode === undefined || sNode === null)
984
+ return cNode;
985
+ if (cNode === undefined || cNode === null)
986
+ return sNode;
987
+ const regEntry = finalRegistry[tableKey];
988
+ const clientPkField = regEntry.clientPk?.[0] || regEntry.pk?.[0];
989
+ const dbPkField = regEntry.pk?.[0] || clientPkField;
990
+ if (Array.isArray(cNode)) {
991
+ if (!Array.isArray(sNode))
992
+ return cNode;
993
+ return cNode.map((cItem, index) => {
994
+ let sItem = undefined;
995
+ if (clientPkField && cItem[clientPkField] !== undefined) {
996
+ sItem = sNode.find((s) => s[clientPkField] === cItem[clientPkField]);
997
+ }
998
+ if (!sItem && dbPkField && cItem[dbPkField] !== undefined) {
999
+ sItem = sNode.find((s) => s[dbPkField] === cItem[dbPkField]);
1000
+ }
1001
+ if (!sItem && sNode[index]) {
1002
+ sItem = sNode[index];
1003
+ }
1004
+ return mergeTrees(cItem, sItem, tableKey, sel);
1005
+ });
1006
+ }
1007
+ if (typeof cNode === "object" && typeof sNode === "object") {
1008
+ const merged = { ...cNode };
1009
+ for (const key in sNode) {
1010
+ const selValue = typeof sel === "object" ? sel[key] : undefined;
1011
+ const relField = regEntry.rawSchema[key];
1012
+ const isRelation = !!(selValue && relField?.config?.sql?.schema);
1013
+ if (isRelation) {
1014
+ const nextTableKey = tableNameToRegistryKeyMap[relField.config.sql.schema()._tableName];
1015
+ merged[key] = mergeTrees(cNode[key], sNode[key], nextTableKey, selValue);
1016
+ }
1017
+ else {
1018
+ merged[key] = sNode[key];
1019
+ }
1020
+ }
1021
+ if (clientPkField &&
1022
+ dbPkField &&
1023
+ clientPkField !== dbPkField &&
1024
+ merged[dbPkField] !== undefined &&
1025
+ merged[dbPkField] !== null) {
1026
+ delete merged[clientPkField];
1027
+ }
1028
+ return merged;
1029
+ }
1030
+ return sNode !== undefined ? sNode : cNode;
1031
+ };
1032
+ return mergeTrees(clientData, parsedServerData, tableName, selection);
1033
+ },
1034
+ };
1035
+ };
949
1036
  return {
950
1037
  definition: entry.rawSchema,
951
1038
  schemaKey: tableName,
@@ -958,16 +1045,19 @@ export function createSchemaBox(schemas, resolutions) {
958
1045
  toClient: viewToClient,
959
1046
  toDb: viewToDb,
960
1047
  parseForDb: (appData) => {
961
- // FIX: Now correctly validates against the view's server schema first
962
1048
  const validData = view.server.parse(appData);
963
1049
  return viewToDb(validData);
964
1050
  },
1051
+ parsePatchForDb: (patchData) => {
1052
+ const validPatch = view.server.partial().parse(patchData);
1053
+ return viewToDb(validPatch);
1054
+ },
965
1055
  parseFromDb: (dbData) => {
966
- // FIX: Now correctly validates against the view's client schema after mapping
967
1056
  const mappedData = view.sql.parse(dbData);
968
1057
  return viewToClient(mappedData);
969
1058
  },
970
1059
  },
1060
+ reconcile,
971
1061
  defaults: () => computeViewDefaults(tableName, selection, finalRegistry, tableNameToRegistryKeyMap),
972
1062
  defaultsDefinition: () => computeViewDefaultsDefinition(tableName, selection, finalRegistry, tableNameToRegistryKeyMap),
973
1063
  pk: entry.zodSchemas.pk,