@gencow/core 0.1.18 → 0.1.21

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.
Files changed (43) hide show
  1. package/dist/crud.d.ts +18 -0
  2. package/dist/crud.js +231 -50
  3. package/dist/index.d.ts +3 -2
  4. package/dist/index.js +2 -2
  5. package/dist/rls-db.d.ts +3 -5
  6. package/dist/rls-db.js +3 -5
  7. package/dist/rls.d.ts +44 -1
  8. package/dist/rls.js +62 -2
  9. package/dist/server.d.ts +1 -0
  10. package/dist/storage.d.ts +29 -2
  11. package/dist/storage.js +404 -15
  12. package/dist/v.js +5 -1
  13. package/package.json +42 -39
  14. package/src/__tests__/crud-owner-rls.test.ts +380 -0
  15. package/src/__tests__/fixtures/basic/auth.ts +32 -0
  16. package/src/__tests__/fixtures/basic/drizzle.config.ts +15 -0
  17. package/src/__tests__/fixtures/basic/index.ts +6 -0
  18. package/src/__tests__/fixtures/basic/migrations/0000_faithful_silver_sable.sql +66 -0
  19. package/src/__tests__/fixtures/basic/migrations/meta/0000_snapshot.json +438 -0
  20. package/src/__tests__/fixtures/basic/migrations/meta/_journal.json +13 -0
  21. package/src/__tests__/fixtures/basic/schema.ts +35 -0
  22. package/src/__tests__/fixtures/basic/tasks.ts +15 -0
  23. package/src/__tests__/fixtures/common/auth-schema.ts +63 -0
  24. package/src/__tests__/helpers/pglite-migrations.ts +35 -0
  25. package/src/__tests__/helpers/pglite-rls-session.ts +54 -0
  26. package/src/__tests__/helpers/seed-like-fill.ts +196 -0
  27. package/src/__tests__/helpers/test-gencow-ctx-rls.ts +53 -0
  28. package/src/__tests__/image-optimization.test.ts +652 -0
  29. package/src/__tests__/rls-crud-basic.test.ts +431 -0
  30. package/src/__tests__/storage.test.ts +113 -0
  31. package/src/__tests__/tsconfig.json +8 -0
  32. package/src/__tests__/validator.test.ts +35 -0
  33. package/src/crud.ts +270 -47
  34. package/src/index.ts +3 -2
  35. package/src/rls-db.ts +3 -5
  36. package/src/rls.ts +87 -3
  37. package/src/server.ts +1 -0
  38. package/src/storage.ts +481 -15
  39. package/src/v.ts +5 -1
  40. package/dist/scoped-db.d.ts +0 -34
  41. package/dist/scoped-db.js +0 -364
  42. package/dist/table.d.ts +0 -67
  43. package/dist/table.js +0 -98
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Merge explicit partial insert rows with drizzle-seed-style values for missing columns.
3
+ * Uses drizzle-seed's `SeedService.selectGeneratorForPostgresColumn` so new schema columns get
4
+ * the same generators as `seed()` without listing every column in test fixtures.
5
+ */
6
+
7
+ import { getTableColumns, getTableName } from "drizzle-orm";
8
+ import type { InferInsertModel } from "drizzle-orm";
9
+ import type { PgColumn, PgTable } from "drizzle-orm/pg-core";
10
+ import { SeedService } from "drizzle-seed";
11
+
12
+ type SeedSvc = InstanceType<typeof SeedService>;
13
+ type SeedTable = Parameters<SeedSvc["selectGeneratorForPostgresColumn"]>[0];
14
+ type SeedColumn = Parameters<SeedSvc["selectGeneratorForPostgresColumn"]>[1];
15
+
16
+ /** Fields drizzle-seed reads from ORM columns (mirrors `getPostgresInfo` in drizzle-seed). */
17
+ type PgColumnSeedExtras = PgColumn & {
18
+ size?: number;
19
+ baseColumn?: PgColumn;
20
+ };
21
+
22
+ function pgColumnForSeed(column: PgColumn): PgColumnSeedExtras {
23
+ return column as PgColumnSeedExtras;
24
+ }
25
+
26
+ function hashSeed(str: string): number {
27
+ let h = 0;
28
+ for (let i = 0; i < str.length; i++) {
29
+ h = (Math.imul(31, h) + str.charCodeAt(i)) | 0;
30
+ }
31
+ return h >>> 0;
32
+ }
33
+
34
+ function getTypeParams(sqlType: string): SeedColumn["typeParams"] {
35
+ const typeParams: Record<string, number> = {};
36
+ if (sqlType.includes("[")) {
37
+ const match = sqlType.match(/\[\w*]/g);
38
+ if (match) {
39
+ typeParams.dimensions = match.length;
40
+ }
41
+ }
42
+ if (
43
+ sqlType.startsWith("numeric") ||
44
+ sqlType.startsWith("decimal") ||
45
+ sqlType.startsWith("double precision") ||
46
+ sqlType.startsWith("real")
47
+ ) {
48
+ const match = sqlType.match(/\((\d+), *(\d+)\)/);
49
+ if (match) {
50
+ typeParams.precision = Number(match[1]);
51
+ typeParams.scale = Number(match[2]);
52
+ }
53
+ } else if (
54
+ sqlType.startsWith("varchar") ||
55
+ sqlType.startsWith("bpchar") ||
56
+ sqlType.startsWith("char") ||
57
+ sqlType.startsWith("bit") ||
58
+ sqlType.startsWith("time") ||
59
+ sqlType.startsWith("timestamp") ||
60
+ sqlType.startsWith("interval")
61
+ ) {
62
+ const match = sqlType.match(/\((\d+)\)/);
63
+ if (match) {
64
+ typeParams.length = Number(match[1]);
65
+ }
66
+ }
67
+ return typeParams;
68
+ }
69
+
70
+ function getAllBaseColumns(baseColumn: PgColumn): NonNullable<SeedColumn["baseColumn"]> {
71
+ const b = pgColumnForSeed(baseColumn);
72
+ return {
73
+ name: baseColumn.name,
74
+ columnType: baseColumn.getSQLType(),
75
+ typeParams: getTypeParams(baseColumn.getSQLType()),
76
+ dataType: baseColumn.dataType,
77
+ size: b.size,
78
+ hasDefault: baseColumn.hasDefault,
79
+ default: baseColumn.default,
80
+ enumValues: baseColumn.enumValues,
81
+ isUnique: baseColumn.isUnique,
82
+ notNull: baseColumn.notNull,
83
+ primary: baseColumn.primary,
84
+ baseColumn:
85
+ b.baseColumn === undefined ? undefined : getAllBaseColumns(b.baseColumn),
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Drizzle `PgTable` → drizzle-seed `Table` (same shape as `getPostgresInfo` in drizzle-seed).
91
+ */
92
+ export function drizzlePgTableToSeedTable(
93
+ pgTable: PgTable,
94
+ schemaKey: string
95
+ ): SeedTable {
96
+ const colsMap = getTableColumns(pgTable) as Record<string, PgColumn>;
97
+ const columns: SeedColumn[] = Object.entries(colsMap).map(([tsName, column]) => {
98
+ const c = pgColumnForSeed(column);
99
+ return {
100
+ name: tsName,
101
+ columnType: column.getSQLType(),
102
+ typeParams: getTypeParams(column.getSQLType()),
103
+ dataType: column.dataType,
104
+ size: c.size,
105
+ hasDefault: column.hasDefault,
106
+ default: column.default,
107
+ enumValues: column.enumValues,
108
+ isUnique: column.isUnique,
109
+ notNull: column.notNull,
110
+ primary: column.primary,
111
+ generatedIdentityType: column.generatedIdentity?.type,
112
+ baseColumn:
113
+ c.baseColumn === undefined ? undefined : getAllBaseColumns(c.baseColumn),
114
+ };
115
+ });
116
+ const primaryKeys = Object.keys(colsMap).filter((k) => colsMap[k]!.primary);
117
+ return { name: schemaKey, columns, primaryKeys };
118
+ }
119
+
120
+ /**
121
+ * For each row, use explicit keys from `partialRows[i]`; for any other column, run the same
122
+ * generator drizzle-seed would use for that column type (same `generate({ i })` loop as seed).
123
+ *
124
+ * `schemaKey` for drizzle-seed defaults to `getTableName(pgTable)` (the SQL table name from
125
+ * `pgTable("name", …)`). Pass `options.schemaKey` only when you need a different seed namespace.
126
+ */
127
+ export function fillPartialRowsForInsert<T extends PgTable>(
128
+ pgTable: T,
129
+ partialRows: Array<Partial<InferInsertModel<T>>>,
130
+ options?: { seed?: number; version?: number; schemaKey?: string }
131
+ ): InferInsertModel<T>[] {
132
+ const schemaKey = options?.schemaKey ?? getTableName(pgTable);
133
+ const seedService = new SeedService();
134
+ /** `selectVersionOfGenerator` reads this; `seed()` sets it via `generatePossibleGenerators`. */
135
+ Reflect.set(seedService, "version", options?.version ?? 2);
136
+ const seedTable = drizzlePgTableToSeedTable(pgTable, schemaKey);
137
+ const colsMap = getTableColumns(pgTable) as Record<string, PgColumn>;
138
+ const tsNames = Object.keys(colsMap);
139
+ const baseSeed = options?.seed ?? 0;
140
+ const count = partialRows.length;
141
+
142
+ const generators: Record<
143
+ string,
144
+ { generate: (args: { i: number }) => unknown }
145
+ > = {};
146
+
147
+ for (const tsName of tsNames) {
148
+ const drizzleCol = colsMap[tsName]!;
149
+ if (drizzleCol.generatedIdentity?.type === "always") {
150
+ continue;
151
+ }
152
+ const needsGen = partialRows.some((p) => !Object.hasOwn(p, tsName));
153
+ if (!needsGen) {
154
+ continue;
155
+ }
156
+ const seedCol = seedTable.columns.find((c) => c.name === tsName);
157
+ if (!seedCol) {
158
+ throw new Error(`[seed-like-fill] column ${tsName} not in seed table`);
159
+ }
160
+ const rawGen = seedService.selectGeneratorForPostgresColumn(seedTable, seedCol);
161
+ if (rawGen === undefined) {
162
+ throw new Error(
163
+ `[seed-like-fill] unsupported column type for ${schemaKey}.${tsName}: ${seedCol.columnType}`
164
+ );
165
+ }
166
+ const gen = seedService.selectVersionOfGenerator(rawGen);
167
+ const pRNGSeed = baseSeed + hashSeed(`${schemaKey}.${tsName}`);
168
+ gen.init({ count, seed: pRNGSeed });
169
+ generators[tsName] = gen;
170
+ }
171
+
172
+ const out: InferInsertModel<T>[] = [];
173
+ for (let i = 0; i < count; i++) {
174
+ const partial = partialRows[i]!;
175
+ const row: Record<string, unknown> = {};
176
+ for (const tsName of tsNames) {
177
+ const drizzleCol = colsMap[tsName]!;
178
+ if (drizzleCol.generatedIdentity?.type === "always") {
179
+ continue;
180
+ }
181
+ if (Object.hasOwn(partial, tsName)) {
182
+ row[tsName] = partial[tsName as keyof typeof partial];
183
+ } else {
184
+ const g = generators[tsName];
185
+ if (!g) {
186
+ throw new Error(
187
+ `[seed-like-fill] missing generator for ${schemaKey}.${tsName} (partial row without value)`
188
+ );
189
+ }
190
+ row[tsName] = g.generate({ i });
191
+ }
192
+ }
193
+ out.push(row as InferInsertModel<T>);
194
+ }
195
+ return out;
196
+ }
@@ -0,0 +1,53 @@
1
+ import { drizzle } from "drizzle-orm/pglite";
2
+
3
+ import { createRlsDb } from "../../rls-db";
4
+ import type { GencowCtx, UserIdentity } from "../../reactive";
5
+
6
+ export function makeTestGencowCtxWithRls(
7
+ db: ReturnType<typeof drizzle>,
8
+ identity: UserIdentity
9
+ ): GencowCtx {
10
+ const scoped = createRlsDb(db, identity.id);
11
+ return {
12
+ db: scoped,
13
+ unsafeDb: db,
14
+ auth: {
15
+ getUserIdentity: () => identity,
16
+ requireAuth: () => identity,
17
+ },
18
+ storage: {} as GencowCtx["storage"],
19
+ scheduler: {} as GencowCtx["scheduler"],
20
+ realtime: { emit: () => {}, refresh: () => {} },
21
+ retry: async (fn) => fn(),
22
+ };
23
+ }
24
+
25
+ const FORCE_ROLLBACK = Symbol("force-rollback");
26
+
27
+ /**
28
+ * Executes a callback inside one outer transaction and always rolls it back.
29
+ *
30
+ * This is useful for mutation tests: handlers can run multiple operations and
31
+ * see intermediate writes, but database state is discarded after the callback.
32
+ */
33
+ export async function runWithRollbackTestGencowCtxWithRls<T>(
34
+ db: ReturnType<typeof drizzle>,
35
+ identity: UserIdentity,
36
+ run: (ctx: GencowCtx) => Promise<T>
37
+ ): Promise<T> {
38
+ let result!: T;
39
+
40
+ try {
41
+ await (db as any).transaction(async (outerTx: ReturnType<typeof drizzle>) => {
42
+ const txCtx = makeTestGencowCtxWithRls(outerTx, identity);
43
+ result = await run(txCtx);
44
+ throw FORCE_ROLLBACK;
45
+ });
46
+ } catch (error) {
47
+ if (error !== FORCE_ROLLBACK) {
48
+ throw error;
49
+ }
50
+ }
51
+
52
+ return result;
53
+ }