@gencow/core 0.1.12 → 0.1.13
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 +67 -25
- package/dist/crud.js +221 -101
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -1
- package/dist/scheduler.js +9 -1
- package/package.json +39 -38
- package/src/__tests__/crud.test.ts +527 -0
- package/src/__tests__/scheduler-exec.test.ts +15 -18
- package/src/crud.ts +269 -121
- package/src/index.ts +4 -1
- package/src/scheduler.ts +8 -1
- 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
package/dist/crud.d.ts
CHANGED
|
@@ -1,42 +1,84 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* packages/core/src/crud.ts — Zero-Boilerplate CRUD API Auto-Generator (v2)
|
|
3
|
+
*
|
|
4
|
+
* 사용자 코드 5줄로 완전한 CRUD API를 자동 생성:
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { crud } from "@gencow/core";
|
|
9
|
+
* import { tasks } from "./schema";
|
|
10
|
+
*
|
|
11
|
+
* // query(tasks.list), query(tasks.get), mutation(tasks.create/update/remove) 자동 등록
|
|
12
|
+
* export const { list, get, create, update, remove } = crud(tasks);
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* list 반환값: `{ data: T[], total: number }`
|
|
16
|
+
* → 프론트: `const result = useQuery(api.tasks.list); result?.data.map(...)`
|
|
17
|
+
* → total은 동일 필터 조건의 전체 레코드 수 (페이지네이션 UI 지원)
|
|
18
|
+
*
|
|
19
|
+
* 내부적으로 query()/mutation()을 호출하여 글로벌 레지스트리에 등록하므로,
|
|
20
|
+
* 프론트엔드에서 useQuery(api.tasks.list) / useMutation(api.tasks.create) 로 바로 사용 가능.
|
|
21
|
+
*
|
|
22
|
+
* 📄 Spec: docs/specs/spec-crud-v2-enhancements.md
|
|
23
|
+
* 📦 참조: saas-js/drizzle-crud 팩토리 패턴
|
|
24
|
+
*/
|
|
25
|
+
import type { PgTable } from "drizzle-orm/pg-core";
|
|
2
26
|
type CrudOptions<T extends PgTable> = {
|
|
27
|
+
/** 검색 대상 필드 — list의 search 파라미터에 사용 */
|
|
3
28
|
searchFields?: (keyof T["_"]["columns"])[];
|
|
29
|
+
/** 소프트 삭제 컬럼 (e.g. deletedAt) */
|
|
4
30
|
softDelete?: {
|
|
5
31
|
field: keyof T["_"]["columns"];
|
|
6
32
|
};
|
|
33
|
+
/** 필터링 허용 필드 — allowedFilters에 없는 키는 무시 (보안) */
|
|
7
34
|
allowedFilters?: (keyof T["_"]["columns"])[];
|
|
35
|
+
/** 기본 페이지 크기 (default: 20) */
|
|
8
36
|
defaultLimit?: number;
|
|
37
|
+
/** 최대 페이지 크기 (default: 100) */
|
|
9
38
|
maxLimit?: number;
|
|
39
|
+
/** 라이프사이클 훅 */
|
|
10
40
|
hooks?: {
|
|
11
41
|
beforeCreate?: (data: any) => any | Promise<any>;
|
|
12
42
|
beforeUpdate?: (data: any) => any | Promise<any>;
|
|
13
43
|
};
|
|
44
|
+
/** true면 인증 없이 접근 가능 (기본: false — Secure by Default) */
|
|
45
|
+
public?: boolean;
|
|
46
|
+
/** true면 mutation 후 realtime push (기본: true) */
|
|
47
|
+
realtime?: boolean;
|
|
48
|
+
/** 키 접두사 오버라이드 (기본: 테이블명) */
|
|
49
|
+
prefix?: string;
|
|
14
50
|
};
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
51
|
+
/**
|
|
52
|
+
* 테이블을 받아 query/mutation을 자동 등록하고,
|
|
53
|
+
* { list, get, create, update, remove } 를 반환한다.
|
|
54
|
+
*
|
|
55
|
+
* list 반환값: `{ data: T[], total: number }`
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* export const { list, get, create, update, remove } = crud(tasks);
|
|
60
|
+
* export const { list, get, create, update, remove } = crud(tasks, { public: true });
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare function crud<T extends PgTable>(table: T, options?: CrudOptions<T>): {
|
|
64
|
+
list: import("./reactive").QueryDef<{
|
|
65
|
+
page: import("./v").Validator<number | undefined>;
|
|
66
|
+
limit: import("./v").Validator<number | undefined>;
|
|
67
|
+
search: import("./v").Validator<string | undefined>;
|
|
68
|
+
orderBy: import("./v").Validator<string | undefined>;
|
|
69
|
+
orderDir: import("./v").Validator<string | undefined>;
|
|
70
|
+
filters: import("./v").Validator<any>;
|
|
71
|
+
}, {
|
|
72
|
+
data: any;
|
|
34
73
|
total: number;
|
|
35
74
|
}>;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
75
|
+
get: import("./reactive").QueryDef<{
|
|
76
|
+
id: import("./v").Validator<string> | import("./v").Validator<number>;
|
|
77
|
+
}, any>;
|
|
78
|
+
create: import("./reactive").MutationDef<any, any>;
|
|
79
|
+
update: import("./reactive").MutationDef<any, any>;
|
|
80
|
+
remove: import("./reactive").MutationDef<any, {
|
|
81
|
+
success: boolean;
|
|
82
|
+
}>;
|
|
41
83
|
};
|
|
42
84
|
export {};
|
package/dist/crud.js
CHANGED
|
@@ -1,130 +1,250 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
/**
|
|
2
|
+
* packages/core/src/crud.ts — Zero-Boilerplate CRUD API Auto-Generator (v2)
|
|
3
|
+
*
|
|
4
|
+
* 사용자 코드 5줄로 완전한 CRUD API를 자동 생성:
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { crud } from "@gencow/core";
|
|
9
|
+
* import { tasks } from "./schema";
|
|
10
|
+
*
|
|
11
|
+
* // query(tasks.list), query(tasks.get), mutation(tasks.create/update/remove) 자동 등록
|
|
12
|
+
* export const { list, get, create, update, remove } = crud(tasks);
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* list 반환값: `{ data: T[], total: number }`
|
|
16
|
+
* → 프론트: `const result = useQuery(api.tasks.list); result?.data.map(...)`
|
|
17
|
+
* → total은 동일 필터 조건의 전체 레코드 수 (페이지네이션 UI 지원)
|
|
18
|
+
*
|
|
19
|
+
* 내부적으로 query()/mutation()을 호출하여 글로벌 레지스트리에 등록하므로,
|
|
20
|
+
* 프론트엔드에서 useQuery(api.tasks.list) / useMutation(api.tasks.create) 로 바로 사용 가능.
|
|
21
|
+
*
|
|
22
|
+
* 📄 Spec: docs/specs/spec-crud-v2-enhancements.md
|
|
23
|
+
* 📦 참조: saas-js/drizzle-crud 팩토리 패턴
|
|
24
|
+
*/
|
|
25
|
+
import { eq, desc, asc, ilike, or, and, count as drizzleCount, getTableName } from "drizzle-orm";
|
|
26
|
+
import { query, mutation } from "./reactive";
|
|
27
|
+
import { v } from "./v";
|
|
28
|
+
// ─── Helpers ────────────────────────────────────────────
|
|
29
|
+
/**
|
|
30
|
+
* id 컬럼의 Drizzle dataType을 검사하여 적절한 validator를 반환.
|
|
31
|
+
* serial/integer/bigint → v.number()
|
|
32
|
+
* text/uuid/varchar → v.string()
|
|
33
|
+
*/
|
|
34
|
+
function detectIdType(column) {
|
|
35
|
+
const colType = column.dataType;
|
|
36
|
+
if (colType === "string")
|
|
37
|
+
return v.string();
|
|
38
|
+
return v.number();
|
|
39
|
+
}
|
|
40
|
+
// ─── crud — Zero-Boilerplate API Factory ──────────
|
|
41
|
+
/**
|
|
42
|
+
* 테이블을 받아 query/mutation을 자동 등록하고,
|
|
43
|
+
* { list, get, create, update, remove } 를 반환한다.
|
|
44
|
+
*
|
|
45
|
+
* list 반환값: `{ data: T[], total: number }`
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* export const { list, get, create, update, remove } = crud(tasks);
|
|
50
|
+
* export const { list, get, create, update, remove } = crud(tasks, { public: true });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function crud(table, options) {
|
|
54
|
+
const anyTable = table;
|
|
55
|
+
const tableName = getTableName(table); // Drizzle 공식 API
|
|
56
|
+
const prefix = options?.prefix || tableName;
|
|
57
|
+
const isPublic = options?.public ?? false;
|
|
58
|
+
const useRealtime = options?.realtime ?? true;
|
|
59
|
+
const defaultLimit = options?.defaultLimit ?? 20;
|
|
60
|
+
const maxLimit = options?.maxLimit ?? 100;
|
|
61
|
+
const pk = anyTable["id"];
|
|
62
|
+
if (!pk) {
|
|
63
|
+
throw new Error(`[crud] Table "${tableName}" must have an 'id' column.`);
|
|
64
|
+
}
|
|
65
|
+
// id 타입 자동 감지 (serial → v.number(), text/uuid → v.string())
|
|
66
|
+
const idValidator = detectIdType(pk);
|
|
67
|
+
// createdAt 컬럼 (정렬용, 없으면 id 사용)
|
|
68
|
+
const createdAtCol = anyTable["createdAt"];
|
|
69
|
+
const defaultOrderCol = createdAtCol || pk;
|
|
70
|
+
// userId 컬럼 (자동 주입용)
|
|
71
|
+
const userIdCol = anyTable["userId"];
|
|
72
|
+
// ── 내부 헬퍼: WHERE 조건 빌드 (list + count + realtime 공유) ──
|
|
73
|
+
function buildWhereConditions(args) {
|
|
74
|
+
const conditions = [];
|
|
75
|
+
// Soft delete
|
|
76
|
+
if (options?.softDelete) {
|
|
77
|
+
const sdField = anyTable[options.softDelete.field];
|
|
78
|
+
conditions.push(eq(sdField, null));
|
|
8
79
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
80
|
+
// Search
|
|
81
|
+
if (args?.search && options?.searchFields?.length) {
|
|
82
|
+
const searchConds = options.searchFields.map((f) => ilike(anyTable[f], `%${args.search}%`));
|
|
83
|
+
conditions.push(or(...searchConds));
|
|
84
|
+
}
|
|
85
|
+
// Dynamic filters (allowedFilters 화이트리스트만 허용)
|
|
86
|
+
if (args?.filters && options?.allowedFilters?.length) {
|
|
87
|
+
for (const [key, value] of Object.entries(args.filters)) {
|
|
88
|
+
if (options.allowedFilters.includes(key) && anyTable[key]) {
|
|
89
|
+
conditions.push(eq(anyTable[key], value));
|
|
90
|
+
}
|
|
91
|
+
// allowedFilters에 없는 키는 무시 (보안)
|
|
13
92
|
}
|
|
14
|
-
const [result] = await db.insert(anyTable).values(insertData).returning();
|
|
15
|
-
return result;
|
|
16
93
|
}
|
|
17
|
-
|
|
18
|
-
|
|
94
|
+
return conditions.length > 0 ? and(...conditions) : undefined;
|
|
95
|
+
}
|
|
96
|
+
// ── 내부 헬퍼: list+count 데이터 가져오기 (realtime push용 재사용) ──
|
|
97
|
+
async function fetchListWithTotal(db, whereClause) {
|
|
98
|
+
const [data, countResult] = await Promise.all([
|
|
99
|
+
db.select().from(anyTable).where(whereClause).orderBy(desc(defaultOrderCol)),
|
|
100
|
+
db.select({ count: drizzleCount() }).from(anyTable).where(whereClause),
|
|
101
|
+
]);
|
|
102
|
+
return { data, total: Number(countResult[0]?.count ?? 0) };
|
|
103
|
+
}
|
|
104
|
+
// ── list ──────────────────────────────────────
|
|
105
|
+
const listDef = query(`${prefix}.list`, {
|
|
106
|
+
public: isPublic,
|
|
107
|
+
args: {
|
|
108
|
+
page: v.optional(v.number()),
|
|
109
|
+
limit: v.optional(v.number()),
|
|
110
|
+
search: v.optional(v.string()),
|
|
111
|
+
orderBy: v.optional(v.string()),
|
|
112
|
+
orderDir: v.optional(v.string()),
|
|
113
|
+
filters: v.optional(v.any()),
|
|
114
|
+
},
|
|
115
|
+
handler: async (ctx, args) => {
|
|
116
|
+
if (!isPublic)
|
|
117
|
+
ctx.auth.requireAuth();
|
|
118
|
+
const page = Math.max(1, args?.page || 1);
|
|
119
|
+
const limit = Math.min(Math.max(1, args?.limit || defaultLimit), maxLimit);
|
|
120
|
+
const offset = (page - 1) * limit;
|
|
121
|
+
const whereClause = buildWhereConditions(args);
|
|
122
|
+
// Order
|
|
123
|
+
let orderByClause;
|
|
124
|
+
if (args?.orderBy && anyTable[args.orderBy]) {
|
|
125
|
+
const col = anyTable[args.orderBy];
|
|
126
|
+
orderByClause = args.orderDir === "asc" ? asc(col) : desc(col);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
orderByClause = desc(defaultOrderCol);
|
|
130
|
+
}
|
|
131
|
+
// SELECT + COUNT 병렬 실행
|
|
132
|
+
const [results, countResult] = await Promise.all([
|
|
133
|
+
ctx.db.select()
|
|
134
|
+
.from(anyTable)
|
|
135
|
+
.where(whereClause)
|
|
136
|
+
.orderBy(orderByClause)
|
|
137
|
+
.limit(limit)
|
|
138
|
+
.offset(offset),
|
|
139
|
+
ctx.db.select({ count: drizzleCount() })
|
|
140
|
+
.from(anyTable)
|
|
141
|
+
.where(whereClause),
|
|
142
|
+
]);
|
|
143
|
+
return {
|
|
144
|
+
data: results,
|
|
145
|
+
total: Number(countResult[0]?.count ?? 0),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
// ── get ───────────────────────────────────────
|
|
150
|
+
const getDef = query(`${prefix}.get`, {
|
|
151
|
+
public: isPublic,
|
|
152
|
+
args: { id: idValidator },
|
|
153
|
+
handler: async (ctx, args) => {
|
|
154
|
+
if (!isPublic)
|
|
155
|
+
ctx.auth.requireAuth();
|
|
156
|
+
let whereCond = eq(pk, args.id);
|
|
19
157
|
if (options?.softDelete) {
|
|
20
158
|
const sdField = anyTable[options.softDelete.field];
|
|
21
|
-
whereCond = and(whereCond,
|
|
159
|
+
whereCond = and(whereCond, eq(sdField, null));
|
|
22
160
|
}
|
|
23
|
-
const [result] = await db.select().from(anyTable).where(whereCond).limit(1);
|
|
24
|
-
return result
|
|
161
|
+
const [result] = await ctx.db.select().from(anyTable).where(whereCond).limit(1);
|
|
162
|
+
return result ?? null;
|
|
25
163
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
164
|
+
});
|
|
165
|
+
// ── create ────────────────────────────────────
|
|
166
|
+
const createDef = mutation(`${prefix}.create`, {
|
|
167
|
+
public: isPublic,
|
|
168
|
+
invalidates: [],
|
|
169
|
+
handler: async (ctx, args) => {
|
|
170
|
+
const user = isPublic ? null : ctx.auth.requireAuth();
|
|
171
|
+
let insertData = { ...args };
|
|
172
|
+
// userId 자동 주입 (테이블에 userId 컬럼이 있고 인증된 경우)
|
|
173
|
+
if (userIdCol && user && !insertData.userId) {
|
|
174
|
+
insertData.userId = user.id;
|
|
34
175
|
}
|
|
35
|
-
//
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
conditions.push(or(...searchConds));
|
|
176
|
+
// beforeCreate 훅
|
|
177
|
+
if (options?.hooks?.beforeCreate) {
|
|
178
|
+
insertData = await options.hooks.beforeCreate(insertData);
|
|
39
179
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
}
|
|
180
|
+
const [result] = await ctx.db.insert(anyTable).values(insertData).returning();
|
|
181
|
+
// Realtime push — { data, total } 형태로 emit
|
|
182
|
+
if (useRealtime) {
|
|
183
|
+
const listResult = await fetchListWithTotal(ctx.db);
|
|
184
|
+
ctx.realtime.emit(`${prefix}.list`, listResult);
|
|
47
185
|
}
|
|
48
|
-
|
|
49
|
-
// Order By
|
|
50
|
-
const orderByArgs = (params?.orderBy || []).map((o) => {
|
|
51
|
-
const col = anyTable[o.field];
|
|
52
|
-
return o.direction === 'desc' ? desc(col) : asc(col);
|
|
53
|
-
});
|
|
54
|
-
// Base count
|
|
55
|
-
const [{ count: total }] = await db.select({ count: count() }).from(anyTable).where(whereClause);
|
|
56
|
-
const results = await db.select()
|
|
57
|
-
.from(anyTable)
|
|
58
|
-
.where(whereClause)
|
|
59
|
-
.orderBy(...orderByArgs)
|
|
60
|
-
.limit(limit)
|
|
61
|
-
.offset(offset);
|
|
62
|
-
return {
|
|
63
|
-
results,
|
|
64
|
-
page,
|
|
65
|
-
limit,
|
|
66
|
-
total: Number(total),
|
|
67
|
-
};
|
|
186
|
+
return result;
|
|
68
187
|
}
|
|
69
|
-
|
|
70
|
-
|
|
188
|
+
});
|
|
189
|
+
// ── update ────────────────────────────────────
|
|
190
|
+
const updateDef = mutation(`${prefix}.update`, {
|
|
191
|
+
public: isPublic,
|
|
192
|
+
invalidates: [],
|
|
193
|
+
handler: async (ctx, args) => {
|
|
194
|
+
if (!isPublic)
|
|
195
|
+
ctx.auth.requireAuth();
|
|
196
|
+
const { id, ...updates } = args;
|
|
197
|
+
let updateData = { ...updates };
|
|
198
|
+
// updatedAt 자동 갱신
|
|
199
|
+
if (anyTable["updatedAt"]) {
|
|
200
|
+
updateData.updatedAt = new Date();
|
|
201
|
+
}
|
|
202
|
+
// beforeUpdate 훅
|
|
71
203
|
if (options?.hooks?.beforeUpdate) {
|
|
72
204
|
updateData = await options.hooks.beforeUpdate(updateData);
|
|
73
205
|
}
|
|
74
|
-
const [result] = await db.update(anyTable)
|
|
206
|
+
const [result] = await ctx.db.update(anyTable)
|
|
75
207
|
.set(updateData)
|
|
76
208
|
.where(eq(pk, id))
|
|
77
209
|
.returning();
|
|
210
|
+
// Realtime push (list + get 양쪽) — { data, total } 형태
|
|
211
|
+
if (useRealtime) {
|
|
212
|
+
const listResult = await fetchListWithTotal(ctx.db);
|
|
213
|
+
ctx.realtime.emit(`${prefix}.list`, listResult);
|
|
214
|
+
ctx.realtime.emit(`${prefix}.get`, result);
|
|
215
|
+
}
|
|
78
216
|
return result;
|
|
79
217
|
}
|
|
80
|
-
|
|
218
|
+
});
|
|
219
|
+
// ── remove ────────────────────────────────────
|
|
220
|
+
const removeDef = mutation(`${prefix}.remove`, {
|
|
221
|
+
public: isPublic,
|
|
222
|
+
invalidates: [],
|
|
223
|
+
handler: async (ctx, args) => {
|
|
224
|
+
if (!isPublic)
|
|
225
|
+
ctx.auth.requireAuth();
|
|
81
226
|
if (options?.softDelete) {
|
|
82
227
|
const sdField = options.softDelete.field;
|
|
83
|
-
await db.update(anyTable)
|
|
228
|
+
await ctx.db.update(anyTable)
|
|
84
229
|
.set({ [sdField]: new Date() })
|
|
85
|
-
.where(eq(pk, id));
|
|
230
|
+
.where(eq(pk, args.id));
|
|
86
231
|
}
|
|
87
232
|
else {
|
|
88
|
-
await db.delete(anyTable).where(eq(pk, id));
|
|
233
|
+
await ctx.db.delete(anyTable).where(eq(pk, args.id));
|
|
89
234
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
await db.update(anyTable)
|
|
95
|
-
.set({ [sdField]: null })
|
|
96
|
-
.where(eq(pk, id));
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
async function bulkCreate(dataArray) {
|
|
100
|
-
let insertData = [...dataArray];
|
|
101
|
-
if (options?.hooks?.beforeCreate) {
|
|
102
|
-
insertData = await Promise.all(insertData.map((d) => options.hooks.beforeCreate(d)));
|
|
103
|
-
}
|
|
104
|
-
return await db.insert(anyTable).values(insertData).returning();
|
|
105
|
-
}
|
|
106
|
-
async function bulkDelete(ids) {
|
|
107
|
-
if (ids.length === 0)
|
|
108
|
-
return;
|
|
109
|
-
if (options?.softDelete) {
|
|
110
|
-
const sdField = options.softDelete.field;
|
|
111
|
-
await db.update(anyTable)
|
|
112
|
-
.set({ [sdField]: new Date() })
|
|
113
|
-
.where(inArray(pk, ids));
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
await db.delete(anyTable).where(inArray(pk, ids));
|
|
235
|
+
// Realtime push — { data, total } 형태
|
|
236
|
+
if (useRealtime) {
|
|
237
|
+
const listResult = await fetchListWithTotal(ctx.db);
|
|
238
|
+
ctx.realtime.emit(`${prefix}.list`, listResult);
|
|
117
239
|
}
|
|
240
|
+
return { success: true };
|
|
118
241
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
bulkCreate,
|
|
127
|
-
bulkDelete,
|
|
128
|
-
};
|
|
242
|
+
});
|
|
243
|
+
return {
|
|
244
|
+
list: listDef,
|
|
245
|
+
get: getDef,
|
|
246
|
+
create: createDef,
|
|
247
|
+
update: updateDef,
|
|
248
|
+
remove: removeDef,
|
|
129
249
|
};
|
|
130
250
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -19,4 +19,5 @@ export { defineAuth } from "./auth-config";
|
|
|
19
19
|
export type { GencowAuthConfig, AuthEmailVerification } from "./auth-config";
|
|
20
20
|
export { ownerRls } from "./rls";
|
|
21
21
|
export { createRlsDb } from "./rls-db";
|
|
22
|
-
export {
|
|
22
|
+
export { crud } from "./crud";
|
|
23
|
+
export { crud as gencowCrud } from "./crud";
|
package/dist/index.js
CHANGED
|
@@ -13,4 +13,6 @@ export { defineAuth } from "./auth-config";
|
|
|
13
13
|
// ─── RLS + CRUD Factory ───────────
|
|
14
14
|
export { ownerRls } from "./rls";
|
|
15
15
|
export { createRlsDb } from "./rls-db";
|
|
16
|
-
export {
|
|
16
|
+
export { crud } from "./crud";
|
|
17
|
+
// Deprecated alias — 하위호환용, 향후 메이저 버전에서 제거 예정
|
|
18
|
+
export { crud as gencowCrud } from "./crud";
|
package/dist/scheduler.js
CHANGED
|
@@ -131,7 +131,15 @@ export function createScheduler() {
|
|
|
131
131
|
actions.set(name, handler);
|
|
132
132
|
},
|
|
133
133
|
async executeAction(name, args) {
|
|
134
|
-
|
|
134
|
+
try {
|
|
135
|
+
await executeAction(name, args);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
// 공개 API는 에러를 삼긴다 (호출자 크래시 방지)
|
|
139
|
+
// handler 에러도 로깅 (내부 함수는 미등록만 로깅, handler 에러는 미로깅)
|
|
140
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
141
|
+
console.error(`[scheduler] executeAction("${name}") failed: ${msg}`);
|
|
142
|
+
}
|
|
135
143
|
},
|
|
136
144
|
};
|
|
137
145
|
}
|
package/package.json
CHANGED
|
@@ -1,41 +1,42 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
"name": "@gencow/core",
|
|
3
|
+
"version": "0.1.13",
|
|
4
|
+
"description": "Gencow core library — defineQuery, defineMutation, reactive subscriptions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
},
|
|
14
|
+
"./server": {
|
|
15
|
+
"import": "./dist/server.js",
|
|
16
|
+
"require": "./dist/server.js",
|
|
17
|
+
"types": "./dist/server.d.ts"
|
|
18
|
+
}
|
|
13
19
|
},
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
"files": [
|
|
21
|
+
"dist/",
|
|
22
|
+
"src/"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"prepublishOnly": "npm run build",
|
|
28
|
+
"postinstall": "tsc"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@electric-sql/pglite": "^0.3.15",
|
|
32
|
+
"drizzle-orm": "^0.45.1",
|
|
33
|
+
"hono": "^4.12.0",
|
|
34
|
+
"node-cron": "^4.2.1"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/bun": "^1.3.9",
|
|
38
|
+
"@types/node": "^25.3.0",
|
|
39
|
+
"@types/node-cron": "^3.0.11",
|
|
40
|
+
"typescript": "^5.9.3"
|
|
18
41
|
}
|
|
19
|
-
|
|
20
|
-
"files": [
|
|
21
|
-
"dist/",
|
|
22
|
-
"src/"
|
|
23
|
-
],
|
|
24
|
-
"dependencies": {
|
|
25
|
-
"@electric-sql/pglite": "^0.3.15",
|
|
26
|
-
"drizzle-orm": "^0.45.1",
|
|
27
|
-
"hono": "^4.12.0",
|
|
28
|
-
"node-cron": "^4.2.1"
|
|
29
|
-
},
|
|
30
|
-
"devDependencies": {
|
|
31
|
-
"@types/bun": "^1.3.9",
|
|
32
|
-
"@types/node": "^25.3.0",
|
|
33
|
-
"@types/node-cron": "^3.0.11",
|
|
34
|
-
"typescript": "^5.9.3"
|
|
35
|
-
},
|
|
36
|
-
"scripts": {
|
|
37
|
-
"build": "tsc",
|
|
38
|
-
"typecheck": "tsc --noEmit",
|
|
39
|
-
"postinstall": "tsc"
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
+
}
|