@gencow/core 0.1.26 → 0.1.28
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 +12 -0
- package/dist/crud.js +16 -0
- package/dist/db.d.ts +13 -0
- package/dist/db.js +16 -0
- package/dist/document-types.d.ts +65 -0
- package/dist/document-types.js +15 -0
- package/dist/grounded-answer-types.d.ts +62 -0
- package/dist/grounded-answer-types.js +6 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.js +5 -1
- package/dist/rag-ingest-types.d.ts +39 -0
- package/dist/rag-ingest-types.js +1 -0
- package/dist/rag-operations-types.d.ts +81 -0
- package/dist/rag-operations-types.js +1 -0
- package/dist/rag-schema.d.ts +1557 -0
- package/dist/rag-schema.js +87 -0
- package/dist/reactive.d.ts +13 -0
- package/dist/rls-db.d.ts +9 -2
- package/dist/runtime-env-policy.d.ts +5 -0
- package/dist/runtime-env-policy.js +56 -0
- package/dist/search-types.d.ts +83 -0
- package/dist/search-types.js +1 -0
- package/dist/server.d.ts +1 -2
- package/dist/server.js +0 -1
- package/dist/storage-shared.d.ts +36 -0
- package/dist/storage-shared.js +39 -0
- package/dist/storage.d.ts +2 -26
- package/dist/storage.js +19 -15
- package/dist/workflow-types.d.ts +3 -1
- package/package.json +1 -1
- package/src/crud.ts +33 -0
- package/src/document-types.ts +95 -0
- package/src/grounded-answer-types.ts +78 -0
- package/src/index.ts +68 -2
- package/src/rag-ingest-types.ts +52 -0
- package/src/rag-operations-types.ts +90 -0
- package/src/rag-schema.ts +94 -0
- package/src/reactive.ts +13 -0
- package/src/rls-db.ts +9 -4
- package/src/runtime-env-policy.ts +66 -0
- package/src/search-types.ts +91 -0
- package/src/server.ts +1 -2
- package/src/storage-shared.ts +74 -0
- package/src/storage.ts +29 -46
- package/src/workflow-types.ts +3 -1
- package/src/__tests__/auth.test.ts +0 -118
- package/src/__tests__/crons.test.ts +0 -83
- package/src/__tests__/crud-codegen-integration.test.ts +0 -246
- package/src/__tests__/crud-owner-rls.test.ts +0 -387
- package/src/__tests__/crud.test.ts +0 -930
- package/src/__tests__/dist-exports.test.ts +0 -176
- package/src/__tests__/fixtures/basic/auth.ts +0 -32
- package/src/__tests__/fixtures/basic/drizzle.config.ts +0 -12
- package/src/__tests__/fixtures/basic/index.ts +0 -6
- package/src/__tests__/fixtures/basic/migrations/0000_last_warstar.sql +0 -75
- package/src/__tests__/fixtures/basic/migrations/meta/0000_snapshot.json +0 -497
- package/src/__tests__/fixtures/basic/migrations/meta/_journal.json +0 -13
- package/src/__tests__/fixtures/basic/schema.ts +0 -51
- package/src/__tests__/fixtures/basic/tasks.ts +0 -15
- package/src/__tests__/fixtures/common/auth-schema.ts +0 -67
- package/src/__tests__/helpers/basic-rls-fixture.ts +0 -135
- package/src/__tests__/helpers/pglite-migrations.ts +0 -32
- package/src/__tests__/helpers/pglite-rls-session.ts +0 -51
- package/src/__tests__/helpers/seed-like-fill.ts +0 -202
- package/src/__tests__/helpers/test-gencow-ctx-rls.ts +0 -50
- package/src/__tests__/httpaction.test.ts +0 -122
- package/src/__tests__/image-optimization.test.ts +0 -648
- package/src/__tests__/load.test.ts +0 -389
- package/src/__tests__/network-sim.test.ts +0 -319
- package/src/__tests__/reactive.test.ts +0 -479
- package/src/__tests__/retry.test.ts +0 -113
- package/src/__tests__/rls-crud-basic.test.ts +0 -317
- package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +0 -117
- package/src/__tests__/rls-custom-mutation-handlers.test.ts +0 -142
- package/src/__tests__/rls-custom-query-handlers.test.ts +0 -128
- package/src/__tests__/rls-db-leased-connection.test.ts +0 -118
- package/src/__tests__/rls-session-and-policies.test.ts +0 -228
- package/src/__tests__/scheduler-durable-v2.test.ts +0 -288
- package/src/__tests__/scheduler-durable.test.ts +0 -173
- package/src/__tests__/scheduler-exec.test.ts +0 -328
- package/src/__tests__/scheduler.test.ts +0 -187
- package/src/__tests__/storage.test.ts +0 -334
- package/src/__tests__/tsconfig.json +0 -8
- package/src/__tests__/validator.test.ts +0 -323
- package/src/__tests__/workflow.test.ts +0 -606
- package/src/__tests__/ws-integration.test.ts +0 -309
- package/src/__tests__/ws-scale.test.ts +0 -241
- package/src/auth.ts +0 -155
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Custom **query** handlers (not `crud()` factory) that use `ctx.db` with Drizzle must respect RLS.
|
|
3
|
-
*
|
|
4
|
-
* Run: bun test packages/core/src/__tests__/rls-custom-query-handlers.test.ts
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
|
8
|
-
import { eq, sql } from "drizzle-orm";
|
|
9
|
-
import { PGlite } from "@electric-sql/pglite";
|
|
10
|
-
import { drizzle } from "drizzle-orm/pglite";
|
|
11
|
-
|
|
12
|
-
import type { GencowCtx } from "../reactive.js";
|
|
13
|
-
import { tasks } from "./fixtures/basic/schema.js";
|
|
14
|
-
import type { BasicTaskRow } from "./helpers/basic-rls-fixture.js";
|
|
15
|
-
import {
|
|
16
|
-
basicFixtureTasks,
|
|
17
|
-
basicFixtureUsers,
|
|
18
|
-
basicUser0Identity,
|
|
19
|
-
basicUser1Identity,
|
|
20
|
-
createBasicRlsEnvironment,
|
|
21
|
-
} from "./helpers/basic-rls-fixture.js";
|
|
22
|
-
import { makeTestGencowCtxWithRls } from "./helpers/test-gencow-ctx-rls.js";
|
|
23
|
-
|
|
24
|
-
/** Simulates a user-defined query: list all tasks the ORM can see (RLS limits rows). */
|
|
25
|
-
async function customQueryListAllTasks(ctx: GencowCtx) {
|
|
26
|
-
return ctx.db.select().from(tasks);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Simulates a dashboard query with filters — still subject to RLS. */
|
|
30
|
-
async function customQueryTasksMatchingTitle(ctx: GencowCtx, substring: string) {
|
|
31
|
-
return ctx.db
|
|
32
|
-
.select()
|
|
33
|
-
.from(tasks)
|
|
34
|
-
.where(sql`${tasks.title} ilike ${"%" + substring + "%"}`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Simulates fetching a single row by id (e.g. detail view). */
|
|
38
|
-
async function customQueryTaskById(ctx: GencowCtx, id: string) {
|
|
39
|
-
const rows = await ctx.db.select().from(tasks).where(eq(tasks.id, id));
|
|
40
|
-
return rows[0] ?? null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Raw SQL count — must run with RLS session vars on the same connection semantics as ORM builders. */
|
|
44
|
-
async function customQueryTaskCountRaw(ctx: GencowCtx) {
|
|
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
|
-
}
|
|
49
|
-
|
|
50
|
-
describe("Custom query handlers + ctx.db + RLS", () => {
|
|
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
|
-
});
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "bun:test";
|
|
2
|
-
import { withRlsLeasedConnection } from "../rls-db.js";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Mirrors production **node-pg Pool** path (`rls-db.ts` path 4): `connect()` → BEGIN →
|
|
6
|
-
* `set_config` for each RLS field → user work → COMMIT / ROLLBACK → `release()`.
|
|
7
|
-
* Real pools hit PostgreSQL; these tests assert ordering and side effects on a mock client.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
describe("withRlsLeasedConnection (pg Pool leased client path)", () => {
|
|
11
|
-
it("commits on success and releases exactly once", async () => {
|
|
12
|
-
const phases: string[] = [];
|
|
13
|
-
let released = 0;
|
|
14
|
-
const leased = {
|
|
15
|
-
async query(sql: string) {
|
|
16
|
-
phases.push(sql);
|
|
17
|
-
return {};
|
|
18
|
-
},
|
|
19
|
-
release() {
|
|
20
|
-
released++;
|
|
21
|
-
},
|
|
22
|
-
};
|
|
23
|
-
const result = await withRlsLeasedConnection(leased, { userId: "u1" }, async () => "done");
|
|
24
|
-
expect(result).toBe("done");
|
|
25
|
-
expect(phases[0]).toBe("begin");
|
|
26
|
-
expect(phases[phases.length - 1]).toBe("commit");
|
|
27
|
-
expect(phases).not.toContain("rollback");
|
|
28
|
-
expect(released).toBe(1);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("rolls back when work fails and never commits", async () => {
|
|
32
|
-
const phases: string[] = [];
|
|
33
|
-
let released = 0;
|
|
34
|
-
const leased = {
|
|
35
|
-
async query(sql: string) {
|
|
36
|
-
phases.push(sql);
|
|
37
|
-
return {};
|
|
38
|
-
},
|
|
39
|
-
release() {
|
|
40
|
-
released++;
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
let thrown: unknown;
|
|
44
|
-
try {
|
|
45
|
-
await withRlsLeasedConnection(leased, { userId: "u1" }, async () => {
|
|
46
|
-
throw new Error("work failed");
|
|
47
|
-
});
|
|
48
|
-
} catch (e) {
|
|
49
|
-
thrown = e;
|
|
50
|
-
}
|
|
51
|
-
expect(thrown).toBeInstanceOf(Error);
|
|
52
|
-
expect((thrown as Error).message).toBe("work failed");
|
|
53
|
-
|
|
54
|
-
expect(phases[0]).toBe("begin");
|
|
55
|
-
expect(phases[phases.length - 1]).toBe("rollback");
|
|
56
|
-
expect(phases.includes("commit")).toBe(false);
|
|
57
|
-
expect(released).toBe(1);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("issues set_config for userId, role, tenantId, and custom vars before commit", async () => {
|
|
61
|
-
const phases: string[] = [];
|
|
62
|
-
let released = 0;
|
|
63
|
-
const leased = {
|
|
64
|
-
async query(q: string) {
|
|
65
|
-
phases.push(q);
|
|
66
|
-
return {};
|
|
67
|
-
},
|
|
68
|
-
release() {
|
|
69
|
-
released++;
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
await withRlsLeasedConnection(
|
|
73
|
-
leased,
|
|
74
|
-
{
|
|
75
|
-
userId: "u1",
|
|
76
|
-
role: "admin",
|
|
77
|
-
tenantId: "t1",
|
|
78
|
-
vars: { "app.org_slug": "acme" },
|
|
79
|
-
},
|
|
80
|
-
async () => "ok",
|
|
81
|
-
);
|
|
82
|
-
expect(phases[0]).toBe("begin");
|
|
83
|
-
const setCalls = phases.filter((p) => p.includes("set_config"));
|
|
84
|
-
expect(setCalls.length).toBe(4);
|
|
85
|
-
expect(phases[phases.length - 1]).toBe("commit");
|
|
86
|
-
expect(released).toBe(1);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("still rolls back and releases when commit fails after successful work", async () => {
|
|
90
|
-
const phases: string[] = [];
|
|
91
|
-
let released = 0;
|
|
92
|
-
let commitCalls = 0;
|
|
93
|
-
const leased = {
|
|
94
|
-
async query(q: string) {
|
|
95
|
-
phases.push(q);
|
|
96
|
-
if (q === "commit") {
|
|
97
|
-
commitCalls++;
|
|
98
|
-
throw new Error("commit failed");
|
|
99
|
-
}
|
|
100
|
-
return {};
|
|
101
|
-
},
|
|
102
|
-
release() {
|
|
103
|
-
released++;
|
|
104
|
-
},
|
|
105
|
-
};
|
|
106
|
-
let thrown: unknown;
|
|
107
|
-
try {
|
|
108
|
-
await withRlsLeasedConnection(leased, { userId: "u1" }, async () => "done");
|
|
109
|
-
} catch (e) {
|
|
110
|
-
thrown = e;
|
|
111
|
-
}
|
|
112
|
-
expect(thrown).toBeInstanceOf(Error);
|
|
113
|
-
expect((thrown as Error).message).toBe("commit failed");
|
|
114
|
-
expect(commitCalls).toBe(1);
|
|
115
|
-
expect(phases[phases.length - 1]).toBe("rollback");
|
|
116
|
-
expect(released).toBe(1);
|
|
117
|
-
});
|
|
118
|
-
});
|
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RLS session GUCs + PostgreSQL policies — PGlite integration tests.
|
|
3
|
-
*
|
|
4
|
-
* **Parity with production (`packages/server/src/index.ts`):**
|
|
5
|
-
* - Server builds `ctx.db` with `createRlsDb(db, { userId: authCtx?.getUserIdentity()?.id || "" })`.
|
|
6
|
-
* - Tests use the same helper shape via {@link makeTestGencowCtxWithRls} (only `userId` from identity),
|
|
7
|
-
* or `createRlsDb` directly with `role` / `tenantId` / `vars` when exercising those code paths.
|
|
8
|
-
*
|
|
9
|
-
* **Parity with PostgreSQL RLS enforcement:**
|
|
10
|
-
* - After {@link createPgliteRlsAppRole} + {@link setPgliteSessionRole}, the session runs as a
|
|
11
|
-
* non-table-owner role, so `ENABLE ROW LEVEL SECURITY` policies apply (same as production DB users).
|
|
12
|
-
* - Policies in `fixtures/basic/migrations` use `current_setting('app.current_user_id', true)` with
|
|
13
|
-
* `missing_ok`, matching `ownerRls()` in `rls.ts` and avoiding errors before `set_config`.
|
|
14
|
-
*
|
|
15
|
-
* **Execution paths exercised:**
|
|
16
|
-
* - **Autocommit / PGlite `transaction()`**: `makeTestGencowCtxWithRls(db)` — each statement opens a
|
|
17
|
-
* short transaction with session GUCs (see `rls-db.ts` path 3).
|
|
18
|
-
* - **Reuse outer Drizzle transaction**: `runWithRollbackTestGencowCtxWithRls` — matches handlers that
|
|
19
|
-
* run inside an outer `db.transaction()`; `createRlsDb` detects the tx object and applies GUCs on
|
|
20
|
-
* the same connection without a nested top-level `BEGIN` (path 1).
|
|
21
|
-
*
|
|
22
|
-
* Run: bun test packages/core/src/__tests__/rls-session-and-policies.test.ts
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
|
26
|
-
import { eq, sql } from "drizzle-orm";
|
|
27
|
-
|
|
28
|
-
import { createRlsDb } from "../rls-db.js";
|
|
29
|
-
import { tasks } from "./fixtures/basic/schema.js";
|
|
30
|
-
import {
|
|
31
|
-
basicFixtureTasks,
|
|
32
|
-
basicUser0Identity,
|
|
33
|
-
basicUser1Identity,
|
|
34
|
-
createBasicRlsEnvironment,
|
|
35
|
-
} from "./helpers/basic-rls-fixture.js";
|
|
36
|
-
import { fillPartialRowsForInsert } from "./helpers/seed-like-fill.js";
|
|
37
|
-
import {
|
|
38
|
-
makeTestGencowCtxWithRls,
|
|
39
|
-
runWithRollbackTestGencowCtxWithRls,
|
|
40
|
-
} from "./helpers/test-gencow-ctx-rls.js";
|
|
41
|
-
|
|
42
|
-
type PgliteDb = ReturnType<typeof import("drizzle-orm/pglite").drizzle>;
|
|
43
|
-
|
|
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;
|
|
64
|
-
}
|
|
65
|
-
|
|
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(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
|
-
});
|
|
228
|
-
});
|