@gencow/core 0.1.24 → 0.1.26

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 (75) hide show
  1. package/dist/crud.d.ts +2 -2
  2. package/dist/crud.js +225 -208
  3. package/dist/index.d.ts +5 -5
  4. package/dist/index.js +2 -2
  5. package/dist/reactive.js +10 -3
  6. package/dist/retry.js +1 -1
  7. package/dist/rls-db.d.ts +2 -2
  8. package/dist/rls-db.js +1 -5
  9. package/dist/scheduler.d.ts +2 -0
  10. package/dist/scheduler.js +16 -6
  11. package/dist/server.d.ts +0 -1
  12. package/dist/server.js +0 -1
  13. package/dist/storage.js +29 -22
  14. package/dist/v.d.ts +2 -2
  15. package/dist/workflow.js +4 -11
  16. package/dist/workflows-api.js +5 -12
  17. package/package.json +45 -42
  18. package/src/__tests__/auth.test.ts +90 -86
  19. package/src/__tests__/crons.test.ts +69 -67
  20. package/src/__tests__/crud-codegen-integration.test.ts +164 -170
  21. package/src/__tests__/crud-owner-rls.test.ts +308 -301
  22. package/src/__tests__/crud.test.ts +694 -711
  23. package/src/__tests__/dist-exports.test.ts +120 -120
  24. package/src/__tests__/fixtures/basic/auth.ts +16 -16
  25. package/src/__tests__/fixtures/basic/drizzle.config.ts +1 -4
  26. package/src/__tests__/fixtures/basic/index.ts +1 -1
  27. package/src/__tests__/fixtures/basic/schema.ts +1 -1
  28. package/src/__tests__/fixtures/basic/tasks.ts +4 -4
  29. package/src/__tests__/fixtures/common/auth-schema.ts +38 -34
  30. package/src/__tests__/helpers/basic-rls-fixture.ts +80 -78
  31. package/src/__tests__/helpers/pglite-migrations.ts +2 -5
  32. package/src/__tests__/helpers/pglite-rls-session.ts +13 -16
  33. package/src/__tests__/helpers/seed-like-fill.ts +47 -41
  34. package/src/__tests__/helpers/test-gencow-ctx-rls.ts +4 -7
  35. package/src/__tests__/httpaction.test.ts +91 -91
  36. package/src/__tests__/image-optimization.test.ts +570 -574
  37. package/src/__tests__/load.test.ts +321 -308
  38. package/src/__tests__/network-sim.test.ts +238 -215
  39. package/src/__tests__/reactive.test.ts +380 -358
  40. package/src/__tests__/retry.test.ts +99 -84
  41. package/src/__tests__/rls-crud-basic.test.ts +172 -245
  42. package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +81 -81
  43. package/src/__tests__/rls-custom-mutation-handlers.test.ts +47 -94
  44. package/src/__tests__/rls-custom-query-handlers.test.ts +92 -92
  45. package/src/__tests__/rls-db-leased-connection.test.ts +2 -6
  46. package/src/__tests__/rls-session-and-policies.test.ts +181 -199
  47. package/src/__tests__/scheduler-durable-v2.test.ts +199 -181
  48. package/src/__tests__/scheduler-durable.test.ts +117 -117
  49. package/src/__tests__/scheduler-exec.test.ts +258 -246
  50. package/src/__tests__/scheduler.test.ts +129 -111
  51. package/src/__tests__/storage.test.ts +282 -269
  52. package/src/__tests__/tsconfig.json +6 -6
  53. package/src/__tests__/validator.test.ts +236 -232
  54. package/src/__tests__/workflow.test.ts +309 -286
  55. package/src/__tests__/ws-integration.test.ts +223 -218
  56. package/src/__tests__/ws-scale.test.ts +168 -159
  57. package/src/auth-config.ts +18 -18
  58. package/src/auth.ts +106 -106
  59. package/src/crons.ts +77 -77
  60. package/src/crud.ts +523 -479
  61. package/src/index.ts +69 -5
  62. package/src/reactive.ts +357 -331
  63. package/src/retry.ts +51 -54
  64. package/src/rls-db.ts +195 -205
  65. package/src/rls.ts +33 -36
  66. package/src/scheduler.ts +237 -211
  67. package/src/server.ts +0 -1
  68. package/src/storage.ts +632 -593
  69. package/src/v.ts +119 -114
  70. package/src/workflow-types.ts +67 -70
  71. package/src/workflow.ts +99 -116
  72. package/src/workflows-api.ts +231 -241
  73. package/dist/db.d.ts +0 -13
  74. package/dist/db.js +0 -16
  75. package/src/db.ts +0 -18
@@ -13,105 +13,105 @@ import { crud } from "../crud.js";
13
13
  import { createRlsDb } from "../rls-db.js";
14
14
  import { news } from "./fixtures/basic/schema.js";
15
15
  import {
16
- assertTableRowLevelSecurityDisabled,
17
- basicUser0Identity,
18
- basicUser1Identity,
19
- createBasicRlsEnvironment,
16
+ assertTableRowLevelSecurityDisabled,
17
+ basicUser0Identity,
18
+ basicUser1Identity,
19
+ createBasicRlsEnvironment,
20
20
  } from "./helpers/basic-rls-fixture.js";
21
21
  import {
22
- makeTestGencowCtxWithRls,
23
- runWithRollbackTestGencowCtxWithRls,
22
+ makeTestGencowCtxWithRls,
23
+ runWithRollbackTestGencowCtxWithRls,
24
24
  } from "./helpers/test-gencow-ctx-rls.js";
25
25
 
26
26
  describe("crud + PGlite: news without ownerRls, public crud", () => {
27
- let client: import("@electric-sql/pglite").PGlite;
28
- let db: ReturnType<typeof import("drizzle-orm/pglite").drizzle>;
29
- let listHandler: (ctx: unknown, args: unknown) => Promise<{ data: unknown[]; total: number }>;
30
- let getHandler: (ctx: unknown, args: unknown) => Promise<unknown>;
31
- let createHandler: (ctx: unknown, args: unknown) => Promise<unknown>;
32
- let updateHandler: (ctx: unknown, args: unknown) => Promise<unknown>;
33
- let removeHandler: (ctx: unknown, args: unknown) => Promise<{ success: boolean }>;
27
+ let client: import("@electric-sql/pglite").PGlite;
28
+ let db: ReturnType<typeof import("drizzle-orm/pglite").drizzle>;
29
+ let listHandler: (ctx: unknown, args: unknown) => Promise<{ data: unknown[]; total: number }>;
30
+ let getHandler: (ctx: unknown, args: unknown) => Promise<unknown>;
31
+ let createHandler: (ctx: unknown, args: unknown) => Promise<unknown>;
32
+ let updateHandler: (ctx: unknown, args: unknown) => Promise<unknown>;
33
+ let removeHandler: (ctx: unknown, args: unknown) => Promise<{ success: boolean }>;
34
34
 
35
- beforeAll(async () => {
36
- const env = await createBasicRlsEnvironment();
37
- client = env.client;
38
- db = env.db;
35
+ beforeAll(async () => {
36
+ const env = await createBasicRlsEnvironment();
37
+ client = env.client;
38
+ db = env.db;
39
39
 
40
- const defs = crud(news, {
41
- prefix: "fixture_basic_news",
42
- public: true,
43
- defaultLimit: 50,
44
- });
45
- listHandler = defs.list!.handler as (typeof listHandler);
46
- getHandler = defs.get!.handler as (typeof getHandler);
47
- createHandler = defs.create!.handler as (typeof createHandler);
48
- updateHandler = defs.update!.handler as (typeof updateHandler);
49
- removeHandler = defs.remove!.handler as (typeof removeHandler);
40
+ const defs = crud(news, {
41
+ prefix: "fixture_basic_news",
42
+ public: true,
43
+ defaultLimit: 50,
50
44
  });
45
+ listHandler = defs.list!.handler as typeof listHandler;
46
+ getHandler = defs.get!.handler as typeof getHandler;
47
+ createHandler = defs.create!.handler as typeof createHandler;
48
+ updateHandler = defs.update!.handler as typeof updateHandler;
49
+ removeHandler = defs.remove!.handler as typeof removeHandler;
50
+ });
51
51
 
52
- afterAll(async () => {
53
- try {
54
- await client.close();
55
- } catch {
56
- /* ignore */
57
- }
58
- });
52
+ afterAll(async () => {
53
+ try {
54
+ await client.close();
55
+ } catch {
56
+ /* ignore */
57
+ }
58
+ });
59
59
 
60
- it("list: authenticated user sees all rows (no owner filter)", async () => {
61
- const ctx = makeTestGencowCtxWithRls(db, basicUser0Identity);
62
- const result = await listHandler(ctx, {});
63
- expect(result.total).toBe(2);
64
- expect(result.data).toHaveLength(2);
65
- const ids = new Set(result.data.map((r: any) => r.id));
66
- expect(ids.has("nw-000")).toBe(true);
67
- expect(ids.has("nw-001")).toBe(true);
68
- });
60
+ it("list: authenticated user sees all rows (no owner filter)", async () => {
61
+ const ctx = makeTestGencowCtxWithRls(db, basicUser0Identity);
62
+ const result = await listHandler(ctx, {});
63
+ expect(result.total).toBe(2);
64
+ expect(result.data).toHaveLength(2);
65
+ const ids = new Set(result.data.map((r: any) => r.id));
66
+ expect(ids.has("nw-000")).toBe(true);
67
+ expect(ids.has("nw-001")).toBe(true);
68
+ });
69
69
 
70
- it("get: can read another user row by id (no owner filter)", async () => {
71
- const ctx = makeTestGencowCtxWithRls(db, basicUser0Identity);
72
- const row = await getHandler(ctx, { id: "nw-001" });
73
- expect(row).not.toBeNull();
74
- expect((row as any).userId).toBe(basicUser1Identity.id);
75
- });
70
+ it("get: can read another user row by id (no owner filter)", async () => {
71
+ const ctx = makeTestGencowCtxWithRls(db, basicUser0Identity);
72
+ const row = await getHandler(ctx, { id: "nw-001" });
73
+ expect(row).not.toBeNull();
74
+ expect((row as any).userId).toBe(basicUser1Identity.id);
75
+ });
76
76
 
77
- it("createRlsDb + bare select: no DB policies — still full table visibility", async () => {
78
- const scoped = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
79
- userId: basicUser0Identity.id,
80
- });
81
- const rows = await scoped.select().from(news);
82
- expect(rows.length).toBe(2);
77
+ it("createRlsDb + bare select: no DB policies — still full table visibility", async () => {
78
+ const scoped = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
79
+ userId: basicUser0Identity.id,
83
80
  });
81
+ const rows = await scoped.select().from(news);
82
+ expect(rows.length).toBe(2);
83
+ });
84
84
 
85
- it("create: with public crud, pass userId explicitly (no auth-based auto-inject)", async () => {
86
- await runWithRollbackTestGencowCtxWithRls(db, basicUser0Identity, async (ctx) => {
87
- const created = await createHandler(ctx, {
88
- id: "nw-created",
89
- title: "new row",
90
- userId: basicUser0Identity.id,
91
- });
92
- expect((created as any).userId).toBe(basicUser0Identity.id);
93
- });
85
+ it("create: with public crud, pass userId explicitly (no auth-based auto-inject)", async () => {
86
+ await runWithRollbackTestGencowCtxWithRls(db, basicUser0Identity, async (ctx) => {
87
+ const created = await createHandler(ctx, {
88
+ id: "nw-created",
89
+ title: "new row",
90
+ userId: basicUser0Identity.id,
91
+ });
92
+ expect((created as any).userId).toBe(basicUser0Identity.id);
94
93
  });
94
+ });
95
95
 
96
- it("update: can change another user row by id (no owner WHERE)", async () => {
97
- await runWithRollbackTestGencowCtxWithRls(db, basicUser0Identity, async (ctx) => {
98
- const updated = await updateHandler(ctx, {
99
- id: "nw-001",
100
- title: "touched by user0",
101
- });
102
- expect((updated as any).title).toBe("touched by user0");
103
- });
96
+ it("update: can change another user row by id (no owner WHERE)", async () => {
97
+ await runWithRollbackTestGencowCtxWithRls(db, basicUser0Identity, async (ctx) => {
98
+ const updated = await updateHandler(ctx, {
99
+ id: "nw-001",
100
+ title: "touched by user0",
101
+ });
102
+ expect((updated as any).title).toBe("touched by user0");
104
103
  });
104
+ });
105
105
 
106
- it("remove: can delete another user row by id (no owner WHERE)", async () => {
107
- await runWithRollbackTestGencowCtxWithRls(db, basicUser0Identity, async (ctx) => {
108
- await removeHandler(ctx, { id: "nw-001" });
109
- const after = await getHandler(ctx, { id: "nw-001" });
110
- expect(after).toBeNull();
111
- });
106
+ it("remove: can delete another user row by id (no owner WHERE)", async () => {
107
+ await runWithRollbackTestGencowCtxWithRls(db, basicUser0Identity, async (ctx) => {
108
+ await removeHandler(ctx, { id: "nw-001" });
109
+ const after = await getHandler(ctx, { id: "nw-001" });
110
+ expect(after).toBeNull();
112
111
  });
112
+ });
113
113
 
114
- it("pg_catalog: news has row security disabled", async () => {
115
- await assertTableRowLevelSecurityDisabled(db, news);
116
- });
114
+ it("pg_catalog: news has row security disabled", async () => {
115
+ await assertTableRowLevelSecurityDisabled(db, news);
116
+ });
117
117
  });
@@ -25,16 +25,8 @@ import {
25
25
  } from "./helpers/test-gencow-ctx-rls.js";
26
26
 
27
27
  /** User-defined mutation: update title by task id. */
28
- async function customMutationUpdateTitle(
29
- ctx: GencowCtx,
30
- taskId: string,
31
- title: string
32
- ) {
33
- return ctx.db
34
- .update(tasks)
35
- .set({ title, updatedAt: new Date() })
36
- .where(eq(tasks.id, taskId))
37
- .returning();
28
+ async function customMutationUpdateTitle(ctx: GencowCtx, taskId: string, title: string) {
29
+ return ctx.db.update(tasks).set({ title, updatedAt: new Date() }).where(eq(tasks.id, taskId)).returning();
38
30
  }
39
31
 
40
32
  /** User-defined mutation: delete by id. */
@@ -45,7 +37,7 @@ async function customMutationDeleteById(ctx: GencowCtx, taskId: string) {
45
37
  /** User-defined mutation: insert a new task for the current user (caller supplies logical fields). */
46
38
  async function customMutationInsertTask(
47
39
  ctx: GencowCtx,
48
- values: { id: string; title: string; userId: string; done?: boolean }
40
+ values: { id: string; title: string; userId: string; done?: boolean },
49
41
  ) {
50
42
  const [row] = fillPartialRowsForInsert(tasks, [
51
43
  {
@@ -77,108 +69,69 @@ describe("Custom mutation handlers + ctx.db + RLS", () => {
77
69
  });
78
70
 
79
71
  it("custom update: can change own task", async () => {
80
- await runWithRollbackTestGencowCtxWithRls(
81
- db,
82
- basicUser0Identity,
83
- async (ctx) => {
84
- const updated = await customMutationUpdateTitle(
85
- ctx,
86
- "tk-003",
87
- "Updated by custom handler"
88
- );
89
- expect(updated.length).toBe(1);
90
- expect(updated[0]!.title).toBe("Updated by custom handler");
91
- }
92
- );
72
+ await runWithRollbackTestGencowCtxWithRls(db, basicUser0Identity, async (ctx) => {
73
+ const updated = await customMutationUpdateTitle(ctx, "tk-003", "Updated by custom handler");
74
+ expect(updated.length).toBe(1);
75
+ expect(updated[0]!.title).toBe("Updated by custom handler");
76
+ });
93
77
  });
94
78
 
95
79
  it("custom update: cannot modify another user's task (0 rows)", async () => {
96
- await runWithRollbackTestGencowCtxWithRls(
97
- db,
98
- basicUser0Identity,
99
- async (ctx) => {
100
- const updated = await customMutationUpdateTitle(
101
- ctx,
102
- "tk-001",
103
- "Should not apply"
104
- );
105
- expect(updated.length).toBe(0);
106
- }
107
- );
80
+ await runWithRollbackTestGencowCtxWithRls(db, basicUser0Identity, async (ctx) => {
81
+ const updated = await customMutationUpdateTitle(ctx, "tk-001", "Should not apply");
82
+ expect(updated.length).toBe(0);
83
+ });
108
84
  });
109
85
 
110
86
  it("custom delete: can remove own task", async () => {
111
- await runWithRollbackTestGencowCtxWithRls(
112
- db,
113
- basicUser0Identity,
114
- async (ctx) => {
115
- const removed = await customMutationDeleteById(ctx, "tk-000");
116
- expect(removed.length).toBe(1);
117
- }
118
- );
87
+ await runWithRollbackTestGencowCtxWithRls(db, basicUser0Identity, async (ctx) => {
88
+ const removed = await customMutationDeleteById(ctx, "tk-000");
89
+ expect(removed.length).toBe(1);
90
+ });
119
91
  });
120
92
 
121
93
  it("custom delete: cannot remove another user's task (0 rows)", async () => {
122
- await runWithRollbackTestGencowCtxWithRls(
123
- db,
124
- basicUser0Identity,
125
- async (ctx) => {
126
- const removed = await customMutationDeleteById(ctx, "tk-004");
127
- expect(removed.length).toBe(0);
128
- }
129
- );
94
+ await runWithRollbackTestGencowCtxWithRls(db, basicUser0Identity, async (ctx) => {
95
+ const removed = await customMutationDeleteById(ctx, "tk-004");
96
+ expect(removed.length).toBe(0);
97
+ });
130
98
  });
131
99
 
132
100
  it("custom insert: succeeds when userId matches session", async () => {
133
- await runWithRollbackTestGencowCtxWithRls(
134
- db,
135
- basicUser0Identity,
136
- async (ctx) => {
137
- const inserted = await customMutationInsertTask(ctx, {
138
- id: "tk-custom-own",
139
- title: "Custom insert OK",
140
- userId: basicUser0Identity.id,
141
- });
142
- expect(inserted.length).toBe(1);
143
- expect(inserted[0]!.userId).toBe(basicUser0Identity.id);
144
- }
145
- );
101
+ await runWithRollbackTestGencowCtxWithRls(db, basicUser0Identity, async (ctx) => {
102
+ const inserted = await customMutationInsertTask(ctx, {
103
+ id: "tk-custom-own",
104
+ title: "Custom insert OK",
105
+ userId: basicUser0Identity.id,
106
+ });
107
+ expect(inserted.length).toBe(1);
108
+ expect(inserted[0]!.userId).toBe(basicUser0Identity.id);
109
+ });
146
110
  });
147
111
 
148
112
  it("custom insert: forged userId (another user) violates RLS withCheck", async () => {
149
- await runWithRollbackTestGencowCtxWithRls(
150
- db,
151
- basicUser0Identity,
152
- async (ctx) => {
153
- let thrown: unknown;
154
- try {
155
- await customMutationInsertTask(ctx, {
156
- id: "tk-custom-forge",
157
- title: "Forged owner",
158
- userId: basicFixtureUsers[1].id,
159
- });
160
- } catch (e) {
161
- thrown = e;
162
- }
163
- expect(thrown).toBeInstanceOf(Error);
113
+ await runWithRollbackTestGencowCtxWithRls(db, basicUser0Identity, async (ctx) => {
114
+ let thrown: unknown;
115
+ try {
116
+ await customMutationInsertTask(ctx, {
117
+ id: "tk-custom-forge",
118
+ title: "Forged owner",
119
+ userId: basicFixtureUsers[1].id,
120
+ });
121
+ } catch (e) {
122
+ thrown = e;
164
123
  }
165
- );
124
+ expect(thrown).toBeInstanceOf(Error);
125
+ });
166
126
  });
167
127
 
168
128
  it("nested db.transaction in custom handler still applies RLS on inner selects", async () => {
169
- await runWithRollbackTestGencowCtxWithRls(
170
- db,
171
- basicUser0Identity,
172
- async (ctx) => {
173
- const inner = await ctx.db.transaction(async (tx: typeof ctx.db) => {
174
- return tx
175
- .select()
176
- .from(tasks)
177
- .where(eq(tasks.userId, basicFixtureUsers[1].id));
178
- });
179
- expect(inner.length).toBe(0);
180
- }
181
- );
129
+ await runWithRollbackTestGencowCtxWithRls(db, basicUser0Identity, async (ctx) => {
130
+ const inner = await ctx.db.transaction(async (tx: typeof ctx.db) => {
131
+ return tx.select().from(tasks).where(eq(tasks.userId, basicFixtureUsers[1].id));
132
+ });
133
+ expect(inner.length).toBe(0);
134
+ });
182
135
  });
183
136
 
184
137
  it("direct ctx.db without outer rollback wrapper: mutation still RLS-scoped", async () => {
@@ -13,116 +13,116 @@ import type { GencowCtx } from "../reactive.js";
13
13
  import { tasks } from "./fixtures/basic/schema.js";
14
14
  import type { BasicTaskRow } from "./helpers/basic-rls-fixture.js";
15
15
  import {
16
- basicFixtureTasks,
17
- basicFixtureUsers,
18
- basicUser0Identity,
19
- basicUser1Identity,
20
- createBasicRlsEnvironment,
16
+ basicFixtureTasks,
17
+ basicFixtureUsers,
18
+ basicUser0Identity,
19
+ basicUser1Identity,
20
+ createBasicRlsEnvironment,
21
21
  } from "./helpers/basic-rls-fixture.js";
22
22
  import { makeTestGencowCtxWithRls } from "./helpers/test-gencow-ctx-rls.js";
23
23
 
24
24
  /** Simulates a user-defined query: list all tasks the ORM can see (RLS limits rows). */
25
25
  async function customQueryListAllTasks(ctx: GencowCtx) {
26
- return ctx.db.select().from(tasks);
26
+ return ctx.db.select().from(tasks);
27
27
  }
28
28
 
29
29
  /** Simulates a dashboard query with filters — still subject to RLS. */
30
30
  async function customQueryTasksMatchingTitle(ctx: GencowCtx, substring: string) {
31
- return ctx.db
32
- .select()
33
- .from(tasks)
34
- .where(sql`${tasks.title} ilike ${"%" + substring + "%"}`);
31
+ return ctx.db
32
+ .select()
33
+ .from(tasks)
34
+ .where(sql`${tasks.title} ilike ${"%" + substring + "%"}`);
35
35
  }
36
36
 
37
37
  /** Simulates fetching a single row by id (e.g. detail view). */
38
38
  async function customQueryTaskById(ctx: GencowCtx, id: string) {
39
- const rows = await ctx.db.select().from(tasks).where(eq(tasks.id, id));
40
- return rows[0] ?? null;
39
+ const rows = await ctx.db.select().from(tasks).where(eq(tasks.id, id));
40
+ return rows[0] ?? null;
41
41
  }
42
42
 
43
43
  /** Raw SQL count — must run with RLS session vars on the same connection semantics as ORM builders. */
44
44
  async function customQueryTaskCountRaw(ctx: GencowCtx) {
45
- const r = await ctx.db.execute(sql`select count(*)::int as c from ${tasks}`);
46
- const rows = r.rows as { c: number }[];
47
- return rows[0]?.c ?? 0;
45
+ const r = await ctx.db.execute(sql`select count(*)::int as c from ${tasks}`);
46
+ const rows = r.rows as { c: number }[];
47
+ return rows[0]?.c ?? 0;
48
48
  }
49
49
 
50
50
  describe("Custom query handlers + ctx.db + RLS", () => {
51
- let client: PGlite;
52
- let db: ReturnType<typeof drizzle>;
53
-
54
- beforeAll(async () => {
55
- const env = await createBasicRlsEnvironment();
56
- client = env.client;
57
- db = env.db;
58
- });
59
-
60
- afterAll(async () => {
61
- try {
62
- await client.close();
63
- } catch {
64
- /* ignore */
65
- }
66
- });
67
-
68
- it("custom list: user0 only sees own rows (not user1 tasks)", async () => {
69
- const ctx = makeTestGencowCtxWithRls(db, basicUser0Identity);
70
- const rows = await customQueryListAllTasks(ctx);
71
- expect(rows.every((r: BasicTaskRow) => r.userId === basicUser0Identity.id)).toBe(true);
72
- const expectedOwn = basicFixtureTasks.filter((t) => t.userId === basicFixtureUsers[0].id).length;
73
- expect(rows.length).toBe(expectedOwn);
74
- });
75
-
76
- it("custom list: user1 sees a different row set than user0", async () => {
77
- const ctx0 = makeTestGencowCtxWithRls(db, basicUser0Identity);
78
- const ctx1 = makeTestGencowCtxWithRls(db, basicUser1Identity);
79
- const u0 = await customQueryListAllTasks(ctx0);
80
- const u1 = await customQueryListAllTasks(ctx1);
81
- const u0Ids = new Set(u0.map((r: BasicTaskRow) => r.id));
82
- const u1Ids = new Set(u1.map((r: BasicTaskRow) => r.id));
83
- expect(u0Ids.has("tk-001")).toBe(false);
84
- expect(u1Ids.has("tk-001")).toBe(true);
85
- expect(u1Ids.has("tk-000")).toBe(false);
86
- });
87
-
88
- it("custom filtered query: cannot see other user's rows even if title matches", async () => {
89
- const ctx = makeTestGencowCtxWithRls(db, basicUser0Identity);
90
- const rows = await customQueryTasksMatchingTitle(ctx, "Project Alpha");
91
- expect(rows.every((r: BasicTaskRow) => r.userId === basicUser0Identity.id)).toBe(true);
92
- expect(rows.some((r: BasicTaskRow) => r.id === "tk-001")).toBe(false);
93
- });
94
-
95
- it("custom get by id: returns null for another user's task id", async () => {
96
- const ctx = makeTestGencowCtxWithRls(db, basicUser0Identity);
97
- const row = await customQueryTaskById(ctx, "tk-001");
98
- expect(row).toBeNull();
99
- });
100
-
101
- it("custom get by id: returns own task", async () => {
102
- const ctx = makeTestGencowCtxWithRls(db, basicUser0Identity);
103
- const row = await customQueryTaskById(ctx, "tk-003");
104
- expect(row).not.toBeNull();
105
- expect(row!.id).toBe("tk-003");
106
- });
107
-
108
- it("raw SQL count on ctx.db matches RLS-visible rows only", async () => {
109
- const ctx0 = makeTestGencowCtxWithRls(db, basicUser0Identity);
110
- const ctx1 = makeTestGencowCtxWithRls(db, basicUser1Identity);
111
- const c0 = await customQueryTaskCountRaw(ctx0);
112
- const c1 = await customQueryTaskCountRaw(ctx1);
113
- const n0 = basicFixtureTasks.filter((t) => t.userId === basicFixtureUsers[0].id).length;
114
- const n1 = basicFixtureTasks.filter((t) => t.userId === basicFixtureUsers[1].id).length;
115
- expect(c0).toBe(n0);
116
- expect(c1).toBe(n1);
117
- expect(c0 + c1).toBe(basicFixtureTasks.length);
118
- });
119
-
120
- it("unsafeDb without createRlsDb: no session GUC — owner RLS yields no visible rows", async () => {
121
- const ctx = makeTestGencowCtxWithRls(db, basicUser0Identity);
122
- const rlsRows = await customQueryListAllTasks(ctx);
123
- expect(rlsRows.length).toBeGreaterThan(0);
124
-
125
- const raw = await ctx.unsafeDb.select().from(tasks);
126
- expect(raw.length).toBe(0);
127
- });
51
+ let client: PGlite;
52
+ let db: ReturnType<typeof drizzle>;
53
+
54
+ beforeAll(async () => {
55
+ const env = await createBasicRlsEnvironment();
56
+ client = env.client;
57
+ db = env.db;
58
+ });
59
+
60
+ afterAll(async () => {
61
+ try {
62
+ await client.close();
63
+ } catch {
64
+ /* ignore */
65
+ }
66
+ });
67
+
68
+ it("custom list: user0 only sees own rows (not user1 tasks)", async () => {
69
+ const ctx = makeTestGencowCtxWithRls(db, basicUser0Identity);
70
+ const rows = await customQueryListAllTasks(ctx);
71
+ expect(rows.every((r: BasicTaskRow) => r.userId === basicUser0Identity.id)).toBe(true);
72
+ const expectedOwn = basicFixtureTasks.filter((t) => t.userId === basicFixtureUsers[0].id).length;
73
+ expect(rows.length).toBe(expectedOwn);
74
+ });
75
+
76
+ it("custom list: user1 sees a different row set than user0", async () => {
77
+ const ctx0 = makeTestGencowCtxWithRls(db, basicUser0Identity);
78
+ const ctx1 = makeTestGencowCtxWithRls(db, basicUser1Identity);
79
+ const u0 = await customQueryListAllTasks(ctx0);
80
+ const u1 = await customQueryListAllTasks(ctx1);
81
+ const u0Ids = new Set(u0.map((r: BasicTaskRow) => r.id));
82
+ const u1Ids = new Set(u1.map((r: BasicTaskRow) => r.id));
83
+ expect(u0Ids.has("tk-001")).toBe(false);
84
+ expect(u1Ids.has("tk-001")).toBe(true);
85
+ expect(u1Ids.has("tk-000")).toBe(false);
86
+ });
87
+
88
+ it("custom filtered query: cannot see other user's rows even if title matches", async () => {
89
+ const ctx = makeTestGencowCtxWithRls(db, basicUser0Identity);
90
+ const rows = await customQueryTasksMatchingTitle(ctx, "Project Alpha");
91
+ expect(rows.every((r: BasicTaskRow) => r.userId === basicUser0Identity.id)).toBe(true);
92
+ expect(rows.some((r: BasicTaskRow) => r.id === "tk-001")).toBe(false);
93
+ });
94
+
95
+ it("custom get by id: returns null for another user's task id", async () => {
96
+ const ctx = makeTestGencowCtxWithRls(db, basicUser0Identity);
97
+ const row = await customQueryTaskById(ctx, "tk-001");
98
+ expect(row).toBeNull();
99
+ });
100
+
101
+ it("custom get by id: returns own task", async () => {
102
+ const ctx = makeTestGencowCtxWithRls(db, basicUser0Identity);
103
+ const row = await customQueryTaskById(ctx, "tk-003");
104
+ expect(row).not.toBeNull();
105
+ expect(row!.id).toBe("tk-003");
106
+ });
107
+
108
+ it("raw SQL count on ctx.db matches RLS-visible rows only", async () => {
109
+ const ctx0 = makeTestGencowCtxWithRls(db, basicUser0Identity);
110
+ const ctx1 = makeTestGencowCtxWithRls(db, basicUser1Identity);
111
+ const c0 = await customQueryTaskCountRaw(ctx0);
112
+ const c1 = await customQueryTaskCountRaw(ctx1);
113
+ const n0 = basicFixtureTasks.filter((t) => t.userId === basicFixtureUsers[0].id).length;
114
+ const n1 = basicFixtureTasks.filter((t) => t.userId === basicFixtureUsers[1].id).length;
115
+ expect(c0).toBe(n0);
116
+ expect(c1).toBe(n1);
117
+ expect(c0 + c1).toBe(basicFixtureTasks.length);
118
+ });
119
+
120
+ it("unsafeDb without createRlsDb: no session GUC — owner RLS yields no visible rows", async () => {
121
+ const ctx = makeTestGencowCtxWithRls(db, basicUser0Identity);
122
+ const rlsRows = await customQueryListAllTasks(ctx);
123
+ expect(rlsRows.length).toBeGreaterThan(0);
124
+
125
+ const raw = await ctx.unsafeDb.select().from(tasks);
126
+ expect(raw.length).toBe(0);
127
+ });
128
128
  });
@@ -20,11 +20,7 @@ describe("withRlsLeasedConnection (pg Pool leased client path)", () => {
20
20
  released++;
21
21
  },
22
22
  };
23
- const result = await withRlsLeasedConnection(
24
- leased,
25
- { userId: "u1" },
26
- async () => "done"
27
- );
23
+ const result = await withRlsLeasedConnection(leased, { userId: "u1" }, async () => "done");
28
24
  expect(result).toBe("done");
29
25
  expect(phases[0]).toBe("begin");
30
26
  expect(phases[phases.length - 1]).toBe("commit");
@@ -81,7 +77,7 @@ describe("withRlsLeasedConnection (pg Pool leased client path)", () => {
81
77
  tenantId: "t1",
82
78
  vars: { "app.org_slug": "acme" },
83
79
  },
84
- async () => "ok"
80
+ async () => "ok",
85
81
  );
86
82
  expect(phases[0]).toBe("begin");
87
83
  const setCalls = phases.filter((p) => p.includes("set_config"));