@gencow/core 0.1.27 → 0.1.29

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 (130) hide show
  1. package/dist/auth-config.d.ts +92 -5
  2. package/dist/config.d.ts +107 -0
  3. package/dist/config.js +12 -0
  4. package/dist/context.d.ts +139 -0
  5. package/dist/context.js +3 -0
  6. package/dist/crud.d.ts +5 -5
  7. package/dist/crud.js +19 -35
  8. package/dist/document-types.d.ts +65 -0
  9. package/dist/document-types.js +15 -0
  10. package/dist/grounded-answer-types.d.ts +62 -0
  11. package/dist/grounded-answer-types.js +6 -0
  12. package/dist/http-action.d.ts +77 -0
  13. package/dist/http-action.js +41 -0
  14. package/dist/index.d.ts +30 -5
  15. package/dist/index.js +15 -2
  16. package/dist/platform-capacity-profile.d.ts +19 -0
  17. package/dist/platform-capacity-profile.js +94 -0
  18. package/dist/procedure.d.ts +58 -0
  19. package/dist/procedure.js +115 -0
  20. package/dist/rag-ingest-types.d.ts +39 -0
  21. package/dist/rag-ingest-types.js +1 -0
  22. package/dist/rag-operations-types.d.ts +81 -0
  23. package/dist/rag-operations-types.js +1 -0
  24. package/dist/rag-schema.d.ts +1466 -0
  25. package/dist/rag-schema.js +87 -0
  26. package/dist/reactive-mutation-types.d.ts +11 -0
  27. package/dist/reactive-mutation-types.js +1 -0
  28. package/dist/reactive-mutation.d.ts +51 -0
  29. package/dist/reactive-mutation.js +75 -0
  30. package/dist/reactive-query-types.d.ts +12 -0
  31. package/dist/reactive-query-types.js +1 -0
  32. package/dist/reactive-query.d.ts +14 -0
  33. package/dist/reactive-query.js +28 -0
  34. package/dist/reactive-realtime.d.ts +48 -0
  35. package/dist/reactive-realtime.js +236 -0
  36. package/dist/reactive.d.ts +29 -5
  37. package/dist/reactive.js +65 -0
  38. package/dist/rls-db.d.ts +9 -2
  39. package/dist/runtime-env-policy.d.ts +5 -0
  40. package/dist/runtime-env-policy.js +56 -0
  41. package/dist/search-types.d.ts +83 -0
  42. package/dist/search-types.js +1 -0
  43. package/dist/server.d.ts +1 -2
  44. package/dist/server.js +0 -1
  45. package/dist/storage-metering.d.ts +13 -0
  46. package/dist/storage-metering.js +18 -0
  47. package/dist/storage-shared.d.ts +36 -0
  48. package/dist/storage-shared.js +39 -0
  49. package/dist/storage.d.ts +5 -27
  50. package/dist/storage.js +30 -22
  51. package/dist/wake-app-result.d.ts +22 -0
  52. package/dist/wake-app-result.js +11 -0
  53. package/dist/workflow-types.d.ts +16 -2
  54. package/dist/workflow.d.ts +1 -1
  55. package/dist/workflow.js +136 -11
  56. package/dist/workflows-api.js +71 -3
  57. package/package.json +11 -7
  58. package/src/auth-config.ts +104 -3
  59. package/src/config.ts +119 -0
  60. package/src/context.ts +152 -0
  61. package/src/crud.ts +18 -35
  62. package/src/document-types.ts +102 -0
  63. package/src/grounded-answer-types.ts +78 -0
  64. package/src/http-action.ts +101 -0
  65. package/src/index.ts +142 -19
  66. package/src/platform-capacity-profile.ts +114 -0
  67. package/src/procedure.ts +283 -0
  68. package/src/rag-ingest-types.ts +52 -0
  69. package/src/rag-operations-types.ts +90 -0
  70. package/src/rag-schema.ts +94 -0
  71. package/src/reactive-mutation-types.ts +13 -0
  72. package/src/reactive-mutation.ts +115 -0
  73. package/src/reactive-query-types.ts +14 -0
  74. package/src/reactive-query.ts +48 -0
  75. package/src/reactive-realtime.ts +267 -0
  76. package/src/rls-db.ts +9 -4
  77. package/src/runtime-env-policy.ts +66 -0
  78. package/src/search-types.ts +91 -0
  79. package/src/server.ts +6 -2
  80. package/src/storage-metering.ts +35 -0
  81. package/src/storage-shared.ts +74 -0
  82. package/src/storage.ts +44 -53
  83. package/src/wake-app-result.ts +37 -0
  84. package/src/workflow-types.ts +16 -2
  85. package/src/workflow.ts +166 -12
  86. package/src/workflows-api.ts +82 -3
  87. package/src/__tests__/auth.test.ts +0 -118
  88. package/src/__tests__/crons.test.ts +0 -83
  89. package/src/__tests__/crud-codegen-integration.test.ts +0 -246
  90. package/src/__tests__/crud-owner-rls.test.ts +0 -387
  91. package/src/__tests__/crud.test.ts +0 -930
  92. package/src/__tests__/dist-exports.test.ts +0 -176
  93. package/src/__tests__/fixtures/basic/auth.ts +0 -32
  94. package/src/__tests__/fixtures/basic/drizzle.config.ts +0 -12
  95. package/src/__tests__/fixtures/basic/index.ts +0 -6
  96. package/src/__tests__/fixtures/basic/migrations/0000_last_warstar.sql +0 -75
  97. package/src/__tests__/fixtures/basic/migrations/meta/0000_snapshot.json +0 -497
  98. package/src/__tests__/fixtures/basic/migrations/meta/_journal.json +0 -13
  99. package/src/__tests__/fixtures/basic/schema.ts +0 -51
  100. package/src/__tests__/fixtures/basic/tasks.ts +0 -15
  101. package/src/__tests__/fixtures/common/auth-schema.ts +0 -67
  102. package/src/__tests__/helpers/basic-rls-fixture.ts +0 -135
  103. package/src/__tests__/helpers/pglite-migrations.ts +0 -32
  104. package/src/__tests__/helpers/pglite-rls-session.ts +0 -51
  105. package/src/__tests__/helpers/seed-like-fill.ts +0 -202
  106. package/src/__tests__/helpers/test-gencow-ctx-rls.ts +0 -50
  107. package/src/__tests__/httpaction.test.ts +0 -122
  108. package/src/__tests__/image-optimization.test.ts +0 -648
  109. package/src/__tests__/load.test.ts +0 -389
  110. package/src/__tests__/network-sim.test.ts +0 -319
  111. package/src/__tests__/reactive.test.ts +0 -479
  112. package/src/__tests__/retry.test.ts +0 -113
  113. package/src/__tests__/rls-crud-basic.test.ts +0 -317
  114. package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +0 -117
  115. package/src/__tests__/rls-custom-mutation-handlers.test.ts +0 -142
  116. package/src/__tests__/rls-custom-query-handlers.test.ts +0 -128
  117. package/src/__tests__/rls-db-leased-connection.test.ts +0 -118
  118. package/src/__tests__/rls-session-and-policies.test.ts +0 -228
  119. package/src/__tests__/scheduler-durable-v2.test.ts +0 -288
  120. package/src/__tests__/scheduler-durable.test.ts +0 -173
  121. package/src/__tests__/scheduler-exec.test.ts +0 -328
  122. package/src/__tests__/scheduler.test.ts +0 -187
  123. package/src/__tests__/storage.test.ts +0 -334
  124. package/src/__tests__/tsconfig.json +0 -8
  125. package/src/__tests__/validator.test.ts +0 -323
  126. package/src/__tests__/workflow.test.ts +0 -606
  127. package/src/__tests__/ws-integration.test.ts +0 -309
  128. package/src/__tests__/ws-scale.test.ts +0 -241
  129. package/src/auth.ts +0 -155
  130. package/src/reactive.ts +0 -580
@@ -1,317 +0,0 @@
1
- /**
2
- * fixtures/basic + PGlite — apply migrations, explicit fixtures, verify tasks.list CRUD handler.
3
- *
4
- * Users and tasks: fixed `id` / FK / `done` per row. Any other column omitted from the fixture is
5
- * filled by drizzle-seed’s same per-type generators as `seed()` (see `fillPartialRowsForInsert`).
6
- *
7
- * Migrations run as the PGlite bootstrap user (table owner). We then create `gencow_rls_app` and
8
- * `SET ROLE` so the session is non-owner → RLS policies apply. `createRlsDb` injects
9
- * `app.current_user_id` for every query path (including bare `select()` / `execute()`).
10
- * We rely on `current_setting('app.current_user_id', true)` (missing_ok=true): this avoids
11
- * missing-GUC errors before `set_config` and keeps PGlite behavior aligned with PostgreSQL.
12
- *
13
- * Run: bun test packages/core/src/__tests__/rls-crud-basic.test.ts
14
- */
15
-
16
- import { describe, it, expect, beforeAll, afterAll } from "bun:test";
17
- import { eq, type InferSelectModel } from "drizzle-orm";
18
- import { PGlite } from "@electric-sql/pglite";
19
- import { drizzle } from "drizzle-orm/pglite";
20
- import { crud } from "../crud.js";
21
- import { createRlsDb } from "../rls-db.js";
22
- import { tasks } from "./fixtures/basic/schema.js";
23
- import {
24
- basicFixtureUsers as fixtureUsers,
25
- basicFixtureTasks as fixtureTasks,
26
- basicUser0Identity as user0Identity,
27
- basicUser1Identity as user1Identity,
28
- createBasicRlsEnvironment,
29
- } from "./helpers/basic-rls-fixture.js";
30
- import {
31
- makeTestGencowCtxWithRls,
32
- runWithRollbackTestGencowCtxWithRls,
33
- } from "./helpers/test-gencow-ctx-rls.js";
34
-
35
- type TaskRow = InferSelectModel<typeof tasks>;
36
- type CrudDefs = ReturnType<typeof crud<typeof tasks>>;
37
- type TaskListResult = { data: TaskRow[]; total: number };
38
-
39
- async function createSeededCrudEnv() {
40
- const { client, db, taskRows } = await createBasicRlsEnvironment();
41
-
42
- const defs = crud(tasks, {
43
- prefix: "fixture_basic_pglite_tasks",
44
- searchFields: ["title", "description"],
45
- defaultLimit: 50,
46
- });
47
-
48
- return { client, db, taskRows, defs };
49
- }
50
-
51
- describe("fixtures/basic + PGlite + CRUD + RLS", () => {
52
- let client: PGlite;
53
- let db: ReturnType<typeof drizzle>;
54
- let seededTaskRows: TaskRow[];
55
- let listDef: CrudDefs["list"];
56
- let listHandler: (ctx: unknown, args: unknown) => Promise<TaskListResult>;
57
- let getHandler: NonNullable<CrudDefs["get"]>["handler"];
58
- let createHandler: NonNullable<CrudDefs["create"]>["handler"];
59
- let updateHandler: NonNullable<CrudDefs["update"]>["handler"];
60
- let removeHandler: NonNullable<CrudDefs["remove"]>["handler"];
61
-
62
- beforeAll(async () => {
63
- const env = await createSeededCrudEnv();
64
- client = env.client;
65
- db = env.db;
66
- seededTaskRows = env.taskRows;
67
- listDef = env.defs.list;
68
- listHandler = env.defs.list!.handler as (ctx: unknown, args: unknown) => Promise<TaskListResult>;
69
- getHandler = env.defs.get!.handler as (ctx: unknown, args: unknown) => Promise<TaskRow | null>;
70
- createHandler = env.defs.create!.handler as (ctx: unknown, args: unknown) => Promise<TaskRow>;
71
- updateHandler = env.defs.update!.handler as (ctx: unknown, args: unknown) => Promise<TaskRow | undefined>;
72
- removeHandler = env.defs.remove!.handler as (
73
- ctx: unknown,
74
- args: unknown,
75
- ) => Promise<{ success: boolean }>;
76
- });
77
-
78
- afterAll(async () => {
79
- try {
80
- await client.close();
81
- } catch {
82
- /* ignore */
83
- }
84
- });
85
-
86
- it("createRlsDb: bare select() applies RLS without explicit db.transaction", async () => {
87
- const scoped = createRlsDb(db as any, {
88
- userId: user0Identity.id,
89
- role: "user",
90
- tenantId: "tenant_fixture",
91
- vars: { "app.note": "ok" },
92
- });
93
- const otherUserRows = await scoped.select().from(tasks).where(eq(tasks.userId, fixtureUsers[1].id));
94
- expect(otherUserRows.length).toBe(0);
95
-
96
- const ownRows = await scoped.select().from(tasks).where(eq(tasks.userId, user0Identity.id));
97
- expect(ownRows.length).toBe(seededTaskRows.filter((r) => r.userId === user0Identity.id).length);
98
- });
99
-
100
- it("tasks.list returns only the current user (us_000) rows per RLS and total matches", async () => {
101
- expect(listDef).toBeDefined();
102
-
103
- await runWithRollbackTestGencowCtxWithRls(db, user0Identity, async (ctx) => {
104
- const listResult = await listHandler(ctx, {});
105
-
106
- const expected = seededTaskRows.filter((r) => r.userId === fixtureUsers[0].id);
107
-
108
- expect(listResult).toHaveProperty("data");
109
- expect(listResult).toHaveProperty("total");
110
- expect(listResult.total).toBe(expected.length);
111
-
112
- const rows = listResult.data;
113
-
114
- const byId = new Map(rows.map((r) => [r.id, r]));
115
- expect(byId.size).toBe(expected.length);
116
-
117
- for (const seeded of expected) {
118
- const row = byId.get(seeded.id);
119
- expect(row).toBeDefined();
120
- expect(row!.id).toBe(seeded.id);
121
- expect(row!.userId).toBe(seeded.userId);
122
- expect(row!.done).toBe(seeded.done);
123
- expect(row!.title).toBe(seeded.title);
124
- }
125
- });
126
- });
127
-
128
- it("search parameter applies to title/description ilike search", async () => {
129
- expect(listDef).toBeDefined();
130
-
131
- await runWithRollbackTestGencowCtxWithRls(db, user0Identity, async (ctx) => {
132
- const sharedSubstring = "Project Alpha";
133
- const expectedForUser0 = seededTaskRows.filter(
134
- (r) => r.userId === user0Identity.id && r.title.toLowerCase().includes(sharedSubstring.toLowerCase()),
135
- );
136
- expect(expectedForUser0.length).toBe(2);
137
-
138
- const result = await listHandler(ctx, { search: sharedSubstring });
139
- const rows = result.data;
140
- expect(rows.length).toBe(expectedForUser0.length);
141
-
142
- const byId = new Map(rows.map((r) => [r.id, r]));
143
- for (const seeded of expectedForUser0) {
144
- expect(byId.get(seeded.id)?.title).toBe(seeded.title);
145
- }
146
- });
147
- });
148
-
149
- it("tasks.get succeeds when current user reads own task", async () => {
150
- const ownTaskId = "tk-003";
151
-
152
- await runWithRollbackTestGencowCtxWithRls(db, user0Identity, async (ctx) => {
153
- const task = await getHandler(ctx, { id: ownTaskId });
154
-
155
- expect(task).toBeDefined();
156
- expect(task?.id).toBe(ownTaskId);
157
- expect(task?.userId).toBe(user0Identity.id);
158
- });
159
- });
160
-
161
- it("tasks.get fails when current user tries to read another user's task", async () => {
162
- const user1TaskId = "tk-001";
163
-
164
- await runWithRollbackTestGencowCtxWithRls(db, user0Identity, async (ctx) => {
165
- const task = await getHandler(ctx, { id: user1TaskId });
166
- expect(task).toBeNull();
167
- });
168
- });
169
-
170
- it("tasks.update succeeds when updating a task owned by current user", async () => {
171
- const ownTaskId = "tk-000";
172
- const updatedTitle = "Project Alpha — updated by owner";
173
- const updatedDone = true;
174
-
175
- await runWithRollbackTestGencowCtxWithRls(db, user0Identity, async (ctx) => {
176
- const before = await getHandler(ctx, {
177
- id: ownTaskId,
178
- });
179
- expect(before?.title).not.toBe(updatedTitle);
180
- expect(before?.done).not.toBe(updatedDone);
181
-
182
- const updated = await updateHandler(ctx, {
183
- id: ownTaskId,
184
- title: updatedTitle,
185
- done: updatedDone,
186
- });
187
-
188
- expect(updated).toBeDefined();
189
- expect(updated?.id).toBe(ownTaskId);
190
- expect(updated?.userId).toBe(user0Identity.id);
191
- expect(updated?.title).toBe(updatedTitle);
192
- expect(updated?.done).toBe(updatedDone);
193
-
194
- const fetched = await getHandler(ctx, {
195
- id: ownTaskId,
196
- });
197
- expect(fetched?.id).toBe(ownTaskId);
198
- expect(fetched?.title).toBe(updatedTitle);
199
- expect(fetched?.done).toBe(updatedDone);
200
- });
201
- });
202
-
203
- it("tasks.create succeeds when creating a task with current userId", async () => {
204
- const ownTaskId = "tk-own-create";
205
- const ownTaskTitle = "Project Delta — created by owner";
206
-
207
- await runWithRollbackTestGencowCtxWithRls(db, user0Identity, async (user0Ctx) => {
208
- const created = await createHandler(user0Ctx, {
209
- id: ownTaskId,
210
- userId: user0Identity.id,
211
- title: ownTaskTitle,
212
- done: false,
213
- });
214
-
215
- expect(created).toBeDefined();
216
- expect(created.id).toBe(ownTaskId);
217
- expect(created.userId).toBe(user0Identity.id);
218
- expect(created.title).toBe(ownTaskTitle);
219
- expect(created.done).toBe(false);
220
-
221
- const fetched = await getHandler(user0Ctx, {
222
- id: ownTaskId,
223
- });
224
- expect(fetched).toBeDefined();
225
- expect(fetched?.id).toBe(ownTaskId);
226
- expect(fetched?.userId).toBe(user0Identity.id);
227
- });
228
- });
229
-
230
- it("tasks.create fails when current user tries to create with another userId", async () => {
231
- const unauthorizedTaskId = "tk-other-user-create";
232
- await runWithRollbackTestGencowCtxWithRls(db, user0Identity, async (user0Ctx) => {
233
- let thrown: unknown;
234
- try {
235
- await createHandler(user0Ctx, {
236
- id: unauthorizedTaskId,
237
- userId: user1Identity.id,
238
- title: "Unauthorized create attempt",
239
- done: false,
240
- });
241
- } catch (e) {
242
- thrown = e;
243
- }
244
- expect(thrown).toBeInstanceOf(Error);
245
-
246
- const user1Ctx = makeTestGencowCtxWithRls(user0Ctx.unsafeDb as any, user1Identity);
247
- const after = await getHandler(user1Ctx, { id: unauthorizedTaskId });
248
- expect(after).toBeNull();
249
- });
250
- });
251
-
252
- it("tasks.update fails when current user tries to update user1 task", async () => {
253
- const user1TaskId = "tk-001";
254
- await runWithRollbackTestGencowCtxWithRls(db, user0Identity, async (user0Ctx) => {
255
- const user1Ctx = makeTestGencowCtxWithRls(user0Ctx.unsafeDb as any, user1Identity);
256
-
257
- const before = await getHandler(user1Ctx, {
258
- id: user1TaskId,
259
- });
260
- expect(before).toBeDefined();
261
- const beforeTitle = before?.title;
262
- const beforeDone = before?.done ?? false;
263
-
264
- const unauthorizedUpdate = await updateHandler(user0Ctx, {
265
- id: user1TaskId,
266
- title: "Unauthorized update attempt",
267
- done: !beforeDone,
268
- });
269
-
270
- expect(unauthorizedUpdate).toBeUndefined();
271
-
272
- const after = await getHandler(user1Ctx, {
273
- id: user1TaskId,
274
- });
275
- expect(after).toBeDefined();
276
- expect(after?.title).toBe(beforeTitle);
277
- expect(after?.done).toBe(beforeDone);
278
- });
279
- });
280
-
281
- it("tasks.remove succeeds when deleting a task owned by current user", async () => {
282
- const ownTaskId = "tk-002";
283
- await runWithRollbackTestGencowCtxWithRls(db, user0Identity, async (user0Ctx) => {
284
- const before = await getHandler(user0Ctx, { id: ownTaskId });
285
- expect(before).toBeDefined();
286
-
287
- const removeResult = await removeHandler(user0Ctx, {
288
- id: ownTaskId,
289
- });
290
- expect(removeResult.success).toBe(true);
291
-
292
- const after = await getHandler(user0Ctx, { id: ownTaskId });
293
- expect(after).toBeNull();
294
- });
295
- });
296
-
297
- it("tasks.remove cannot delete a task owned by another user", async () => {
298
- const user1TaskId = "tk-004";
299
- await runWithRollbackTestGencowCtxWithRls(db, user0Identity, async (user0Ctx) => {
300
- const user1Ctx = makeTestGencowCtxWithRls(user0Ctx.unsafeDb as any, user1Identity);
301
-
302
- const before = await getHandler(user1Ctx, { id: user1TaskId });
303
- expect(before).toBeDefined();
304
-
305
- const removeResult = await removeHandler(user0Ctx, {
306
- id: user1TaskId,
307
- });
308
- expect(removeResult.success).toBe(true);
309
-
310
- const after = await getHandler(user1Ctx, {
311
- id: user1TaskId,
312
- });
313
- expect(after).toBeDefined();
314
- expect(after?.id).toBe(user1TaskId);
315
- });
316
- });
317
- });
@@ -1,117 +0,0 @@
1
- /**
2
- * `news` has **no** `ownerRls()` and **no** PostgreSQL RLS (`fixtures/basic/migrations/0001_news.sql`).
3
- * `crud(news, { public: true })` keeps legacy behavior: no Layer-1 owner filter on list/get/update/remove.
4
- *
5
- * Complements mock-based `crud-owner-rls.test.ts` (“하위호환”) with PGlite + non-owner session.
6
- *
7
- * Run: bun test packages/core/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts
8
- */
9
-
10
- import { describe, it, expect, beforeAll, afterAll } from "bun:test";
11
-
12
- import { crud } from "../crud.js";
13
- import { createRlsDb } from "../rls-db.js";
14
- import { news } from "./fixtures/basic/schema.js";
15
- import {
16
- assertTableRowLevelSecurityDisabled,
17
- basicUser0Identity,
18
- basicUser1Identity,
19
- createBasicRlsEnvironment,
20
- } from "./helpers/basic-rls-fixture.js";
21
- import {
22
- makeTestGencowCtxWithRls,
23
- runWithRollbackTestGencowCtxWithRls,
24
- } from "./helpers/test-gencow-ctx-rls.js";
25
-
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 }>;
34
-
35
- beforeAll(async () => {
36
- const env = await createBasicRlsEnvironment();
37
- client = env.client;
38
- db = env.db;
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;
50
- });
51
-
52
- afterAll(async () => {
53
- try {
54
- await client.close();
55
- } catch {
56
- /* ignore */
57
- }
58
- });
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
- });
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
- });
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);
83
- });
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
- });
94
- });
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
- });
104
- });
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
- });
112
- });
113
-
114
- it("pg_catalog: news has row security disabled", async () => {
115
- await assertTableRowLevelSecurityDisabled(db, news);
116
- });
117
- });
@@ -1,142 +0,0 @@
1
- /**
2
- * Custom **mutation** handlers (not `crud()` factory) that use `ctx.db` must respect RLS
3
- * (update/delete/insert withCheck).
4
- *
5
- * Run: bun test packages/core/src/__tests__/rls-custom-mutation-handlers.test.ts
6
- */
7
-
8
- import { describe, it, expect, beforeAll, afterAll } from "bun:test";
9
- import { eq } from "drizzle-orm";
10
- import { PGlite } from "@electric-sql/pglite";
11
- import { drizzle } from "drizzle-orm/pglite";
12
-
13
- import type { GencowCtx } from "../reactive.js";
14
- import { tasks } from "./fixtures/basic/schema.js";
15
- import {
16
- basicFixtureUsers,
17
- basicUser0Identity,
18
- basicUser1Identity,
19
- createBasicRlsEnvironment,
20
- } from "./helpers/basic-rls-fixture.js";
21
- import { fillPartialRowsForInsert } from "./helpers/seed-like-fill.js";
22
- import {
23
- makeTestGencowCtxWithRls,
24
- runWithRollbackTestGencowCtxWithRls,
25
- } from "./helpers/test-gencow-ctx-rls.js";
26
-
27
- /** User-defined mutation: update title by task id. */
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();
30
- }
31
-
32
- /** User-defined mutation: delete by id. */
33
- async function customMutationDeleteById(ctx: GencowCtx, taskId: string) {
34
- return ctx.db.delete(tasks).where(eq(tasks.id, taskId)).returning();
35
- }
36
-
37
- /** User-defined mutation: insert a new task for the current user (caller supplies logical fields). */
38
- async function customMutationInsertTask(
39
- ctx: GencowCtx,
40
- values: { id: string; title: string; userId: string; done?: boolean },
41
- ) {
42
- const [row] = fillPartialRowsForInsert(tasks, [
43
- {
44
- id: values.id,
45
- title: values.title,
46
- userId: values.userId,
47
- done: values.done ?? false,
48
- },
49
- ]);
50
- return ctx.db.insert(tasks).values(row).returning();
51
- }
52
-
53
- describe("Custom mutation handlers + ctx.db + RLS", () => {
54
- let client: PGlite;
55
- let db: ReturnType<typeof drizzle>;
56
-
57
- beforeAll(async () => {
58
- const env = await createBasicRlsEnvironment();
59
- client = env.client;
60
- db = env.db;
61
- });
62
-
63
- afterAll(async () => {
64
- try {
65
- await client.close();
66
- } catch {
67
- /* ignore */
68
- }
69
- });
70
-
71
- it("custom update: can change own task", async () => {
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
- });
77
- });
78
-
79
- it("custom update: cannot modify another user's task (0 rows)", async () => {
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
- });
84
- });
85
-
86
- it("custom delete: can remove own task", async () => {
87
- await runWithRollbackTestGencowCtxWithRls(db, basicUser0Identity, async (ctx) => {
88
- const removed = await customMutationDeleteById(ctx, "tk-000");
89
- expect(removed.length).toBe(1);
90
- });
91
- });
92
-
93
- it("custom delete: cannot remove another user's task (0 rows)", async () => {
94
- await runWithRollbackTestGencowCtxWithRls(db, basicUser0Identity, async (ctx) => {
95
- const removed = await customMutationDeleteById(ctx, "tk-004");
96
- expect(removed.length).toBe(0);
97
- });
98
- });
99
-
100
- it("custom insert: succeeds when userId matches session", async () => {
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
- });
110
- });
111
-
112
- it("custom insert: forged userId (another user) violates RLS withCheck", async () => {
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;
123
- }
124
- expect(thrown).toBeInstanceOf(Error);
125
- });
126
- });
127
-
128
- it("nested db.transaction in custom handler still applies RLS on inner selects", async () => {
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
- });
135
- });
136
-
137
- it("direct ctx.db without outer rollback wrapper: mutation still RLS-scoped", async () => {
138
- const ctx = makeTestGencowCtxWithRls(db, basicUser1Identity);
139
- const updated = await customMutationUpdateTitle(ctx, "tk-000", "hax");
140
- expect(updated.length).toBe(0);
141
- });
142
- });