cogsbox-shape 0.5.189 → 0.5.191
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.
|
@@ -50,8 +50,9 @@ function extractTableMeta(entry) {
|
|
|
50
50
|
clientToDbName.set(key, dbName);
|
|
51
51
|
if (sqlConfig.pk)
|
|
52
52
|
pkFields.push(dbName);
|
|
53
|
-
if (sqlConfig.isClientPk)
|
|
53
|
+
if (sqlConfig.isClientPk && !sqlConfig.isForeignKey) {
|
|
54
54
|
clientPkFields.push(key);
|
|
55
|
+
}
|
|
55
56
|
if (sqlConfig.sqlOnly) {
|
|
56
57
|
sqlOnlyFields.add(dbName);
|
|
57
58
|
sqlOnlyClientFields.add(key);
|
|
@@ -96,6 +97,84 @@ function enhanceTable(entry, meta, db) {
|
|
|
96
97
|
},
|
|
97
98
|
});
|
|
98
99
|
}
|
|
100
|
+
function registryKeyForTableName(registry, tableName) {
|
|
101
|
+
return Object.keys(registry).find((key) => registry[key]?.rawSchema?._tableName === tableName);
|
|
102
|
+
}
|
|
103
|
+
function dbNameForClientKey(meta, clientKey) {
|
|
104
|
+
return meta.clientToDbName.get(clientKey) ?? clientKey;
|
|
105
|
+
}
|
|
106
|
+
function clientKeyForRelationTarget(targetField, targetDefinition) {
|
|
107
|
+
const metaKey = targetField?.__meta?._key;
|
|
108
|
+
if (typeof metaKey === "string")
|
|
109
|
+
return metaKey;
|
|
110
|
+
for (const [key, field] of Object.entries(targetDefinition)) {
|
|
111
|
+
if (field === targetField)
|
|
112
|
+
return key;
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
function createViewHydrator(db, registry, baseRegistryKey, selection) {
|
|
117
|
+
const metas = new Map();
|
|
118
|
+
const getMeta = (registryKey) => {
|
|
119
|
+
let meta = metas.get(registryKey);
|
|
120
|
+
if (!meta) {
|
|
121
|
+
const entry = registry[registryKey];
|
|
122
|
+
meta = extractTableMeta({
|
|
123
|
+
definition: entry?.rawSchema,
|
|
124
|
+
deriveDependencies: entry?.deriveDependencies,
|
|
125
|
+
});
|
|
126
|
+
metas.set(registryKey, meta);
|
|
127
|
+
}
|
|
128
|
+
return meta;
|
|
129
|
+
};
|
|
130
|
+
const hydrate = async (row, currentRegistryKey, currentSelection) => {
|
|
131
|
+
if (!row || typeof currentSelection !== "object")
|
|
132
|
+
return row;
|
|
133
|
+
const currentEntry = registry[currentRegistryKey];
|
|
134
|
+
if (!currentEntry)
|
|
135
|
+
return row;
|
|
136
|
+
const currentMeta = getMeta(currentRegistryKey);
|
|
137
|
+
const hydrated = { ...row };
|
|
138
|
+
for (const [relationKey, relationSelection] of Object.entries(currentSelection)) {
|
|
139
|
+
if (!relationSelection)
|
|
140
|
+
continue;
|
|
141
|
+
const relationField = currentEntry.rawSchema?.[relationKey];
|
|
142
|
+
const relationConfig = relationField?.config?.sql;
|
|
143
|
+
if (!relationConfig?.schema)
|
|
144
|
+
continue;
|
|
145
|
+
const targetTableName = relationConfig.schema()._tableName;
|
|
146
|
+
const targetRegistryKey = registryKeyForTableName(registry, targetTableName);
|
|
147
|
+
if (!targetRegistryKey)
|
|
148
|
+
continue;
|
|
149
|
+
const targetEntry = registry[targetRegistryKey];
|
|
150
|
+
const targetMeta = getMeta(targetRegistryKey);
|
|
151
|
+
const fromDbName = dbNameForClientKey(currentMeta, relationConfig.fromKey);
|
|
152
|
+
const targetClientKey = clientKeyForRelationTarget(relationConfig.toKey?.(), targetEntry.rawSchema);
|
|
153
|
+
if (!targetClientKey)
|
|
154
|
+
continue;
|
|
155
|
+
const targetDbName = dbNameForClientKey(targetMeta, targetClientKey);
|
|
156
|
+
const fromValue = row[fromDbName];
|
|
157
|
+
if (fromValue === undefined || fromValue === null) {
|
|
158
|
+
hydrated[relationKey] = ["hasMany", "manyToMany"].includes(relationConfig.type)
|
|
159
|
+
? []
|
|
160
|
+
: null;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const qb = db;
|
|
164
|
+
const relatedRows = (await qb
|
|
165
|
+
.selectFrom(targetMeta.tableName)
|
|
166
|
+
.selectAll()
|
|
167
|
+
.where(targetDbName, "=", fromValue)
|
|
168
|
+
.execute());
|
|
169
|
+
const hydratedRelated = await Promise.all(relatedRows.map((relatedRow) => hydrate(relatedRow, targetRegistryKey, relationSelection)));
|
|
170
|
+
hydrated[relationKey] = ["hasMany", "manyToMany"].includes(relationConfig.type)
|
|
171
|
+
? hydratedRelated
|
|
172
|
+
: hydratedRelated[0] ?? null;
|
|
173
|
+
}
|
|
174
|
+
return hydrated;
|
|
175
|
+
};
|
|
176
|
+
return (row) => hydrate(row, baseRegistryKey, selection);
|
|
177
|
+
}
|
|
99
178
|
export function connect(box, db) {
|
|
100
179
|
const result = {};
|
|
101
180
|
for (const key of Object.keys(box)) {
|
|
@@ -114,13 +193,19 @@ export function connect(box, db) {
|
|
|
114
193
|
const viewMeta = { ...meta };
|
|
115
194
|
const viewTransforms = view.transforms ?? {};
|
|
116
195
|
const reconcile = view.reconcile;
|
|
196
|
+
const registry = view.__registry;
|
|
197
|
+
const baseTable = view.baseTable;
|
|
198
|
+
const viewSelection = view.viewSelection;
|
|
199
|
+
const hydrateRow = registry && baseTable && viewSelection
|
|
200
|
+
? createViewHydrator(db, registry, baseTable, viewSelection)
|
|
201
|
+
: undefined;
|
|
117
202
|
const viewDb = new TableDB(db, viewMeta, {
|
|
118
203
|
toClient: viewTransforms.toClient ?? ((r) => r),
|
|
119
204
|
toDb: viewTransforms.toDb ?? ((r) => r),
|
|
120
205
|
parseForDb: viewTransforms.parseForDb ?? ((r) => r),
|
|
121
206
|
parsePatchForDb: viewTransforms.parsePatchForDb ?? viewTransforms.toDb ?? ((r) => r),
|
|
122
207
|
parseFromDb: viewTransforms.parseFromDb ?? ((r) => r),
|
|
123
|
-
}, reconcile);
|
|
208
|
+
}, reconcile, hydrateRow);
|
|
124
209
|
return new Proxy(view, {
|
|
125
210
|
get(target, prop, receiver) {
|
|
126
211
|
if (prop === "db")
|
|
@@ -10,6 +10,7 @@ export declare class TableDB<TClient extends Record<string, unknown>, TCreate, T
|
|
|
10
10
|
private meta;
|
|
11
11
|
private transforms;
|
|
12
12
|
private reconcile?;
|
|
13
|
+
private hydrateRow?;
|
|
13
14
|
constructor(db: Kysely<unknown>, meta: TableMeta, transforms: {
|
|
14
15
|
toClient: (row: Record<string, unknown>) => TClient;
|
|
15
16
|
toDb: (row: Record<string, unknown>) => Record<string, unknown>;
|
|
@@ -18,7 +19,7 @@ export declare class TableDB<TClient extends Record<string, unknown>, TCreate, T
|
|
|
18
19
|
parseFromDb: (data: Record<string, unknown>) => TClient;
|
|
19
20
|
}, reconcile?: ((clientData: unknown) => {
|
|
20
21
|
withServer: (serverData: unknown) => unknown;
|
|
21
|
-
}) | undefined);
|
|
22
|
+
}) | undefined, hydrateRow?: ((row: Record<string, unknown>) => Promise<Record<string, unknown>>) | undefined);
|
|
22
23
|
findMany(opts?: FindManyOpts<TClient>): Promise<TClient[]>;
|
|
23
24
|
findById(id: unknown): Promise<TClient | null>;
|
|
24
25
|
insert(data: TCreate, ...args: InsertDbOnlyArgs<TDbOnly>): {
|
|
@@ -6,11 +6,13 @@ export class TableDB {
|
|
|
6
6
|
meta;
|
|
7
7
|
transforms;
|
|
8
8
|
reconcile;
|
|
9
|
-
|
|
9
|
+
hydrateRow;
|
|
10
|
+
constructor(db, meta, transforms, reconcile, hydrateRow) {
|
|
10
11
|
this.db = db;
|
|
11
12
|
this.meta = meta;
|
|
12
13
|
this.transforms = transforms;
|
|
13
14
|
this.reconcile = reconcile;
|
|
15
|
+
this.hydrateRow = hydrateRow;
|
|
14
16
|
}
|
|
15
17
|
async findMany(opts) {
|
|
16
18
|
const qb = this.db;
|
|
@@ -36,7 +38,10 @@ export class TableDB {
|
|
|
36
38
|
query = query.offset(opts.offset);
|
|
37
39
|
}
|
|
38
40
|
const rows = (await query.execute());
|
|
39
|
-
|
|
41
|
+
const hydratedRows = this.hydrateRow
|
|
42
|
+
? await Promise.all(rows.map((row) => this.hydrateRow(row)))
|
|
43
|
+
: rows;
|
|
44
|
+
return hydratedRows.map((r) => this.transforms.parseFromDb(r));
|
|
40
45
|
}
|
|
41
46
|
async findById(id) {
|
|
42
47
|
const pkValues = Array.isArray(id) ? id : [id];
|
|
@@ -54,7 +59,8 @@ export class TableDB {
|
|
|
54
59
|
const row = rows[0] ?? null;
|
|
55
60
|
if (!row)
|
|
56
61
|
return null;
|
|
57
|
-
|
|
62
|
+
const hydratedRow = this.hydrateRow ? await this.hydrateRow(row) : row;
|
|
63
|
+
return this.transforms.parseFromDb(hydratedRow);
|
|
58
64
|
}
|
|
59
65
|
insert(data, ...args) {
|
|
60
66
|
const dbOnlyData = args[0];
|
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 PickSqlOnlyKeys<T extends ShapeSchema> = {
|
|
205
|
+
[K in keyof T]: T[K] extends {
|
|
206
|
+
config: {
|
|
207
|
+
sql: {
|
|
208
|
+
sqlOnly: true;
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
} ? 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 PickSqlOnlyKeys<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,34 @@ 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)
|
|
628
624
|
continue;
|
|
629
625
|
const dbKey = clientToDbKeys[clientKey] || clientKey;
|
|
630
626
|
const transform = fieldTransforms[clientKey]?.toDb;
|
|
631
627
|
dbObject[dbKey] = transform
|
|
632
|
-
? transform(
|
|
633
|
-
:
|
|
628
|
+
? transform(clientObject[clientKey])
|
|
629
|
+
: clientObject[clientKey];
|
|
630
|
+
}
|
|
631
|
+
// 2. Map Database ONLY derives directly to the dbObject
|
|
632
|
+
if (derives?.forDb) {
|
|
633
|
+
for (const schemaKey in derives.forDb) {
|
|
634
|
+
// Resolve custom DB column name if they used s.sql({ field: "custom_name" })
|
|
635
|
+
const sqlConfig = fullSchema[schemaKey]?.config?.sql;
|
|
636
|
+
const dbKey = sqlConfig?.field || schemaKey;
|
|
637
|
+
dbObject[dbKey] = derives.forDb[schemaKey]?.(clientObject);
|
|
638
|
+
}
|
|
634
639
|
}
|
|
635
640
|
return dbObject;
|
|
636
641
|
};
|
|
@@ -639,9 +644,9 @@ export function createSchema(schema, relations) {
|
|
|
639
644
|
const finalClientSchema = z.object(clientFields);
|
|
640
645
|
const finalValidationSchema = z.object(serverFields);
|
|
641
646
|
const deriveDependencies = {};
|
|
642
|
-
if (derives) {
|
|
647
|
+
if (derives?.forClient) {
|
|
643
648
|
const trackingSeed = { ...defaultValues };
|
|
644
|
-
for (const key in derives) {
|
|
649
|
+
for (const key in derives.forClient) {
|
|
645
650
|
const accessed = new Set();
|
|
646
651
|
const trackingRow = new Proxy(trackingSeed, {
|
|
647
652
|
get(target, prop, receiver) {
|
|
@@ -652,7 +657,7 @@ export function createSchema(schema, relations) {
|
|
|
652
657
|
},
|
|
653
658
|
});
|
|
654
659
|
try {
|
|
655
|
-
derives[key](trackingRow);
|
|
660
|
+
derives.forClient[key]?.(trackingRow);
|
|
656
661
|
}
|
|
657
662
|
catch (e) { }
|
|
658
663
|
deriveDependencies[key] = Array.from(accessed);
|
|
@@ -760,7 +765,11 @@ export function createSchemaBox(schemas, resolutions) {
|
|
|
760
765
|
if (targetField && targetField.config) {
|
|
761
766
|
const newConfig = {
|
|
762
767
|
...targetField.config,
|
|
763
|
-
sql: {
|
|
768
|
+
sql: {
|
|
769
|
+
...targetField.config.sql,
|
|
770
|
+
field: fieldName,
|
|
771
|
+
isForeignKey: true,
|
|
772
|
+
},
|
|
764
773
|
};
|
|
765
774
|
resolvedSchemas[tableName][fieldName] = {
|
|
766
775
|
...targetField,
|
|
@@ -941,9 +950,10 @@ export function createSchemaBox(schemas, resolutions) {
|
|
|
941
950
|
}
|
|
942
951
|
}
|
|
943
952
|
}
|
|
944
|
-
if (regEntry.rawSchema.__derives) {
|
|
945
|
-
for (const key in regEntry.rawSchema.__derives) {
|
|
946
|
-
baseMapped[key] =
|
|
953
|
+
if (regEntry.rawSchema.__derives?.forClient) {
|
|
954
|
+
for (const key in regEntry.rawSchema.__derives.forClient) {
|
|
955
|
+
baseMapped[key] =
|
|
956
|
+
regEntry.rawSchema.__derives.forClient[key](baseMapped);
|
|
947
957
|
}
|
|
948
958
|
}
|
|
949
959
|
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.191",
|
|
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",
|