cogsbox-shape 0.5.193 → 0.5.194
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 +67 -3
- package/cogsbox-shape-db/dist/connect.d.ts +2 -4
- package/dist/schema.d.ts +37 -3
- package/dist/schema.js +67 -3
- package/dist/vitest/fullSchema.test.d.ts +1 -0
- package/dist/vitest/fullSchema.test.js +1367 -0
- package/dist/vitest/generateSQL.test.d.ts +1 -0
- package/dist/vitest/generateSQL.test.js +70 -0
- package/dist/vitest/packageExports.test.d.ts +1 -0
- package/dist/vitest/packageExports.test.js +15 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ s.sqlite()/s.postgres()/s.mysql() → .clientInput() → .client() → .se
|
|
|
44
44
|
| `.server(fn)` | Server-side validation. Stricter rules before database writes. |
|
|
45
45
|
| `.transform({ toClient, toDb })` | Converts between database and client representations. |
|
|
46
46
|
|
|
47
|
-
Note: `.derive()`
|
|
47
|
+
Note: `.derive()` and `.refine()` are schema-level methods, not chainable on individual fields.
|
|
48
48
|
|
|
49
49
|
### 1. SQL — Define Your Database Schema
|
|
50
50
|
|
|
@@ -231,9 +231,71 @@ const users = schema({
|
|
|
231
231
|
});
|
|
232
232
|
```
|
|
233
233
|
|
|
234
|
-
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.
|
|
234
|
+
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.
|
|
235
235
|
|
|
236
|
-
###
|
|
236
|
+
### 7. Refinement (`.refine()`)
|
|
237
|
+
|
|
238
|
+
`.refine()` adds cross-field validation rules that the entire row must satisfy. Unlike `.client()`/`.server()` which validate individual fields, `refine` can check relationships between fields.
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
const events = schema({
|
|
242
|
+
_tableName: "events",
|
|
243
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
244
|
+
startDate: s.sqlite({ type: "varchar" }).clientInput({ value: "" }),
|
|
245
|
+
endDate: s.sqlite({ type: "varchar" }).clientInput({ value: "" }),
|
|
246
|
+
content: s.sqlite({ type: "varchar", nullable: true }).clientInput({
|
|
247
|
+
value: null,
|
|
248
|
+
schema: z.string().nullable(),
|
|
249
|
+
}),
|
|
250
|
+
isPublished: s.sqlite({ type: "boolean" }).clientInput({ value: false }),
|
|
251
|
+
}).refine({
|
|
252
|
+
server: (row) => {
|
|
253
|
+
const errors: { path: string[]; message: string }[] = [];
|
|
254
|
+
if (row.startDate && row.endDate && row.startDate > row.endDate) {
|
|
255
|
+
errors.push({ path: ["endDate"], message: "End date must be after start date" });
|
|
256
|
+
}
|
|
257
|
+
if (row.isPublished && !row.content) {
|
|
258
|
+
errors.push({ path: ["content"], message: "Published events must have content" });
|
|
259
|
+
}
|
|
260
|
+
return errors.length > 0 ? errors : undefined;
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const box = createSchemaBox({ events }, { events: {} });
|
|
265
|
+
|
|
266
|
+
// Server refinement runs on parseForDb (before DB writes)
|
|
267
|
+
box.events.transforms.parseForDb({
|
|
268
|
+
id: 1, startDate: "2024-12-31", endDate: "2024-01-01",
|
|
269
|
+
content: null, isPublished: false,
|
|
270
|
+
});
|
|
271
|
+
// Throws: "End date must be after start date"
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The `refine()` config accepts two optional callbacks:
|
|
275
|
+
|
|
276
|
+
| Callback | Runs on | Purpose |
|
|
277
|
+
|----------|---------|---------|
|
|
278
|
+
| `server` | `parseForDb()` | Cross-field validation before DB writes |
|
|
279
|
+
| `client` | `clientInput` schema | Cross-field validation on client input |
|
|
280
|
+
|
|
281
|
+
Each callback receives the full row and returns:
|
|
282
|
+
- `undefined` or `null` — validation passes
|
|
283
|
+
- A single `{ path: string[]; message: string }` — one error
|
|
284
|
+
- An array of `{ path: string[]; message: string }` — multiple errors
|
|
285
|
+
|
|
286
|
+
**Dependency tracking**: Same proxy-based approach as `derive()` — the library tracks which fields the refine function reads. This is used by the ORM to know which fields to include during partial updates. The same caveat applies: conditional branches with falsy defaults can hide dependencies.
|
|
287
|
+
|
|
288
|
+
**Chaining**: `refine()` can be chained after `derive()`:
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
schema({ ... })
|
|
292
|
+
.derive({ forDb: { fullName: (row) => `${row.firstName} ${row.lastName}` } })
|
|
293
|
+
.refine({ server: (row) => { ... } });
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Note**: `parsePatchForDb` uses the base schema (without refinement) since partial data may not satisfy cross-field rules.
|
|
297
|
+
|
|
298
|
+
### Schema Object Structure
|
|
237
299
|
|
|
238
300
|
The returned schema object has a clear separation of concerns:
|
|
239
301
|
|
|
@@ -247,6 +309,8 @@ schema.generateDefaults; // Function to generate fresh defaults (executes random
|
|
|
247
309
|
schema.pk; // Primary key field names
|
|
248
310
|
schema.clientPk; // Client-side primary key field names
|
|
249
311
|
schema.isClientRecord; // Function to check if a record is client-created
|
|
312
|
+
schema.deriveDependencies; // Derive function dependencies ({ [field]: string[] })
|
|
313
|
+
schema.refineDependencies; // Refinement dependencies ({ server: string[], client: string[] })
|
|
250
314
|
```
|
|
251
315
|
|
|
252
316
|
## Using Schemas
|
|
@@ -6,7 +6,7 @@ type Row<T> = T extends readonly (infer TItem)[] ? TItem : T;
|
|
|
6
6
|
type Prettify<T> = {
|
|
7
7
|
[K in keyof T]: T[K];
|
|
8
8
|
} & {};
|
|
9
|
-
type SchemaMetaKey = "_tableName" | "__primaryKeySQL" | "__derives" | "primaryKeySQL" | "derive";
|
|
9
|
+
type SchemaMetaKey = "_tableName" | "__primaryKeySQL" | "__derives" | "__refinements" | "primaryKeySQL" | "derive" | "refine";
|
|
10
10
|
type SqlConfigOf<TField> = TField extends {
|
|
11
11
|
config: {
|
|
12
12
|
sql: infer TSql;
|
|
@@ -54,9 +54,7 @@ type IsDerivedDbField<TTable, TKey> = TTable extends {
|
|
|
54
54
|
forDb?: infer TForDb;
|
|
55
55
|
};
|
|
56
56
|
};
|
|
57
|
-
} ? TKey extends keyof NonNullable<TForDb> ? true : false :
|
|
58
|
-
deriveDependencies: infer TDerives;
|
|
59
|
-
} ? TKey extends keyof TDerives ? true : false : false;
|
|
57
|
+
} ? TKey extends keyof NonNullable<TForDb> ? true : false : false;
|
|
60
58
|
type SqlOnlyInput<T> = T extends {
|
|
61
59
|
definition: infer TDefinition;
|
|
62
60
|
} ? Prettify<{
|
package/dist/schema.d.ts
CHANGED
|
@@ -205,6 +205,11 @@ export type EnrichFields<T extends ShapeSchema> = {
|
|
|
205
205
|
[K in keyof T]: K extends "_tableName" ? T[K] : K extends string ? EnrichedField<K, T[K], T> : T[K];
|
|
206
206
|
};
|
|
207
207
|
export declare const SchemaWrapperBrand: unique symbol;
|
|
208
|
+
export type RefinementError = {
|
|
209
|
+
path: string[];
|
|
210
|
+
message: string;
|
|
211
|
+
};
|
|
212
|
+
export type RefinementFn<T> = (row: T) => RefinementError | RefinementError[] | undefined | null;
|
|
208
213
|
type PickPrimaryKeys<T extends ShapeSchema> = {
|
|
209
214
|
[K in keyof T as T[K] extends {
|
|
210
215
|
config: {
|
|
@@ -237,6 +242,10 @@ type SchemaBuilder<T extends ShapeSchema> = Prettify<EnrichFields<T>> & {
|
|
|
237
242
|
forClient?: Record<string, (row: any) => any>;
|
|
238
243
|
forDb?: Record<string, (row: any) => any>;
|
|
239
244
|
};
|
|
245
|
+
__refinements?: {
|
|
246
|
+
server?: RefinementFn<InferClientRow<T>>;
|
|
247
|
+
client?: RefinementFn<z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<EnrichFields<T>, "zodClientInputSchema">>>>>;
|
|
248
|
+
};
|
|
240
249
|
primaryKeySQL: (definer: (pkFields: PickPrimaryKeys<T>) => string) => SchemaBuilder<T>;
|
|
241
250
|
derive: (derivers: {
|
|
242
251
|
forClient?: {
|
|
@@ -246,6 +255,10 @@ type SchemaBuilder<T extends ShapeSchema> = Prettify<EnrichFields<T>> & {
|
|
|
246
255
|
[K in PickDbFieldKeys<T>]?: (row: InferClientRow<T>) => any;
|
|
247
256
|
};
|
|
248
257
|
}) => SchemaBuilder<T>;
|
|
258
|
+
refine: (refinements: {
|
|
259
|
+
server?: RefinementFn<InferClientRow<T>>;
|
|
260
|
+
client?: RefinementFn<z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<EnrichFields<T>, "zodClientInputSchema">>>>>;
|
|
261
|
+
}) => SchemaBuilder<T>;
|
|
249
262
|
};
|
|
250
263
|
export declare function schema<T extends string, U extends ShapeSchema<T>>(schema: U): SchemaBuilder<U>;
|
|
251
264
|
export type RelationType = "hasMany" | "hasOne" | "manyToMany";
|
|
@@ -285,6 +298,10 @@ export declare function createSchema<T extends {
|
|
|
285
298
|
pk: string[] | null;
|
|
286
299
|
clientPk: string[] | null;
|
|
287
300
|
deriveDependencies: Record<string, string[]>;
|
|
301
|
+
refineDependencies: {
|
|
302
|
+
server: string[];
|
|
303
|
+
client: string[];
|
|
304
|
+
};
|
|
288
305
|
isClientRecord: (record: any) => boolean;
|
|
289
306
|
sqlSchema: z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodSqlSchema">>>;
|
|
290
307
|
clientInputSchema: z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodClientInputSchema">>>;
|
|
@@ -366,6 +383,10 @@ type ResolvedRegistryWithSchemas<S extends Record<string, SchemaWithPlaceholders
|
|
|
366
383
|
};
|
|
367
384
|
pk: string[] | null;
|
|
368
385
|
clientPk: string[] | null;
|
|
386
|
+
refineDependencies: {
|
|
387
|
+
server: string[];
|
|
388
|
+
client: string[];
|
|
389
|
+
};
|
|
369
390
|
isClientRecord: (record: any) => boolean;
|
|
370
391
|
generateDefaults: () => Prettify<DeriveDefaults<ResolveSchema<S[K], K extends keyof R ? (R[K] extends object ? R[K] : {}) : {}>>>;
|
|
371
392
|
};
|
|
@@ -512,6 +533,10 @@ type RegistryShape = Record<string, {
|
|
|
512
533
|
defaultValues: any;
|
|
513
534
|
stateType: any;
|
|
514
535
|
deriveDependencies: Record<string, string[]>;
|
|
536
|
+
refineDependencies: {
|
|
537
|
+
server: string[];
|
|
538
|
+
client: string[];
|
|
539
|
+
};
|
|
515
540
|
};
|
|
516
541
|
transforms: {
|
|
517
542
|
toClient: (dbObject: any) => any;
|
|
@@ -523,6 +548,10 @@ type RegistryShape = Record<string, {
|
|
|
523
548
|
pk: string[] | null;
|
|
524
549
|
clientPk: string[] | null;
|
|
525
550
|
deriveDependencies: Record<string, string[]>;
|
|
551
|
+
refineDependencies: {
|
|
552
|
+
server: string[];
|
|
553
|
+
client: string[];
|
|
554
|
+
};
|
|
526
555
|
isClientRecord: (record: any) => boolean;
|
|
527
556
|
generateDefaults: () => any;
|
|
528
557
|
}>;
|
|
@@ -549,6 +578,11 @@ type CreateSchemaBoxReturn<S extends Record<string, SchemaWithPlaceholders>, R e
|
|
|
549
578
|
generateDefaults: () => Resolved[K]["zodSchemas"]["defaultValues"];
|
|
550
579
|
pk: string[] | null;
|
|
551
580
|
clientPk: string[] | null;
|
|
581
|
+
deriveDependencies: Record<string, string[]>;
|
|
582
|
+
refineDependencies: {
|
|
583
|
+
server: string[];
|
|
584
|
+
client: string[];
|
|
585
|
+
};
|
|
552
586
|
isClientRecord: (record: any) => boolean;
|
|
553
587
|
nav: NavigationProxy<K & string, Resolved>;
|
|
554
588
|
RelationSelection: NavigationToSelection<NavigationProxy<K & string, Resolved>>;
|
|
@@ -574,7 +608,7 @@ type GetDbKey<K, Field> = Field extends Reference<infer TGetter> ? ReturnType<TG
|
|
|
574
608
|
};
|
|
575
609
|
} ? string extends F ? K : F : K;
|
|
576
610
|
type DeriveSchemaByKey<T, Key extends "zodSqlSchema" | "zodClientInputSchema" | "zodClientSchema" | "zodValidationSchema", Depth extends any[] = []> = Depth["length"] extends 10 ? any : {
|
|
577
|
-
[K in keyof T as K extends "_tableName" | typeof SchemaWrapperBrand | "__primaryKeySQL" | "primaryKeySQL" | "derive" | "__derives" ? never : K extends keyof T ? T[K] extends {
|
|
611
|
+
[K in keyof T as K extends "_tableName" | typeof SchemaWrapperBrand | "__primaryKeySQL" | "primaryKeySQL" | "derive" | "__derives" | "refine" | "__refinements" ? never : K extends keyof T ? T[K] extends {
|
|
578
612
|
config: {
|
|
579
613
|
sql: {
|
|
580
614
|
sqlOnly: true;
|
|
@@ -601,7 +635,7 @@ type DeriveSchemaByKey<T, Key extends "zodSqlSchema" | "zodClientInputSchema" |
|
|
|
601
635
|
} ? ZodSchema : never;
|
|
602
636
|
};
|
|
603
637
|
type DeriveDefaults<T, Depth extends any[] = []> = Prettify<Depth["length"] extends 10 ? any : {
|
|
604
|
-
[K in keyof T as K extends "_tableName" | typeof SchemaWrapperBrand | "__primaryKeySQL" | "primaryKeySQL" | "derive" | "__derives" ? never : K extends keyof T ? T[K] extends {
|
|
638
|
+
[K in keyof T as K extends "_tableName" | typeof SchemaWrapperBrand | "__primaryKeySQL" | "primaryKeySQL" | "derive" | "__derives" | "refine" | "__refinements" ? never : K extends keyof T ? T[K] extends {
|
|
605
639
|
config: {
|
|
606
640
|
sql: {
|
|
607
641
|
sqlOnly: true;
|
|
@@ -624,7 +658,7 @@ type DeriveDefaults<T, Depth extends any[] = []> = Prettify<Depth["length"] exte
|
|
|
624
658
|
} ? z.infer<TClient> : never;
|
|
625
659
|
}>;
|
|
626
660
|
type DeriveStateType<T, Depth extends any[] = []> = Prettify<Depth["length"] extends 10 ? any : {
|
|
627
|
-
[K in keyof T as K extends "_tableName" | typeof SchemaWrapperBrand | "__primaryKeySQL" | "primaryKeySQL" | "derive" | "__derives" ? never : K extends keyof T ? T[K] extends {
|
|
661
|
+
[K in keyof T as K extends "_tableName" | typeof SchemaWrapperBrand | "__primaryKeySQL" | "primaryKeySQL" | "derive" | "__derives" | "refine" | "__refinements" ? never : K extends keyof T ? T[K] extends {
|
|
628
662
|
config: {
|
|
629
663
|
sql: {
|
|
630
664
|
sqlOnly: true;
|
package/dist/schema.js
CHANGED
|
@@ -357,6 +357,7 @@ export function schema(schema) {
|
|
|
357
357
|
enrichedSchema[SchemaWrapperBrand] = true;
|
|
358
358
|
enrichedSchema.__primaryKeySQL = undefined;
|
|
359
359
|
enrichedSchema.__derives = undefined;
|
|
360
|
+
enrichedSchema.__refinements = undefined;
|
|
360
361
|
enrichedSchema.primaryKeySQL = function (definer) {
|
|
361
362
|
const pkFieldsOnly = {};
|
|
362
363
|
for (const key in schema) {
|
|
@@ -374,6 +375,10 @@ export function schema(schema) {
|
|
|
374
375
|
enrichedSchema.__derives = derivers;
|
|
375
376
|
return enrichedSchema;
|
|
376
377
|
};
|
|
378
|
+
enrichedSchema.refine = function (refinements) {
|
|
379
|
+
enrichedSchema.__refinements = refinements;
|
|
380
|
+
return enrichedSchema;
|
|
381
|
+
};
|
|
377
382
|
return enrichedSchema;
|
|
378
383
|
}
|
|
379
384
|
function inferDefaultFromZod(zodType, sqlConfig) {
|
|
@@ -440,6 +445,7 @@ export function createSchema(schema, relations) {
|
|
|
440
445
|
let pkKeys = [];
|
|
441
446
|
let clientPkKeys = [];
|
|
442
447
|
const derives = schema.__derives;
|
|
448
|
+
const refinements = schema.__refinements;
|
|
443
449
|
for (const key in fullSchema) {
|
|
444
450
|
const value = fullSchema[key];
|
|
445
451
|
if (key === "_tableName" ||
|
|
@@ -678,22 +684,78 @@ export function createSchema(schema, relations) {
|
|
|
678
684
|
};
|
|
679
685
|
trackDeriveDependencies(derives?.forClient);
|
|
680
686
|
trackDeriveDependencies(derives?.forDb);
|
|
687
|
+
const refineDependencies = { server: [], client: [] };
|
|
688
|
+
const trackRefinementDependencies = (fn, key) => {
|
|
689
|
+
if (!fn)
|
|
690
|
+
return;
|
|
691
|
+
const accessed = new Set();
|
|
692
|
+
const trackingRow = new Proxy(defaultValues, {
|
|
693
|
+
get(target, prop, receiver) {
|
|
694
|
+
if (typeof prop === "string") {
|
|
695
|
+
accessed.add(prop);
|
|
696
|
+
}
|
|
697
|
+
return Reflect.get(target, prop, receiver);
|
|
698
|
+
},
|
|
699
|
+
});
|
|
700
|
+
try {
|
|
701
|
+
fn(trackingRow);
|
|
702
|
+
}
|
|
703
|
+
catch (e) { }
|
|
704
|
+
refineDependencies[key] = Array.from(accessed);
|
|
705
|
+
};
|
|
706
|
+
trackRefinementDependencies(refinements?.server, "server");
|
|
707
|
+
trackRefinementDependencies(refinements?.client, "client");
|
|
708
|
+
let refinedValidationSchema = finalValidationSchema;
|
|
709
|
+
if (refinements?.server) {
|
|
710
|
+
const serverRefine = refinements.server;
|
|
711
|
+
refinedValidationSchema = finalValidationSchema.superRefine((data, ctx) => {
|
|
712
|
+
const result = serverRefine(data);
|
|
713
|
+
if (!result)
|
|
714
|
+
return;
|
|
715
|
+
const errors = Array.isArray(result) ? result : [result];
|
|
716
|
+
for (const err of errors) {
|
|
717
|
+
ctx.addIssue({
|
|
718
|
+
code: z.ZodIssueCode.custom,
|
|
719
|
+
message: err.message,
|
|
720
|
+
path: err.path,
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
let refinedClientInputSchema = finalClientInputSchema;
|
|
726
|
+
if (refinements?.client) {
|
|
727
|
+
const clientRefine = refinements.client;
|
|
728
|
+
refinedClientInputSchema = finalClientInputSchema.superRefine((data, ctx) => {
|
|
729
|
+
const result = clientRefine(data);
|
|
730
|
+
if (!result)
|
|
731
|
+
return;
|
|
732
|
+
const errors = Array.isArray(result) ? result : [result];
|
|
733
|
+
for (const err of errors) {
|
|
734
|
+
ctx.addIssue({
|
|
735
|
+
code: z.ZodIssueCode.custom,
|
|
736
|
+
message: err.message,
|
|
737
|
+
path: err.path,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
}
|
|
681
742
|
return {
|
|
682
743
|
pk: pkKeys.length ? pkKeys : null,
|
|
683
744
|
clientPk: clientPkKeys.length ? clientPkKeys : null,
|
|
684
745
|
deriveDependencies,
|
|
746
|
+
refineDependencies,
|
|
685
747
|
isClientRecord,
|
|
686
748
|
sqlSchema: finalSqlSchema,
|
|
687
|
-
clientInputSchema:
|
|
749
|
+
clientInputSchema: refinedClientInputSchema,
|
|
688
750
|
clientSchema: finalClientSchema,
|
|
689
|
-
serverSchema:
|
|
751
|
+
serverSchema: refinedValidationSchema,
|
|
690
752
|
defaultValues: defaultValues,
|
|
691
753
|
stateType: {},
|
|
692
754
|
generateDefaults,
|
|
693
755
|
toClient,
|
|
694
756
|
toDb,
|
|
695
757
|
parseForDb: (appData) => {
|
|
696
|
-
const validData =
|
|
758
|
+
const validData = refinedValidationSchema.parse(appData);
|
|
697
759
|
return toDb(validData);
|
|
698
760
|
},
|
|
699
761
|
parsePatchForDb: (patchData) => {
|
|
@@ -849,6 +911,7 @@ export function createSchemaBox(schemas, resolutions) {
|
|
|
849
911
|
pk: zodSchemas.pk,
|
|
850
912
|
clientPk: zodSchemas.clientPk,
|
|
851
913
|
deriveDependencies: zodSchemas.deriveDependencies,
|
|
914
|
+
refineDependencies: zodSchemas.refineDependencies,
|
|
852
915
|
isClientRecord: zodSchemas.isClientRecord,
|
|
853
916
|
generateDefaults: zodSchemas.generateDefaults,
|
|
854
917
|
};
|
|
@@ -934,6 +997,7 @@ export function createSchemaBox(schemas, resolutions) {
|
|
|
934
997
|
pk: entry.pk,
|
|
935
998
|
clientPk: entry.clientPk,
|
|
936
999
|
deriveDependencies: entry.deriveDependencies,
|
|
1000
|
+
refineDependencies: entry.refineDependencies,
|
|
937
1001
|
isClientRecord: entry.isClientRecord,
|
|
938
1002
|
nav: createNavProxy(tableName, finalRegistry),
|
|
939
1003
|
createView: (selection) => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|