@gencow/core 0.1.23 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/crud.d.ts +2 -2
  2. package/dist/crud.js +225 -208
  3. package/dist/index.d.ts +7 -3
  4. package/dist/index.js +4 -1
  5. package/dist/reactive.js +10 -3
  6. package/dist/retry.js +1 -1
  7. package/dist/rls-db.d.ts +2 -2
  8. package/dist/rls-db.js +1 -5
  9. package/dist/scheduler.d.ts +2 -0
  10. package/dist/scheduler.js +16 -6
  11. package/dist/server.d.ts +0 -1
  12. package/dist/server.js +0 -1
  13. package/dist/storage.js +29 -22
  14. package/dist/v.d.ts +2 -2
  15. package/dist/workflow-types.d.ts +81 -0
  16. package/dist/workflow-types.js +12 -0
  17. package/dist/workflow.d.ts +30 -0
  18. package/dist/workflow.js +150 -0
  19. package/dist/workflows-api.d.ts +13 -0
  20. package/dist/workflows-api.js +321 -0
  21. package/package.json +46 -42
  22. package/src/__tests__/auth.test.ts +90 -86
  23. package/src/__tests__/crons.test.ts +69 -67
  24. package/src/__tests__/crud-codegen-integration.test.ts +164 -170
  25. package/src/__tests__/crud-owner-rls.test.ts +308 -301
  26. package/src/__tests__/crud.test.ts +694 -711
  27. package/src/__tests__/dist-exports.test.ts +120 -114
  28. package/src/__tests__/fixtures/basic/auth.ts +16 -16
  29. package/src/__tests__/fixtures/basic/drizzle.config.ts +1 -4
  30. package/src/__tests__/fixtures/basic/index.ts +1 -1
  31. package/src/__tests__/fixtures/basic/schema.ts +1 -1
  32. package/src/__tests__/fixtures/basic/tasks.ts +4 -4
  33. package/src/__tests__/fixtures/common/auth-schema.ts +38 -34
  34. package/src/__tests__/helpers/basic-rls-fixture.ts +80 -78
  35. package/src/__tests__/helpers/pglite-migrations.ts +2 -5
  36. package/src/__tests__/helpers/pglite-rls-session.ts +13 -16
  37. package/src/__tests__/helpers/seed-like-fill.ts +50 -44
  38. package/src/__tests__/helpers/test-gencow-ctx-rls.ts +4 -7
  39. package/src/__tests__/httpaction.test.ts +91 -91
  40. package/src/__tests__/image-optimization.test.ts +570 -574
  41. package/src/__tests__/load.test.ts +321 -308
  42. package/src/__tests__/network-sim.test.ts +238 -215
  43. package/src/__tests__/reactive.test.ts +380 -358
  44. package/src/__tests__/retry.test.ts +99 -84
  45. package/src/__tests__/rls-crud-basic.test.ts +172 -245
  46. package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +81 -81
  47. package/src/__tests__/rls-custom-mutation-handlers.test.ts +47 -94
  48. package/src/__tests__/rls-custom-query-handlers.test.ts +92 -92
  49. package/src/__tests__/rls-db-leased-connection.test.ts +2 -6
  50. package/src/__tests__/rls-session-and-policies.test.ts +181 -199
  51. package/src/__tests__/scheduler-durable-v2.test.ts +199 -181
  52. package/src/__tests__/scheduler-durable.test.ts +117 -117
  53. package/src/__tests__/scheduler-exec.test.ts +258 -246
  54. package/src/__tests__/scheduler.test.ts +129 -111
  55. package/src/__tests__/storage.test.ts +282 -269
  56. package/src/__tests__/tsconfig.json +6 -6
  57. package/src/__tests__/validator.test.ts +236 -232
  58. package/src/__tests__/workflow.test.ts +606 -0
  59. package/src/__tests__/ws-integration.test.ts +223 -218
  60. package/src/__tests__/ws-scale.test.ts +168 -159
  61. package/src/auth-config.ts +18 -18
  62. package/src/auth.ts +106 -106
  63. package/src/crons.ts +77 -77
  64. package/src/crud.ts +523 -479
  65. package/src/index.ts +71 -6
  66. package/src/reactive.ts +357 -331
  67. package/src/retry.ts +51 -54
  68. package/src/rls-db.ts +195 -205
  69. package/src/rls.ts +33 -36
  70. package/src/scheduler.ts +237 -211
  71. package/src/server.ts +0 -1
  72. package/src/storage.ts +632 -593
  73. package/src/v.ts +119 -114
  74. package/src/workflow-types.ts +108 -0
  75. package/src/workflow.ts +188 -0
  76. package/src/workflows-api.ts +415 -0
  77. package/src/db.ts +0 -18
@@ -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,82 +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("v export가 존재하고 객체이다", async () => {
95
- if (!distModule) distModule = await import(DIST_INDEX);
96
- expect(distModule.v).toBeDefined();
97
- expect(typeof distModule.v).toBe("object");
98
- });
99
-
100
- it("httpAction export가 존재하고 함수이다", async () => {
101
- if (!distModule) distModule = await import(DIST_INDEX);
102
- expect(distModule.httpAction).toBeDefined();
103
- expect(typeof distModule.httpAction).toBe("function");
104
- });
105
-
106
- // ── Registry exports (codegen 의존) ────────────────────────
107
-
108
- it("getRegisteredQueries export가 존재한다", async () => {
109
- if (!distModule) distModule = await import(DIST_INDEX);
110
- expect(distModule.getRegisteredQueries).toBeDefined();
111
- expect(typeof distModule.getRegisteredQueries).toBe("function");
112
- });
113
-
114
- it("getRegisteredMutations export가 존재한다", async () => {
115
- if (!distModule) distModule = await import(DIST_INDEX);
116
- expect(distModule.getRegisteredMutations).toBeDefined();
117
- expect(typeof distModule.getRegisteredMutations).toBe("function");
118
- });
119
-
120
- // ── RLS + Auth exports ─────────────────────────────────────
121
-
122
- it("ownerRls export가 존재한다", async () => {
123
- if (!distModule) distModule = await import(DIST_INDEX);
124
- expect(distModule.ownerRls).toBeDefined();
125
- });
126
-
127
- it("defineAuth export가 존재한다", async () => {
128
- if (!distModule) distModule = await import(DIST_INDEX);
129
- expect(distModule.defineAuth).toBeDefined();
130
- });
131
-
132
- it("cronJobs export가 존재한다", async () => {
133
- if (!distModule) distModule = await import(DIST_INDEX);
134
- expect(distModule.cronJobs).toBeDefined();
135
- });
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
+ });
136
142
  });
137
143
 
138
144
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -140,31 +146,31 @@ describe("dist/index.js 필수 export", () => {
140
146
  // ═══════════════════════════════════════════════════════════════════════════════
141
147
 
142
148
  describe("dist/crud.js — v2 구현 검증", () => {
143
- it("crud(table) 시그니처이다 (커링 gencowCrud(db)(table) 아님)", async () => {
144
- const distModule = await import(DIST_INDEX);
145
- const { crud } = distModule;
146
-
147
- // v2: crud(table, options?) → { list, get, create, update, remove }
148
- // 구버전: gencowCrud(db) → (table, options?) → { create, findById, list, ... }
149
-
150
- // 실제 Drizzle 테이블로 테스트
151
- const { pgTable, serial, text } = await import("drizzle-orm/pg-core");
152
- const testTable = pgTable("dist_smoke_test", {
153
- id: serial("id").primaryKey(),
154
- name: text("name"),
155
- });
156
-
157
- const result = crud(testTable, { public: true });
158
-
159
- // v2는 { list, get, create, update, remove } 반환
160
- expect(result).toHaveProperty("list");
161
- expect(result).toHaveProperty("get");
162
- expect(result).toHaveProperty("create");
163
- expect(result).toHaveProperty("update");
164
- expect(result).toHaveProperty("remove");
165
-
166
- // 구버전(커링)은 함수를 반환하므로 이 검증이 실패함
167
- expect(typeof result).toBe("object");
168
- 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"),
169
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
+ });
170
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
  }