cogsbox-shape 0.5.173 → 0.5.175

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/README.md CHANGED
@@ -36,13 +36,13 @@ Define a field by chaining methods. Each step is optional — use only what you
36
36
  s.sql() → .initialState() → .client() → .server() → .transform()
37
37
  ```
38
38
 
39
- | Method | Purpose |
40
- | ---------------------------------- | -------------------------------------------------------------- |
41
- | `s.sql({ type })` | Database column type. The starting point for every field. |
42
- | `.initialState({ value, schema })` | Default value and type for new records created on the client. |
43
- | `.client(fn)` | Client-side validation. Overrides the client type if needed. |
44
- | `.server(fn)` | Server-side validation. Stricter rules before database writes. |
45
- | `.transform({ toClient, toDb })` | Converts between database and client representations. |
39
+ | Method | Purpose |
40
+ | -------------------------------------------- | -------------------------------------------------------------- |
41
+ | `s.sql({ type })` | Database column type. The starting point for every field. |
42
+ | `.initialState({ value, schema, clientPk })` | Default value and type for new records created on the client. |
43
+ | `.client(fn)` | Client-side validation. Overrides the client type if needed. |
44
+ | `.server(fn)` | Server-side validation. Stricter rules before database writes. |
45
+ | `.transform({ toClient, toDb })` | Converts between database and client representations. |
46
46
 
47
47
  ### 1. SQL — Define Your Database Schema
48
48
 
@@ -72,6 +72,7 @@ const userSchema = schema({
72
72
  id: s.sql({ type: "int", pk: true }).initialState({
73
73
  value: () => crypto.randomUUID(),
74
74
  schema: z.string(),
75
+ clientPk: true, // Explicitly marks this as a client PK, auto-creating a union type
75
76
  }),
76
77
  // Client type becomes: string | number (union of SQL + initialState)
77
78
  // Default value: a generated UUID string
@@ -175,6 +176,7 @@ const contactSchema = schema({
175
176
  id: s.sql({ type: "int", pk: true }).initialState({
176
177
  value: () => `new_${crypto.randomUUID().slice(0, 8)}`,
177
178
  schema: z.string(),
179
+ clientPk: true,
178
180
  }),
179
181
  name: s.sql({ type: "varchar" }).server(({ sql }) => sql.min(2)),
180
182
  email: s.sql({ type: "varchar" }).server(({ sql }) => sql.email()),
@@ -189,23 +191,24 @@ const contactSchema = schema({
189
191
 
190
192
  const {
191
193
  clientSchema, // Zod schema for client-side validation
192
- validationSchema, // Zod schema with .server() rules
194
+ serverSchema, // Zod schema with .server() rules
193
195
  sqlSchema, // Zod schema matching DB column types
194
196
  defaultValues, // Typed defaults matching clientSchema
195
- toClient, // DB row client object
196
- toDb, // Client object DB row
197
+ generateDefaults, // Generates fresh defaults (executes randomizers/dates)
198
+ parseForDb, // Validates client app data & transforms to DB format
199
+ parseFromDb, // Transforms DB data & validates to Client format
197
200
  } = createSchema(contactSchema);
198
201
 
199
202
  // Use in a form
200
- const [data, setData] = useState(defaultValues);
203
+ const [data, setData] = useState(generateDefaults());
201
204
  // { id: "new_a1b2c3d4", name: "", email: "", isArchived: false }
202
205
 
203
- // Validate
204
- const result = validationSchema.safeParse(data);
206
+ // Validate explicitly
207
+ const result = serverSchema.safeParse(data);
205
208
 
206
- // Transform (on the server)
207
- const dbRow = toDb(data); // { isArchived: 0, ... }
208
- const clientObj = toClient(row); // { isArchived: true, ... }
209
+ // Or handle validation & transformation in a single step!
210
+ const safeDbRow = parseForDb(data);
211
+ // Validates using serverSchema, outputs { isArchived: 0, ... }
209
212
  ```
210
213
 
211
214
  ## Relationships and Views
@@ -258,7 +261,7 @@ type UserClient = z.infer<typeof schemas.client>;
258
261
 
259
262
  ### 4. Create Views to Include Relations
260
263
 
261
- Explicitly select which relations to include:
264
+ Explicitly select which relations to include. The resulting views automatically apply nested transforms and deep schema validations.
262
265
 
263
266
  ```typescript
264
267
  const userWithPosts = box.users.createView({
@@ -273,92 +276,5 @@ type UserWithPosts = z.infer<typeof userWithPosts.schemas.client>;
273
276
  // }
274
277
 
275
278
  const defaults = userWithPosts.defaults;
276
- // { id: 0, name: '', posts: [] }
279
+ // { id: 0, name: '', posts:
277
280
  ```
278
-
279
- ## Complete Example
280
-
281
- ```typescript
282
- import { s, schema, createSchemaBox } from "cogsbox-shape";
283
- import { z } from "zod";
284
-
285
- const users = schema({
286
- _tableName: "users",
287
- id: s.sql({ type: "int", pk: true }).initialState({
288
- value: () => `user_${crypto.randomUUID()}`,
289
- schema: z.string(),
290
- }),
291
- email: s
292
- .sql({ type: "varchar", length: 255 })
293
- .server(({ sql }) => sql.email()),
294
- isActive: s
295
- .sql({ type: "int" })
296
- .client(() => z.boolean())
297
- .transform({
298
- toClient: (val) => val === 1,
299
- toDb: (val) => (val ? 1 : 0),
300
- }),
301
- posts: s.hasMany({ count: 0 }),
302
- });
303
-
304
- const posts = schema({
305
- _tableName: "posts",
306
- id: s.sql({ type: "int", pk: true }),
307
- title: s.sql({ type: "varchar" }),
308
- authorId: s.reference(() => users.id),
309
- });
310
-
311
- const box = createSchemaBox({ users, posts }, (s) => ({
312
- users: {
313
- posts: { fromKey: "id", toKey: s.posts.authorId },
314
- },
315
- }));
316
-
317
- // Base schema (no relations)
318
- const { schemas, defaults, transforms } = box.users;
319
-
320
- // View with relations
321
- const userView = box.users.createView({ posts: true });
322
- const { client, server } = userView.schemas;
323
-
324
- // Type inference
325
- type User = z.infer<typeof client>;
326
-
327
- // Validation
328
- const result = server.safeParse(formData);
329
-
330
- // Transformation (server-side)
331
- const dbRow = transforms.toDb(validated);
332
- const apiResponse = transforms.toClient(dbRow);
333
- ```
334
-
335
- ## API Reference
336
-
337
- ### Schema Definition
338
-
339
- - `s.sql(config)` — Define SQL column type. The starting point for every field.
340
- - `.initialState({ value, schema })` — Set default value for new records. Optionally provide a Zod schema to widen or narrow the client type.
341
- - `.client(schema | fn)` — Define the client-side Zod schema for validation. Use when the client type differs from SQL.
342
- - `.server(schema | fn)` — Add server-side validation rules. Receives the previous schema in the chain for refinement.
343
- - `.transform({ toClient, toDb })` — Define bidirectional conversion between SQL and client types. Runs on the server.
344
-
345
- ### Relationships
346
-
347
- - `s.reference(getter)` — Create a foreign key reference to another schema's field.
348
- - `s.hasMany(config)` — Declare a one-to-many relationship placeholder.
349
- - `s.hasOne(config)` — Declare a one-to-one relationship placeholder.
350
- - `s.manyToMany(config)` — Declare a many-to-many relationship placeholder.
351
-
352
- ### Schema Processing
353
-
354
- - `schema(definition)` — Create a schema definition from field declarations.
355
- - `createSchema(schema)` — Process a single schema. Returns `clientSchema`, `validationSchema`, `sqlSchema`, `defaultValues`, `toClient`, `toDb`.
356
- - `createSchemaBox(schemas, resolver)` — Process multiple schemas with relationships. Returns a registry with:
357
- - `.schemas` — Base Zod schemas (excludes relations).
358
- - `.defaults` — Typed default values.
359
- - `.transforms` — `toClient` and `toDb` functions.
360
- - `.createView(selection)` — Create a view including selected relations.
361
-
362
- ## License
363
-
364
- MIT
package/dist/schema.d.ts CHANGED
@@ -47,21 +47,20 @@ export interface IBuilderMethods<T extends DbConfig, TSql extends z.ZodTypeAny,
47
47
  uuid: () => string;
48
48
  }) => TValue);
49
49
  schema?: never;
50
- clientPk?: boolean;
50
+ clientPk?: boolean | ((val: any) => boolean);
51
51
  }): Prettify<Builder<"new", T, TSql, ZodTypeFromPrimitive<TValue extends () => infer R ? R : TValue>, TValue extends () => infer R ? R : TValue, CollapsedUnion<TSql, ZodTypeFromPrimitive<TValue extends () => infer R ? R : TValue>>, CollapsedUnion<TSql, ZodTypeFromPrimitive<TValue extends () => infer R ? R : TValue>>>>;
52
52
  initialState<const TSchema extends z.ZodTypeAny>(options: {
53
53
  value?: never;
54
54
  schema: TSchema;
55
- clientPk?: boolean;
55
+ clientPk?: boolean | ((val: any) => boolean);
56
56
  }): Prettify<Builder<"new", T, TSql, TSchema, z.infer<TSchema>, CollapsedUnion<TSql, TSchema>, CollapsedUnion<TSql, TSchema>>>;
57
57
  initialState<const TValue, const TSchema extends z.ZodTypeAny>(options: {
58
58
  value: TValue | ((tools: {
59
59
  uuid: () => string;
60
60
  }) => TValue);
61
61
  schema: TSchema | ((base: ZodTypeFromPrimitive<TValue extends () => infer R ? R : TValue>) => TSchema);
62
- clientPk?: boolean;
63
- }): Prettify<Builder<"new", T, TSql, TSchema, z.infer<TSchema>, // <-- THIS IS THE FIX: Use schema's type, not literal value
64
- CollapsedUnion<TSql, TSchema>, CollapsedUnion<TSql, TSchema>>>;
62
+ clientPk?: boolean | ((val: any) => boolean);
63
+ }): Prettify<Builder<"new", T, TSql, TSchema, z.infer<TSchema>, CollapsedUnion<TSql, TSchema>, CollapsedUnion<TSql, TSchema>>>;
65
64
  reference: <TRefSchema extends {
66
65
  _tableName: string;
67
66
  }>(fieldGetter: () => any) => Builder<"sql", T & {
@@ -174,9 +173,7 @@ type PickPrimaryKeys<T extends ShapeSchema> = {
174
173
  };
175
174
  type SchemaBuilder<T extends ShapeSchema> = Prettify<EnrichFields<T>> & {
176
175
  __primaryKeySQL?: string;
177
- __isClientChecker?: (record: any) => boolean;
178
176
  primaryKeySQL: (definer: (pkFields: PickPrimaryKeys<T>) => string) => SchemaBuilder<T>;
179
- isClient: (checker: (record: Prettify<z.infer<z.ZodObject<DeriveSchemaByKey<T, "zodSqlSchema">>> | z.infer<z.ZodObject<DeriveSchemaByKey<T, "zodClientSchema">>>>) => boolean) => SchemaBuilder<T>;
180
177
  };
181
178
  export declare function schema<T extends string, U extends ShapeSchema<T>>(schema: U): SchemaBuilder<U>;
182
179
  export type RelationType = "hasMany" | "hasOne" | "manyToMany";
@@ -215,7 +212,7 @@ export declare function createSchema<T extends {
215
212
  }, R extends Record<string, any> = {}, TActualSchema extends Omit<T & R, typeof SchemaWrapperBrand> = Omit<T & R, typeof SchemaWrapperBrand>>(schema: T, relations?: R): {
216
213
  pk: string[] | null;
217
214
  clientPk: string[] | null;
218
- isClientRecord: ((record: any) => boolean) | undefined;
215
+ isClientRecord: (record: any) => boolean;
219
216
  sqlSchema: z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodSqlSchema">>>;
220
217
  clientSchema: z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodClientSchema">>>;
221
218
  serverSchema: z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodValidationSchema">>>;
@@ -246,15 +243,7 @@ type KnownKeys<T> = keyof {
246
243
  type ResolutionMap<S extends Record<string, SchemaWithPlaceholders>> = {
247
244
  [TableName in keyof S]?: {
248
245
  [FieldName in keyof S[TableName] as S[TableName][FieldName] extends PlaceholderReference | PlaceholderRelation<any> ? FieldName : never]?: S[TableName][FieldName] extends PlaceholderRelation<any> ? {
249
- /**
250
- * The key on the current table (`users`) to join from.
251
- * Autocompletes with: 'id', 'name', etc.
252
- */
253
246
  fromKey: KnownKeys<S[TableName]>;
254
- /**
255
- * The target key on the related table.
256
- * Must be a field reference from the proxy, e.g., `s.pets.userId`
257
- */
258
247
  toKey: {
259
248
  __meta: any;
260
249
  __parentTableType: any;
@@ -297,7 +286,7 @@ type ResolvedRegistryWithSchemas<S extends Record<string, SchemaWithPlaceholders
297
286
  parseFromDb: (dbData: any) => any;
298
287
  pk: string[] | null;
299
288
  clientPk: string[] | null;
300
- isClientRecord: ((record: any) => boolean) | undefined;
289
+ isClientRecord: (record: any) => boolean;
301
290
  generateDefaults: () => Prettify<DeriveDefaults<ResolveSchema<S[K], K extends keyof R ? (R[K] extends object ? R[K] : {}) : {}>>>;
302
291
  };
303
292
  };
@@ -414,7 +403,7 @@ type RegistryShape = Record<string, {
414
403
  parseFromDb: (dbData: any) => any;
415
404
  pk: string[] | null;
416
405
  clientPk: string[] | null;
417
- isClientRecord: ((record: any) => boolean) | undefined;
406
+ isClientRecord: (record: any) => boolean;
418
407
  generateDefaults: () => any;
419
408
  };
420
409
  }>;
@@ -438,7 +427,7 @@ type CreateSchemaBoxReturn<S extends Record<string, SchemaWithPlaceholders>, R e
438
427
  generateDefaults: () => Resolved[K]["zodSchemas"]["defaultValues"];
439
428
  pk: string[] | null;
440
429
  clientPk: string[] | null;
441
- isClientRecord: ((record: any) => boolean) | undefined;
430
+ isClientRecord: (record: any) => boolean;
442
431
  nav: NavigationProxy<K & string, Resolved>;
443
432
  RelationSelection: NavigationToSelection<NavigationProxy<K & string, Resolved>>;
444
433
  createView: <const TSelection extends NavigationToSelection<NavigationProxy<K & string, Resolved>>>(selection: TSelection) => DeriveViewResult<K & string, TSelection, Resolved>;
@@ -476,7 +465,7 @@ type GetDbKey<K, Field> = Field extends Reference<infer TGetter> ? ReturnType<TG
476
465
  };
477
466
  } ? string extends F ? K : F : K;
478
467
  type DeriveSchemaByKey<T, Key extends "zodSqlSchema" | "zodClientSchema" | "zodValidationSchema", Depth extends any[] = []> = Depth["length"] extends 10 ? any : {
479
- [K in keyof T as K extends "_tableName" | typeof SchemaWrapperBrand | "__primaryKeySQL" | "__isClientChecker" | "primaryKeySQL" | "isClient" ? never : K extends keyof T ? T[K] extends Reference<any> ? Key extends "zodSqlSchema" ? GetDbKey<K, T[K]> : K : T[K] extends {
468
+ [K in keyof T as K extends "_tableName" | typeof SchemaWrapperBrand | "__primaryKeySQL" | "primaryKeySQL" ? never : K extends keyof T ? T[K] extends Reference<any> ? Key extends "zodSqlSchema" ? GetDbKey<K, T[K]> : K : T[K] extends {
480
469
  config: {
481
470
  sql: {
482
471
  type: "hasMany" | "manyToMany" | "hasOne" | "belongsTo";
@@ -493,7 +482,7 @@ type DeriveSchemaByKey<T, Key extends "zodSqlSchema" | "zodClientSchema" | "zodV
493
482
  } ? ZodSchema : never;
494
483
  };
495
484
  type DeriveDefaults<T, Depth extends any[] = []> = Prettify<Depth["length"] extends 10 ? any : {
496
- [K in keyof T as K extends "_tableName" | typeof SchemaWrapperBrand | "__primaryKeySQL" | "__isClientChecker" | "primaryKeySQL" | "isClient" ? never : K extends keyof T ? T[K] extends Reference<any> ? K : T[K] extends {
485
+ [K in keyof T as K extends "_tableName" | typeof SchemaWrapperBrand | "__primaryKeySQL" | "primaryKeySQL" ? never : K extends keyof T ? T[K] extends Reference<any> ? K : T[K] extends {
497
486
  config: {
498
487
  sql: {
499
488
  type: "hasMany" | "manyToMany" | "hasOne" | "belongsTo";
@@ -513,7 +502,7 @@ type DeriveDefaults<T, Depth extends any[] = []> = Prettify<Depth["length"] exte
513
502
  } ? TNew extends TSql ? z.infer<TClient> : D extends () => infer R ? R : D : never;
514
503
  }>;
515
504
  type DeriveStateType<T, Depth extends any[] = []> = Prettify<Depth["length"] extends 10 ? any : {
516
- [K in keyof T as K extends "_tableName" | typeof SchemaWrapperBrand | "__primaryKeySQL" | "__isClientChecker" | "primaryKeySQL" | "isClient" ? never : K extends keyof T ? T[K] extends Reference<any> ? K : T[K] extends {
505
+ [K in keyof T as K extends "_tableName" | typeof SchemaWrapperBrand | "__primaryKeySQL" | "primaryKeySQL" ? never : K extends keyof T ? T[K] extends Reference<any> ? K : T[K] extends {
517
506
  config: {
518
507
  sql: {
519
508
  type: "hasMany" | "manyToMany" | "hasOne" | "belongsTo";
package/dist/schema.js CHANGED
@@ -103,7 +103,6 @@ export const s = {
103
103
  });
104
104
  },
105
105
  };
106
- // PASTE THIS ENTIRE FUNCTION OVER YOUR EXISTING createBuilder
107
106
  function createBuilder(config) {
108
107
  const completedStages = config.completedStages || new Set([config.stage]);
109
108
  const builderObject = {
@@ -115,8 +114,8 @@ function createBuilder(config) {
115
114
  inferDefaultFromZod(config.clientZod, config.sqlConfig),
116
115
  zodClientSchema: config.clientZod,
117
116
  zodValidationSchema: config.validationZod,
118
- clientTransform: config.clientTransform, // <-- FIX: Make sure transform is passed through
119
- validationTransform: config.validationTransform, // <-- FIX: Make sure transform is passed through
117
+ clientTransform: config.clientTransform,
118
+ validationTransform: config.validationTransform,
120
119
  },
121
120
  initialState: (options) => {
122
121
  if (completedStages.has("new")) {
@@ -169,11 +168,10 @@ function createBuilder(config) {
169
168
  const newCompletedStages = new Set(completedStages);
170
169
  newCompletedStages.add("new");
171
170
  const newConfig = { ...config.sqlConfig };
172
- if (clientPk) {
173
- newConfig.isClientPk = true;
171
+ if (clientPk !== undefined) {
172
+ // Store the boolean OR the function directly into the config
173
+ newConfig.isClientPk = clientPk;
174
174
  }
175
- // When clientPk is true, ALWAYS union the SQL type with the client type
176
- // because records can be either DB-sourced (number) or client-created (string)
177
175
  let clientAndServerSchema;
178
176
  if (clientPk) {
179
177
  // Always union for clientPk fields
@@ -221,11 +219,8 @@ function createBuilder(config) {
221
219
  ...config,
222
220
  stage: "client",
223
221
  completedStages: newCompletedStages,
224
- // Store the transform function to be used later
225
222
  clientTransform: (baseSchema) => {
226
223
  if (isFunction(assert)) {
227
- // We use `as any` here to resolve the complex generic type error.
228
- // It's safe because we know the baseSchema will have the necessary Zod methods.
229
224
  return assert({
230
225
  sql: baseSchema,
231
226
  initialState: config.newZod,
@@ -235,7 +230,6 @@ function createBuilder(config) {
235
230
  },
236
231
  });
237
232
  }
238
- // This is the original logic for non-relation fields
239
233
  const clientSchema = isFunction(assert)
240
234
  ? assert({ sql: config.sqlZod, initialState: config.newZod })
241
235
  : assert;
@@ -247,9 +241,7 @@ function createBuilder(config) {
247
241
  completedStages: newCompletedStages,
248
242
  });
249
243
  },
250
- server: (
251
- // ... this validation function remains unchanged ...
252
- assert) => {
244
+ server: (assert) => {
253
245
  if (completedStages.has("server")) {
254
246
  throw new Error("validation() can only be called once in the chain");
255
247
  }
@@ -305,13 +297,9 @@ export function schema(schema) {
305
297
  }
306
298
  }
307
299
  enrichedSchema[SchemaWrapperBrand] = true;
308
- // Add private properties
309
300
  enrichedSchema.__primaryKeySQL = undefined;
310
- enrichedSchema.__isClientChecker = undefined;
311
- // Add methods directly
312
301
  enrichedSchema.primaryKeySQL = function (definer) {
313
302
  const pkFieldsOnly = {};
314
- // Find all PK fields
315
303
  for (const key in schema) {
316
304
  const field = schema[key];
317
305
  if (field &&
@@ -323,29 +311,21 @@ export function schema(schema) {
323
311
  enrichedSchema.__primaryKeySQL = definer(pkFieldsOnly);
324
312
  return enrichedSchema;
325
313
  };
326
- enrichedSchema.isClient = function (checker) {
327
- enrichedSchema.__isClientChecker = checker;
328
- return enrichedSchema;
329
- };
330
314
  return enrichedSchema;
331
315
  }
332
316
  function inferDefaultFromZod(zodType, sqlConfig) {
333
- // --- START OF FIX ---
334
- // If the database is responsible for the default, the client shouldn't generate a value.
335
317
  if (sqlConfig &&
336
318
  "default" in sqlConfig &&
337
319
  sqlConfig.default === "CURRENT_TIMESTAMP") {
338
320
  return undefined;
339
321
  }
340
- // --- END OF FIX ---
341
322
  if (sqlConfig && typeof sqlConfig === "object" && "type" in sqlConfig) {
342
323
  if ("default" in sqlConfig && sqlConfig.default !== undefined) {
343
- // This part now runs only if default is not CURRENT_TIMESTAMP
344
324
  return sqlConfig.default;
345
325
  }
346
326
  if (typeof sqlConfig.type === "string" &&
347
327
  ["hasMany", "hasOne", "belongsTo", "manyToMany"].includes(sqlConfig.type)) {
348
- // ... (rest of the function is unchanged)
328
+ // ...
349
329
  }
350
330
  const sqlTypeConfig = sqlConfig;
351
331
  if (sqlTypeConfig.type && !sqlTypeConfig.nullable) {
@@ -361,7 +341,7 @@ function inferDefaultFromZod(zodType, sqlConfig) {
361
341
  return false;
362
342
  case "date":
363
343
  case "datetime":
364
- case "timestamp": // Added timestamp here for completeness
344
+ case "timestamp":
365
345
  return new Date();
366
346
  }
367
347
  }
@@ -381,7 +361,6 @@ function inferDefaultFromZod(zodType, sqlConfig) {
381
361
  }
382
362
  return undefined;
383
363
  }
384
- // Helper to check if something is a reference
385
364
  function isReference(value) {
386
365
  return value && typeof value === "object" && value.__type === "reference";
387
366
  }
@@ -392,7 +371,6 @@ export function createSchema(schema, relations) {
392
371
  const defaultValues = {};
393
372
  const defaultGenerators = {};
394
373
  const fieldTransforms = {};
395
- // NEW: Track the runtime mappings
396
374
  const clientToDbKeys = {};
397
375
  const dbToClientKeys = {};
398
376
  const fullSchema = { ...schema, ...(relations || {}) };
@@ -403,7 +381,6 @@ export function createSchema(schema, relations) {
403
381
  if (key === "_tableName" ||
404
382
  key.startsWith("__") ||
405
383
  key === String(SchemaWrapperBrand) ||
406
- key === "isClient" ||
407
384
  key === "primaryKeySQL" ||
408
385
  typeof value === "function")
409
386
  continue;
@@ -412,11 +389,9 @@ export function createSchema(schema, relations) {
412
389
  const targetField = definition.getter();
413
390
  if (targetField && targetField.config) {
414
391
  const config = targetField.config;
415
- // Track mapping
416
392
  const dbFieldName = config.sql?.field || key;
417
393
  clientToDbKeys[key] = dbFieldName;
418
394
  dbToClientKeys[dbFieldName] = key;
419
- // ALL SCHEMAS STRICTLY USE THE JS KEY
420
395
  sqlFields[dbFieldName] = config.zodSqlSchema;
421
396
  clientFields[key] = config.zodClientSchema;
422
397
  serverFields[key] = config.zodValidationSchema;
@@ -445,11 +420,9 @@ export function createSchema(schema, relations) {
445
420
  continue;
446
421
  }
447
422
  else {
448
- // Track mapping
449
423
  const dbFieldName = sqlConfig?.field || key;
450
424
  clientToDbKeys[key] = dbFieldName;
451
425
  dbToClientKeys[dbFieldName] = key;
452
- // ALL SCHEMAS STRICTLY USE THE JS KEY
453
426
  sqlFields[dbFieldName] = config.zodSqlSchema;
454
427
  clientFields[key] = config.zodClientSchema;
455
428
  serverFields[key] = config.zodValidationSchema;
@@ -470,23 +443,51 @@ export function createSchema(schema, relations) {
470
443
  }
471
444
  }
472
445
  }
473
- let isClientRecord;
474
- const explicitChecker = fullSchema.__isClientChecker;
475
- if (explicitChecker) {
476
- isClientRecord = explicitChecker;
477
- }
478
- else if (clientPkKeys.length > 0) {
479
- const autoChecks = [];
446
+ // --- NEW: SMART CHECKER BUILDER ---
447
+ let isClientRecord = () => false;
448
+ if (clientPkKeys.length > 0) {
449
+ const checkers = [];
480
450
  for (const key of clientPkKeys) {
481
451
  const field = fullSchema[key];
482
- const sqlType = field?.config?.sql?.type;
483
- const initialValue = field?.config?.initialValue;
484
- if (sqlType === "int" && typeof initialValue === "string") {
485
- autoChecks.push({ key, check: (val) => typeof val === "string" });
452
+ const sqlConfig = field?.config?.sql;
453
+ const dbKey = sqlConfig?.field || key;
454
+ const isClientPkVal = sqlConfig?.isClientPk;
455
+ if (typeof isClientPkVal === "function") {
456
+ // Explicit checker provided directly in the field!
457
+ checkers.push({ clientKey: key, dbKey, check: isClientPkVal });
458
+ }
459
+ else {
460
+ // Fallback auto-detection: If they just passed `true`
461
+ const initialValueOrFn = field?.config?.initialValue;
462
+ let sampleValue = initialValueOrFn;
463
+ // Safely execute the function once to figure out its return type!
464
+ if (isFunction(initialValueOrFn)) {
465
+ try {
466
+ sampleValue = initialValueOrFn({ uuid });
467
+ }
468
+ catch (e) {
469
+ // Ignore if the factory fails with a dummy payload
470
+ }
471
+ }
472
+ if (sqlConfig?.type === "int" && typeof sampleValue === "string") {
473
+ checkers.push({
474
+ clientKey: key,
475
+ dbKey,
476
+ check: (val) => typeof val === "string",
477
+ });
478
+ }
486
479
  }
487
480
  }
488
- if (autoChecks.length > 0) {
489
- isClientRecord = (record) => autoChecks.some(({ key, check }) => check(record[key]));
481
+ if (checkers.length > 0) {
482
+ isClientRecord = (record) => {
483
+ if (!record || typeof record !== "object")
484
+ return false;
485
+ return checkers.some(({ clientKey, dbKey, check }) => {
486
+ // Look at both the client shape key AND the db shape key safely
487
+ const val = record[clientKey] !== undefined ? record[clientKey] : record[dbKey];
488
+ return check(val);
489
+ });
490
+ };
490
491
  }
491
492
  }
492
493
  const generateDefaults = () => {
@@ -502,7 +503,6 @@ export function createSchema(schema, relations) {
502
503
  }
503
504
  return freshDefaults;
504
505
  };
505
- // --- RUNTIME TRANSFORMERS: Map keys and apply value transforms ---
506
506
  const toClient = (dbObject) => {
507
507
  const clientObject = {};
508
508
  for (const dbKey in dbObject) {
@@ -529,7 +529,6 @@ export function createSchema(schema, relations) {
529
529
  }
530
530
  return dbObject;
531
531
  };
532
- // Create the final Zod objects once
533
532
  const finalSqlSchema = z.object(sqlFields);
534
533
  const finalClientSchema = z.object(clientFields);
535
534
  const finalValidationSchema = z.object(serverFields);
@@ -556,16 +555,13 @@ export function createSchema(schema, relations) {
556
555
  };
557
556
  }
558
557
  function createViewObject(initialRegistryKey, selection, registry, tableNameToRegistryKeyMap) {
559
- // Add a flag to track if all tables support reconciliation
560
558
  let allTablesSupportsReconciliation = true;
561
- // Debug: track which tables are checked
562
559
  const checkedTables = {};
563
560
  function buildView(currentRegistryKey, subSelection, schemaType) {
564
561
  const registryEntry = registry[currentRegistryKey];
565
562
  if (!registryEntry) {
566
563
  throw new Error(`Schema with key "${currentRegistryKey}" not found in the registry.`);
567
564
  }
568
- // FIX: Check at the correct path - registryEntry.zodSchemas
569
565
  if (!(currentRegistryKey in checkedTables)) {
570
566
  const hasPks = !!(registryEntry.zodSchemas?.pk &&
571
567
  registryEntry.zodSchemas.pk.length > 0 &&
@@ -599,7 +595,6 @@ function createViewObject(initialRegistryKey, selection, registry, tableNameToRe
599
595
  if (!nextRegistryKey) {
600
596
  throw new Error(`Could not resolve registry key for table "${targetTableName}"`);
601
597
  }
602
- // Recursive call will also check that table's pk/clientPk
603
598
  const relationSchema = buildView(nextRegistryKey, subSelection[relationKey], schemaType);
604
599
  if (["hasMany", "manyToMany"].includes(relationConfig.type)) {
605
600
  selectedRelationShapes[relationKey] = z.array(relationSchema);
@@ -621,7 +616,6 @@ function createViewObject(initialRegistryKey, selection, registry, tableNameToRe
621
616
  };
622
617
  }
623
618
  export function createSchemaBox(schemas, resolver) {
624
- // Your existing implementation stays exactly the same
625
619
  const schemaProxy = new Proxy({}, {
626
620
  get(target, tableName) {
627
621
  const schema = schemas[tableName];
@@ -647,14 +641,12 @@ export function createSchemaBox(schemas, resolver) {
647
641
  });
648
642
  const resolutionConfig = resolver(schemaProxy);
649
643
  const resolvedSchemas = schemas;
650
- // STAGE 1: Resolve references
651
644
  for (const tableName in schemas) {
652
645
  for (const fieldName in schemas[tableName]) {
653
646
  const field = schemas[tableName][fieldName];
654
647
  if (isReference(field)) {
655
648
  const targetField = field.getter();
656
649
  if (targetField && targetField.config) {
657
- // FIX: Shallow copy preserving Zod instances
658
650
  const newConfig = {
659
651
  ...targetField.config,
660
652
  sql: { ...targetField.config.sql, isForeignKey: true },
@@ -670,7 +662,6 @@ export function createSchemaBox(schemas, resolver) {
670
662
  }
671
663
  }
672
664
  }
673
- // STAGE 2: Resolve relations
674
665
  for (const tableName in schemas) {
675
666
  const tableConfig = resolutionConfig[tableName];
676
667
  if (!tableConfig)
@@ -744,7 +735,6 @@ export function createSchemaBox(schemas, resolver) {
744
735
  const tableName = finalRegistry[key].rawSchema._tableName;
745
736
  tableNameToRegistryKeyMap[tableName] = key;
746
737
  }
747
- // Inside createSchemaBox, in the cleanerRegistry loop:
748
738
  for (const tableName in finalRegistry) {
749
739
  const entry = finalRegistry[tableName];
750
740
  cleanerRegistry[tableName] = {
@@ -759,13 +749,11 @@ export function createSchemaBox(schemas, resolver) {
759
749
  toClient: entry.zodSchemas.toClient,
760
750
  toDb: entry.zodSchemas.toDb,
761
751
  },
762
- // ADD: parse functions
763
752
  parseForDb: entry.zodSchemas.parseForDb,
764
753
  parseFromDb: entry.zodSchemas.parseFromDb,
765
754
  defaults: entry.zodSchemas.defaultValues,
766
755
  stateType: entry.zodSchemas.stateType,
767
756
  generateDefaults: entry.zodSchemas.generateDefaults,
768
- // ADD: PK info
769
757
  pk: entry.zodSchemas.pk,
770
758
  clientPk: entry.zodSchemas.clientPk,
771
759
  isClientRecord: entry.zodSchemas.isClientRecord,
@@ -773,6 +761,32 @@ export function createSchemaBox(schemas, resolver) {
773
761
  createView: (selection) => {
774
762
  const view = createViewObject(tableName, selection, finalRegistry, tableNameToRegistryKeyMap);
775
763
  const defaults = computeViewDefaults(tableName, selection, finalRegistry, tableNameToRegistryKeyMap);
764
+ const deepToClient = (dbData, currentSelection, currentKey) => {
765
+ if (!dbData)
766
+ return dbData;
767
+ if (Array.isArray(dbData))
768
+ return dbData.map((item) => deepToClient(item, currentSelection, currentKey));
769
+ const regEntry = finalRegistry[currentKey];
770
+ const baseMapped = regEntry.zodSchemas.toClient(dbData);
771
+ if (typeof currentSelection === "object") {
772
+ for (const relKey in currentSelection) {
773
+ if (currentSelection[relKey] &&
774
+ dbData[relKey] !== undefined &&
775
+ dbData[relKey] !== null) {
776
+ const relField = regEntry.rawSchema[relKey];
777
+ if (relField?.config?.sql?.schema) {
778
+ const targetTableName = relField.config.sql.schema()._tableName;
779
+ const nextRegKey = tableNameToRegistryKeyMap[targetTableName];
780
+ if (nextRegKey) {
781
+ baseMapped[relKey] = deepToClient(dbData[relKey], currentSelection[relKey], nextRegKey);
782
+ }
783
+ }
784
+ }
785
+ }
786
+ }
787
+ return baseMapped;
788
+ };
789
+ const viewToClient = (dbData) => deepToClient(dbData, selection, tableName);
776
790
  return {
777
791
  definition: entry.rawSchema,
778
792
  schemaKey: tableName,
@@ -782,10 +796,13 @@ export function createSchemaBox(schemas, resolver) {
782
796
  server: view.server,
783
797
  },
784
798
  transforms: {
785
- toClient: entry.zodSchemas.toClient,
799
+ toClient: viewToClient,
786
800
  toDb: entry.zodSchemas.toDb,
787
- parseForDb: entry.zodSchemas.parseForDb,
788
- parseFromDb: entry.zodSchemas.parseFromDb,
801
+ },
802
+ parseForDb: entry.zodSchemas.parseForDb,
803
+ parseFromDb: (dbData) => {
804
+ const mapped = viewToClient(dbData);
805
+ return view.client.parse(mapped);
789
806
  },
790
807
  defaults: defaults,
791
808
  pk: entry.zodSchemas.pk,
@@ -798,23 +815,18 @@ export function createSchemaBox(schemas, resolver) {
798
815
  };
799
816
  },
800
817
  RelationSelection: {},
801
- __registry: finalRegistry, // ADD THIS - was in type but not runtime
818
+ __registry: finalRegistry,
802
819
  };
803
820
  }
804
821
  return cleanerRegistry;
805
822
  }
806
- function computeViewDefaults(currentRegistryKey, // Renamed for clarity, e.g., "users"
807
- selection, registry, tableNameToRegistryKeyMap, // Accept the map
808
- visited = new Set()) {
823
+ function computeViewDefaults(currentRegistryKey, selection, registry, tableNameToRegistryKeyMap, visited = new Set()) {
809
824
  if (visited.has(currentRegistryKey)) {
810
- return undefined; // Prevent circular references
825
+ return undefined;
811
826
  }
812
827
  visited.add(currentRegistryKey);
813
- // This lookup now uses the correct key every time.
814
828
  const entry = registry[currentRegistryKey];
815
- // This check prevents the crash.
816
829
  if (!entry) {
817
- // This case should ideally not be hit if the map is correct, but it's safe to have.
818
830
  console.warn(`Could not find entry for key "${currentRegistryKey}" in registry while computing defaults.`);
819
831
  return {};
820
832
  }
@@ -823,7 +835,6 @@ visited = new Set()) {
823
835
  if (selection === true || typeof selection !== "object") {
824
836
  return baseDefaults;
825
837
  }
826
- // Add relation defaults based on selection
827
838
  for (const key in selection) {
828
839
  if (!selection[key])
829
840
  continue;
@@ -831,14 +842,10 @@ visited = new Set()) {
831
842
  if (!field?.config?.sql?.schema)
832
843
  continue;
833
844
  const relationConfig = field.config.sql;
834
- // --- THE CORE FIX ---
835
- // 1. Get the internal _tableName of the related schema (e.g., "post_table")
836
845
  const targetTableName = relationConfig.schema()._tableName;
837
- // 2. Use the map to find the correct registry key for it (e.g., "posts")
838
846
  const nextRegistryKey = tableNameToRegistryKeyMap[targetTableName];
839
847
  if (!nextRegistryKey)
840
- continue; // Could not resolve, skip this relation
841
- // Handle different default configurations
848
+ continue;
842
849
  const defaultConfig = relationConfig.defaultConfig;
843
850
  if (defaultConfig === undefined) {
844
851
  delete baseDefaults[key];
@@ -852,17 +859,10 @@ visited = new Set()) {
852
859
  else if (relationConfig.type === "hasMany" ||
853
860
  relationConfig.type === "manyToMany") {
854
861
  const count = defaultConfig?.count || relationConfig.defaultCount || 1;
855
- baseDefaults[key] = Array.from({ length: count }, () =>
856
- // 3. Make the recursive call with the CORRECT key
857
- computeViewDefaults(nextRegistryKey, selection[key], registry, tableNameToRegistryKeyMap, // Pass the map along
858
- new Set(visited)));
862
+ baseDefaults[key] = Array.from({ length: count }, () => computeViewDefaults(nextRegistryKey, selection[key], registry, tableNameToRegistryKeyMap, new Set(visited)));
859
863
  }
860
864
  else {
861
- // hasOne or belongsTo
862
- baseDefaults[key] =
863
- // 3. Make the recursive call with the CORRECT key
864
- computeViewDefaults(nextRegistryKey, selection[key], registry, tableNameToRegistryKeyMap, // Pass the map along
865
- new Set(visited));
865
+ baseDefaults[key] = computeViewDefaults(nextRegistryKey, selection[key], registry, tableNameToRegistryKeyMap, new Set(visited));
866
866
  }
867
867
  }
868
868
  return baseDefaults;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cogsbox-shape",
3
- "version": "0.5.173",
3
+ "version": "0.5.175",
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",