@gencow/core 0.1.10 → 0.1.12
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 +42 -0
- package/dist/crud.js +130 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -3
- package/dist/rls-db.d.ts +10 -0
- package/dist/rls-db.js +25 -0
- package/dist/rls.d.ts +4 -0
- package/dist/rls.js +11 -0
- package/dist/scheduler.d.ts +20 -2
- package/dist/scheduler.js +70 -19
- package/package.json +38 -37
- package/src/__tests__/scheduler-exec.test.ts +78 -5
- package/src/crud.ts +174 -0
- package/src/index.ts +5 -5
- package/src/rls-db.ts +29 -0
- package/src/rls.ts +15 -0
- package/src/scheduler.ts +98 -21
- package/src/__tests__/scoped-db.test.ts +0 -442
- package/src/__tests__/table.test.ts +0 -324
- package/src/scoped-db.ts +0 -416
- package/src/table.ts +0 -165
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* packages/core/src/__tests__/table.test.ts
|
|
3
|
-
*
|
|
4
|
-
* Tests for gencowTable(), ownerFilter(), and table access registry.
|
|
5
|
-
*
|
|
6
|
-
* Run: bun test packages/core/src/__tests__/table.test.ts
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, it, expect, beforeEach } from "bun:test";
|
|
10
|
-
import {
|
|
11
|
-
gencowTable,
|
|
12
|
-
ownerFilter,
|
|
13
|
-
getTableAccessMeta,
|
|
14
|
-
isGencowTable,
|
|
15
|
-
getAllGencowTables,
|
|
16
|
-
_resetTableRegistry,
|
|
17
|
-
} from "../table";
|
|
18
|
-
import type { GencowCtx } from "../reactive";
|
|
19
|
-
import { pgTable, serial, text, integer } from "drizzle-orm/pg-core";
|
|
20
|
-
import { eq } from "drizzle-orm";
|
|
21
|
-
|
|
22
|
-
// ─── Test Helpers ────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
function makeCtx(userId: string, role = "user"): GencowCtx {
|
|
25
|
-
return {
|
|
26
|
-
auth: {
|
|
27
|
-
getUserIdentity: () => ({ id: userId, email: `${userId}@test.com`, name: "Test" }),
|
|
28
|
-
requireAuth: () => ({ id: userId, email: `${userId}@test.com`, name: "Test", role }),
|
|
29
|
-
},
|
|
30
|
-
db: {},
|
|
31
|
-
unsafeDb: {},
|
|
32
|
-
storage: {} as any,
|
|
33
|
-
scheduler: {} as any,
|
|
34
|
-
realtime: {} as any,
|
|
35
|
-
retry: {} as any,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function makeAnonCtx(): GencowCtx {
|
|
40
|
-
return {
|
|
41
|
-
auth: {
|
|
42
|
-
getUserIdentity: () => null,
|
|
43
|
-
requireAuth: () => { throw new Error("Authentication required"); },
|
|
44
|
-
},
|
|
45
|
-
db: {},
|
|
46
|
-
unsafeDb: {},
|
|
47
|
-
storage: {} as any,
|
|
48
|
-
scheduler: {} as any,
|
|
49
|
-
realtime: {} as any,
|
|
50
|
-
retry: {} as any,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ─── Tests ───────────────────────────────────────────────
|
|
55
|
-
|
|
56
|
-
describe("gencowTable()", () => {
|
|
57
|
-
beforeEach(() => {
|
|
58
|
-
_resetTableRegistry();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("유효한 Drizzle 테이블을 반환한다", () => {
|
|
62
|
-
const table = gencowTable("test_tasks", {
|
|
63
|
-
id: serial("id").primaryKey(),
|
|
64
|
-
title: text("title").notNull(),
|
|
65
|
-
}, {
|
|
66
|
-
filter: () => true,
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// Drizzle 테이블은 Symbol 기반 내부 속성을 가짐
|
|
70
|
-
expect(table).toBeDefined();
|
|
71
|
-
expect(typeof table).toBe("object");
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("filter 메타데이터를 레지스트리에 저장한다", () => {
|
|
75
|
-
const filterFn = (ctx: GencowCtx) => true;
|
|
76
|
-
const table = gencowTable("meta_test", {
|
|
77
|
-
id: serial("id").primaryKey(),
|
|
78
|
-
}, {
|
|
79
|
-
filter: filterFn,
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
const meta = getTableAccessMeta(table);
|
|
83
|
-
expect(meta).toBeDefined();
|
|
84
|
-
expect(meta!.tableName).toBe("meta_test");
|
|
85
|
-
expect(meta!.filter).toBe(filterFn);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("filter 옵션이 없으면 에러를 던진다", () => {
|
|
89
|
-
expect(() => {
|
|
90
|
-
gencowTable("no_filter", {
|
|
91
|
-
id: serial("id").primaryKey(),
|
|
92
|
-
}, {} as any);
|
|
93
|
-
}).toThrow("requires a filter option");
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("filter가 함수가 아니면 에러를 던진다", () => {
|
|
97
|
-
expect(() => {
|
|
98
|
-
gencowTable("bad_filter", {
|
|
99
|
-
id: serial("id").primaryKey(),
|
|
100
|
-
}, {
|
|
101
|
-
filter: "not a function" as any,
|
|
102
|
-
});
|
|
103
|
-
}).toThrow("requires a filter option");
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("options 자체가 없으면 에러를 던진다", () => {
|
|
107
|
-
expect(() => {
|
|
108
|
-
(gencowTable as any)("no_opts", {
|
|
109
|
-
id: serial("id").primaryKey(),
|
|
110
|
-
});
|
|
111
|
-
}).toThrow();
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("fieldAccess 메타데이터도 저장한다", () => {
|
|
115
|
-
const readCheck = (ctx: GencowCtx) => true;
|
|
116
|
-
const table = gencowTable("field_test", {
|
|
117
|
-
id: serial("id").primaryKey(),
|
|
118
|
-
salary: integer("salary"),
|
|
119
|
-
}, {
|
|
120
|
-
filter: () => true,
|
|
121
|
-
fieldAccess: {
|
|
122
|
-
salary: { read: readCheck },
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
const meta = getTableAccessMeta(table);
|
|
127
|
-
expect(meta!.fieldAccess).toBeDefined();
|
|
128
|
-
expect(meta!.fieldAccess!.salary.read).toBe(readCheck);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("filter 함수가 ctx를 받아 SQL 조건을 반환할 수 있다", () => {
|
|
132
|
-
const table = gencowTable("sql_filter", {
|
|
133
|
-
id: serial("id").primaryKey(),
|
|
134
|
-
userId: text("user_id").notNull(),
|
|
135
|
-
}, {
|
|
136
|
-
filter: (ctx) => {
|
|
137
|
-
const user = ctx.auth.requireAuth();
|
|
138
|
-
return eq((table as any).userId, user.id);
|
|
139
|
-
},
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
const meta = getTableAccessMeta(table);
|
|
143
|
-
const ctx = makeCtx("user-123");
|
|
144
|
-
const result = meta!.filter(ctx);
|
|
145
|
-
// Should return a SQL condition, not a boolean
|
|
146
|
-
expect(result).not.toBe(true);
|
|
147
|
-
expect(result).not.toBe(false);
|
|
148
|
-
expect(typeof result).toBe("object"); // Drizzle SQL object
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it("filter가 true를 반환하면 전체 접근", () => {
|
|
152
|
-
const table = gencowTable("public_table", {
|
|
153
|
-
id: serial("id").primaryKey(),
|
|
154
|
-
}, {
|
|
155
|
-
filter: () => true,
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
const meta = getTableAccessMeta(table);
|
|
159
|
-
expect(meta!.filter({} as GencowCtx)).toBe(true);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it("filter가 false를 반환하면 전체 차단", () => {
|
|
163
|
-
const table = gencowTable("blocked_table", {
|
|
164
|
-
id: serial("id").primaryKey(),
|
|
165
|
-
}, {
|
|
166
|
-
filter: () => false,
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
const meta = getTableAccessMeta(table);
|
|
170
|
-
expect(meta!.filter({} as GencowCtx)).toBe(false);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("RBAC filter — admin은 true, 일반은 SQL 조건", () => {
|
|
174
|
-
const table = gencowTable("rbac_test", {
|
|
175
|
-
id: serial("id").primaryKey(),
|
|
176
|
-
userId: text("user_id").notNull(),
|
|
177
|
-
}, {
|
|
178
|
-
filter: (ctx) => {
|
|
179
|
-
const user = ctx.auth.requireAuth() as any;
|
|
180
|
-
if (user.role === "admin") return true;
|
|
181
|
-
return eq((table as any).userId, user.id);
|
|
182
|
-
},
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
const meta = getTableAccessMeta(table);
|
|
186
|
-
|
|
187
|
-
// admin → true
|
|
188
|
-
const adminCtx = makeCtx("admin-1", "admin");
|
|
189
|
-
expect(meta!.filter(adminCtx)).toBe(true);
|
|
190
|
-
|
|
191
|
-
// user → SQL condition
|
|
192
|
-
const userCtx = makeCtx("user-1", "user");
|
|
193
|
-
const result = meta!.filter(userCtx);
|
|
194
|
-
expect(result).not.toBe(true);
|
|
195
|
-
expect(typeof result).toBe("object");
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it("requireAuth 포함 filter — 비인증 시 에러", () => {
|
|
199
|
-
const table = gencowTable("auth_required", {
|
|
200
|
-
id: serial("id").primaryKey(),
|
|
201
|
-
userId: text("user_id").notNull(),
|
|
202
|
-
}, {
|
|
203
|
-
filter: (ctx) => {
|
|
204
|
-
ctx.auth.requireAuth(); // throws if not authenticated
|
|
205
|
-
return true;
|
|
206
|
-
},
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
const meta = getTableAccessMeta(table);
|
|
210
|
-
const anonCtx = makeAnonCtx();
|
|
211
|
-
expect(() => meta!.filter(anonCtx)).toThrow("Authentication required");
|
|
212
|
-
});
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
// ─── ownerFilter() ──────────────────────────────────────
|
|
216
|
-
|
|
217
|
-
describe("ownerFilter()", () => {
|
|
218
|
-
beforeEach(() => {
|
|
219
|
-
_resetTableRegistry();
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it("기본 컬럼명 userId로 필터를 생성한다", () => {
|
|
223
|
-
const table = gencowTable("owner_default", {
|
|
224
|
-
id: serial("id").primaryKey(),
|
|
225
|
-
userId: text("user_id").notNull(),
|
|
226
|
-
}, ownerFilter("userId"));
|
|
227
|
-
|
|
228
|
-
const meta = getTableAccessMeta(table);
|
|
229
|
-
expect(meta).toBeDefined();
|
|
230
|
-
expect(meta!.tableName).toBe("owner_default");
|
|
231
|
-
|
|
232
|
-
// Filter should work with auth ctx
|
|
233
|
-
const ctx = makeCtx("user-abc");
|
|
234
|
-
const result = meta!.filter(ctx);
|
|
235
|
-
expect(result).not.toBe(true); // Returns SQL condition, not boolean
|
|
236
|
-
expect(typeof result).toBe("object");
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it("커스텀 컬럼명을 지원한다 (ownerId)", () => {
|
|
240
|
-
const table = gencowTable("owner_custom", {
|
|
241
|
-
id: serial("id").primaryKey(),
|
|
242
|
-
ownerId: text("owner_id").notNull(),
|
|
243
|
-
}, ownerFilter("ownerId"));
|
|
244
|
-
|
|
245
|
-
const meta = getTableAccessMeta(table);
|
|
246
|
-
expect(meta).toBeDefined();
|
|
247
|
-
|
|
248
|
-
const ctx = makeCtx("user-xyz");
|
|
249
|
-
const result = meta!.filter(ctx);
|
|
250
|
-
expect(typeof result).toBe("object"); // SQL condition
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it("존재하지 않는 컬럼명이면 에러", () => {
|
|
254
|
-
expect(() => {
|
|
255
|
-
gencowTable("owner_bad", {
|
|
256
|
-
id: serial("id").primaryKey(),
|
|
257
|
-
title: text("title"),
|
|
258
|
-
}, ownerFilter("nonExistentColumn"));
|
|
259
|
-
}).toThrow("not found on table");
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it("비인증 사용자 시 requireAuth에서 에러", () => {
|
|
263
|
-
const table = gencowTable("owner_auth", {
|
|
264
|
-
id: serial("id").primaryKey(),
|
|
265
|
-
userId: text("user_id").notNull(),
|
|
266
|
-
}, ownerFilter("userId"));
|
|
267
|
-
|
|
268
|
-
const meta = getTableAccessMeta(table);
|
|
269
|
-
const anonCtx = makeAnonCtx();
|
|
270
|
-
expect(() => meta!.filter(anonCtx)).toThrow("Authentication required");
|
|
271
|
-
});
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
// ─── isGencowTable / getAllGencowTables ──────────────────
|
|
275
|
-
|
|
276
|
-
describe("isGencowTable() / getAllGencowTables()", () => {
|
|
277
|
-
beforeEach(() => {
|
|
278
|
-
_resetTableRegistry();
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it("gencowTable로 생성한 테이블은 true", () => {
|
|
282
|
-
const table = gencowTable("is_test", {
|
|
283
|
-
id: serial("id").primaryKey(),
|
|
284
|
-
}, { filter: () => true });
|
|
285
|
-
|
|
286
|
-
expect(isGencowTable(table)).toBe(true);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it("pgTable로 생성한 테이블은 false", () => {
|
|
290
|
-
const table = pgTable("plain_pg", {
|
|
291
|
-
id: serial("id").primaryKey(),
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
expect(isGencowTable(table)).toBe(false);
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
it("getTableAccessMeta — pgTable은 undefined 반환", () => {
|
|
298
|
-
const table = pgTable("no_meta", {
|
|
299
|
-
id: serial("id").primaryKey(),
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
expect(getTableAccessMeta(table)).toBeUndefined();
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it("getAllGencowTables — 등록된 모든 테이블 반환", () => {
|
|
306
|
-
gencowTable("all_a", { id: serial("id").primaryKey() }, { filter: () => true });
|
|
307
|
-
gencowTable("all_b", { id: serial("id").primaryKey() }, { filter: () => true });
|
|
308
|
-
|
|
309
|
-
const all = getAllGencowTables();
|
|
310
|
-
expect(all.size).toBe(2);
|
|
311
|
-
|
|
312
|
-
const names = Array.from(all.values()).map(m => m.tableName);
|
|
313
|
-
expect(names).toContain("all_a");
|
|
314
|
-
expect(names).toContain("all_b");
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
it("_resetTableRegistry — 레지스트리 초기화", () => {
|
|
318
|
-
gencowTable("reset_test", { id: serial("id").primaryKey() }, { filter: () => true });
|
|
319
|
-
expect(getAllGencowTables().size).toBe(1);
|
|
320
|
-
|
|
321
|
-
_resetTableRegistry();
|
|
322
|
-
expect(getAllGencowTables().size).toBe(0);
|
|
323
|
-
});
|
|
324
|
-
});
|