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
- constructor(db, meta, transforms, reconcile) {
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
- return rows.map((r) => this.transforms.parseFromDb(r));
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
- return this.transforms.parseFromDb(row);
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?: 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 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
- 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,34 @@ 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)
628
624
  continue;
629
625
  const dbKey = clientToDbKeys[clientKey] || clientKey;
630
626
  const transform = fieldTransforms[clientKey]?.toDb;
631
627
  dbObject[dbKey] = transform
632
- ? transform(clientWithDerives[clientKey])
633
- : clientWithDerives[clientKey];
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: { ...targetField.config.sql, isForeignKey: true },
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] = regEntry.rawSchema.__derives[key](baseMapped);
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.189",
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",