cogsbox-shape 0.5.196 → 0.5.198

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.
@@ -129,14 +129,13 @@ function createViewHydrator(db, registry, baseRegistryKey, selection) {
129
129
  }
130
130
  return meta;
131
131
  };
132
- const hydrate = async (row, currentRegistryKey, currentSelection) => {
133
- if (!row || typeof currentSelection !== "object")
134
- return row;
132
+ const hydrate = async (rows, currentRegistryKey, currentSelection) => {
133
+ if (!rows.length || typeof currentSelection !== "object")
134
+ return rows;
135
135
  const currentEntry = registry[currentRegistryKey];
136
136
  if (!currentEntry)
137
- return row;
137
+ return rows;
138
138
  const currentMeta = getMeta(currentRegistryKey);
139
- const hydrated = { ...row };
140
139
  for (const [relationKey, relationSelection] of Object.entries(currentSelection)) {
141
140
  if (!relationSelection)
142
141
  continue;
@@ -155,27 +154,41 @@ function createViewHydrator(db, registry, baseRegistryKey, selection) {
155
154
  if (!targetClientKey)
156
155
  continue;
157
156
  const targetDbName = dbNameForClientKey(targetMeta, targetClientKey);
158
- const fromValue = row[fromDbName];
159
- if (fromValue === undefined || fromValue === null) {
160
- hydrated[relationKey] = ["hasMany", "manyToMany"].includes(relationConfig.type)
161
- ? []
162
- : null;
157
+ const isCollection = ["hasMany", "manyToMany"].includes(relationConfig.type);
158
+ const fromValues = rows
159
+ .map((row) => row[fromDbName])
160
+ .filter((v) => v !== undefined && v !== null);
161
+ if (fromValues.length === 0) {
162
+ for (const row of rows) {
163
+ row[relationKey] = isCollection ? [] : null;
164
+ }
163
165
  continue;
164
166
  }
165
167
  const qb = db;
166
168
  const relatedRows = (await qb
167
169
  .selectFrom(targetMeta.tableName)
168
170
  .selectAll()
169
- .where(targetDbName, "=", fromValue)
171
+ .where(targetDbName, "in", fromValues)
170
172
  .execute());
171
- const hydratedRelated = await Promise.all(relatedRows.map((relatedRow) => hydrate(relatedRow, targetRegistryKey, relationSelection)));
172
- hydrated[relationKey] = ["hasMany", "manyToMany"].includes(relationConfig.type)
173
- ? hydratedRelated
174
- : hydratedRelated[0] ?? null;
173
+ const hydratedRelated = await hydrate(relatedRows, targetRegistryKey, relationSelection);
174
+ const grouped = new Map();
175
+ for (const related of hydratedRelated) {
176
+ const key = related[targetDbName];
177
+ if (!grouped.has(key))
178
+ grouped.set(key, []);
179
+ grouped.get(key).push(related);
180
+ }
181
+ for (const row of rows) {
182
+ const fk = row[fromDbName];
183
+ const matches = fk !== undefined && fk !== null ? grouped.get(fk) : undefined;
184
+ row[relationKey] = isCollection
185
+ ? (matches ?? [])
186
+ : (matches?.[0] ?? null);
187
+ }
175
188
  }
176
- return hydrated;
189
+ return rows;
177
190
  };
178
- return (row) => hydrate(row, baseRegistryKey, selection);
191
+ return (rows) => hydrate(rows, baseRegistryKey, selection);
179
192
  }
180
193
  export function connect(box, db) {
181
194
  const result = {};
@@ -198,7 +211,7 @@ export function connect(box, db) {
198
211
  const registry = view.__registry;
199
212
  const baseTable = view.baseTable;
200
213
  const viewSelection = view.viewSelection;
201
- const hydrateRow = registry && baseTable && viewSelection
214
+ const hydrateRows = registry && baseTable && viewSelection
202
215
  ? createViewHydrator(db, registry, baseTable, viewSelection)
203
216
  : undefined;
204
217
  const viewDb = new TableDB(db, viewMeta, {
@@ -207,7 +220,7 @@ export function connect(box, db) {
207
220
  parseForDb: viewTransforms.parseForDb ?? ((r) => r),
208
221
  parsePatchForDb: viewTransforms.parsePatchForDb ?? viewTransforms.toDb ?? ((r) => r),
209
222
  parseFromDb: viewTransforms.parseFromDb ?? ((r) => r),
210
- }, reconcile, hydrateRow);
223
+ }, reconcile, hydrateRows);
211
224
  return new Proxy(view, {
212
225
  get(target, prop, receiver) {
213
226
  if (prop in viewDb) {
@@ -5,13 +5,13 @@ type RequiredKeys<T> = {
5
5
  [K in keyof T]-?: Record<string, never> extends Pick<T, K> ? never : K;
6
6
  }[keyof T];
7
7
  type InsertDbOnlyArgs<T extends Record<string, unknown>> = keyof T extends never ? [] : RequiredKeys<T> extends never ? [dbOnlyData?: Partial<T>] : [dbOnlyData: T];
8
- export type TableDBApi<TClient extends Record<string, unknown>, TCreate, TDbOnly extends Record<string, unknown> = Record<string, never>> = Pick<TableDB<TClient, TCreate, TDbOnly>, "findMany" | "findById" | "byId" | "insert" | "create" | "update" | "delete" | "count" | "reconcileIds">;
8
+ export type TableDBApi<TClient extends Record<string, unknown>, TCreate, TDbOnly extends Record<string, unknown> = Record<string, never>> = Pick<TableDB<TClient, TCreate, TDbOnly>, "findMany" | "findById" | "byId" | "insert" | "insertOrIgnore" | "create" | "update" | "delete" | "deleteMany" | "count" | "reconcileIds" | "kysely">;
9
9
  export declare class TableDB<TClient extends Record<string, unknown>, TCreate, TDbOnly extends Record<string, unknown> = Record<string, never>> {
10
10
  private db;
11
11
  private meta;
12
12
  private transforms;
13
13
  private reconcile?;
14
- private hydrateRow?;
14
+ private hydrateRows?;
15
15
  constructor(db: Kysely<any>, meta: TableMeta, transforms: {
16
16
  toClient: (row: Record<string, unknown>) => TClient;
17
17
  toDb: (row: Record<string, unknown>) => Record<string, unknown>;
@@ -20,7 +20,8 @@ export declare class TableDB<TClient extends Record<string, unknown>, TCreate, T
20
20
  parseFromDb: (data: Record<string, unknown>) => TClient;
21
21
  }, reconcile?: ((clientData: unknown) => {
22
22
  withServer: (serverData: unknown) => unknown;
23
- }) | undefined, hydrateRow?: ((row: Record<string, unknown>) => Promise<Record<string, unknown>>) | undefined);
23
+ }) | undefined, hydrateRows?: ((rows: Record<string, unknown>[]) => Promise<Record<string, unknown>[]>) | undefined);
24
+ get kysely(): Kysely<any>;
24
25
  findMany(opts?: FindManyOpts<TClient>): Promise<TClient[]>;
25
26
  findById(id: unknown): Promise<TClient | null>;
26
27
  byId(id: unknown): {
@@ -55,6 +56,13 @@ export declare class TableDB<TClient extends Record<string, unknown>, TCreate, T
55
56
  delete(id: unknown): Promise<{
56
57
  deleted: boolean;
57
58
  }>;
59
+ deleteMany(where: WhereInput<Partial<TClient>>): Promise<{
60
+ deleted: number;
61
+ }>;
62
+ insertOrIgnore(data: TCreate, ...args: InsertDbOnlyArgs<TDbOnly>): Promise<{
63
+ ids: () => Promise<Record<string, unknown>>;
64
+ full: () => Promise<TClient>;
65
+ }>;
58
66
  count(where?: WhereInput<Partial<TClient>>): Promise<number>;
59
67
  }
60
68
  export {};
@@ -6,13 +6,16 @@ export class TableDB {
6
6
  meta;
7
7
  transforms;
8
8
  reconcile;
9
- hydrateRow;
10
- constructor(db, meta, transforms, reconcile, hydrateRow) {
9
+ hydrateRows;
10
+ constructor(db, meta, transforms, reconcile, hydrateRows) {
11
11
  this.db = db;
12
12
  this.meta = meta;
13
13
  this.transforms = transforms;
14
14
  this.reconcile = reconcile;
15
- this.hydrateRow = hydrateRow;
15
+ this.hydrateRows = hydrateRows;
16
+ }
17
+ get kysely() {
18
+ return this.db;
16
19
  }
17
20
  async findMany(opts) {
18
21
  const qb = this.db;
@@ -38,8 +41,8 @@ export class TableDB {
38
41
  query = query.offset(opts.offset);
39
42
  }
40
43
  const rows = (await query.execute());
41
- const hydratedRows = this.hydrateRow
42
- ? await Promise.all(rows.map((row) => this.hydrateRow(row)))
44
+ const hydratedRows = this.hydrateRows
45
+ ? await this.hydrateRows(rows)
43
46
  : rows;
44
47
  return hydratedRows.map((r) => this.transforms.parseFromDb(r));
45
48
  }
@@ -59,8 +62,10 @@ export class TableDB {
59
62
  const row = rows[0] ?? null;
60
63
  if (!row)
61
64
  return null;
62
- const hydratedRow = this.hydrateRow ? await this.hydrateRow(row) : row;
63
- return this.transforms.parseFromDb(hydratedRow);
65
+ const hydratedRows = this.hydrateRows
66
+ ? await this.hydrateRows([row])
67
+ : [row];
68
+ return this.transforms.parseFromDb(hydratedRows[0]);
64
69
  }
65
70
  byId(id) {
66
71
  return {
@@ -83,7 +88,7 @@ export class TableDB {
83
88
  const dbOnlyData = args[0];
84
89
  return this.insertIds(data, dbOnlyData);
85
90
  }
86
- async insertIds(data, dbOnlyData) {
91
+ async insertIds(data, dbOnlyData, opts) {
87
92
  const dbData = this.transforms.parseForDb(data);
88
93
  const parsedDbOnlyData = this.parseDbOnlyData(dbOnlyData, {
89
94
  requireRequired: true,
@@ -102,10 +107,11 @@ export class TableDB {
102
107
  }
103
108
  Object.assign(insertData, parsedDbOnlyData);
104
109
  const qb = this.db;
105
- const result = await qb
106
- .insertInto(this.meta.tableName)
107
- .values(insertData)
108
- .execute();
110
+ let query = qb.insertInto(this.meta.tableName).values(insertData);
111
+ if (opts?.onConflict === "ignore") {
112
+ query = query.onConflict((oc) => oc.doNothing());
113
+ }
114
+ const result = await query.execute();
109
115
  const insertId = result[0]?.insertId;
110
116
  if (insertId !== undefined && this.meta.pkFields.length > 0) {
111
117
  const dbPkField = this.meta.pkFields[0];
@@ -152,7 +158,7 @@ export class TableDB {
152
158
  .set(dbData)
153
159
  .where(sql.join(conditions, sql ` AND `))
154
160
  .execute();
155
- const numUpdated = result[0]?.numUpdatedRows ?? 0n;
161
+ const numUpdated = result[0]?.numUpdatedRows ?? result[0]?.numAffectedRows ?? 0n;
156
162
  if (Number(numUpdated) === 0) {
157
163
  throw new RecordNotFoundError(this.meta.tableName, id);
158
164
  }
@@ -314,9 +320,30 @@ export class TableDB {
314
320
  .deleteFrom(this.meta.tableName)
315
321
  .where(sql.join(conditions, sql ` AND `))
316
322
  .execute();
317
- const numDeleted = result[0]?.numDeletedRows ?? 0n;
323
+ const numDeleted = result[0]?.numDeletedRows ?? result[0]?.numAffectedRows ?? 0n;
318
324
  return { deleted: Number(numDeleted) > 0 };
319
325
  }
326
+ async deleteMany(where) {
327
+ const qb = this.db;
328
+ let query = qb.deleteFrom(this.meta.tableName);
329
+ const conditions = buildWhereConditions(where, this.meta);
330
+ if (conditions.length > 0) {
331
+ query = query.where(sql.join(conditions, sql ` AND `));
332
+ }
333
+ const result = await query.execute();
334
+ const numDeleted = result[0]?.numDeletedRows ?? result[0]?.numAffectedRows ?? 0n;
335
+ return { deleted: Number(numDeleted) };
336
+ }
337
+ async insertOrIgnore(data, ...args) {
338
+ const dbOnlyData = args[0];
339
+ return {
340
+ ids: () => this.insertIds(data, dbOnlyData, { onConflict: "ignore" }),
341
+ full: async () => {
342
+ const ids = await this.insertIds(data, dbOnlyData, { onConflict: "ignore" });
343
+ return this.reconcileIds(data, ids);
344
+ },
345
+ };
346
+ }
320
347
  async count(where) {
321
348
  const qb = this.db;
322
349
  let query = qb
package/dist/schema.d.ts CHANGED
@@ -215,9 +215,13 @@ type RefineEntry = {
215
215
  deps: string[] | null;
216
216
  check: (row: any) => RefinementError | RefinementError[] | undefined | null;
217
217
  };
218
- type RefineHelper = {
219
- (layers: RefineLayer | RefineLayer[], check: (row: any) => RefinementError | RefinementError[] | undefined | null): RefineEntry;
220
- (layers: RefineLayer | RefineLayer[], check: (row: any) => RefinementError | RefinementError[] | undefined | null, deps: string | string[]): RefineEntry;
218
+ type RefineHelper<T extends ShapeSchema> = {
219
+ (layer: "client", check: (row: InferClientRow<T>) => RefinementError | RefinementError[] | undefined | null, deps?: string | string[]): RefineEntry;
220
+ (layer: "clientInput", check: (row: InferClientInputRow<T>) => RefinementError | RefinementError[] | undefined | null, deps?: string | string[]): RefineEntry;
221
+ (layer: "server", check: (row: InferValidationRow<T>) => RefinementError | RefinementError[] | undefined | null, deps?: string | string[]): RefineEntry;
222
+ (layer: "sql", check: (row: InferSqlRow<T>) => RefinementError | RefinementError[] | undefined | null, deps?: string | string[]): RefineEntry;
223
+ (layer: "all", check: (row: InferClientRow<T>) => RefinementError | RefinementError[] | undefined | null, deps?: string | string[]): RefineEntry;
224
+ (layer: RefineLayer[], check: (row: InferClientRow<T>) => RefinementError | RefinementError[] | undefined | null, deps?: string | string[]): RefineEntry;
221
225
  };
222
226
  type PickPrimaryKeys<T extends ShapeSchema> = {
223
227
  [K in keyof T as T[K] extends {
@@ -245,6 +249,9 @@ type PickDbFieldKeys<T extends ShapeSchema> = {
245
249
  } ? never : K : never;
246
250
  }[keyof T];
247
251
  type InferClientRow<T extends ShapeSchema> = Prettify<z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<T, "zodClientSchema">>>>>;
252
+ type InferClientInputRow<T extends ShapeSchema> = Prettify<z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<T, "zodClientInputSchema">>>>>;
253
+ type InferValidationRow<T extends ShapeSchema> = Prettify<z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<T, "zodValidationSchema">>>>>;
254
+ type InferSqlRow<T extends ShapeSchema> = Prettify<z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<T, "zodSqlSchema">>>>>;
248
255
  type SchemaBuilder<T extends ShapeSchema> = Prettify<EnrichFields<T>> & {
249
256
  __primaryKeySQL?: string;
250
257
  __derives?: {
@@ -261,7 +268,7 @@ type SchemaBuilder<T extends ShapeSchema> = Prettify<EnrichFields<T>> & {
261
268
  [K in PickDbFieldKeys<T>]?: (row: InferClientRow<T>) => any;
262
269
  };
263
270
  }) => SchemaBuilder<T>;
264
- refine: (fn: (r: RefineHelper) => RefineEntry[]) => SchemaBuilder<T>;
271
+ refine: (fn: (r: RefineHelper<T>) => RefineEntry[]) => SchemaBuilder<T>;
265
272
  };
266
273
  export declare function schema<T extends string, U extends ShapeSchema<T>>(schema: U): SchemaBuilder<U>;
267
274
  export type RelationType = "hasMany" | "hasOne" | "manyToMany";
@@ -1071,7 +1071,7 @@ describe("refine", () => {
1071
1071
  password: s.sqlite({ type: "varchar" }).clientInput({ value: "" }),
1072
1072
  confirmPassword: s.sqlite({ type: "varchar" }).clientInput({ value: "" }),
1073
1073
  }).refine((r) => [
1074
- r("clientInput", (row) => {
1074
+ r(["clientInput", "client"], (row) => {
1075
1075
  if (row.password !== row.confirmPassword) {
1076
1076
  return {
1077
1077
  path: ["confirmPassword"],
@@ -10,7 +10,7 @@ describe("refine runtime behavior", () => {
10
10
  max: s.sqlite({ type: "int", nullable: true }).clientInput({ value: null, schema: z.number().nullable() }),
11
11
  label: s.sqlite({ type: "varchar" }).clientInput({ value: "" }),
12
12
  }).refine((r) => [
13
- r("clientInput", (row) => {
13
+ r(["clientInput", "client"], (row) => {
14
14
  if (row.min !== null && row.max !== null && row.min >= row.max) {
15
15
  return { path: ["max"], message: "Max must be > min" };
16
16
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cogsbox-shape",
3
- "version": "0.5.196",
3
+ "version": "0.5.198",
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",