@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
@@ -28,219 +28,201 @@ import { eq, sql } from "drizzle-orm";
28
28
  import { createRlsDb } from "../rls-db.js";
29
29
  import { tasks } from "./fixtures/basic/schema.js";
30
30
  import {
31
- basicFixtureTasks,
32
- basicUser0Identity,
33
- basicUser1Identity,
34
- createBasicRlsEnvironment,
31
+ basicFixtureTasks,
32
+ basicUser0Identity,
33
+ basicUser1Identity,
34
+ createBasicRlsEnvironment,
35
35
  } from "./helpers/basic-rls-fixture.js";
36
36
  import { fillPartialRowsForInsert } from "./helpers/seed-like-fill.js";
37
37
  import {
38
- makeTestGencowCtxWithRls,
39
- runWithRollbackTestGencowCtxWithRls,
38
+ makeTestGencowCtxWithRls,
39
+ runWithRollbackTestGencowCtxWithRls,
40
40
  } from "./helpers/test-gencow-ctx-rls.js";
41
41
 
42
42
  type PgliteDb = ReturnType<typeof import("drizzle-orm/pglite").drizzle>;
43
43
 
44
44
  describe("RLS session + policies (PGlite, non-owner session)", () => {
45
- let client: import("@electric-sql/pglite").PGlite;
46
- let db: PgliteDb;
47
-
48
- beforeAll(async () => {
49
- const env = await createBasicRlsEnvironment();
50
- client = env.client;
51
- db = env.db;
52
- });
53
-
54
- afterAll(async () => {
55
- try {
56
- await client.close();
57
- } catch {
58
- /* ignore */
59
- }
60
- });
61
-
62
- function expectedCountForUser(userId: string): number {
63
- return basicFixtureTasks.filter((t) => t.userId === userId).length;
45
+ let client: import("@electric-sql/pglite").PGlite;
46
+ let db: PgliteDb;
47
+
48
+ beforeAll(async () => {
49
+ const env = await createBasicRlsEnvironment();
50
+ client = env.client;
51
+ db = env.db;
52
+ });
53
+
54
+ afterAll(async () => {
55
+ try {
56
+ await client.close();
57
+ } catch {
58
+ /* ignore */
64
59
  }
60
+ });
65
61
 
66
- it("empty userId matches unauthenticated server contract — no rows visible", async () => {
67
- const scoped = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
68
- userId: "",
69
- });
70
- const rows = await scoped.select().from(tasks);
71
- expect(rows.length).toBe(0);
72
- });
73
-
74
- it("authenticated userId sees only own rows (policy USING)", async () => {
75
- const scoped = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
76
- userId: basicUser0Identity.id,
77
- });
78
- const rows = await scoped.select().from(tasks);
79
- expect(rows.length).toBe(expectedCountForUser(basicUser0Identity.id));
80
- expect(rows.every((r) => r.userId === basicUser0Identity.id)).toBe(true);
81
- });
82
-
83
- it("createRlsDb sets optional role and tenantId GUCs (readable via execute)", async () => {
84
- const scoped = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
85
- userId: basicUser0Identity.id,
86
- role: "editor",
87
- tenantId: "tenant_test",
88
- });
89
- const r = await scoped.execute(
90
- sql`select current_setting('app.current_user_role', true) as role,
91
- current_setting('app.tenant_id', true) as tid`,
92
- );
93
- const row = (r as { rows: { role: string; tid: string }[] }).rows[0];
94
- expect(row?.role).toBe("editor");
95
- expect(row?.tid).toBe("tenant_test");
96
- });
97
-
98
- it("custom vars (validated app.* names) are applied for set_config", async () => {
99
- const scoped = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
100
- userId: basicUser0Identity.id,
101
- vars: { "app.org_slug": "acme" },
102
- });
103
- const r = await scoped.execute(
104
- sql`select current_setting('app.org_slug', true) as slug`,
105
- );
106
- const row = (r as { rows: { slug: string }[] }).rows[0];
107
- expect(row?.slug).toBe("acme");
108
- });
109
-
110
- it("invalid GUC name in vars throws before executing SQL", async () => {
111
- const bad = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
112
- userId: basicUser0Identity.id,
113
- vars: { "App.invalid": "x" },
114
- });
115
- await expect(Promise.resolve(bad.select().from(tasks))).rejects.toThrow(
116
- /invalid/,
117
- );
118
- });
119
-
120
- it("reserved keys must not appear in vars", async () => {
121
- const bad = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
122
- userId: basicUser0Identity.id,
123
- vars: { "app.current_user_id": "hijack" },
124
- });
125
- await expect(Promise.resolve(bad.select().from(tasks))).rejects.toThrow(
126
- /vars must not set/,
127
- );
128
- });
129
-
130
- it("autocommit path and outer-transaction path return the same RLS-visible counts", async () => {
131
- const expected = expectedCountForUser(basicUser0Identity.id);
132
-
133
- const ctxPlain = makeTestGencowCtxWithRls(db, basicUser0Identity);
134
- const plainCount = (await ctxPlain.db.select().from(tasks)).length;
135
- expect(plainCount).toBe(expected);
136
-
137
- await runWithRollbackTestGencowCtxWithRls(
138
- db,
139
- basicUser0Identity,
140
- async (ctx) => {
141
- const txCount = (await ctx.db.select().from(tasks)).length;
142
- expect(txCount).toBe(expected);
143
- },
144
- );
145
- });
146
-
147
- it("createRlsDb(db).transaction() injects session vars — inner selects respect RLS", async () => {
148
- const scoped = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
149
- userId: basicUser0Identity.id,
150
- });
151
- await scoped.transaction(async (tx) => {
152
- const rows = await tx.select().from(tasks);
153
- expect(rows.length).toBe(expectedCountForUser(basicUser0Identity.id));
154
- });
155
- });
156
-
157
- it("parallel selects on the same ctx.db both see RLS-consistent results", async () => {
158
- const ctx = makeTestGencowCtxWithRls(db, basicUser0Identity);
159
- const expected = expectedCountForUser(basicUser0Identity.id);
160
- const [a, b] = await Promise.all([
161
- ctx.db.select().from(tasks),
162
- ctx.db.select().from(tasks),
163
- ]);
164
- expect(a.length).toBe(expected);
165
- expect(b.length).toBe(expected);
166
- });
167
-
168
- it("unsafeDb INSERT without session GUC fails RLS withCheck (non-owner)", async () => {
169
- const [row] = fillPartialRowsForInsert(tasks, [
170
- {
171
- id: "tk-rls-unsafe-insert",
172
- title: "should not persist",
173
- userId: basicUser0Identity.id,
174
- done: false,
175
- },
176
- ]);
177
- await expect(Promise.resolve(db.insert(tasks).values(row))).rejects.toThrow();
178
- });
179
-
180
- it("unsafeDb UPDATE touches 0 rows when session GUC is unset", async () => {
181
- const updated = await db
182
- .update(tasks)
183
- .set({ title: "nope", updatedAt: new Date() })
184
- .where(eq(tasks.id, "tk-000"))
185
- .returning();
186
- expect(updated.length).toBe(0);
187
- });
188
-
189
- it("scoped INSERT cannot forge another user row (withCheck)", async () => {
190
- const scoped = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
191
- userId: basicUser0Identity.id,
192
- });
193
- const [row] = fillPartialRowsForInsert(tasks, [
194
- {
195
- id: "tk-forge-other",
196
- title: "forged",
197
- userId: basicUser1Identity.id,
198
- done: false,
199
- },
200
- ]);
201
- await expect(Promise.resolve(scoped.insert(tasks).values(row))).rejects.toThrow();
202
- });
203
-
204
- it("wrong scoped userId cannot read other user's row by primary key", async () => {
205
- const scoped = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
206
- userId: basicUser0Identity.id,
207
- });
208
- const rows = await scoped
209
- .select()
210
- .from(tasks)
211
- .where(eq(tasks.id, "tk-001"));
212
- expect(rows.length).toBe(0);
213
- });
62
+ function expectedCountForUser(userId: string): number {
63
+ return basicFixtureTasks.filter((t) => t.userId === userId).length;
64
+ }
214
65
 
215
- it("scoped UPDATE affects 0 rows for another user's task (policy USING)", async () => {
216
- const scoped = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
217
- userId: basicUser0Identity.id,
218
- });
219
- const updated = await scoped
220
- .update(tasks)
221
- .set({ title: "blocked", updatedAt: new Date() })
222
- .where(eq(tasks.id, "tk-001"))
223
- .returning();
224
- expect(updated.length).toBe(0);
66
+ it("empty userId matches unauthenticated server contract no rows visible", async () => {
67
+ const scoped = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
68
+ userId: "",
225
69
  });
70
+ const rows = await scoped.select().from(tasks);
71
+ expect(rows.length).toBe(0);
72
+ });
226
73
 
227
- it("scoped DELETE removes 0 rows for another user's task (policy USING)", async () => {
228
- const scoped = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
229
- userId: basicUser0Identity.id,
230
- });
231
- const removed = await scoped.delete(tasks).where(eq(tasks.id, "tk-001")).returning();
232
- expect(removed.length).toBe(0);
74
+ it("authenticated userId sees only own rows (policy USING)", async () => {
75
+ const scoped = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
76
+ userId: basicUser0Identity.id,
233
77
  });
78
+ const rows = await scoped.select().from(tasks);
79
+ expect(rows.length).toBe(expectedCountForUser(basicUser0Identity.id));
80
+ expect(rows.every((r) => r.userId === basicUser0Identity.id)).toBe(true);
81
+ });
234
82
 
235
- it("nested raw SQL count matches builder select count for same session", async () => {
236
- const scoped = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
237
- userId: basicUser0Identity.id,
238
- });
239
- const fromBuilder = await scoped.select().from(tasks);
240
- const raw = await scoped.execute(
241
- sql`select count(*)::int as c from ${tasks}`,
242
- );
243
- const c = (raw as { rows: { c: number }[] }).rows[0]?.c ?? -1;
244
- expect(c).toBe(fromBuilder.length);
83
+ it("createRlsDb sets optional role and tenantId GUCs (readable via execute)", async () => {
84
+ const scoped = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
85
+ userId: basicUser0Identity.id,
86
+ role: "editor",
87
+ tenantId: "tenant_test",
245
88
  });
89
+ const r = await scoped.execute(
90
+ sql`select current_setting('app.current_user_role', true) as role,
91
+ current_setting('app.tenant_id', true) as tid`,
92
+ );
93
+ const row = (r as { rows: { role: string; tid: string }[] }).rows[0];
94
+ expect(row?.role).toBe("editor");
95
+ expect(row?.tid).toBe("tenant_test");
96
+ });
97
+
98
+ it("custom vars (validated app.* names) are applied for set_config", async () => {
99
+ const scoped = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
100
+ userId: basicUser0Identity.id,
101
+ vars: { "app.org_slug": "acme" },
102
+ });
103
+ const r = await scoped.execute(sql`select current_setting('app.org_slug', true) as slug`);
104
+ const row = (r as { rows: { slug: string }[] }).rows[0];
105
+ expect(row?.slug).toBe("acme");
106
+ });
107
+
108
+ it("invalid GUC name in vars throws before executing SQL", async () => {
109
+ const bad = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
110
+ userId: basicUser0Identity.id,
111
+ vars: { "App.invalid": "x" },
112
+ });
113
+ await expect(Promise.resolve(bad.select().from(tasks))).rejects.toThrow(/invalid/);
114
+ });
115
+
116
+ it("reserved keys must not appear in vars", async () => {
117
+ const bad = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
118
+ userId: basicUser0Identity.id,
119
+ vars: { "app.current_user_id": "hijack" },
120
+ });
121
+ await expect(Promise.resolve(bad.select().from(tasks))).rejects.toThrow(/vars must not set/);
122
+ });
123
+
124
+ it("autocommit path and outer-transaction path return the same RLS-visible counts", async () => {
125
+ const expected = expectedCountForUser(basicUser0Identity.id);
126
+
127
+ const ctxPlain = makeTestGencowCtxWithRls(db, basicUser0Identity);
128
+ const plainCount = (await ctxPlain.db.select().from(tasks)).length;
129
+ expect(plainCount).toBe(expected);
130
+
131
+ await runWithRollbackTestGencowCtxWithRls(db, basicUser0Identity, async (ctx) => {
132
+ const txCount = (await ctx.db.select().from(tasks)).length;
133
+ expect(txCount).toBe(expected);
134
+ });
135
+ });
136
+
137
+ it("createRlsDb(db).transaction() injects session vars — inner selects respect RLS", async () => {
138
+ const scoped = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
139
+ userId: basicUser0Identity.id,
140
+ });
141
+ await scoped.transaction(async (tx) => {
142
+ const rows = await tx.select().from(tasks);
143
+ expect(rows.length).toBe(expectedCountForUser(basicUser0Identity.id));
144
+ });
145
+ });
146
+
147
+ it("parallel selects on the same ctx.db both see RLS-consistent results", async () => {
148
+ const ctx = makeTestGencowCtxWithRls(db, basicUser0Identity);
149
+ const expected = expectedCountForUser(basicUser0Identity.id);
150
+ const [a, b] = await Promise.all([ctx.db.select().from(tasks), ctx.db.select().from(tasks)]);
151
+ expect(a.length).toBe(expected);
152
+ expect(b.length).toBe(expected);
153
+ });
154
+
155
+ it("unsafeDb INSERT without session GUC fails RLS withCheck (non-owner)", async () => {
156
+ const [row] = fillPartialRowsForInsert(tasks, [
157
+ {
158
+ id: "tk-rls-unsafe-insert",
159
+ title: "should not persist",
160
+ userId: basicUser0Identity.id,
161
+ done: false,
162
+ },
163
+ ]);
164
+ await expect(Promise.resolve(db.insert(tasks).values(row))).rejects.toThrow();
165
+ });
166
+
167
+ it("unsafeDb UPDATE touches 0 rows when session GUC is unset", async () => {
168
+ const updated = await db
169
+ .update(tasks)
170
+ .set({ title: "nope", updatedAt: new Date() })
171
+ .where(eq(tasks.id, "tk-000"))
172
+ .returning();
173
+ expect(updated.length).toBe(0);
174
+ });
175
+
176
+ it("scoped INSERT cannot forge another user row (withCheck)", async () => {
177
+ const scoped = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
178
+ userId: basicUser0Identity.id,
179
+ });
180
+ const [row] = fillPartialRowsForInsert(tasks, [
181
+ {
182
+ id: "tk-forge-other",
183
+ title: "forged",
184
+ userId: basicUser1Identity.id,
185
+ done: false,
186
+ },
187
+ ]);
188
+ await expect(Promise.resolve(scoped.insert(tasks).values(row))).rejects.toThrow();
189
+ });
190
+
191
+ it("wrong scoped userId cannot read other user's row by primary key", async () => {
192
+ const scoped = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
193
+ userId: basicUser0Identity.id,
194
+ });
195
+ const rows = await scoped.select().from(tasks).where(eq(tasks.id, "tk-001"));
196
+ expect(rows.length).toBe(0);
197
+ });
198
+
199
+ it("scoped UPDATE affects 0 rows for another user's task (policy USING)", async () => {
200
+ const scoped = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
201
+ userId: basicUser0Identity.id,
202
+ });
203
+ const updated = await scoped
204
+ .update(tasks)
205
+ .set({ title: "blocked", updatedAt: new Date() })
206
+ .where(eq(tasks.id, "tk-001"))
207
+ .returning();
208
+ expect(updated.length).toBe(0);
209
+ });
210
+
211
+ it("scoped DELETE removes 0 rows for another user's task (policy USING)", async () => {
212
+ const scoped = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
213
+ userId: basicUser0Identity.id,
214
+ });
215
+ const removed = await scoped.delete(tasks).where(eq(tasks.id, "tk-001")).returning();
216
+ expect(removed.length).toBe(0);
217
+ });
218
+
219
+ it("nested raw SQL count matches builder select count for same session", async () => {
220
+ const scoped = createRlsDb(db as Parameters<typeof createRlsDb>[0], {
221
+ userId: basicUser0Identity.id,
222
+ });
223
+ const fromBuilder = await scoped.select().from(tasks);
224
+ const raw = await scoped.execute(sql`select count(*)::int as c from ${tasks}`);
225
+ const c = (raw as { rows: { c: number }[] }).rows[0]?.c ?? -1;
226
+ expect(c).toBe(fromBuilder.length);
227
+ });
246
228
  });