@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.
- package/dist/crud.d.ts +2 -2
- package/dist/crud.js +225 -208
- package/dist/index.d.ts +7 -3
- package/dist/index.js +4 -1
- package/dist/reactive.js +10 -3
- package/dist/retry.js +1 -1
- package/dist/rls-db.d.ts +2 -2
- package/dist/rls-db.js +1 -5
- package/dist/scheduler.d.ts +2 -0
- package/dist/scheduler.js +16 -6
- package/dist/server.d.ts +0 -1
- package/dist/server.js +0 -1
- package/dist/storage.js +29 -22
- package/dist/v.d.ts +2 -2
- package/dist/workflow-types.d.ts +81 -0
- package/dist/workflow-types.js +12 -0
- package/dist/workflow.d.ts +30 -0
- package/dist/workflow.js +150 -0
- package/dist/workflows-api.d.ts +13 -0
- package/dist/workflows-api.js +321 -0
- package/package.json +46 -42
- package/src/__tests__/auth.test.ts +90 -86
- package/src/__tests__/crons.test.ts +69 -67
- package/src/__tests__/crud-codegen-integration.test.ts +164 -170
- package/src/__tests__/crud-owner-rls.test.ts +308 -301
- package/src/__tests__/crud.test.ts +694 -711
- package/src/__tests__/dist-exports.test.ts +120 -114
- package/src/__tests__/fixtures/basic/auth.ts +16 -16
- package/src/__tests__/fixtures/basic/drizzle.config.ts +1 -4
- package/src/__tests__/fixtures/basic/index.ts +1 -1
- package/src/__tests__/fixtures/basic/schema.ts +1 -1
- package/src/__tests__/fixtures/basic/tasks.ts +4 -4
- package/src/__tests__/fixtures/common/auth-schema.ts +38 -34
- package/src/__tests__/helpers/basic-rls-fixture.ts +80 -78
- package/src/__tests__/helpers/pglite-migrations.ts +2 -5
- package/src/__tests__/helpers/pglite-rls-session.ts +13 -16
- package/src/__tests__/helpers/seed-like-fill.ts +50 -44
- package/src/__tests__/helpers/test-gencow-ctx-rls.ts +4 -7
- package/src/__tests__/httpaction.test.ts +91 -91
- package/src/__tests__/image-optimization.test.ts +570 -574
- package/src/__tests__/load.test.ts +321 -308
- package/src/__tests__/network-sim.test.ts +238 -215
- package/src/__tests__/reactive.test.ts +380 -358
- package/src/__tests__/retry.test.ts +99 -84
- package/src/__tests__/rls-crud-basic.test.ts +172 -245
- package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +81 -81
- package/src/__tests__/rls-custom-mutation-handlers.test.ts +47 -94
- package/src/__tests__/rls-custom-query-handlers.test.ts +92 -92
- package/src/__tests__/rls-db-leased-connection.test.ts +2 -6
- package/src/__tests__/rls-session-and-policies.test.ts +181 -199
- package/src/__tests__/scheduler-durable-v2.test.ts +199 -181
- package/src/__tests__/scheduler-durable.test.ts +117 -117
- package/src/__tests__/scheduler-exec.test.ts +258 -246
- package/src/__tests__/scheduler.test.ts +129 -111
- package/src/__tests__/storage.test.ts +282 -269
- package/src/__tests__/tsconfig.json +6 -6
- package/src/__tests__/validator.test.ts +236 -232
- package/src/__tests__/workflow.test.ts +606 -0
- package/src/__tests__/ws-integration.test.ts +223 -218
- package/src/__tests__/ws-scale.test.ts +168 -159
- package/src/auth-config.ts +18 -18
- package/src/auth.ts +106 -106
- package/src/crons.ts +77 -77
- package/src/crud.ts +523 -479
- package/src/index.ts +71 -6
- package/src/reactive.ts +357 -331
- package/src/retry.ts +51 -54
- package/src/rls-db.ts +195 -205
- package/src/rls.ts +33 -36
- package/src/scheduler.ts +237 -211
- package/src/server.ts +0 -1
- package/src/storage.ts +632 -593
- package/src/v.ts +119 -114
- package/src/workflow-types.ts +108 -0
- package/src/workflow.ts +188 -0
- package/src/workflows-api.ts +415 -0
- 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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
assertTableRowLevelSecurityDisabled,
|
|
17
|
+
basicUser0Identity,
|
|
18
|
+
basicUser1Identity,
|
|
19
|
+
createBasicRlsEnvironment,
|
|
20
20
|
} from "./helpers/basic-rls-fixture.js";
|
|
21
21
|
import {
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
beforeAll(async () => {
|
|
36
|
+
const env = await createBasicRlsEnvironment();
|
|
37
|
+
client = env.client;
|
|
38
|
+
db = env.db;
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
afterAll(async () => {
|
|
53
|
+
try {
|
|
54
|
+
await client.close();
|
|
55
|
+
} catch {
|
|
56
|
+
/* ignore */
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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:
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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"));
|