cogsbox-shape 0.5.190 → 0.5.192

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
@@ -178,9 +178,12 @@ const products = schema({
178
178
  });
179
179
  ```
180
180
 
181
- #### Derived Fields (`.derive()`)
182
-
183
- `.derive()` populates _existing fields_ dynamically on read and default generation. Because you define the fields first, derived fields can be either standard DB fields or Client-only fields.
181
+ #### Derived Fields (`.derive()`)
182
+
183
+ `.derive()` populates _existing fields_ dynamically. Define the target field first, then choose where the derivation runs:
184
+
185
+ - `forClient` computes client-only fields during `generateDefaults()` and `toClient()`.
186
+ - `forDb` computes DB-backed fields during `toDb()`, `parseForDb()`, and ORM writes. Use `sqlOnly: true` when the computed column should stay hidden from the client.
184
187
 
185
188
  ```typescript
186
189
  const users = schema({
@@ -188,16 +191,22 @@ const users = schema({
188
191
  firstName: s.sql({ type: "varchar" }).clientInput({ value: "John" }),
189
192
  lastName: s.sql({ type: "varchar" }).clientInput({ value: "Doe" }),
190
193
 
191
- // 1. Defined purely as a client field
192
- fullName: s.clientInput(""),
193
- // 2. Defined as a DB field
194
- searchIndex: s.sql({ type: "varchar" }),
195
- }).derive({
196
- // Computes on toClient() and generateDefaults()
197
- fullName: (row) => `${row.firstName} ${row.lastName}`,
198
- searchIndex: (row) => `${row.firstName} ${row.lastName}`.toLowerCase(),
199
- });
200
- ```
194
+ // Virtual field. It exists in app/view state, not SQL.
195
+ fullName: s.clientInput(""),
196
+
197
+ // Hidden DB column. It is written to SQL, but not sent to the client.
198
+ searchIndex: s.sql({ type: "varchar", sqlOnly: true }),
199
+ }).derive({
200
+ forClient: {
201
+ fullName: (row) => `${row.firstName} ${row.lastName}`,
202
+ },
203
+ forDb: {
204
+ searchIndex: (row) => `${row.firstName} ${row.lastName}`.toLowerCase(),
205
+ },
206
+ });
207
+ ```
208
+
209
+ During partial ORM updates, DB-backed derivations fetch only missing dependency fields they actually read, then recompute the affected `forDb` fields. Client-only derived fields are ignored by SQL writes.
201
210
 
202
211
  ### Schema Object Structure
203
212
 
@@ -337,12 +346,31 @@ type UserWithPosts = z.infer<typeof userWithPosts.schemas.client>;
337
346
  // posts: { id: number; title: string; authorId: number; }[]
338
347
  // }
339
348
 
340
- // Views also have transforms for the selected fields
341
- const { defaults, transforms } = userWithPosts;
342
- // transforms.toClient() handles nested relation transforms automatically
343
- ```
344
-
345
- ### 5. Nested Defaults and Form Definitions (`defaultsDefinition`)
349
+ // Views also have transforms for the selected fields
350
+ const { defaults, transforms } = userWithPosts;
351
+ // transforms.toClient() handles nested relation transforms automatically
352
+ ```
353
+
354
+ When a box is connected to the ORM, view reads hydrate the selected relation tree before parsing:
355
+
356
+ ```typescript
357
+ import { connect } from "cogsbox-shape/db";
358
+ import { createSqliteDb } from "cogsbox-shape/db/sqlite";
359
+
360
+ const db = createSqliteDb("app.sqlite");
361
+ const bx = connect(box, db);
362
+
363
+ const userView = bx.users.createView({
364
+ posts: true,
365
+ });
366
+
367
+ const user = await userView.db.findById(1);
368
+ // user.posts is loaded and validated as part of the view shape
369
+ ```
370
+
371
+ Use `insert(data).ids()` when you only need the database identity, or `insert(data).full()` when you want optimistic client IDs reconciled back into the submitted client object. `create()` is kept as an alias for older code; prefer `insert()` in new code.
372
+
373
+ ### 5. Nested Defaults and Form Definitions (`defaultsDefinition`)
346
374
 
347
375
  When working with forms and nested array relations (like `hasMany`), you often need the default state for a _single new item_ to add to a form array.
348
376
 
@@ -80,6 +80,7 @@ export class TableDB {
80
80
  const dbData = this.transforms.parseForDb(data);
81
81
  const parsedDbOnlyData = this.parseDbOnlyData(dbOnlyData, {
82
82
  requireRequired: true,
83
+ presentDbData: dbData,
83
84
  });
84
85
  const clientPkClientKeys = this.meta.clientPkFields;
85
86
  const pkDbNames = new Set(clientPkClientKeys.map((k) => {
@@ -225,7 +226,11 @@ export class TableDB {
225
226
  parseDbOnlyData(dbOnlyData, opts = { requireRequired: false }) {
226
227
  if (opts.requireRequired) {
227
228
  for (const requiredKey of this.meta.sqlOnlyRequiredClientFields) {
228
- if (!dbOnlyData || dbOnlyData[requiredKey] === undefined) {
229
+ const field = this.meta.dbFields.get(requiredKey);
230
+ const dbName = field?.dbName ?? requiredKey;
231
+ const alreadyPresent = opts.presentDbData?.[dbName] !== undefined;
232
+ if (!alreadyPresent &&
233
+ (!dbOnlyData || dbOnlyData[requiredKey] === undefined)) {
229
234
  throw new Error(`Missing required sqlOnly field "${requiredKey}" for "${this.meta.tableName}".`);
230
235
  }
231
236
  }
package/dist/schema.d.ts CHANGED
@@ -194,11 +194,38 @@ type PickPrimaryKeys<T extends ShapeSchema> = {
194
194
  };
195
195
  } ? K : never]: T[K];
196
196
  };
197
+ type PickClientOnlyKeys<T extends ShapeSchema> = {
198
+ [K in keyof T]: T[K] extends {
199
+ config: {
200
+ sql: null;
201
+ };
202
+ } ? K : never;
203
+ }[keyof T];
204
+ type PickDbFieldKeys<T extends ShapeSchema> = {
205
+ [K in keyof T]: T[K] extends {
206
+ config: {
207
+ sql: infer TSql;
208
+ };
209
+ } ? TSql extends null ? never : TSql extends {
210
+ type: "hasMany" | "hasOne" | "belongsTo" | "manyToMany";
211
+ } ? never : K : never;
212
+ }[keyof T];
213
+ type InferClientRow<T extends ShapeSchema> = Prettify<z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<T, "zodClientSchema">>>>>;
197
214
  type SchemaBuilder<T extends ShapeSchema> = Prettify<EnrichFields<T>> & {
198
215
  __primaryKeySQL?: string;
199
- __derives?: Record<string, (row: any) => any>;
216
+ __derives?: {
217
+ forClient?: Record<string, (row: any) => any>;
218
+ forDb?: Record<string, (row: any) => any>;
219
+ };
200
220
  primaryKeySQL: (definer: (pkFields: PickPrimaryKeys<T>) => string) => SchemaBuilder<T>;
201
- derive: <D extends Partial<Record<keyof T, (row: Prettify<z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<T, "zodClientSchema">>>>>) => any>>>(derivers: D) => SchemaBuilder<T>;
221
+ derive: (derivers: {
222
+ forClient?: {
223
+ [K in PickClientOnlyKeys<T>]?: (row: InferClientRow<T>) => any;
224
+ };
225
+ forDb?: {
226
+ [K in PickDbFieldKeys<T>]?: (row: InferClientRow<T>) => any;
227
+ };
228
+ }) => SchemaBuilder<T>;
202
229
  };
203
230
  export declare function schema<T extends string, U extends ShapeSchema<T>>(schema: U): SchemaBuilder<U>;
204
231
  export type RelationType = "hasMany" | "hasOne" | "manyToMany";
package/dist/schema.js CHANGED
@@ -578,6 +578,7 @@ export function createSchema(schema, relations) {
578
578
  const generateDefaults = () => {
579
579
  const freshDefaults = {};
580
580
  for (const key in defaultGenerators) {
581
+ // ... same logic for mapping standard defaults ...
581
582
  const generatorOrValue = defaultGenerators[key];
582
583
  let rawValue = isFunction(generatorOrValue)
583
584
  ? generatorOrValue({ uuid })
@@ -586,9 +587,10 @@ export function createSchema(schema, relations) {
586
587
  ? fieldTransforms[key].toClient(rawValue)
587
588
  : rawValue;
588
589
  }
589
- if (derives) {
590
- for (const key in derives) {
591
- freshDefaults[key] = derives[key](freshDefaults);
590
+ // Only apply client derivations
591
+ if (derives?.forClient) {
592
+ for (const key in derives.forClient) {
593
+ freshDefaults[key] = derives.forClient[key]?.(freshDefaults);
592
594
  }
593
595
  }
594
596
  return freshDefaults;
@@ -606,31 +608,36 @@ export function createSchema(schema, relations) {
606
608
  ? transform(dbObject[dbKey])
607
609
  : dbObject[dbKey];
608
610
  }
609
- if (derives) {
610
- for (const key in derives) {
611
- clientObject[key] = derives[key](clientObject);
611
+ // Only apply Client derives AFTER mapping standard fields
612
+ if (derives?.forClient) {
613
+ for (const key in derives.forClient) {
614
+ clientObject[key] = derives.forClient[key]?.(clientObject);
612
615
  }
613
616
  }
614
617
  return clientObject;
615
618
  };
616
619
  const toDb = (clientObject) => {
617
- // 1. Calculate derives FIRST based on the client data
618
- const clientWithDerives = { ...clientObject };
619
- if (derives) {
620
- for (const key in derives) {
621
- clientWithDerives[key] = derives[key](clientWithDerives);
622
- }
623
- }
624
- // 2. Map the data (including the newly derived fields) to the DB object
625
620
  const dbObject = {};
626
- for (const clientKey in clientWithDerives) {
627
- if (clientWithDerives[clientKey] === undefined)
621
+ // 1. Map standard client fields to DB fields
622
+ for (const clientKey in clientObject) {
623
+ if (clientObject[clientKey] === undefined)
624
+ continue;
625
+ const dbKey = clientToDbKeys[clientKey];
626
+ if (!dbKey)
628
627
  continue;
629
- const dbKey = clientToDbKeys[clientKey] || clientKey;
630
628
  const transform = fieldTransforms[clientKey]?.toDb;
631
629
  dbObject[dbKey] = transform
632
- ? transform(clientWithDerives[clientKey])
633
- : clientWithDerives[clientKey];
630
+ ? transform(clientObject[clientKey])
631
+ : clientObject[clientKey];
632
+ }
633
+ // 2. Map Database ONLY derives directly to the dbObject
634
+ if (derives?.forDb) {
635
+ for (const schemaKey in derives.forDb) {
636
+ // Resolve custom DB column name if they used s.sql({ field: "custom_name" })
637
+ const sqlConfig = fullSchema[schemaKey]?.config?.sql;
638
+ const dbKey = sqlConfig?.field || schemaKey;
639
+ dbObject[dbKey] = derives.forDb[schemaKey]?.(clientObject);
640
+ }
634
641
  }
635
642
  return dbObject;
636
643
  };
@@ -639,9 +646,11 @@ export function createSchema(schema, relations) {
639
646
  const finalClientSchema = z.object(clientFields);
640
647
  const finalValidationSchema = z.object(serverFields);
641
648
  const deriveDependencies = {};
642
- if (derives) {
649
+ const trackDeriveDependencies = (deriveGroup) => {
650
+ if (!deriveGroup)
651
+ return;
643
652
  const trackingSeed = { ...defaultValues };
644
- for (const key in derives) {
653
+ for (const key in deriveGroup) {
645
654
  const accessed = new Set();
646
655
  const trackingRow = new Proxy(trackingSeed, {
647
656
  get(target, prop, receiver) {
@@ -652,12 +661,14 @@ export function createSchema(schema, relations) {
652
661
  },
653
662
  });
654
663
  try {
655
- derives[key](trackingRow);
664
+ deriveGroup[key]?.(trackingRow);
656
665
  }
657
666
  catch (e) { }
658
- deriveDependencies[key] = Array.from(accessed);
667
+ deriveDependencies[key] = Array.from(new Set([...(deriveDependencies[key] ?? []), ...accessed]));
659
668
  }
660
- }
669
+ };
670
+ trackDeriveDependencies(derives?.forClient);
671
+ trackDeriveDependencies(derives?.forDb);
661
672
  return {
662
673
  pk: pkKeys.length ? pkKeys : null,
663
674
  clientPk: clientPkKeys.length ? clientPkKeys : null,
@@ -701,10 +712,6 @@ function createViewObject(initialRegistryKey, selection, registry, tableNameToRe
701
712
  registryEntry.zodSchemas.clientPk.length > 0);
702
713
  checkedTables[currentRegistryKey] = hasPks;
703
714
  if (!hasPks) {
704
- console.log(`Table ${currentRegistryKey} missing pk/clientPk:`, {
705
- pk: registryEntry.zodSchemas?.pk,
706
- clientPk: registryEntry.zodSchemas?.clientPk,
707
- });
708
715
  allTablesSupportsReconciliation = false;
709
716
  }
710
717
  }
@@ -945,9 +952,10 @@ export function createSchemaBox(schemas, resolutions) {
945
952
  }
946
953
  }
947
954
  }
948
- if (regEntry.rawSchema.__derives) {
949
- for (const key in regEntry.rawSchema.__derives) {
950
- baseMapped[key] = regEntry.rawSchema.__derives[key](baseMapped);
955
+ if (regEntry.rawSchema.__derives?.forClient) {
956
+ for (const key in regEntry.rawSchema.__derives.forClient) {
957
+ baseMapped[key] =
958
+ regEntry.rawSchema.__derives.forClient[key](baseMapped);
951
959
  }
952
960
  }
953
961
  return baseMapped;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cogsbox-shape",
3
- "version": "0.5.190",
3
+ "version": "0.5.192",
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",
@@ -22,16 +22,20 @@
22
22
  "exports": {
23
23
  ".": {
24
24
  "types": "./dist/index.d.ts",
25
- "import": "./dist/index.js"
25
+ "import": "./dist/index.js",
26
+ "default": "./dist/index.js"
26
27
  },
27
28
  "./db": {
28
29
  "types": "./cogsbox-shape-db/dist/index.d.ts",
29
- "import": "./cogsbox-shape-db/dist/index.js"
30
+ "import": "./cogsbox-shape-db/dist/index.js",
31
+ "default": "./cogsbox-shape-db/dist/index.js"
30
32
  },
31
33
  "./db/sqlite": {
32
34
  "types": "./cogsbox-shape-db/dist/sqlite/index.d.ts",
33
- "import": "./cogsbox-shape-db/dist/sqlite/index.js"
34
- }
35
+ "import": "./cogsbox-shape-db/dist/sqlite/index.js",
36
+ "default": "./cogsbox-shape-db/dist/sqlite/index.js"
37
+ },
38
+ "./package.json": "./package.json"
35
39
  },
36
40
  "keywords": [
37
41
  "typescript",