@gencow/core 0.1.23 → 0.1.25

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 (77) hide show
  1. package/dist/crud.d.ts +2 -2
  2. package/dist/crud.js +225 -208
  3. package/dist/index.d.ts +7 -3
  4. package/dist/index.js +4 -1
  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-types.d.ts +81 -0
  16. package/dist/workflow-types.js +12 -0
  17. package/dist/workflow.d.ts +30 -0
  18. package/dist/workflow.js +150 -0
  19. package/dist/workflows-api.d.ts +13 -0
  20. package/dist/workflows-api.js +321 -0
  21. package/package.json +46 -42
  22. package/src/__tests__/auth.test.ts +90 -86
  23. package/src/__tests__/crons.test.ts +69 -67
  24. package/src/__tests__/crud-codegen-integration.test.ts +164 -170
  25. package/src/__tests__/crud-owner-rls.test.ts +308 -301
  26. package/src/__tests__/crud.test.ts +694 -711
  27. package/src/__tests__/dist-exports.test.ts +120 -114
  28. package/src/__tests__/fixtures/basic/auth.ts +16 -16
  29. package/src/__tests__/fixtures/basic/drizzle.config.ts +1 -4
  30. package/src/__tests__/fixtures/basic/index.ts +1 -1
  31. package/src/__tests__/fixtures/basic/schema.ts +1 -1
  32. package/src/__tests__/fixtures/basic/tasks.ts +4 -4
  33. package/src/__tests__/fixtures/common/auth-schema.ts +38 -34
  34. package/src/__tests__/helpers/basic-rls-fixture.ts +80 -78
  35. package/src/__tests__/helpers/pglite-migrations.ts +2 -5
  36. package/src/__tests__/helpers/pglite-rls-session.ts +13 -16
  37. package/src/__tests__/helpers/seed-like-fill.ts +50 -44
  38. package/src/__tests__/helpers/test-gencow-ctx-rls.ts +4 -7
  39. package/src/__tests__/httpaction.test.ts +91 -91
  40. package/src/__tests__/image-optimization.test.ts +570 -574
  41. package/src/__tests__/load.test.ts +321 -308
  42. package/src/__tests__/network-sim.test.ts +238 -215
  43. package/src/__tests__/reactive.test.ts +380 -358
  44. package/src/__tests__/retry.test.ts +99 -84
  45. package/src/__tests__/rls-crud-basic.test.ts +172 -245
  46. package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +81 -81
  47. package/src/__tests__/rls-custom-mutation-handlers.test.ts +47 -94
  48. package/src/__tests__/rls-custom-query-handlers.test.ts +92 -92
  49. package/src/__tests__/rls-db-leased-connection.test.ts +2 -6
  50. package/src/__tests__/rls-session-and-policies.test.ts +181 -199
  51. package/src/__tests__/scheduler-durable-v2.test.ts +199 -181
  52. package/src/__tests__/scheduler-durable.test.ts +117 -117
  53. package/src/__tests__/scheduler-exec.test.ts +258 -246
  54. package/src/__tests__/scheduler.test.ts +129 -111
  55. package/src/__tests__/storage.test.ts +282 -269
  56. package/src/__tests__/tsconfig.json +6 -6
  57. package/src/__tests__/validator.test.ts +236 -232
  58. package/src/__tests__/workflow.test.ts +606 -0
  59. package/src/__tests__/ws-integration.test.ts +223 -218
  60. package/src/__tests__/ws-scale.test.ts +168 -159
  61. package/src/auth-config.ts +18 -18
  62. package/src/auth.ts +106 -106
  63. package/src/crons.ts +77 -77
  64. package/src/crud.ts +523 -479
  65. package/src/index.ts +71 -6
  66. package/src/reactive.ts +357 -331
  67. package/src/retry.ts +51 -54
  68. package/src/rls-db.ts +195 -205
  69. package/src/rls.ts +33 -36
  70. package/src/scheduler.ts +237 -211
  71. package/src/server.ts +0 -1
  72. package/src/storage.ts +632 -593
  73. package/src/v.ts +119 -114
  74. package/src/workflow-types.ts +108 -0
  75. package/src/workflow.ts +188 -0
  76. package/src/workflows-api.ts +415 -0
  77. 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
  });