@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
@@ -65,25 +65,13 @@ describe("fixtures/basic + PGlite + CRUD + RLS", () => {
65
65
  db = env.db;
66
66
  seededTaskRows = env.taskRows;
67
67
  listDef = env.defs.list;
68
- listHandler = env.defs.list!.handler as (
69
- ctx: unknown,
70
- args: unknown
71
- ) => Promise<TaskListResult>;
72
- getHandler = env.defs.get!.handler as (
73
- ctx: unknown,
74
- args: unknown
75
- ) => Promise<TaskRow | null>;
76
- createHandler = env.defs.create!.handler as (
77
- ctx: unknown,
78
- args: unknown
79
- ) => Promise<TaskRow>;
80
- updateHandler = env.defs.update!.handler as (
81
- ctx: unknown,
82
- args: unknown
83
- ) => Promise<TaskRow | undefined>;
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>;
84
72
  removeHandler = env.defs.remove!.handler as (
85
73
  ctx: unknown,
86
- args: unknown
74
+ args: unknown,
87
75
  ) => Promise<{ success: boolean }>;
88
76
  });
89
77
 
@@ -102,109 +90,81 @@ describe("fixtures/basic + PGlite + CRUD + RLS", () => {
102
90
  tenantId: "tenant_fixture",
103
91
  vars: { "app.note": "ok" },
104
92
  });
105
- const otherUserRows = await scoped
106
- .select()
107
- .from(tasks)
108
- .where(eq(tasks.userId, fixtureUsers[1].id));
93
+ const otherUserRows = await scoped.select().from(tasks).where(eq(tasks.userId, fixtureUsers[1].id));
109
94
  expect(otherUserRows.length).toBe(0);
110
95
 
111
- const ownRows = await scoped
112
- .select()
113
- .from(tasks)
114
- .where(eq(tasks.userId, user0Identity.id));
115
- expect(ownRows.length).toBe(
116
- seededTaskRows.filter((r) => r.userId === user0Identity.id).length
117
- );
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);
118
98
  });
119
99
 
120
100
  it("tasks.list returns only the current user (us_000) rows per RLS and total matches", async () => {
121
101
  expect(listDef).toBeDefined();
122
102
 
123
- await runWithRollbackTestGencowCtxWithRls(
124
- db,
125
- user0Identity,
126
- async (ctx) => {
127
- const listResult = await listHandler(ctx, {});
128
-
129
- const expected = seededTaskRows.filter(
130
- (r) => r.userId === fixtureUsers[0].id
131
- );
132
-
133
- expect(listResult).toHaveProperty("data");
134
- expect(listResult).toHaveProperty("total");
135
- expect(listResult.total).toBe(expected.length);
136
-
137
- const rows = listResult.data;
138
-
139
- const byId = new Map(rows.map((r) => [r.id, r]));
140
- expect(byId.size).toBe(expected.length);
141
-
142
- for (const seeded of expected) {
143
- const row = byId.get(seeded.id);
144
- expect(row).toBeDefined();
145
- expect(row!.id).toBe(seeded.id);
146
- expect(row!.userId).toBe(seeded.userId);
147
- expect(row!.done).toBe(seeded.done);
148
- expect(row!.title).toBe(seeded.title);
149
- }
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);
150
124
  }
151
- );
125
+ });
152
126
  });
153
127
 
154
128
  it("search parameter applies to title/description ilike search", async () => {
155
129
  expect(listDef).toBeDefined();
156
130
 
157
- await runWithRollbackTestGencowCtxWithRls(
158
- db,
159
- user0Identity,
160
- async (ctx) => {
161
- const sharedSubstring = "Project Alpha";
162
- const expectedForUser0 = seededTaskRows.filter(
163
- (r) =>
164
- r.userId === user0Identity.id &&
165
- r.title.toLowerCase().includes(sharedSubstring.toLowerCase())
166
- );
167
- expect(expectedForUser0.length).toBe(2);
168
-
169
- const result = await listHandler(ctx, { search: sharedSubstring });
170
- const rows = result.data;
171
- expect(rows.length).toBe(expectedForUser0.length);
172
-
173
- const byId = new Map(rows.map((r) => [r.id, r]));
174
- for (const seeded of expectedForUser0) {
175
- expect(byId.get(seeded.id)?.title).toBe(seeded.title);
176
- }
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);
177
145
  }
178
- );
146
+ });
179
147
  });
180
148
 
181
149
  it("tasks.get succeeds when current user reads own task", async () => {
182
150
  const ownTaskId = "tk-003";
183
151
 
184
- await runWithRollbackTestGencowCtxWithRls(
185
- db,
186
- user0Identity,
187
- async (ctx) => {
188
- const task = await getHandler(ctx, { id: ownTaskId });
152
+ await runWithRollbackTestGencowCtxWithRls(db, user0Identity, async (ctx) => {
153
+ const task = await getHandler(ctx, { id: ownTaskId });
189
154
 
190
- expect(task).toBeDefined();
191
- expect(task?.id).toBe(ownTaskId);
192
- expect(task?.userId).toBe(user0Identity.id);
193
- }
194
- );
155
+ expect(task).toBeDefined();
156
+ expect(task?.id).toBe(ownTaskId);
157
+ expect(task?.userId).toBe(user0Identity.id);
158
+ });
195
159
  });
196
160
 
197
161
  it("tasks.get fails when current user tries to read another user's task", async () => {
198
162
  const user1TaskId = "tk-001";
199
163
 
200
- await runWithRollbackTestGencowCtxWithRls(
201
- db,
202
- user0Identity,
203
- async (ctx) => {
204
- const task = await getHandler(ctx, { id: user1TaskId });
205
- expect(task).toBeNull();
206
- }
207
- );
164
+ await runWithRollbackTestGencowCtxWithRls(db, user0Identity, async (ctx) => {
165
+ const task = await getHandler(ctx, { id: user1TaskId });
166
+ expect(task).toBeNull();
167
+ });
208
168
  });
209
169
 
210
170
  it("tasks.update succeeds when updating a task owned by current user", async () => {
@@ -212,179 +172,146 @@ describe("fixtures/basic + PGlite + CRUD + RLS", () => {
212
172
  const updatedTitle = "Project Alpha — updated by owner";
213
173
  const updatedDone = true;
214
174
 
215
- await runWithRollbackTestGencowCtxWithRls(
216
- db,
217
- user0Identity,
218
- async (ctx) => {
219
- const before = await getHandler(ctx, {
220
- id: ownTaskId,
221
- });
222
- expect(before?.title).not.toBe(updatedTitle);
223
- expect(before?.done).not.toBe(updatedDone);
224
-
225
- const updated = await updateHandler(ctx, {
226
- id: ownTaskId,
227
- title: updatedTitle,
228
- done: updatedDone,
229
- });
230
-
231
- expect(updated).toBeDefined();
232
- expect(updated?.id).toBe(ownTaskId);
233
- expect(updated?.userId).toBe(user0Identity.id);
234
- expect(updated?.title).toBe(updatedTitle);
235
- expect(updated?.done).toBe(updatedDone);
236
-
237
- const fetched = await getHandler(ctx, {
238
- id: ownTaskId,
239
- });
240
- expect(fetched?.id).toBe(ownTaskId);
241
- expect(fetched?.title).toBe(updatedTitle);
242
- expect(fetched?.done).toBe(updatedDone);
243
- }
244
- );
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
+ });
245
201
  });
246
202
 
247
203
  it("tasks.create succeeds when creating a task with current userId", async () => {
248
204
  const ownTaskId = "tk-own-create";
249
205
  const ownTaskTitle = "Project Delta — created by owner";
250
206
 
251
- await runWithRollbackTestGencowCtxWithRls(
252
- db,
253
- user0Identity,
254
- async (user0Ctx) => {
255
- const created = await createHandler(user0Ctx, {
256
- id: ownTaskId,
257
- userId: user0Identity.id,
258
- title: ownTaskTitle,
259
- done: false,
260
- });
261
-
262
- expect(created).toBeDefined();
263
- expect(created.id).toBe(ownTaskId);
264
- expect(created.userId).toBe(user0Identity.id);
265
- expect(created.title).toBe(ownTaskTitle);
266
- expect(created.done).toBe(false);
267
-
268
- const fetched = await getHandler(user0Ctx, {
269
- id: ownTaskId,
270
- });
271
- expect(fetched).toBeDefined();
272
- expect(fetched?.id).toBe(ownTaskId);
273
- expect(fetched?.userId).toBe(user0Identity.id);
274
- }
275
- );
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
+ });
276
228
  });
277
229
 
278
230
  it("tasks.create fails when current user tries to create with another userId", async () => {
279
231
  const unauthorizedTaskId = "tk-other-user-create";
280
- await runWithRollbackTestGencowCtxWithRls(
281
- db,
282
- user0Identity,
283
- async (user0Ctx) => {
284
- let thrown: unknown;
285
- try {
286
- await createHandler(user0Ctx, {
287
- id: unauthorizedTaskId,
288
- userId: user1Identity.id,
289
- title: "Unauthorized create attempt",
290
- done: false,
291
- });
292
- } catch (e) {
293
- thrown = e;
294
- }
295
- expect(thrown).toBeInstanceOf(Error);
296
-
297
- const user1Ctx = makeTestGencowCtxWithRls(
298
- user0Ctx.unsafeDb as any,
299
- user1Identity
300
- );
301
- const after = await getHandler(user1Ctx, { id: unauthorizedTaskId });
302
- expect(after).toBeNull();
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;
303
243
  }
304
- );
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
+ });
305
250
  });
306
251
 
307
252
  it("tasks.update fails when current user tries to update user1 task", async () => {
308
253
  const user1TaskId = "tk-001";
309
- await runWithRollbackTestGencowCtxWithRls(
310
- db,
311
- user0Identity,
312
- async (user0Ctx) => {
313
- const user1Ctx = makeTestGencowCtxWithRls(
314
- user0Ctx.unsafeDb as any,
315
- user1Identity
316
- );
317
-
318
- const before = await getHandler(user1Ctx, {
319
- id: user1TaskId,
320
- });
321
- expect(before).toBeDefined();
322
- const beforeTitle = before?.title;
323
- const beforeDone = before?.done ?? false;
324
-
325
- const unauthorizedUpdate = await updateHandler(user0Ctx, {
326
- id: user1TaskId,
327
- title: "Unauthorized update attempt",
328
- done: !beforeDone,
329
- });
330
-
331
- expect(unauthorizedUpdate).toBeUndefined();
332
-
333
- const after = await getHandler(user1Ctx, {
334
- id: user1TaskId,
335
- });
336
- expect(after).toBeDefined();
337
- expect(after?.title).toBe(beforeTitle);
338
- expect(after?.done).toBe(beforeDone);
339
- }
340
- );
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
+ });
341
279
  });
342
280
 
343
281
  it("tasks.remove succeeds when deleting a task owned by current user", async () => {
344
282
  const ownTaskId = "tk-002";
345
- await runWithRollbackTestGencowCtxWithRls(
346
- db,
347
- user0Identity,
348
- async (user0Ctx) => {
349
- const before = await getHandler(user0Ctx, { id: ownTaskId });
350
- expect(before).toBeDefined();
351
-
352
- const removeResult = await removeHandler(user0Ctx, {
353
- id: ownTaskId,
354
- });
355
- expect(removeResult.success).toBe(true);
283
+ await runWithRollbackTestGencowCtxWithRls(db, user0Identity, async (user0Ctx) => {
284
+ const before = await getHandler(user0Ctx, { id: ownTaskId });
285
+ expect(before).toBeDefined();
356
286
 
357
- const after = await getHandler(user0Ctx, { id: ownTaskId });
358
- expect(after).toBeNull();
359
- }
360
- );
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
+ });
361
295
  });
362
296
 
363
297
  it("tasks.remove cannot delete a task owned by another user", async () => {
364
298
  const user1TaskId = "tk-004";
365
- await runWithRollbackTestGencowCtxWithRls(
366
- db,
367
- user0Identity,
368
- async (user0Ctx) => {
369
- const user1Ctx = makeTestGencowCtxWithRls(
370
- user0Ctx.unsafeDb as any,
371
- user1Identity
372
- );
373
-
374
- const before = await getHandler(user1Ctx, { id: user1TaskId });
375
- expect(before).toBeDefined();
376
-
377
- const removeResult = await removeHandler(user0Ctx, {
378
- id: user1TaskId,
379
- });
380
- expect(removeResult.success).toBe(true);
381
-
382
- const after = await getHandler(user1Ctx, {
383
- id: user1TaskId,
384
- });
385
- expect(after).toBeDefined();
386
- expect(after?.id).toBe(user1TaskId);
387
- }
388
- );
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
+ });
389
316
  });
390
317
  });