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 +47 -19
- package/cogsbox-shape-db/dist/table-db.js +6 -1
- package/dist/schema.d.ts +29 -2
- package/dist/schema.js +39 -31
- package/package.json +9 -5
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
|
|
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
|
-
//
|
|
192
|
-
fullName: s.clientInput(""),
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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?:
|
|
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:
|
|
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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
-
|
|
627
|
-
|
|
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(
|
|
633
|
-
:
|
|
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
|
-
|
|
649
|
+
const trackDeriveDependencies = (deriveGroup) => {
|
|
650
|
+
if (!deriveGroup)
|
|
651
|
+
return;
|
|
643
652
|
const trackingSeed = { ...defaultValues };
|
|
644
|
-
for (const key in
|
|
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
|
-
|
|
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] =
|
|
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.
|
|
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",
|