@gencow/core 0.1.24 → 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 +5 -5
- package/dist/index.js +2 -2
- 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.js +4 -11
- package/dist/workflows-api.js +5 -12
- 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 -120
- 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 +309 -286
- 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 +69 -5
- 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 +67 -70
- package/src/workflow.ts +99 -116
- package/src/workflows-api.ts +231 -241
- package/src/db.ts +0 -18
|
@@ -35,21 +35,21 @@ const DIST_INDEX = resolve(CORE_ROOT, "dist/index.js");
|
|
|
35
35
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
36
36
|
|
|
37
37
|
describe("dist/ 빌드 산출물 존재", () => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
it("dist/index.js 파일이 존재한다", () => {
|
|
39
|
+
expect(existsSync(DIST_INDEX)).toBe(true);
|
|
40
|
+
});
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
it("dist/crud.js 파일이 존재한다", () => {
|
|
43
|
+
expect(existsSync(resolve(CORE_ROOT, "dist/crud.js"))).toBe(true);
|
|
44
|
+
});
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
it("dist/reactive.js 파일이 존재한다", () => {
|
|
47
|
+
expect(existsSync(resolve(CORE_ROOT, "dist/reactive.js"))).toBe(true);
|
|
48
|
+
});
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
it("dist/v.js 파일이 존재한다", () => {
|
|
51
|
+
expect(existsSync(resolve(CORE_ROOT, "dist/v.js"))).toBe(true);
|
|
52
|
+
});
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -57,88 +57,88 @@ describe("dist/ 빌드 산출물 존재", () => {
|
|
|
57
57
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
58
58
|
|
|
59
59
|
describe("dist/index.js 필수 export", () => {
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
60
|
+
let distModule: Record<string, unknown>;
|
|
61
|
+
|
|
62
|
+
// dist/index.js를 dynamic import (빌드 결과물 검증)
|
|
63
|
+
it("dist/index.js를 import할 수 있다", async () => {
|
|
64
|
+
distModule = await import(DIST_INDEX);
|
|
65
|
+
expect(distModule).toBeDefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ── Core API exports ──────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
it("crud export가 존재하고 함수이다", async () => {
|
|
71
|
+
if (!distModule) distModule = await import(DIST_INDEX);
|
|
72
|
+
expect(distModule.crud).toBeDefined();
|
|
73
|
+
expect(typeof distModule.crud).toBe("function");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("gencowCrud deprecated alias가 존재하고 crud와 동일하다", async () => {
|
|
77
|
+
if (!distModule) distModule = await import(DIST_INDEX);
|
|
78
|
+
expect(distModule.gencowCrud).toBeDefined();
|
|
79
|
+
expect(distModule.gencowCrud).toBe(distModule.crud);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("query export가 존재하고 함수이다", async () => {
|
|
83
|
+
if (!distModule) distModule = await import(DIST_INDEX);
|
|
84
|
+
expect(distModule.query).toBeDefined();
|
|
85
|
+
expect(typeof distModule.query).toBe("function");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("mutation export가 존재하고 함수이다", async () => {
|
|
89
|
+
if (!distModule) distModule = await import(DIST_INDEX);
|
|
90
|
+
expect(distModule.mutation).toBeDefined();
|
|
91
|
+
expect(typeof distModule.mutation).toBe("function");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("workflow export가 존재하고 함수이다", async () => {
|
|
95
|
+
if (!distModule) distModule = await import(DIST_INDEX);
|
|
96
|
+
expect(distModule.workflow).toBeDefined();
|
|
97
|
+
expect(typeof distModule.workflow).toBe("function");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("v export가 존재하고 객체이다", async () => {
|
|
101
|
+
if (!distModule) distModule = await import(DIST_INDEX);
|
|
102
|
+
expect(distModule.v).toBeDefined();
|
|
103
|
+
expect(typeof distModule.v).toBe("object");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("httpAction export가 존재하고 함수이다", async () => {
|
|
107
|
+
if (!distModule) distModule = await import(DIST_INDEX);
|
|
108
|
+
expect(distModule.httpAction).toBeDefined();
|
|
109
|
+
expect(typeof distModule.httpAction).toBe("function");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ── Registry exports (codegen 의존) ────────────────────────
|
|
113
|
+
|
|
114
|
+
it("getRegisteredQueries export가 존재한다", async () => {
|
|
115
|
+
if (!distModule) distModule = await import(DIST_INDEX);
|
|
116
|
+
expect(distModule.getRegisteredQueries).toBeDefined();
|
|
117
|
+
expect(typeof distModule.getRegisteredQueries).toBe("function");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("getRegisteredMutations export가 존재한다", async () => {
|
|
121
|
+
if (!distModule) distModule = await import(DIST_INDEX);
|
|
122
|
+
expect(distModule.getRegisteredMutations).toBeDefined();
|
|
123
|
+
expect(typeof distModule.getRegisteredMutations).toBe("function");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ── RLS + Auth exports ─────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
it("ownerRls export가 존재한다", async () => {
|
|
129
|
+
if (!distModule) distModule = await import(DIST_INDEX);
|
|
130
|
+
expect(distModule.ownerRls).toBeDefined();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("defineAuth export가 존재한다", async () => {
|
|
134
|
+
if (!distModule) distModule = await import(DIST_INDEX);
|
|
135
|
+
expect(distModule.defineAuth).toBeDefined();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("cronJobs export가 존재한다", async () => {
|
|
139
|
+
if (!distModule) distModule = await import(DIST_INDEX);
|
|
140
|
+
expect(distModule.cronJobs).toBeDefined();
|
|
141
|
+
});
|
|
142
142
|
});
|
|
143
143
|
|
|
144
144
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -146,31 +146,31 @@ describe("dist/index.js 필수 export", () => {
|
|
|
146
146
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
147
147
|
|
|
148
148
|
describe("dist/crud.js — v2 구현 검증", () => {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
const result = crud(testTable, { public: true });
|
|
164
|
-
|
|
165
|
-
// v2는 { list, get, create, update, remove } 반환
|
|
166
|
-
expect(result).toHaveProperty("list");
|
|
167
|
-
expect(result).toHaveProperty("get");
|
|
168
|
-
expect(result).toHaveProperty("create");
|
|
169
|
-
expect(result).toHaveProperty("update");
|
|
170
|
-
expect(result).toHaveProperty("remove");
|
|
171
|
-
|
|
172
|
-
// 구버전(커링)은 함수를 반환하므로 이 검증이 실패함
|
|
173
|
-
expect(typeof result).toBe("object");
|
|
174
|
-
expect(typeof result.list).not.toBe("undefined");
|
|
149
|
+
it("crud(table) 시그니처이다 (커링 gencowCrud(db)(table) 아님)", async () => {
|
|
150
|
+
const distModule = await import(DIST_INDEX);
|
|
151
|
+
const { crud } = distModule;
|
|
152
|
+
|
|
153
|
+
// v2: crud(table, options?) → { list, get, create, update, remove }
|
|
154
|
+
// 구버전: gencowCrud(db) → (table, options?) → { create, findById, list, ... }
|
|
155
|
+
|
|
156
|
+
// 실제 Drizzle 테이블로 테스트
|
|
157
|
+
const { pgTable, serial, text } = await import("drizzle-orm/pg-core");
|
|
158
|
+
const testTable = pgTable("dist_smoke_test", {
|
|
159
|
+
id: serial("id").primaryKey(),
|
|
160
|
+
name: text("name"),
|
|
175
161
|
});
|
|
162
|
+
|
|
163
|
+
const result = crud(testTable, { public: true });
|
|
164
|
+
|
|
165
|
+
// v2는 { list, get, create, update, remove } 반환
|
|
166
|
+
expect(result).toHaveProperty("list");
|
|
167
|
+
expect(result).toHaveProperty("get");
|
|
168
|
+
expect(result).toHaveProperty("create");
|
|
169
|
+
expect(result).toHaveProperty("update");
|
|
170
|
+
expect(result).toHaveProperty("remove");
|
|
171
|
+
|
|
172
|
+
// 구버전(커링)은 함수를 반환하므로 이 검증이 실패함
|
|
173
|
+
expect(typeof result).toBe("object");
|
|
174
|
+
expect(typeof result.list).not.toBe("undefined");
|
|
175
|
+
});
|
|
176
176
|
});
|
|
@@ -11,22 +11,22 @@
|
|
|
11
11
|
*
|
|
12
12
|
* @see https://docs.gencow.com/auth
|
|
13
13
|
*/
|
|
14
|
-
import { defineAuth } from "../../../auth-config";
|
|
14
|
+
import { defineAuth } from "../../../auth-config.js";
|
|
15
15
|
|
|
16
16
|
export default defineAuth({
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
17
|
+
// ── Email Verification (선택) ──────────────────────
|
|
18
|
+
// 아래 주석을 해제하면 가입 시 이메일 인증이 활성화됩니다.
|
|
19
|
+
//
|
|
20
|
+
// emailVerification: {
|
|
21
|
+
// sendVerificationEmail: async ({ user, url }) => {
|
|
22
|
+
// const { Resend } = await import("resend");
|
|
23
|
+
// const resend = new Resend(process.env.RESEND_API_KEY);
|
|
24
|
+
// await resend.emails.send({
|
|
25
|
+
// from: "noreply@yourapp.com",
|
|
26
|
+
// to: user.email,
|
|
27
|
+
// subject: "이메일 인증",
|
|
28
|
+
// html: `<a href="${url}">인증하기</a>`,
|
|
29
|
+
// });
|
|
30
|
+
// },
|
|
31
|
+
// },
|
|
32
32
|
});
|
|
@@ -7,9 +7,6 @@ const here = dirname(fileURLToPath(import.meta.url));
|
|
|
7
7
|
|
|
8
8
|
export default defineConfig({
|
|
9
9
|
dialect: "postgresql",
|
|
10
|
-
schema: [
|
|
11
|
-
resolve(here, "schema.ts"),
|
|
12
|
-
resolve(here, "../common/auth-schema.ts"),
|
|
13
|
-
],
|
|
10
|
+
schema: [resolve(here, "schema.ts"), resolve(here, "../common/auth-schema.ts")],
|
|
14
11
|
out: resolve(here, "migrations"),
|
|
15
12
|
});
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* - ctx.db uses RLS (Row-Level Security) — auto-filters by userId
|
|
6
6
|
* - crud auto-registers query/mutation with auth + realtime
|
|
7
7
|
*/
|
|
8
|
-
import { crud } from "../../../crud";
|
|
9
|
-
import { tasks } from "./schema";
|
|
8
|
+
import { crud } from "../../../crud.js";
|
|
9
|
+
import { tasks } from "./schema.js";
|
|
10
10
|
|
|
11
11
|
// 자동 생성: list, get, create, update, remove (query + mutation + auth + realtime)
|
|
12
12
|
export const { list, get, create, update, remove } = crud(tasks, {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
searchFields: ["title", "description"],
|
|
14
|
+
defaultLimit: 50,
|
|
15
15
|
});
|
|
@@ -11,53 +11,57 @@ import { pgTable, text, boolean, timestamp } from "drizzle-orm/pg-core";
|
|
|
11
11
|
// ─── user 테이블 ────────────────────────────────────────
|
|
12
12
|
|
|
13
13
|
export const user = pgTable("user", {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
id: text("id").primaryKey(),
|
|
15
|
+
name: text("name").notNull(),
|
|
16
|
+
email: text("email").notNull().unique(),
|
|
17
|
+
emailVerified: boolean("email_verified").notNull().default(false),
|
|
18
|
+
image: text("image"),
|
|
19
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
20
|
+
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
// ─── session 테이블 ─────────────────────────────────────
|
|
24
24
|
|
|
25
25
|
export const session = pgTable("session", {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
id: text("id").primaryKey(),
|
|
27
|
+
expiresAt: timestamp("expires_at").notNull(),
|
|
28
|
+
token: text("token").notNull().unique(),
|
|
29
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
30
|
+
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
|
31
|
+
ipAddress: text("ip_address"),
|
|
32
|
+
userAgent: text("user_agent"),
|
|
33
|
+
userId: text("user_id")
|
|
34
|
+
.notNull()
|
|
35
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
34
36
|
});
|
|
35
37
|
|
|
36
38
|
// ─── account 테이블 ─────────────────────────────────────
|
|
37
39
|
|
|
38
40
|
export const account = pgTable("account", {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
41
|
+
id: text("id").primaryKey(),
|
|
42
|
+
accountId: text("account_id").notNull(),
|
|
43
|
+
providerId: text("provider_id").notNull(),
|
|
44
|
+
userId: text("user_id")
|
|
45
|
+
.notNull()
|
|
46
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
47
|
+
accessToken: text("access_token"),
|
|
48
|
+
refreshToken: text("refresh_token"),
|
|
49
|
+
idToken: text("id_token"),
|
|
50
|
+
accessTokenExpiresAt: timestamp("access_token_expires_at"),
|
|
51
|
+
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
|
|
52
|
+
scope: text("scope"),
|
|
53
|
+
password: text("password"),
|
|
54
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
55
|
+
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
|
52
56
|
});
|
|
53
57
|
|
|
54
58
|
// ─── verification 테이블 ────────────────────────────────
|
|
55
59
|
|
|
56
60
|
export const verification = pgTable("verification", {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
id: text("id").primaryKey(),
|
|
62
|
+
identifier: text("identifier").notNull(),
|
|
63
|
+
value: text("value").notNull(),
|
|
64
|
+
expiresAt: timestamp("expires_at").notNull(),
|
|
65
|
+
createdAt: timestamp("created_at").defaultNow(),
|
|
66
|
+
updatedAt: timestamp("updated_at").defaultNow(),
|
|
63
67
|
});
|
|
@@ -11,77 +11,81 @@ import { drizzle } from "drizzle-orm/pglite";
|
|
|
11
11
|
|
|
12
12
|
import type { UserIdentity } from "../../reactive.js";
|
|
13
13
|
import { news, tasks, user } from "../fixtures/basic/schema.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
createPgliteRlsAppRole,
|
|
16
|
+
DEFAULT_PGLITE_RLS_APP_ROLE,
|
|
17
|
+
setPgliteSessionRole,
|
|
18
|
+
} from "./pglite-rls-session.js";
|
|
15
19
|
import { loadAndApplyMigrations } from "./pglite-migrations.js";
|
|
16
20
|
import { fillPartialRowsForInsert } from "./seed-like-fill.js";
|
|
17
21
|
|
|
18
22
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
23
|
|
|
20
24
|
export const basicFixtureUsers = [
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
{ id: "us_000", name: "User 0", email: "user-0@s.com", emailVerified: true },
|
|
26
|
+
{ id: "us_001", name: "User 1", email: "user-1@s.com", emailVerified: true },
|
|
23
27
|
];
|
|
24
28
|
|
|
25
29
|
/** Stable ids/titles; overlaps across users for search / RLS tests. */
|
|
26
30
|
export const basicFixtureTasks = [
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
31
|
+
{
|
|
32
|
+
id: "tk-000",
|
|
33
|
+
userId: basicFixtureUsers[0].id,
|
|
34
|
+
done: false,
|
|
35
|
+
title: "Project Alpha — Q4 review prep",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "tk-001",
|
|
39
|
+
userId: basicFixtureUsers[1].id,
|
|
40
|
+
done: true,
|
|
41
|
+
title: "Project Alpha — teammate handoff",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "tk-002",
|
|
45
|
+
userId: basicFixtureUsers[0].id,
|
|
46
|
+
done: false,
|
|
47
|
+
title: "Project Alpha — backlog grooming",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "tk-003",
|
|
51
|
+
userId: basicFixtureUsers[0].id,
|
|
52
|
+
done: false,
|
|
53
|
+
title: "Quarterly planning — Q4",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "tk-004",
|
|
57
|
+
userId: basicFixtureUsers[1].id,
|
|
58
|
+
done: false,
|
|
59
|
+
title: "Project Beta — API docs",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "tk-005",
|
|
63
|
+
userId: basicFixtureUsers[1].id,
|
|
64
|
+
done: false,
|
|
65
|
+
title: "Project Gamma — research notes",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "tk-006",
|
|
69
|
+
userId: basicFixtureUsers[0].id,
|
|
70
|
+
done: false,
|
|
71
|
+
title: "Project Beta — spike",
|
|
72
|
+
},
|
|
69
73
|
];
|
|
70
74
|
|
|
71
75
|
/** No `ownerRls()` — two rows for user0 / user1 (no DB RLS on `news`). */
|
|
72
76
|
export const basicFixtureNews = [
|
|
73
|
-
|
|
74
|
-
|
|
77
|
+
{ id: "nw-000", userId: basicFixtureUsers[0].id, title: "owned by user0" },
|
|
78
|
+
{ id: "nw-001", userId: basicFixtureUsers[1].id, title: "owned by user1" },
|
|
75
79
|
];
|
|
76
80
|
|
|
77
81
|
export const basicUser0Identity = {
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
id: basicFixtureUsers[0].id,
|
|
83
|
+
email: basicFixtureUsers[0].email,
|
|
80
84
|
} satisfies UserIdentity;
|
|
81
85
|
|
|
82
86
|
export const basicUser1Identity = {
|
|
83
|
-
|
|
84
|
-
|
|
87
|
+
id: basicFixtureUsers[1].id,
|
|
88
|
+
email: basicFixtureUsers[1].email,
|
|
85
89
|
} satisfies UserIdentity;
|
|
86
90
|
|
|
87
91
|
export type BasicTaskRow = InferSelectModel<typeof tasks>;
|
|
@@ -92,42 +96,40 @@ export type BasicNewsRow = InferSelectModel<typeof news>;
|
|
|
92
96
|
* so PostgreSQL RLS policies apply (same as `rls-crud-basic.test.ts`).
|
|
93
97
|
*/
|
|
94
98
|
export async function createBasicRlsEnvironment(): Promise<{
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
client: PGlite;
|
|
100
|
+
db: ReturnType<typeof drizzle>;
|
|
101
|
+
taskRows: BasicTaskRow[];
|
|
98
102
|
}> {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
103
|
+
const client = new PGlite();
|
|
104
|
+
await client.waitReady;
|
|
105
|
+
await loadAndApplyMigrations(client, join(__dirname, "../fixtures/basic/migrations"));
|
|
106
|
+
const db = drizzle({ client });
|
|
107
|
+
await db.insert(user).values(fillPartialRowsForInsert(user, basicFixtureUsers));
|
|
108
|
+
const taskRows = fillPartialRowsForInsert(tasks, basicFixtureTasks) as BasicTaskRow[];
|
|
109
|
+
await db.insert(tasks).values(taskRows);
|
|
110
|
+
const newsRows = fillPartialRowsForInsert(news, basicFixtureNews) as BasicNewsRow[];
|
|
111
|
+
await db.insert(news).values(newsRows);
|
|
112
|
+
await createPgliteRlsAppRole(client, {
|
|
113
|
+
roleName: DEFAULT_PGLITE_RLS_APP_ROLE,
|
|
114
|
+
});
|
|
115
|
+
await setPgliteSessionRole(client, DEFAULT_PGLITE_RLS_APP_ROLE);
|
|
116
|
+
return { client, db, taskRows };
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
/** Asserts `pg_class.relrowsecurity` is false (table was never `ENABLE ROW LEVEL SECURITY`). */
|
|
116
120
|
export async function assertTableRowLevelSecurityDisabled(
|
|
117
|
-
|
|
118
|
-
|
|
121
|
+
db: ReturnType<typeof drizzle>,
|
|
122
|
+
table: PgTable,
|
|
119
123
|
): Promise<void> {
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
const relname = getTableName(table);
|
|
125
|
+
const q = await db.execute(sql`
|
|
122
126
|
select c.relrowsecurity as rls_enabled
|
|
123
127
|
from pg_class c
|
|
124
128
|
join pg_namespace n on n.oid = c.relnamespace
|
|
125
129
|
where n.nspname = 'public' and c.relname = ${relname}
|
|
126
130
|
`);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
);
|
|
132
|
-
}
|
|
131
|
+
const row = (q as unknown as { rows: { rls_enabled: boolean }[] }).rows[0];
|
|
132
|
+
if (row?.rls_enabled !== false) {
|
|
133
|
+
throw new Error(`expected relrowsecurity = false for public.${relname}, got ${JSON.stringify(row)}`);
|
|
134
|
+
}
|
|
133
135
|
}
|