@gencow/core 0.1.19 → 0.1.21
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 +18 -0
- package/dist/crud.js +231 -50
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -2
- package/dist/rls-db.d.ts +3 -5
- package/dist/rls-db.js +3 -5
- package/dist/rls.d.ts +44 -1
- package/dist/rls.js +62 -2
- package/dist/server.d.ts +1 -0
- package/dist/storage.d.ts +29 -2
- package/dist/storage.js +396 -8
- package/package.json +42 -39
- package/src/__tests__/crud-owner-rls.test.ts +380 -0
- package/src/__tests__/fixtures/basic/auth.ts +32 -0
- package/src/__tests__/fixtures/basic/drizzle.config.ts +15 -0
- package/src/__tests__/fixtures/basic/index.ts +6 -0
- package/src/__tests__/fixtures/basic/migrations/0000_faithful_silver_sable.sql +66 -0
- package/src/__tests__/fixtures/basic/migrations/meta/0000_snapshot.json +438 -0
- package/src/__tests__/fixtures/basic/migrations/meta/_journal.json +13 -0
- package/src/__tests__/fixtures/basic/schema.ts +35 -0
- package/src/__tests__/fixtures/basic/tasks.ts +15 -0
- package/src/__tests__/fixtures/common/auth-schema.ts +63 -0
- package/src/__tests__/helpers/pglite-migrations.ts +35 -0
- package/src/__tests__/helpers/pglite-rls-session.ts +54 -0
- package/src/__tests__/helpers/seed-like-fill.ts +196 -0
- package/src/__tests__/helpers/test-gencow-ctx-rls.ts +53 -0
- package/src/__tests__/image-optimization.test.ts +652 -0
- package/src/__tests__/rls-crud-basic.test.ts +431 -0
- package/src/__tests__/tsconfig.json +8 -0
- package/src/crud.ts +270 -47
- package/src/index.ts +3 -2
- package/src/rls-db.ts +3 -5
- package/src/rls.ts +87 -3
- package/src/server.ts +1 -0
- package/src/storage.ts +473 -8
- package/dist/scoped-db.d.ts +0 -34
- package/dist/scoped-db.js +0 -364
- package/dist/table.d.ts +0 -67
- package/dist/table.js +0 -98
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "5dae5382-aa29-4251-9a52-0df4786c5100",
|
|
3
|
+
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
4
|
+
"version": "7",
|
|
5
|
+
"dialect": "postgresql",
|
|
6
|
+
"tables": {
|
|
7
|
+
"public.tasks": {
|
|
8
|
+
"name": "tasks",
|
|
9
|
+
"schema": "",
|
|
10
|
+
"columns": {
|
|
11
|
+
"id": {
|
|
12
|
+
"name": "id",
|
|
13
|
+
"type": "text",
|
|
14
|
+
"primaryKey": true,
|
|
15
|
+
"notNull": true
|
|
16
|
+
},
|
|
17
|
+
"title": {
|
|
18
|
+
"name": "title",
|
|
19
|
+
"type": "text",
|
|
20
|
+
"primaryKey": false,
|
|
21
|
+
"notNull": true
|
|
22
|
+
},
|
|
23
|
+
"description": {
|
|
24
|
+
"name": "description",
|
|
25
|
+
"type": "text",
|
|
26
|
+
"primaryKey": false,
|
|
27
|
+
"notNull": false
|
|
28
|
+
},
|
|
29
|
+
"done": {
|
|
30
|
+
"name": "done",
|
|
31
|
+
"type": "boolean",
|
|
32
|
+
"primaryKey": false,
|
|
33
|
+
"notNull": true,
|
|
34
|
+
"default": false
|
|
35
|
+
},
|
|
36
|
+
"user_id": {
|
|
37
|
+
"name": "user_id",
|
|
38
|
+
"type": "text",
|
|
39
|
+
"primaryKey": false,
|
|
40
|
+
"notNull": true
|
|
41
|
+
},
|
|
42
|
+
"created_at": {
|
|
43
|
+
"name": "created_at",
|
|
44
|
+
"type": "timestamp",
|
|
45
|
+
"primaryKey": false,
|
|
46
|
+
"notNull": true,
|
|
47
|
+
"default": "now()"
|
|
48
|
+
},
|
|
49
|
+
"updated_at": {
|
|
50
|
+
"name": "updated_at",
|
|
51
|
+
"type": "timestamp",
|
|
52
|
+
"primaryKey": false,
|
|
53
|
+
"notNull": true,
|
|
54
|
+
"default": "now()"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"indexes": {},
|
|
58
|
+
"foreignKeys": {
|
|
59
|
+
"tasks_user_id_user_id_fk": {
|
|
60
|
+
"name": "tasks_user_id_user_id_fk",
|
|
61
|
+
"tableFrom": "tasks",
|
|
62
|
+
"tableTo": "user",
|
|
63
|
+
"columnsFrom": [
|
|
64
|
+
"user_id"
|
|
65
|
+
],
|
|
66
|
+
"columnsTo": [
|
|
67
|
+
"id"
|
|
68
|
+
],
|
|
69
|
+
"onDelete": "cascade",
|
|
70
|
+
"onUpdate": "no action"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"compositePrimaryKeys": {},
|
|
74
|
+
"uniqueConstraints": {},
|
|
75
|
+
"policies": {
|
|
76
|
+
"rls-select": {
|
|
77
|
+
"name": "rls-select",
|
|
78
|
+
"as": "PERMISSIVE",
|
|
79
|
+
"for": "SELECT",
|
|
80
|
+
"to": [
|
|
81
|
+
"public"
|
|
82
|
+
],
|
|
83
|
+
"using": "\"tasks\".\"user_id\" = current_setting('app.current_user_id', true)"
|
|
84
|
+
},
|
|
85
|
+
"rls-insert": {
|
|
86
|
+
"name": "rls-insert",
|
|
87
|
+
"as": "PERMISSIVE",
|
|
88
|
+
"for": "INSERT",
|
|
89
|
+
"to": [
|
|
90
|
+
"public"
|
|
91
|
+
],
|
|
92
|
+
"withCheck": "\"tasks\".\"user_id\" = current_setting('app.current_user_id', true)"
|
|
93
|
+
},
|
|
94
|
+
"rls-update": {
|
|
95
|
+
"name": "rls-update",
|
|
96
|
+
"as": "PERMISSIVE",
|
|
97
|
+
"for": "UPDATE",
|
|
98
|
+
"to": [
|
|
99
|
+
"public"
|
|
100
|
+
],
|
|
101
|
+
"using": "\"tasks\".\"user_id\" = current_setting('app.current_user_id', true)",
|
|
102
|
+
"withCheck": "\"tasks\".\"user_id\" = current_setting('app.current_user_id', true)"
|
|
103
|
+
},
|
|
104
|
+
"rls-delete": {
|
|
105
|
+
"name": "rls-delete",
|
|
106
|
+
"as": "PERMISSIVE",
|
|
107
|
+
"for": "DELETE",
|
|
108
|
+
"to": [
|
|
109
|
+
"public"
|
|
110
|
+
],
|
|
111
|
+
"using": "\"tasks\".\"user_id\" = current_setting('app.current_user_id', true)"
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"checkConstraints": {},
|
|
115
|
+
"isRLSEnabled": false
|
|
116
|
+
},
|
|
117
|
+
"public.user": {
|
|
118
|
+
"name": "user",
|
|
119
|
+
"schema": "",
|
|
120
|
+
"columns": {
|
|
121
|
+
"id": {
|
|
122
|
+
"name": "id",
|
|
123
|
+
"type": "text",
|
|
124
|
+
"primaryKey": true,
|
|
125
|
+
"notNull": true
|
|
126
|
+
},
|
|
127
|
+
"name": {
|
|
128
|
+
"name": "name",
|
|
129
|
+
"type": "text",
|
|
130
|
+
"primaryKey": false,
|
|
131
|
+
"notNull": true
|
|
132
|
+
},
|
|
133
|
+
"email": {
|
|
134
|
+
"name": "email",
|
|
135
|
+
"type": "text",
|
|
136
|
+
"primaryKey": false,
|
|
137
|
+
"notNull": true
|
|
138
|
+
},
|
|
139
|
+
"email_verified": {
|
|
140
|
+
"name": "email_verified",
|
|
141
|
+
"type": "boolean",
|
|
142
|
+
"primaryKey": false,
|
|
143
|
+
"notNull": true,
|
|
144
|
+
"default": false
|
|
145
|
+
},
|
|
146
|
+
"image": {
|
|
147
|
+
"name": "image",
|
|
148
|
+
"type": "text",
|
|
149
|
+
"primaryKey": false,
|
|
150
|
+
"notNull": false
|
|
151
|
+
},
|
|
152
|
+
"created_at": {
|
|
153
|
+
"name": "created_at",
|
|
154
|
+
"type": "timestamp",
|
|
155
|
+
"primaryKey": false,
|
|
156
|
+
"notNull": true,
|
|
157
|
+
"default": "now()"
|
|
158
|
+
},
|
|
159
|
+
"updated_at": {
|
|
160
|
+
"name": "updated_at",
|
|
161
|
+
"type": "timestamp",
|
|
162
|
+
"primaryKey": false,
|
|
163
|
+
"notNull": true,
|
|
164
|
+
"default": "now()"
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
"indexes": {},
|
|
168
|
+
"foreignKeys": {},
|
|
169
|
+
"compositePrimaryKeys": {},
|
|
170
|
+
"uniqueConstraints": {
|
|
171
|
+
"user_email_unique": {
|
|
172
|
+
"name": "user_email_unique",
|
|
173
|
+
"nullsNotDistinct": false,
|
|
174
|
+
"columns": [
|
|
175
|
+
"email"
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
"policies": {},
|
|
180
|
+
"checkConstraints": {},
|
|
181
|
+
"isRLSEnabled": false
|
|
182
|
+
},
|
|
183
|
+
"public.account": {
|
|
184
|
+
"name": "account",
|
|
185
|
+
"schema": "",
|
|
186
|
+
"columns": {
|
|
187
|
+
"id": {
|
|
188
|
+
"name": "id",
|
|
189
|
+
"type": "text",
|
|
190
|
+
"primaryKey": true,
|
|
191
|
+
"notNull": true
|
|
192
|
+
},
|
|
193
|
+
"account_id": {
|
|
194
|
+
"name": "account_id",
|
|
195
|
+
"type": "text",
|
|
196
|
+
"primaryKey": false,
|
|
197
|
+
"notNull": true
|
|
198
|
+
},
|
|
199
|
+
"provider_id": {
|
|
200
|
+
"name": "provider_id",
|
|
201
|
+
"type": "text",
|
|
202
|
+
"primaryKey": false,
|
|
203
|
+
"notNull": true
|
|
204
|
+
},
|
|
205
|
+
"user_id": {
|
|
206
|
+
"name": "user_id",
|
|
207
|
+
"type": "text",
|
|
208
|
+
"primaryKey": false,
|
|
209
|
+
"notNull": true
|
|
210
|
+
},
|
|
211
|
+
"access_token": {
|
|
212
|
+
"name": "access_token",
|
|
213
|
+
"type": "text",
|
|
214
|
+
"primaryKey": false,
|
|
215
|
+
"notNull": false
|
|
216
|
+
},
|
|
217
|
+
"refresh_token": {
|
|
218
|
+
"name": "refresh_token",
|
|
219
|
+
"type": "text",
|
|
220
|
+
"primaryKey": false,
|
|
221
|
+
"notNull": false
|
|
222
|
+
},
|
|
223
|
+
"id_token": {
|
|
224
|
+
"name": "id_token",
|
|
225
|
+
"type": "text",
|
|
226
|
+
"primaryKey": false,
|
|
227
|
+
"notNull": false
|
|
228
|
+
},
|
|
229
|
+
"access_token_expires_at": {
|
|
230
|
+
"name": "access_token_expires_at",
|
|
231
|
+
"type": "timestamp",
|
|
232
|
+
"primaryKey": false,
|
|
233
|
+
"notNull": false
|
|
234
|
+
},
|
|
235
|
+
"refresh_token_expires_at": {
|
|
236
|
+
"name": "refresh_token_expires_at",
|
|
237
|
+
"type": "timestamp",
|
|
238
|
+
"primaryKey": false,
|
|
239
|
+
"notNull": false
|
|
240
|
+
},
|
|
241
|
+
"scope": {
|
|
242
|
+
"name": "scope",
|
|
243
|
+
"type": "text",
|
|
244
|
+
"primaryKey": false,
|
|
245
|
+
"notNull": false
|
|
246
|
+
},
|
|
247
|
+
"password": {
|
|
248
|
+
"name": "password",
|
|
249
|
+
"type": "text",
|
|
250
|
+
"primaryKey": false,
|
|
251
|
+
"notNull": false
|
|
252
|
+
},
|
|
253
|
+
"created_at": {
|
|
254
|
+
"name": "created_at",
|
|
255
|
+
"type": "timestamp",
|
|
256
|
+
"primaryKey": false,
|
|
257
|
+
"notNull": true,
|
|
258
|
+
"default": "now()"
|
|
259
|
+
},
|
|
260
|
+
"updated_at": {
|
|
261
|
+
"name": "updated_at",
|
|
262
|
+
"type": "timestamp",
|
|
263
|
+
"primaryKey": false,
|
|
264
|
+
"notNull": true,
|
|
265
|
+
"default": "now()"
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
"indexes": {},
|
|
269
|
+
"foreignKeys": {
|
|
270
|
+
"account_user_id_user_id_fk": {
|
|
271
|
+
"name": "account_user_id_user_id_fk",
|
|
272
|
+
"tableFrom": "account",
|
|
273
|
+
"tableTo": "user",
|
|
274
|
+
"columnsFrom": [
|
|
275
|
+
"user_id"
|
|
276
|
+
],
|
|
277
|
+
"columnsTo": [
|
|
278
|
+
"id"
|
|
279
|
+
],
|
|
280
|
+
"onDelete": "cascade",
|
|
281
|
+
"onUpdate": "no action"
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
"compositePrimaryKeys": {},
|
|
285
|
+
"uniqueConstraints": {},
|
|
286
|
+
"policies": {},
|
|
287
|
+
"checkConstraints": {},
|
|
288
|
+
"isRLSEnabled": false
|
|
289
|
+
},
|
|
290
|
+
"public.session": {
|
|
291
|
+
"name": "session",
|
|
292
|
+
"schema": "",
|
|
293
|
+
"columns": {
|
|
294
|
+
"id": {
|
|
295
|
+
"name": "id",
|
|
296
|
+
"type": "text",
|
|
297
|
+
"primaryKey": true,
|
|
298
|
+
"notNull": true
|
|
299
|
+
},
|
|
300
|
+
"expires_at": {
|
|
301
|
+
"name": "expires_at",
|
|
302
|
+
"type": "timestamp",
|
|
303
|
+
"primaryKey": false,
|
|
304
|
+
"notNull": true
|
|
305
|
+
},
|
|
306
|
+
"token": {
|
|
307
|
+
"name": "token",
|
|
308
|
+
"type": "text",
|
|
309
|
+
"primaryKey": false,
|
|
310
|
+
"notNull": true
|
|
311
|
+
},
|
|
312
|
+
"created_at": {
|
|
313
|
+
"name": "created_at",
|
|
314
|
+
"type": "timestamp",
|
|
315
|
+
"primaryKey": false,
|
|
316
|
+
"notNull": true,
|
|
317
|
+
"default": "now()"
|
|
318
|
+
},
|
|
319
|
+
"updated_at": {
|
|
320
|
+
"name": "updated_at",
|
|
321
|
+
"type": "timestamp",
|
|
322
|
+
"primaryKey": false,
|
|
323
|
+
"notNull": true,
|
|
324
|
+
"default": "now()"
|
|
325
|
+
},
|
|
326
|
+
"ip_address": {
|
|
327
|
+
"name": "ip_address",
|
|
328
|
+
"type": "text",
|
|
329
|
+
"primaryKey": false,
|
|
330
|
+
"notNull": false
|
|
331
|
+
},
|
|
332
|
+
"user_agent": {
|
|
333
|
+
"name": "user_agent",
|
|
334
|
+
"type": "text",
|
|
335
|
+
"primaryKey": false,
|
|
336
|
+
"notNull": false
|
|
337
|
+
},
|
|
338
|
+
"user_id": {
|
|
339
|
+
"name": "user_id",
|
|
340
|
+
"type": "text",
|
|
341
|
+
"primaryKey": false,
|
|
342
|
+
"notNull": true
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
"indexes": {},
|
|
346
|
+
"foreignKeys": {
|
|
347
|
+
"session_user_id_user_id_fk": {
|
|
348
|
+
"name": "session_user_id_user_id_fk",
|
|
349
|
+
"tableFrom": "session",
|
|
350
|
+
"tableTo": "user",
|
|
351
|
+
"columnsFrom": [
|
|
352
|
+
"user_id"
|
|
353
|
+
],
|
|
354
|
+
"columnsTo": [
|
|
355
|
+
"id"
|
|
356
|
+
],
|
|
357
|
+
"onDelete": "cascade",
|
|
358
|
+
"onUpdate": "no action"
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
"compositePrimaryKeys": {},
|
|
362
|
+
"uniqueConstraints": {
|
|
363
|
+
"session_token_unique": {
|
|
364
|
+
"name": "session_token_unique",
|
|
365
|
+
"nullsNotDistinct": false,
|
|
366
|
+
"columns": [
|
|
367
|
+
"token"
|
|
368
|
+
]
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
"policies": {},
|
|
372
|
+
"checkConstraints": {},
|
|
373
|
+
"isRLSEnabled": false
|
|
374
|
+
},
|
|
375
|
+
"public.verification": {
|
|
376
|
+
"name": "verification",
|
|
377
|
+
"schema": "",
|
|
378
|
+
"columns": {
|
|
379
|
+
"id": {
|
|
380
|
+
"name": "id",
|
|
381
|
+
"type": "text",
|
|
382
|
+
"primaryKey": true,
|
|
383
|
+
"notNull": true
|
|
384
|
+
},
|
|
385
|
+
"identifier": {
|
|
386
|
+
"name": "identifier",
|
|
387
|
+
"type": "text",
|
|
388
|
+
"primaryKey": false,
|
|
389
|
+
"notNull": true
|
|
390
|
+
},
|
|
391
|
+
"value": {
|
|
392
|
+
"name": "value",
|
|
393
|
+
"type": "text",
|
|
394
|
+
"primaryKey": false,
|
|
395
|
+
"notNull": true
|
|
396
|
+
},
|
|
397
|
+
"expires_at": {
|
|
398
|
+
"name": "expires_at",
|
|
399
|
+
"type": "timestamp",
|
|
400
|
+
"primaryKey": false,
|
|
401
|
+
"notNull": true
|
|
402
|
+
},
|
|
403
|
+
"created_at": {
|
|
404
|
+
"name": "created_at",
|
|
405
|
+
"type": "timestamp",
|
|
406
|
+
"primaryKey": false,
|
|
407
|
+
"notNull": false,
|
|
408
|
+
"default": "now()"
|
|
409
|
+
},
|
|
410
|
+
"updated_at": {
|
|
411
|
+
"name": "updated_at",
|
|
412
|
+
"type": "timestamp",
|
|
413
|
+
"primaryKey": false,
|
|
414
|
+
"notNull": false,
|
|
415
|
+
"default": "now()"
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
"indexes": {},
|
|
419
|
+
"foreignKeys": {},
|
|
420
|
+
"compositePrimaryKeys": {},
|
|
421
|
+
"uniqueConstraints": {},
|
|
422
|
+
"policies": {},
|
|
423
|
+
"checkConstraints": {},
|
|
424
|
+
"isRLSEnabled": false
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
"enums": {},
|
|
428
|
+
"schemas": {},
|
|
429
|
+
"sequences": {},
|
|
430
|
+
"roles": {},
|
|
431
|
+
"policies": {},
|
|
432
|
+
"views": {},
|
|
433
|
+
"_meta": {
|
|
434
|
+
"columns": {},
|
|
435
|
+
"schemas": {},
|
|
436
|
+
"tables": {}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gencow/schema.ts — Task App 스키마
|
|
3
|
+
*
|
|
4
|
+
* 🔒 Secure by Default:
|
|
5
|
+
* - pgTable + ownerRls + crud로 사용자별 데이터 자동 격리
|
|
6
|
+
* - onDelete: "cascade"로 유저 삭제 시 관련 데이터 자동 정리
|
|
7
|
+
*
|
|
8
|
+
* 변경 후: gencow dev가 자동 반영
|
|
9
|
+
*/
|
|
10
|
+
import { ownerRls } from "../../../rls";
|
|
11
|
+
import { pgTable } from "drizzle-orm/pg-core";
|
|
12
|
+
import { text, boolean, timestamp } from "drizzle-orm/pg-core";
|
|
13
|
+
import { user } from "../common/auth-schema";
|
|
14
|
+
import { v4 as uuidv4 } from "uuid";
|
|
15
|
+
|
|
16
|
+
export { user } from "../common/auth-schema";
|
|
17
|
+
|
|
18
|
+
export const tasks = pgTable(
|
|
19
|
+
"tasks",
|
|
20
|
+
{
|
|
21
|
+
id: text("id")
|
|
22
|
+
.primaryKey()
|
|
23
|
+
.$defaultFn(() => uuidv4()),
|
|
24
|
+
title: text("title").notNull(),
|
|
25
|
+
description: text("description"),
|
|
26
|
+
done: boolean("done").default(false).notNull(),
|
|
27
|
+
// 🔒 RLS (Role-Level Security)
|
|
28
|
+
userId: text("user_id")
|
|
29
|
+
.notNull()
|
|
30
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
31
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
32
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
33
|
+
},
|
|
34
|
+
(t) => ownerRls(t.userId)
|
|
35
|
+
);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gencow/tasks.ts — Task CRUD (Zero-Boilerplate Pattern)
|
|
3
|
+
*
|
|
4
|
+
* 🔒 Secure by Default:
|
|
5
|
+
* - ctx.db uses RLS (Row-Level Security) — auto-filters by userId
|
|
6
|
+
* - crud auto-registers query/mutation with auth + realtime
|
|
7
|
+
*/
|
|
8
|
+
import { crud } from "../../../crud";
|
|
9
|
+
import { tasks } from "./schema";
|
|
10
|
+
|
|
11
|
+
// 자동 생성: list, get, create, update, remove (query + mutation + auth + realtime)
|
|
12
|
+
export const { list, get, create, update, remove } = crud(tasks, {
|
|
13
|
+
searchFields: ["title", "description"],
|
|
14
|
+
defaultLimit: 50,
|
|
15
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* packages/server/src/auth-schema.ts
|
|
3
|
+
*
|
|
4
|
+
* better-auth가 사용하는 Drizzle 스키마 테이블 정의.
|
|
5
|
+
* better-auth는 user, session, account, verification 4개 테이블을 필요로 합니다.
|
|
6
|
+
*
|
|
7
|
+
* @see https://www.better-auth.com/docs/adapters/drizzle
|
|
8
|
+
*/
|
|
9
|
+
import { pgTable, text, boolean, timestamp } from "drizzle-orm/pg-core";
|
|
10
|
+
|
|
11
|
+
// ─── user 테이블 ────────────────────────────────────────
|
|
12
|
+
|
|
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(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// ─── session 테이블 ─────────────────────────────────────
|
|
24
|
+
|
|
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" }),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// ─── account 테이블 ─────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
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(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ─── verification 테이블 ────────────────────────────────
|
|
55
|
+
|
|
56
|
+
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(),
|
|
63
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import type { PGlite } from "@electric-sql/pglite";
|
|
4
|
+
|
|
5
|
+
function listMigrationSqlFiles(migrationsDir: string): string[] {
|
|
6
|
+
return readdirSync(migrationsDir)
|
|
7
|
+
.filter((f) => f.endsWith(".sql"))
|
|
8
|
+
.sort()
|
|
9
|
+
.map((f) => join(migrationsDir, f));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Apply Drizzle-generated SQL from a folder of `.sql` files (split on `--> statement-breakpoint`).
|
|
14
|
+
*/
|
|
15
|
+
export async function loadAndApplyMigrations(
|
|
16
|
+
client: PGlite,
|
|
17
|
+
migrationsDir: string
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
const files = listMigrationSqlFiles(migrationsDir);
|
|
20
|
+
if (files.length === 0) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`No .sql migration files in ${migrationsDir} (generate migrations with your Drizzle workflow)`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
for (const filePath of files) {
|
|
26
|
+
const raw = readFileSync(filePath, "utf8");
|
|
27
|
+
const chunks = raw
|
|
28
|
+
.split(/--> statement-breakpoint/g)
|
|
29
|
+
.map((s) => s.trim())
|
|
30
|
+
.filter(Boolean);
|
|
31
|
+
for (const sqlChunk of chunks) {
|
|
32
|
+
await client.exec(sqlChunk);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { PGlite } from "@electric-sql/pglite";
|
|
2
|
+
|
|
3
|
+
/** Default role used in PGlite tests when you need RLS to apply (session user must not own the tables). */
|
|
4
|
+
export const DEFAULT_PGLITE_RLS_APP_ROLE = "gencow_rls_app";
|
|
5
|
+
|
|
6
|
+
function quoteIdent(name: string): string {
|
|
7
|
+
if (!/^[a-z_][a-z0-9_]*$/i.test(name)) {
|
|
8
|
+
throw new Error(`Invalid SQL identifier: ${name}`);
|
|
9
|
+
}
|
|
10
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* After migrations (and optional seed as the bootstrap user), create a non-superuser role and grant
|
|
15
|
+
* DML on `public` so queries run as **non-owner** → PostgreSQL applies RLS policies without
|
|
16
|
+
* `FORCE ROW LEVEL SECURITY`.
|
|
17
|
+
*
|
|
18
|
+
* Omits `GRANT CONNECT ON DATABASE` — PGlite’s default DB can be `template1` and that grant has
|
|
19
|
+
* caused engine errors; schema/table privileges are enough for the embedded single-connection case.
|
|
20
|
+
*
|
|
21
|
+
* Call {@link setPgliteSessionRole} on the same `PGlite` instance before running app queries.
|
|
22
|
+
*/
|
|
23
|
+
export async function createPgliteRlsAppRole(
|
|
24
|
+
client: PGlite,
|
|
25
|
+
options?: { roleName?: string }
|
|
26
|
+
): Promise<string> {
|
|
27
|
+
const roleName = options?.roleName ?? DEFAULT_PGLITE_RLS_APP_ROLE;
|
|
28
|
+
const role = quoteIdent(roleName);
|
|
29
|
+
|
|
30
|
+
await client.exec(`
|
|
31
|
+
CREATE ROLE ${role} LOGIN;
|
|
32
|
+
GRANT USAGE ON SCHEMA public TO ${role};
|
|
33
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO ${role};
|
|
34
|
+
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO ${role};
|
|
35
|
+
`);
|
|
36
|
+
|
|
37
|
+
return roleName;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Run follow-up queries in this session as the given role (must not be table owner so RLS applies).
|
|
42
|
+
* The bootstrap user must be allowed to `SET ROLE` (e.g. superuser, or `GRANT rls_role TO bootstrap`).
|
|
43
|
+
*/
|
|
44
|
+
export async function setPgliteSessionRole(
|
|
45
|
+
client: PGlite,
|
|
46
|
+
roleName: string
|
|
47
|
+
): Promise<void> {
|
|
48
|
+
await client.exec(`SET ROLE ${quoteIdent(roleName)}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Restore session user to the original login role (typically the PGlite bootstrap user). */
|
|
52
|
+
export async function resetPgliteSessionRole(client: PGlite): Promise<void> {
|
|
53
|
+
await client.exec("RESET ROLE");
|
|
54
|
+
}
|