@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.
Files changed (73) hide show
  1. package/dist/crud.d.ts +2 -2
  2. package/dist/crud.js +225 -208
  3. package/dist/index.d.ts +5 -5
  4. package/dist/index.js +2 -2
  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.js +4 -11
  16. package/dist/workflows-api.js +5 -12
  17. package/package.json +46 -42
  18. package/src/__tests__/auth.test.ts +90 -86
  19. package/src/__tests__/crons.test.ts +69 -67
  20. package/src/__tests__/crud-codegen-integration.test.ts +164 -170
  21. package/src/__tests__/crud-owner-rls.test.ts +308 -301
  22. package/src/__tests__/crud.test.ts +694 -711
  23. package/src/__tests__/dist-exports.test.ts +120 -120
  24. package/src/__tests__/fixtures/basic/auth.ts +16 -16
  25. package/src/__tests__/fixtures/basic/drizzle.config.ts +1 -4
  26. package/src/__tests__/fixtures/basic/index.ts +1 -1
  27. package/src/__tests__/fixtures/basic/schema.ts +1 -1
  28. package/src/__tests__/fixtures/basic/tasks.ts +4 -4
  29. package/src/__tests__/fixtures/common/auth-schema.ts +38 -34
  30. package/src/__tests__/helpers/basic-rls-fixture.ts +80 -78
  31. package/src/__tests__/helpers/pglite-migrations.ts +2 -5
  32. package/src/__tests__/helpers/pglite-rls-session.ts +13 -16
  33. package/src/__tests__/helpers/seed-like-fill.ts +50 -44
  34. package/src/__tests__/helpers/test-gencow-ctx-rls.ts +4 -7
  35. package/src/__tests__/httpaction.test.ts +91 -91
  36. package/src/__tests__/image-optimization.test.ts +570 -574
  37. package/src/__tests__/load.test.ts +321 -308
  38. package/src/__tests__/network-sim.test.ts +238 -215
  39. package/src/__tests__/reactive.test.ts +380 -358
  40. package/src/__tests__/retry.test.ts +99 -84
  41. package/src/__tests__/rls-crud-basic.test.ts +172 -245
  42. package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +81 -81
  43. package/src/__tests__/rls-custom-mutation-handlers.test.ts +47 -94
  44. package/src/__tests__/rls-custom-query-handlers.test.ts +92 -92
  45. package/src/__tests__/rls-db-leased-connection.test.ts +2 -6
  46. package/src/__tests__/rls-session-and-policies.test.ts +181 -199
  47. package/src/__tests__/scheduler-durable-v2.test.ts +199 -181
  48. package/src/__tests__/scheduler-durable.test.ts +117 -117
  49. package/src/__tests__/scheduler-exec.test.ts +258 -246
  50. package/src/__tests__/scheduler.test.ts +129 -111
  51. package/src/__tests__/storage.test.ts +282 -269
  52. package/src/__tests__/tsconfig.json +6 -6
  53. package/src/__tests__/validator.test.ts +236 -232
  54. package/src/__tests__/workflow.test.ts +309 -286
  55. package/src/__tests__/ws-integration.test.ts +223 -218
  56. package/src/__tests__/ws-scale.test.ts +168 -159
  57. package/src/auth-config.ts +18 -18
  58. package/src/auth.ts +106 -106
  59. package/src/crons.ts +77 -77
  60. package/src/crud.ts +523 -479
  61. package/src/index.ts +69 -5
  62. package/src/reactive.ts +357 -331
  63. package/src/retry.ts +51 -54
  64. package/src/rls-db.ts +195 -205
  65. package/src/rls.ts +33 -36
  66. package/src/scheduler.ts +237 -211
  67. package/src/server.ts +0 -1
  68. package/src/storage.ts +632 -593
  69. package/src/v.ts +119 -114
  70. package/src/workflow-types.ts +67 -70
  71. package/src/workflow.ts +99 -116
  72. package/src/workflows-api.ts +231 -241
  73. 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
- it("dist/index.js 파일이 존재한다", () => {
39
- expect(existsSync(DIST_INDEX)).toBe(true);
40
- });
38
+ it("dist/index.js 파일이 존재한다", () => {
39
+ expect(existsSync(DIST_INDEX)).toBe(true);
40
+ });
41
41
 
42
- it("dist/crud.js 파일이 존재한다", () => {
43
- expect(existsSync(resolve(CORE_ROOT, "dist/crud.js"))).toBe(true);
44
- });
42
+ it("dist/crud.js 파일이 존재한다", () => {
43
+ expect(existsSync(resolve(CORE_ROOT, "dist/crud.js"))).toBe(true);
44
+ });
45
45
 
46
- it("dist/reactive.js 파일이 존재한다", () => {
47
- expect(existsSync(resolve(CORE_ROOT, "dist/reactive.js"))).toBe(true);
48
- });
46
+ it("dist/reactive.js 파일이 존재한다", () => {
47
+ expect(existsSync(resolve(CORE_ROOT, "dist/reactive.js"))).toBe(true);
48
+ });
49
49
 
50
- it("dist/v.js 파일이 존재한다", () => {
51
- expect(existsSync(resolve(CORE_ROOT, "dist/v.js"))).toBe(true);
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
- 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
- });
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
- 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"),
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
- // ── 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
- // },
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
  });
@@ -3,4 +3,4 @@
3
3
  *
4
4
  * Gencow 런타임이 이 파일을 로드하여 query/mutation을 자동 등록합니다.
5
5
  */
6
- import "./tasks";
6
+ import "./tasks.js";
@@ -31,7 +31,7 @@ export const tasks = pgTable(
31
31
  createdAt: timestamp("created_at").defaultNow().notNull(),
32
32
  updatedAt: timestamp("updated_at").defaultNow().notNull(),
33
33
  },
34
- (t) => ownerRls(t.userId)
34
+ (t) => ownerRls(t.userId),
35
35
  );
36
36
 
37
37
  /**
@@ -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
- searchFields: ["title", "description"],
14
- defaultLimit: 50,
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
- 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(),
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
- 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").notNull().references(() => user.id, { onDelete: "cascade" }),
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
- id: text("id").primaryKey(),
40
- accountId: text("account_id").notNull(),
41
- providerId: text("provider_id").notNull(),
42
- userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
43
- accessToken: text("access_token"),
44
- refreshToken: text("refresh_token"),
45
- idToken: text("id_token"),
46
- accessTokenExpiresAt: timestamp("access_token_expires_at"),
47
- refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
48
- scope: text("scope"),
49
- password: text("password"),
50
- createdAt: timestamp("created_at").notNull().defaultNow(),
51
- updatedAt: timestamp("updated_at").notNull().defaultNow(),
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
- id: text("id").primaryKey(),
58
- identifier: text("identifier").notNull(),
59
- value: text("value").notNull(),
60
- expiresAt: timestamp("expires_at").notNull(),
61
- createdAt: timestamp("created_at").defaultNow(),
62
- updatedAt: timestamp("updated_at").defaultNow(),
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 { createPgliteRlsAppRole, DEFAULT_PGLITE_RLS_APP_ROLE, setPgliteSessionRole } from "./pglite-rls-session.js";
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
- { id: "us_000", name: "User 0", email: "user-0@s.com", emailVerified: true },
22
- { id: "us_001", name: "User 1", email: "user-1@s.com", emailVerified: true },
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
- id: "tk-000",
29
- userId: basicFixtureUsers[0].id,
30
- done: false,
31
- title: "Project Alpha — Q4 review prep",
32
- },
33
- {
34
- id: "tk-001",
35
- userId: basicFixtureUsers[1].id,
36
- done: true,
37
- title: "Project Alpha — teammate handoff",
38
- },
39
- {
40
- id: "tk-002",
41
- userId: basicFixtureUsers[0].id,
42
- done: false,
43
- title: "Project Alpha — backlog grooming",
44
- },
45
- {
46
- id: "tk-003",
47
- userId: basicFixtureUsers[0].id,
48
- done: false,
49
- title: "Quarterly planning — Q4",
50
- },
51
- {
52
- id: "tk-004",
53
- userId: basicFixtureUsers[1].id,
54
- done: false,
55
- title: "Project Beta — API docs",
56
- },
57
- {
58
- id: "tk-005",
59
- userId: basicFixtureUsers[1].id,
60
- done: false,
61
- title: "Project Gamma — research notes",
62
- },
63
- {
64
- id: "tk-006",
65
- userId: basicFixtureUsers[0].id,
66
- done: false,
67
- title: "Project Beta — spike",
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
- { id: "nw-000", userId: basicFixtureUsers[0].id, title: "owned by user0" },
74
- { id: "nw-001", userId: basicFixtureUsers[1].id, title: "owned by user1" },
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
- id: basicFixtureUsers[0].id,
79
- email: basicFixtureUsers[0].email,
82
+ id: basicFixtureUsers[0].id,
83
+ email: basicFixtureUsers[0].email,
80
84
  } satisfies UserIdentity;
81
85
 
82
86
  export const basicUser1Identity = {
83
- id: basicFixtureUsers[1].id,
84
- email: basicFixtureUsers[1].email,
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
- client: PGlite;
96
- db: ReturnType<typeof drizzle>;
97
- taskRows: BasicTaskRow[];
99
+ client: PGlite;
100
+ db: ReturnType<typeof drizzle>;
101
+ taskRows: BasicTaskRow[];
98
102
  }> {
99
- const client = new PGlite();
100
- await client.waitReady;
101
- await loadAndApplyMigrations(client, join(__dirname, "../fixtures/basic/migrations"));
102
- const db = drizzle(client);
103
- await db.insert(user).values(fillPartialRowsForInsert(user, basicFixtureUsers));
104
- const taskRows = fillPartialRowsForInsert(tasks, basicFixtureTasks) as BasicTaskRow[];
105
- await db.insert(tasks).values(taskRows);
106
- const newsRows = fillPartialRowsForInsert(news, basicFixtureNews) as BasicNewsRow[];
107
- await db.insert(news).values(newsRows);
108
- await createPgliteRlsAppRole(client, {
109
- roleName: DEFAULT_PGLITE_RLS_APP_ROLE,
110
- });
111
- await setPgliteSessionRole(client, DEFAULT_PGLITE_RLS_APP_ROLE);
112
- return { client, db, taskRows };
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
- db: ReturnType<typeof drizzle>,
118
- table: PgTable,
121
+ db: ReturnType<typeof drizzle>,
122
+ table: PgTable,
119
123
  ): Promise<void> {
120
- const relname = getTableName(table);
121
- const q = await db.execute(sql`
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
- const row = (q as unknown as { rows: { rls_enabled: boolean }[] }).rows[0];
128
- if (row?.rls_enabled !== false) {
129
- throw new Error(
130
- `expected relrowsecurity = false for public.${relname}, got ${JSON.stringify(row)}`,
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
  }