@gencow/core 0.1.12 → 0.1.14

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 CHANGED
@@ -1,42 +1,84 @@
1
- import type { PgDatabase, PgTable } from "drizzle-orm/pg-core";
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
- export declare function gencowCrud(db: PgDatabase<any, any, any>): <T extends PgTable>(table: T, options?: CrudOptions<T>) => {
16
- create: (data: any) => Promise<any>;
17
- findById: (id: any) => Promise<any | null>;
18
- list: (params?: {
19
- page?: number;
20
- limit?: number;
21
- search?: string;
22
- filters?: Record<string, any>;
23
- orderBy?: {
24
- field: string;
25
- direction: "asc" | "desc";
26
- }[];
27
- includeDeleted?: boolean;
28
- }) => Promise<{
29
- results: {
30
- [x: string]: any;
31
- }[];
32
- page: number;
33
- limit: number;
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
- update: (id: any, data: any) => Promise<any>;
37
- deleteOne: (id: any) => Promise<void>;
38
- restore: (id: any) => Promise<void>;
39
- bulkCreate: (dataArray: any[]) => Promise<any[]>;
40
- bulkDelete: (ids: any[]) => Promise<void>;
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,251 @@
1
- import { eq, or, and, ilike, desc, asc, inArray, count, sql } from "drizzle-orm";
2
- export function gencowCrud(db) {
3
- return function createCrud(table, options) {
4
- const anyTable = table;
5
- const pk = anyTable["id"];
6
- if (!pk) {
7
- throw new Error(`[gencowCrud] Table ${anyTable["_"]["name"]} must have an 'id' column.`);
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
- async function create(data) {
10
- let insertData = { ...data };
11
- if (options?.hooks?.beforeCreate) {
12
- insertData = await options.hooks.beforeCreate(insertData);
13
- }
14
- const [result] = await db.insert(anyTable).values(insertData).returning();
15
- return result;
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));
16
84
  }
17
- async function findById(id) {
18
- let whereCond = eq(pk, id);
19
- if (options?.softDelete) {
20
- const sdField = anyTable[options.softDelete.field];
21
- whereCond = and(whereCond, sql `${sdField} IS NULL`);
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에 없는 키는 무시 (보안)
22
92
  }
23
- const [result] = await db.select().from(anyTable).where(whereCond).limit(1);
24
- return result || null;
25
93
  }
26
- async function list(params) {
27
- const page = Math.max(1, params?.page || 1);
28
- const limit = Math.min(Math.max(1, params?.limit || options?.defaultLimit || 20), options?.maxLimit || 100);
94
+ return conditions.length > 0 ? and(...conditions) : undefined;
95
+ }
96
+ // ── 내부 헬퍼: list+count 데이터 가져오기 (realtime push용 재사용) ──
97
+ // 순차 실행: 소규모 커넥션 풀 환경에서 동시 점유 방지
98
+ // ⚠️ limit/offset 없이 전체 SELECT — 대량 데이터 시 성능 저하 주의
99
+ // TODO(P2): realtime emit 시 invalidation 메시지만 전송하고 클라이언트가 re-fetch하는 패턴 검토
100
+ async function fetchListWithTotal(db, whereClause) {
101
+ const data = await db.select().from(anyTable).where(whereClause).orderBy(desc(defaultOrderCol));
102
+ const countResult = await db.select({ count: drizzleCount() }).from(anyTable).where(whereClause);
103
+ return { data, total: Number(countResult[0]?.count ?? 0) };
104
+ }
105
+ // ── list ──────────────────────────────────────
106
+ const listDef = query(`${prefix}.list`, {
107
+ public: isPublic,
108
+ args: {
109
+ page: v.optional(v.number()),
110
+ limit: v.optional(v.number()),
111
+ search: v.optional(v.string()),
112
+ orderBy: v.optional(v.string()),
113
+ orderDir: v.optional(v.string()),
114
+ filters: v.optional(v.any()),
115
+ },
116
+ handler: async (ctx, args) => {
117
+ if (!isPublic)
118
+ ctx.auth.requireAuth();
119
+ const page = Math.max(1, args?.page || 1);
120
+ const limit = Math.min(Math.max(1, args?.limit || defaultLimit), maxLimit);
29
121
  const offset = (page - 1) * limit;
30
- const conditions = [];
31
- // Soft delete
32
- if (options?.softDelete && !params?.includeDeleted) {
33
- conditions.push(sql `${anyTable[options.softDelete.field]} IS NULL`);
122
+ const whereClause = buildWhereConditions(args);
123
+ // Order
124
+ let orderByClause;
125
+ if (args?.orderBy && anyTable[args.orderBy]) {
126
+ const col = anyTable[args.orderBy];
127
+ orderByClause = args.orderDir === "asc" ? asc(col) : desc(col);
34
128
  }
35
- // Search
36
- if (params?.search && options?.searchFields?.length) {
37
- const searchConds = options.searchFields.map((f) => ilike(anyTable[f], `%${params.search}%`));
38
- conditions.push(or(...searchConds));
39
- }
40
- // Filters
41
- if (params?.filters && options?.allowedFilters) {
42
- for (const [k, v] of Object.entries(params.filters)) {
43
- if (options.allowedFilters.includes(k)) {
44
- conditions.push(eq(anyTable[k], v));
45
- }
46
- }
129
+ else {
130
+ orderByClause = desc(defaultOrderCol);
47
131
  }
48
- const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
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()
132
+ // SELECT + COUNT 순차 실행 소규모 커넥션 풀 환경에서 동시 점유 방지
133
+ // Note: data←→count 사이 INSERT/DELETE 시 total 불일치 가능 (BaaS 단일사용자/저부하에서 무시 가능)
134
+ // TODO(P2): 대규모 db.transaction() 래핑 검토
135
+ const results = await ctx.db.select()
57
136
  .from(anyTable)
58
137
  .where(whereClause)
59
- .orderBy(...orderByArgs)
138
+ .orderBy(orderByClause)
60
139
  .limit(limit)
61
140
  .offset(offset);
141
+ const countResult = await ctx.db.select({ count: drizzleCount() })
142
+ .from(anyTable)
143
+ .where(whereClause);
62
144
  return {
63
- results,
64
- page,
65
- limit,
66
- total: Number(total),
145
+ data: results,
146
+ total: Number(countResult[0]?.count ?? 0),
67
147
  };
68
148
  }
69
- async function update(id, data) {
70
- let updateData = { ...data };
149
+ });
150
+ // ── get ───────────────────────────────────────
151
+ const getDef = query(`${prefix}.get`, {
152
+ public: isPublic,
153
+ args: { id: idValidator },
154
+ handler: async (ctx, args) => {
155
+ if (!isPublic)
156
+ ctx.auth.requireAuth();
157
+ let whereCond = eq(pk, args.id);
158
+ if (options?.softDelete) {
159
+ const sdField = anyTable[options.softDelete.field];
160
+ whereCond = and(whereCond, eq(sdField, null));
161
+ }
162
+ const [result] = await ctx.db.select().from(anyTable).where(whereCond).limit(1);
163
+ return result ?? null;
164
+ }
165
+ });
166
+ // ── create ────────────────────────────────────
167
+ const createDef = mutation(`${prefix}.create`, {
168
+ public: isPublic,
169
+ invalidates: [],
170
+ handler: async (ctx, args) => {
171
+ const user = isPublic ? null : ctx.auth.requireAuth();
172
+ let insertData = { ...args };
173
+ // userId 자동 주입 (테이블에 userId 컬럼이 있고 인증된 경우)
174
+ if (userIdCol && user && !insertData.userId) {
175
+ insertData.userId = user.id;
176
+ }
177
+ // beforeCreate 훅
178
+ if (options?.hooks?.beforeCreate) {
179
+ insertData = await options.hooks.beforeCreate(insertData);
180
+ }
181
+ const [result] = await ctx.db.insert(anyTable).values(insertData).returning();
182
+ // Realtime push — { data, total } 형태로 emit
183
+ if (useRealtime) {
184
+ const listResult = await fetchListWithTotal(ctx.db);
185
+ ctx.realtime.emit(`${prefix}.list`, listResult);
186
+ }
187
+ return result;
188
+ }
189
+ });
190
+ // ── update ────────────────────────────────────
191
+ const updateDef = mutation(`${prefix}.update`, {
192
+ public: isPublic,
193
+ invalidates: [],
194
+ handler: async (ctx, args) => {
195
+ if (!isPublic)
196
+ ctx.auth.requireAuth();
197
+ const { id, ...updates } = args;
198
+ let updateData = { ...updates };
199
+ // updatedAt 자동 갱신
200
+ if (anyTable["updatedAt"]) {
201
+ updateData.updatedAt = new Date();
202
+ }
203
+ // beforeUpdate 훅
71
204
  if (options?.hooks?.beforeUpdate) {
72
205
  updateData = await options.hooks.beforeUpdate(updateData);
73
206
  }
74
- const [result] = await db.update(anyTable)
207
+ const [result] = await ctx.db.update(anyTable)
75
208
  .set(updateData)
76
209
  .where(eq(pk, id))
77
210
  .returning();
211
+ // Realtime push (list + get 양쪽) — { data, total } 형태
212
+ if (useRealtime) {
213
+ const listResult = await fetchListWithTotal(ctx.db);
214
+ ctx.realtime.emit(`${prefix}.list`, listResult);
215
+ ctx.realtime.emit(`${prefix}.get`, result);
216
+ }
78
217
  return result;
79
218
  }
80
- async function deleteOne(id) {
219
+ });
220
+ // ── remove ────────────────────────────────────
221
+ const removeDef = mutation(`${prefix}.remove`, {
222
+ public: isPublic,
223
+ invalidates: [],
224
+ handler: async (ctx, args) => {
225
+ if (!isPublic)
226
+ ctx.auth.requireAuth();
81
227
  if (options?.softDelete) {
82
228
  const sdField = options.softDelete.field;
83
- await db.update(anyTable)
229
+ await ctx.db.update(anyTable)
84
230
  .set({ [sdField]: new Date() })
85
- .where(eq(pk, id));
231
+ .where(eq(pk, args.id));
86
232
  }
87
233
  else {
88
- await db.delete(anyTable).where(eq(pk, id));
89
- }
90
- }
91
- async function restore(id) {
92
- if (options?.softDelete) {
93
- const sdField = options.softDelete.field;
94
- await db.update(anyTable)
95
- .set({ [sdField]: null })
96
- .where(eq(pk, id));
234
+ await ctx.db.delete(anyTable).where(eq(pk, args.id));
97
235
  }
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));
236
+ // Realtime push — { data, total } 형태
237
+ if (useRealtime) {
238
+ const listResult = await fetchListWithTotal(ctx.db);
239
+ ctx.realtime.emit(`${prefix}.list`, listResult);
117
240
  }
241
+ return { success: true };
118
242
  }
119
- return {
120
- create,
121
- findById,
122
- list,
123
- update,
124
- deleteOne,
125
- restore,
126
- bulkCreate,
127
- bulkDelete,
128
- };
243
+ });
244
+ return {
245
+ list: listDef,
246
+ get: getDef,
247
+ create: createDef,
248
+ update: updateDef,
249
+ remove: removeDef,
129
250
  };
130
251
  }
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 { gencowCrud } from "./crud";
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 { gencowCrud } from "./crud";
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
- await executeAction(name, args);
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
- "name": "@gencow/core",
3
- "version": "0.1.12",
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"
2
+ "name": "@gencow/core",
3
+ "version": "0.1.14",
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
- "./server": {
15
- "import": "./dist/server.js",
16
- "require": "./dist/server.js",
17
- "types": "./dist/server.d.ts"
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
+ }