gencow 0.1.92 → 0.1.94

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/bin/gencow.mjs CHANGED
@@ -296,10 +296,56 @@ process.exit(0);
296
296
  try {
297
297
  writeFileSync(extractTsPath, script);
298
298
 
299
- const outStr = execSync(`bun .gencow-extract.ts`, {
300
- cwd: serverRoot,
301
- stdio: ["pipe", "pipe", "ignore"]
302
- }).toString();
299
+ let outStr;
300
+ try {
301
+ outStr = execSync(`bun .gencow-extract.ts`, {
302
+ cwd: serverRoot,
303
+ stdio: ["pipe", "pipe", "pipe"] // stderr도 캡처
304
+ }).toString();
305
+ } catch (execError) {
306
+ // bun 실행 자체가 실패한 경우 — stderr에서 구체적 원인 추출
307
+ const stderr = execError.stderr?.toString() || "";
308
+ const stdout = execError.stdout?.toString() || "";
309
+
310
+ error("API Codegen failed — gencow/index.ts를 파싱할 수 없습니다.");
311
+ log("");
312
+
313
+ // stderr에서 핵심 에러 줄 추출 (타입 에러, import 에러 등)
314
+ const errorLines = stderr.split("\n").filter(line =>
315
+ line.includes("error:") ||
316
+ line.includes("Error:") ||
317
+ line.includes("Cannot find") ||
318
+ line.includes("is not assignable") ||
319
+ line.includes("does not exist") ||
320
+ line.includes("Module not found")
321
+ ).slice(0, 5); // 최대 5줄
322
+
323
+ if (errorLines.length > 0) {
324
+ error("원인:");
325
+ for (const line of errorLines) {
326
+ info(` ${RED}${line.trim()}${RESET}`);
327
+ }
328
+ } else if (stderr.trim()) {
329
+ // 패턴 매칭 안 되면 stderr 처음 3줄 출력
330
+ const firstLines = stderr.trim().split("\n").slice(0, 3);
331
+ for (const line of firstLines) {
332
+ info(` ${DIM}${line}${RESET}`);
333
+ }
334
+ }
335
+
336
+ log("");
337
+ info(`${YELLOW}해결 방법:${RESET}`);
338
+ info(" 1. gencow/index.ts에서 TypeScript 에러를 수정하세요");
339
+ info(" 2. import 경로가 올바른지 확인하세요");
340
+ info(" 3. @gencow/core 패키지가 설치되어 있는지 확인하세요");
341
+ log("");
342
+
343
+ // 빈 스텁 api.ts 생성 — IDE가 즉시 에러를 잡을 수 있도록
344
+ const stubContent = `/**\n * ⚠️ API Codegen Failed\n *\n * gencow/index.ts에서 에러가 발생하여 api.ts를 생성할 수 없었습니다.\n * 콘솔의 에러 메시지를 확인하고 수정한 후 gencow dev를 재시작하세요.\n *\n * 이 파일은 자동 생성됩니다 — 직접 수정하지 마세요.\n */\nexport const api = {} as const;\n`;
345
+ writeFileSync(apiTsPath, stubContent);
346
+ warn(`빈 스텁 ${config.functionsDir}/api.ts 생성됨 (에러 수정 후 자동 재생성)`);
347
+ return; // 에러를 throw하지 않고 조용히 반환 — dev 서버는 계속 실행
348
+ }
303
349
 
304
350
  const match = outStr.match(/GENCOW_API_JSON=({.*})/);
305
351
  if (!match) throw new Error("Could not parse registry output");
@@ -382,7 +428,11 @@ process.exit(0);
382
428
 
383
429
 
384
430
  } catch (e) {
385
- warn(`API Codegen failed: ${e.message}`);
431
+ error(`API Codegen failed: ${e.message}`);
432
+ // 빈 스텁 api.ts 생성 — 예기치 않은 에러에서도 IDE가 즉시 에러를 잡을 수 있도록
433
+ const stubContent = `/**\n * ⚠️ API Codegen Failed: ${e.message.replace(/\*/g, "")}\n *\n * 에러를 수정한 후 gencow dev를 재시작하세요.\n * 이 파일은 자동 생성됩니다 — 직접 수정하지 마세요.\n */\nexport const api = {} as const;\n`;
434
+ try { writeFileSync(apiTsPath, stubContent); } catch { /* ignore */ }
435
+ warn(`빈 스텁 ${config.functionsDir}/api.ts 생성됨`);
386
436
  } finally {
387
437
  try { unlinkSync(extractTsPath); } catch { }
388
438
  }
@@ -726,7 +776,7 @@ export default defineConfig({
726
776
  if (existsSync(envPath) && force) {
727
777
  info(".env 이미 존재 — 건너뜁니다.");
728
778
  } else {
729
- const envContent = `# Gencow Environment Variables\n# Add your environment variables here\n`;
779
+ const envContent = `# Gencow Environment Variables\n# Add your environment variables here\n\n# AI 기능 사용 시 필수 (ctx.ai.chat, ai.chat 등)\n# https://platform.openai.com/api-keys 에서 발급\nOPENAI_API_KEY=sk-your-key-here\n`;
730
780
  writeFileSync(envPath, envContent);
731
781
  success("Created .env");
732
782
  }
@@ -1359,7 +1409,7 @@ ${hasPrompt ? `
1359
1409
 
1360
1410
  // tar.gz 생성: gencow/ 폴더 전체 (schema.ts + import된 파일들)
1361
1411
  execSync(
1362
- `tar -czf "${tmpBundle}" -C "${process.cwd()}" "${functionsDir.replace(/^\.\//,'')}/"`,
1412
+ `COPYFILE_DISABLE=1 tar -czf "${tmpBundle}" -C "${process.cwd()}" "${functionsDir.replace(/^\.\//,'')}/"`,
1363
1413
  { stdio: ["ignore", "pipe", "pipe"] }
1364
1414
  );
1365
1415
 
@@ -1941,10 +1991,10 @@ ${BOLD}Examples:${RESET}
1941
1991
  exec(`cp "${src}" "${dst}"`, { cwd: process.cwd() });
1942
1992
  }
1943
1993
  }
1944
- exec(`tar -czf "${tmpBundle}" .`, { cwd: tmpDir });
1994
+ exec(`COPYFILE_DISABLE=1 tar -czf "${tmpBundle}" .`, { cwd: tmpDir });
1945
1995
  exec(`rm -rf "${tmpDir}"`, { cwd: process.cwd() });
1946
1996
  } else {
1947
- exec(`tar -czf "${tmpBundle}" ${filesToPack.join(" ")}`, { cwd: process.cwd() });
1997
+ exec(`COPYFILE_DISABLE=1 tar -czf "${tmpBundle}" ${filesToPack.join(" ")}`, { cwd: process.cwd() });
1948
1998
  }
1949
1999
  } catch (e) {
1950
2000
  error(`패키징 실패: ${e.message}`);
@@ -2046,7 +2096,7 @@ ${BOLD}Examples:${RESET}
2046
2096
 
2047
2097
  // 재배포
2048
2098
  info("재배포 시도...");
2049
- exec(`tar -czf "${tmpBundle}" ${filesToPack.join(" ")}`, { cwd: process.cwd() });
2099
+ exec(`COPYFILE_DISABLE=1 tar -czf "${tmpBundle}" ${filesToPack.join(" ")}`, { cwd: process.cwd() });
2050
2100
  const retryBuffer = readFileSync(tmpBundle);
2051
2101
 
2052
2102
  const retryStartTime = Date.now();
@@ -2217,7 +2267,44 @@ ${BOLD}Examples:${RESET}
2217
2267
  && (existsSync(resolve(parentDir, "gencow.config.ts")) || existsSync(resolve(parentDir, "package.json")));
2218
2268
  const detectedBackend = hasCwdBackend || hasParentBackend;
2219
2269
  const backendRoot = hasCwdBackend ? process.cwd() : (hasParentBackend ? parentDir : null);
2220
- const shouldDeployBackend = detectedBackend && !noBackend;
2270
+
2271
+ // ── 빈 백엔드 자동 감지: gencow/ 내 query/mutation/httpAction 호출이 없으면 skip ──
2272
+ // schema.ts + 빈 API 파일만 있는 경우에도 올바르게 감지
2273
+ let isBackendEmpty = false;
2274
+ if (detectedBackend && backendRoot) {
2275
+ const gencowDir = resolve(backendRoot, "gencow");
2276
+ try {
2277
+ const scanTsFiles = (dir) => {
2278
+ const entries = readdirSync(dir, { withFileTypes: true });
2279
+ const files = [];
2280
+ for (const e of entries) {
2281
+ const p = resolve(dir, e.name);
2282
+ if (e.isDirectory() && e.name !== "node_modules") files.push(...scanTsFiles(p));
2283
+ else if (e.isFile() && (e.name.endsWith(".ts") || e.name.endsWith(".tsx"))) files.push(p);
2284
+ }
2285
+ return files;
2286
+ };
2287
+ const tsFiles = scanTsFiles(gencowDir);
2288
+ // query(, mutation(, httpAction( 호출이 하나라도 있으면 실질적 백엔드
2289
+ const hasApiCalls = tsFiles.some(f => {
2290
+ const src = readFileSync(f, "utf8");
2291
+ return /\b(query|mutation|httpAction)\s*\(/.test(src);
2292
+ });
2293
+ if (!hasApiCalls) {
2294
+ isBackendEmpty = true;
2295
+ }
2296
+ } catch {
2297
+ // 스캔 실패 시 안전하게 백엔드 있다고 가정
2298
+ isBackendEmpty = false;
2299
+ }
2300
+ }
2301
+
2302
+ const shouldDeployBackend = detectedBackend && !noBackend && !isBackendEmpty;
2303
+
2304
+ if (isBackendEmpty && detectedBackend) {
2305
+ info(`${DIM}gencow/ 감지됨 — API 함수(query/mutation) 없음 → 백엔드 배포 건너뜀${RESET}`);
2306
+ info(`${DIM}💡 백엔드가 필요하면 gencow/ 내 .ts 파일에 query() 또는 mutation()을 정의하세요.${RESET}`);
2307
+ }
2221
2308
 
2222
2309
  if (shouldDeployBackend) {
2223
2310
  log(`\n${BOLD}${CYAN}Gencow Deploy (Fullstack)${RESET}\n`);
@@ -2358,10 +2445,10 @@ ${BOLD}Examples:${RESET}
2358
2445
  exec(`cp "${src}" "${dst}"`, { cwd: backendRoot });
2359
2446
  }
2360
2447
  }
2361
- exec(`tar -czf "${tmpBackendBundle}" .`, { cwd: tmpDir });
2448
+ exec(`COPYFILE_DISABLE=1 tar -czf "${tmpBackendBundle}" .`, { cwd: tmpDir });
2362
2449
  exec(`rm -rf "${tmpDir}"`, { cwd: backendRoot });
2363
2450
  } else {
2364
- exec(`tar -czf "${tmpBackendBundle}" ${backendFiles.join(" ")}`, { cwd: backendRoot });
2451
+ exec(`COPYFILE_DISABLE=1 tar -czf "${tmpBackendBundle}" ${backendFiles.join(" ")}`, { cwd: backendRoot });
2365
2452
  }
2366
2453
  } catch (e) {
2367
2454
  error(`백엔드 패키징 실패: ${e.message}`);
@@ -2452,7 +2539,7 @@ ${BOLD}Examples:${RESET}
2452
2539
  mkdirSync(dirname(tmpBundle), { recursive: true });
2453
2540
 
2454
2541
  try {
2455
- exec(`tar -czf "${tmpBundle}" -C "${resolve(process.cwd(), targetDir)}" .`, { cwd: process.cwd() });
2542
+ exec(`COPYFILE_DISABLE=1 tar -czf "${tmpBundle}" -C "${resolve(process.cwd(), targetDir)}" .`, { cwd: process.cwd() });
2456
2543
  } catch (e) {
2457
2544
  error(`패키징 실패: ${e.message}`);
2458
2545
  process.exit(1);
@@ -3366,56 +3453,90 @@ process.exit(0);
3366
3453
  return;
3367
3454
  }
3368
3455
 
3369
- // ── Follow 모드: WebSocket 실시간 스트리밍 ──
3456
+ // ── Follow 모드: WebSocket 실시간 스트리밍 (지수 백오프 재연결) ──
3370
3457
  const config = loadConfig();
3371
3458
  const port = process.env.PORT || config.port || 5456;
3372
- const wsUrl = `ws://localhost:${port}/ws`;
3373
3459
 
3374
3460
  log(`\n${BOLD}${CYAN}Gencow Logs${RESET} ${DIM}— streaming (Ctrl+C to stop)${RESET}\n`);
3375
- info(`Connecting to ${DIM}${wsUrl}${RESET}...`);
3376
3461
 
3377
- const { WebSocket: WS } = await import("ws");
3378
- const ws = new WS(wsUrl);
3462
+ const FOLLOW_BASE_RECONNECT_MS = 3000;
3463
+ const FOLLOW_MAX_RECONNECT_MS = 30000;
3464
+ const FOLLOW_MAX_FAILURES = 5;
3465
+ let followFailures = 0;
3466
+ let followTimer = null;
3467
+ let followWs = null;
3379
3468
 
3380
- ws.on("open", () => {
3381
- success("Connected streaming logs...\n");
3382
- ws.send(JSON.stringify({ type: "log:subscribe" }));
3383
- });
3469
+ async function connectFollowStream() {
3470
+ const wsUrl = `ws://localhost:${port}/ws`;
3471
+ info(`Connecting to ${DIM}${wsUrl}${RESET}...`);
3384
3472
 
3385
- ws.on("message", (raw) => {
3473
+ const { WebSocket: WS } = await import("ws");
3386
3474
  try {
3387
- const msg = JSON.parse(raw.toString());
3475
+ followWs = new WS(wsUrl);
3476
+ } catch (e) {
3477
+ error(`WebSocket error: ${e.message}`);
3478
+ scheduleFollowReconnect();
3479
+ return;
3480
+ }
3388
3481
 
3389
- if (msg.type === "log:subscribed") {
3390
- return; // 구독 확인 무시
3391
- }
3482
+ followWs.on("open", () => {
3483
+ followFailures = 0; // 성공 실패 카운터 리셋
3484
+ success("Connected — streaming logs...\n");
3485
+ followWs.send(JSON.stringify({ type: "log:subscribe" }));
3486
+ });
3392
3487
 
3393
- if (msg.type === "log:entry") {
3394
- const ts = msg.timestamp?.slice(11, 19) || "";
3395
- const lvl = msg.level === "error" ? `${RED}ERR${RESET}`
3396
- : msg.level === "warn" ? `${YELLOW}WRN${RESET}`
3397
- : `${DIM}INF${RESET}`;
3398
- const src = msg.source ? ` ${DIM}[${msg.source}]${RESET}` : "";
3399
- log(` ${DIM}${ts}${RESET} ${lvl}${src} ${msg.message}`);
3488
+ followWs.on("message", (raw) => {
3489
+ try {
3490
+ const msg = JSON.parse(raw.toString());
3491
+
3492
+ if (msg.type === "log:subscribed") {
3493
+ return; // 구독 확인 무시
3494
+ }
3495
+
3496
+ if (msg.type === "log:entry") {
3497
+ const ts = msg.timestamp?.slice(11, 19) || "";
3498
+ const lvl = msg.level === "error" ? `${RED}ERR${RESET}`
3499
+ : msg.level === "warn" ? `${YELLOW}WRN${RESET}`
3500
+ : `${DIM}INF${RESET}`;
3501
+ const src = msg.source ? ` ${DIM}[${msg.source}]${RESET}` : "";
3502
+ log(` ${DIM}${ts}${RESET} ${lvl}${src} ${msg.message}`);
3503
+ }
3504
+ } catch { /* ignore non-JSON */ }
3505
+ });
3506
+
3507
+ followWs.on("error", () => {
3508
+ // 에러는 close 이벤트로 처리
3509
+ });
3510
+
3511
+ followWs.on("close", () => {
3512
+ followFailures++;
3513
+ if (followFailures > FOLLOW_MAX_FAILURES) {
3514
+ error(`연결 불가 — 앱 상태를 확인하세요.`);
3515
+ info(`서버가 실행 중인지 확인: gencow dev`);
3516
+ process.exit(1);
3400
3517
  }
3401
- } catch { /* ignore non-JSON */ }
3402
- });
3518
+ const delay = Math.min(FOLLOW_BASE_RECONNECT_MS * Math.pow(2, followFailures - 1), FOLLOW_MAX_RECONNECT_MS);
3519
+ const delaySec = (delay / 1000).toFixed(0);
3520
+ warn(`연결 끊김 — ${delaySec}초 후 재연결... (${followFailures}/${FOLLOW_MAX_FAILURES})`);
3521
+ scheduleFollowReconnect(delay);
3522
+ });
3523
+ }
3403
3524
 
3404
- ws.on("error", (e) => {
3405
- error(`WebSocket error: ${e.message}`);
3406
- info(`Make sure the server is running (gencow dev)`);
3407
- process.exit(1);
3408
- });
3525
+ function scheduleFollowReconnect(delayMs = FOLLOW_BASE_RECONNECT_MS) {
3526
+ if (followTimer) return;
3527
+ followTimer = setTimeout(() => {
3528
+ followTimer = null;
3529
+ connectFollowStream().catch(() => { });
3530
+ }, delayMs);
3531
+ }
3409
3532
 
3410
- ws.on("close", () => {
3411
- log(`\n${DIM} Connection closed.${RESET}\n`);
3412
- process.exit(0);
3413
- });
3533
+ connectFollowStream().catch(() => { });
3414
3534
 
3415
3535
  // Ctrl+C 처리
3416
3536
  process.on("SIGINT", () => {
3417
3537
  log(`\n\n${DIM} Stopped streaming.${RESET}\n`);
3418
- ws.close();
3538
+ if (followWs) followWs.close();
3539
+ if (followTimer) clearTimeout(followTimer);
3419
3540
  process.exit(0);
3420
3541
  });
3421
3542
  },
@@ -3838,7 +3959,7 @@ process.exit(0);
3838
3959
 
3839
3960
  // 지수 백오프 재연결 상수
3840
3961
  const BASE_RECONNECT_MS = 3000;
3841
- const MAX_RECONNECT_MS = 60000;
3962
+ const MAX_RECONNECT_MS = 30000; // 30s cap (기존 60s → 30s로 축소)
3842
3963
  const MAX_RECONNECT_FAILURES = 5;
3843
3964
  let reconnectFailures = 0;
3844
3965
 
package/core/index.js CHANGED
@@ -1880,131 +1880,188 @@ function ownerRls(userIdColumn, options) {
1880
1880
  // ../core/src/rls-db.ts
1881
1881
  import { sql as sql2 } from "drizzle-orm";
1882
1882
  function createRlsDb(db, userId) {
1883
- return {
1884
- ...db,
1885
- transaction: (async (callback, ...rest) => {
1886
- return await db.transaction(async (tx) => {
1887
- await tx.execute(
1888
- sql2`SELECT set_config('app.current_user_id', ${userId}, true)`
1889
- );
1890
- return await callback(tx);
1891
- }, ...rest);
1892
- })
1893
- };
1883
+ return new Proxy(db, {
1884
+ get(target, prop, receiver) {
1885
+ if (prop === "transaction") {
1886
+ return async (callback, ...rest) => {
1887
+ return await target.transaction(async (tx) => {
1888
+ await tx.execute(
1889
+ sql2`SELECT set_config('app.current_user_id', ${userId}, true)`
1890
+ );
1891
+ return await callback(tx);
1892
+ }, ...rest);
1893
+ };
1894
+ }
1895
+ const value = Reflect.get(target, prop, receiver);
1896
+ return typeof value === "function" ? value.bind(target) : value;
1897
+ }
1898
+ });
1894
1899
  }
1895
1900
 
1896
1901
  // ../core/src/crud.ts
1897
- import { eq, or, and, ilike, desc, asc, inArray, count, sql as sql3 } from "drizzle-orm";
1898
- function gencowCrud(db) {
1899
- return function createCrud(table, options) {
1900
- const anyTable = table;
1901
- const pk = anyTable["id"];
1902
- if (!pk) {
1903
- throw new Error(`[gencowCrud] Table ${anyTable["_"]["name"]} must have an 'id' column.`);
1902
+ import { eq, desc, asc, ilike, or, and, count as drizzleCount, getTableName } from "drizzle-orm";
1903
+ function detectIdType(column) {
1904
+ const colType = column.dataType;
1905
+ if (colType === "string") return v.string();
1906
+ return v.number();
1907
+ }
1908
+ function crud(table, options) {
1909
+ const anyTable = table;
1910
+ const tableName = getTableName(table);
1911
+ const prefix = options?.prefix || tableName;
1912
+ const isPublic = options?.public ?? false;
1913
+ const useRealtime = options?.realtime ?? true;
1914
+ const defaultLimit = options?.defaultLimit ?? 20;
1915
+ const maxLimit = options?.maxLimit ?? 100;
1916
+ const pk = anyTable["id"];
1917
+ if (!pk) {
1918
+ throw new Error(`[crud] Table "${tableName}" must have an 'id' column.`);
1919
+ }
1920
+ const idValidator = detectIdType(pk);
1921
+ const createdAtCol = anyTable["createdAt"];
1922
+ const defaultOrderCol = createdAtCol || pk;
1923
+ const userIdCol = anyTable["userId"];
1924
+ function buildWhereConditions(args) {
1925
+ const conditions = [];
1926
+ if (options?.softDelete) {
1927
+ const sdField = anyTable[options.softDelete.field];
1928
+ conditions.push(eq(sdField, null));
1904
1929
  }
1905
- async function create(data) {
1906
- let insertData = { ...data };
1907
- if (options?.hooks?.beforeCreate) {
1908
- insertData = await options.hooks.beforeCreate(insertData);
1909
- }
1910
- const [result] = await db.insert(anyTable).values(insertData).returning();
1911
- return result;
1930
+ if (args?.search && options?.searchFields?.length) {
1931
+ const searchConds = options.searchFields.map(
1932
+ (f) => ilike(anyTable[f], `%${args.search}%`)
1933
+ );
1934
+ conditions.push(or(...searchConds));
1912
1935
  }
1913
- async function findById(id) {
1914
- let whereCond = eq(pk, id);
1915
- if (options?.softDelete) {
1916
- const sdField = anyTable[options.softDelete.field];
1917
- whereCond = and(whereCond, sql3`${sdField} IS NULL`);
1936
+ if (args?.filters && options?.allowedFilters?.length) {
1937
+ for (const [key, value] of Object.entries(args.filters)) {
1938
+ if (options.allowedFilters.includes(key) && anyTable[key]) {
1939
+ conditions.push(eq(anyTable[key], value));
1940
+ }
1918
1941
  }
1919
- const [result] = await db.select().from(anyTable).where(whereCond).limit(1);
1920
- return result || null;
1921
1942
  }
1922
- async function list(params) {
1923
- const page = Math.max(1, params?.page || 1);
1924
- const limit = Math.min(
1925
- Math.max(1, params?.limit || options?.defaultLimit || 20),
1926
- options?.maxLimit || 100
1927
- );
1943
+ return conditions.length > 0 ? and(...conditions) : void 0;
1944
+ }
1945
+ async function fetchListWithTotal(db, whereClause) {
1946
+ const [data, countResult] = await Promise.all([
1947
+ db.select().from(anyTable).where(whereClause).orderBy(desc(defaultOrderCol)),
1948
+ db.select({ count: drizzleCount() }).from(anyTable).where(whereClause)
1949
+ ]);
1950
+ return { data, total: Number(countResult[0]?.count ?? 0) };
1951
+ }
1952
+ const listDef = query(`${prefix}.list`, {
1953
+ public: isPublic,
1954
+ args: {
1955
+ page: v.optional(v.number()),
1956
+ limit: v.optional(v.number()),
1957
+ search: v.optional(v.string()),
1958
+ orderBy: v.optional(v.string()),
1959
+ orderDir: v.optional(v.string()),
1960
+ filters: v.optional(v.any())
1961
+ },
1962
+ handler: async (ctx, args) => {
1963
+ if (!isPublic) ctx.auth.requireAuth();
1964
+ const page = Math.max(1, args?.page || 1);
1965
+ const limit = Math.min(Math.max(1, args?.limit || defaultLimit), maxLimit);
1928
1966
  const offset = (page - 1) * limit;
1929
- const conditions = [];
1930
- if (options?.softDelete && !params?.includeDeleted) {
1931
- conditions.push(sql3`${anyTable[options.softDelete.field]} IS NULL`);
1932
- }
1933
- if (params?.search && options?.searchFields?.length) {
1934
- const searchConds = options.searchFields.map(
1935
- (f) => ilike(anyTable[f], `%${params.search}%`)
1936
- );
1937
- conditions.push(or(...searchConds));
1938
- }
1939
- if (params?.filters && options?.allowedFilters) {
1940
- for (const [k, v2] of Object.entries(params.filters)) {
1941
- if (options.allowedFilters.includes(k)) {
1942
- conditions.push(eq(anyTable[k], v2));
1943
- }
1944
- }
1967
+ const whereClause = buildWhereConditions(args);
1968
+ let orderByClause;
1969
+ if (args?.orderBy && anyTable[args.orderBy]) {
1970
+ const col = anyTable[args.orderBy];
1971
+ orderByClause = args.orderDir === "asc" ? asc(col) : desc(col);
1972
+ } else {
1973
+ orderByClause = desc(defaultOrderCol);
1945
1974
  }
1946
- const whereClause = conditions.length > 0 ? and(...conditions) : void 0;
1947
- const orderByArgs = (params?.orderBy || []).map((o) => {
1948
- const col = anyTable[o.field];
1949
- return o.direction === "desc" ? desc(col) : asc(col);
1950
- });
1951
- const [{ count: total }] = await db.select({ count: count() }).from(anyTable).where(whereClause);
1952
- const results = await db.select().from(anyTable).where(whereClause).orderBy(...orderByArgs).limit(limit).offset(offset);
1975
+ const [results, countResult] = await Promise.all([
1976
+ ctx.db.select().from(anyTable).where(whereClause).orderBy(orderByClause).limit(limit).offset(offset),
1977
+ ctx.db.select({ count: drizzleCount() }).from(anyTable).where(whereClause)
1978
+ ]);
1953
1979
  return {
1954
- results,
1955
- page,
1956
- limit,
1957
- total: Number(total)
1980
+ data: results,
1981
+ total: Number(countResult[0]?.count ?? 0)
1958
1982
  };
1959
1983
  }
1960
- async function update(id, data) {
1961
- let updateData = { ...data };
1962
- if (options?.hooks?.beforeUpdate) {
1963
- updateData = await options.hooks.beforeUpdate(updateData);
1964
- }
1965
- const [result] = await db.update(anyTable).set(updateData).where(eq(pk, id)).returning();
1966
- return result;
1967
- }
1968
- async function deleteOne(id) {
1984
+ });
1985
+ const getDef = query(`${prefix}.get`, {
1986
+ public: isPublic,
1987
+ args: { id: idValidator },
1988
+ handler: async (ctx, args) => {
1989
+ if (!isPublic) ctx.auth.requireAuth();
1990
+ let whereCond = eq(pk, args.id);
1969
1991
  if (options?.softDelete) {
1970
- const sdField = options.softDelete.field;
1971
- await db.update(anyTable).set({ [sdField]: /* @__PURE__ */ new Date() }).where(eq(pk, id));
1972
- } else {
1973
- await db.delete(anyTable).where(eq(pk, id));
1992
+ const sdField = anyTable[options.softDelete.field];
1993
+ whereCond = and(whereCond, eq(sdField, null));
1974
1994
  }
1995
+ const [result] = await ctx.db.select().from(anyTable).where(whereCond).limit(1);
1996
+ return result ?? null;
1975
1997
  }
1976
- async function restore(id) {
1977
- if (options?.softDelete) {
1978
- const sdField = options.softDelete.field;
1979
- await db.update(anyTable).set({ [sdField]: null }).where(eq(pk, id));
1998
+ });
1999
+ const createDef = mutation(`${prefix}.create`, {
2000
+ public: isPublic,
2001
+ invalidates: [],
2002
+ handler: async (ctx, args) => {
2003
+ const user = isPublic ? null : ctx.auth.requireAuth();
2004
+ let insertData = { ...args };
2005
+ if (userIdCol && user && !insertData.userId) {
2006
+ insertData.userId = user.id;
1980
2007
  }
1981
- }
1982
- async function bulkCreate(dataArray) {
1983
- let insertData = [...dataArray];
1984
2008
  if (options?.hooks?.beforeCreate) {
1985
- insertData = await Promise.all(insertData.map((d) => options.hooks.beforeCreate(d)));
2009
+ insertData = await options.hooks.beforeCreate(insertData);
2010
+ }
2011
+ const [result] = await ctx.db.insert(anyTable).values(insertData).returning();
2012
+ if (useRealtime) {
2013
+ const listResult = await fetchListWithTotal(ctx.db);
2014
+ ctx.realtime.emit(`${prefix}.list`, listResult);
1986
2015
  }
1987
- return await db.insert(anyTable).values(insertData).returning();
2016
+ return result;
2017
+ }
2018
+ });
2019
+ const updateDef = mutation(`${prefix}.update`, {
2020
+ public: isPublic,
2021
+ invalidates: [],
2022
+ handler: async (ctx, args) => {
2023
+ if (!isPublic) ctx.auth.requireAuth();
2024
+ const { id, ...updates } = args;
2025
+ let updateData = { ...updates };
2026
+ if (anyTable["updatedAt"]) {
2027
+ updateData.updatedAt = /* @__PURE__ */ new Date();
2028
+ }
2029
+ if (options?.hooks?.beforeUpdate) {
2030
+ updateData = await options.hooks.beforeUpdate(updateData);
2031
+ }
2032
+ const [result] = await ctx.db.update(anyTable).set(updateData).where(eq(pk, id)).returning();
2033
+ if (useRealtime) {
2034
+ const listResult = await fetchListWithTotal(ctx.db);
2035
+ ctx.realtime.emit(`${prefix}.list`, listResult);
2036
+ ctx.realtime.emit(`${prefix}.get`, result);
2037
+ }
2038
+ return result;
1988
2039
  }
1989
- async function bulkDelete(ids) {
1990
- if (ids.length === 0) return;
2040
+ });
2041
+ const removeDef = mutation(`${prefix}.remove`, {
2042
+ public: isPublic,
2043
+ invalidates: [],
2044
+ handler: async (ctx, args) => {
2045
+ if (!isPublic) ctx.auth.requireAuth();
1991
2046
  if (options?.softDelete) {
1992
2047
  const sdField = options.softDelete.field;
1993
- await db.update(anyTable).set({ [sdField]: /* @__PURE__ */ new Date() }).where(inArray(pk, ids));
2048
+ await ctx.db.update(anyTable).set({ [sdField]: /* @__PURE__ */ new Date() }).where(eq(pk, args.id));
1994
2049
  } else {
1995
- await db.delete(anyTable).where(inArray(pk, ids));
2050
+ await ctx.db.delete(anyTable).where(eq(pk, args.id));
2051
+ }
2052
+ if (useRealtime) {
2053
+ const listResult = await fetchListWithTotal(ctx.db);
2054
+ ctx.realtime.emit(`${prefix}.list`, listResult);
1996
2055
  }
2056
+ return { success: true };
1997
2057
  }
1998
- return {
1999
- create,
2000
- findById,
2001
- list,
2002
- update,
2003
- deleteOne,
2004
- restore,
2005
- bulkCreate,
2006
- bulkDelete
2007
- };
2058
+ });
2059
+ return {
2060
+ list: listDef,
2061
+ get: getDef,
2062
+ create: createDef,
2063
+ update: updateDef,
2064
+ remove: removeDef
2008
2065
  };
2009
2066
  }
2010
2067
  export {
@@ -2013,9 +2070,10 @@ export {
2013
2070
  createRlsDb,
2014
2071
  createScheduler,
2015
2072
  cronJobs,
2073
+ crud,
2016
2074
  defineAuth,
2017
2075
  deregisterClient,
2018
- gencowCrud,
2076
+ crud as gencowCrud,
2019
2077
  getQueryDef,
2020
2078
  getQueryHandler,
2021
2079
  getRegisteredHttpActions,