gencow 0.1.119 → 0.1.121
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 +87 -25
- package/core/index.js +62 -20
- package/lib/__tests__/readme-codegen.test.ts +2 -1
- package/lib/readme-codegen.mjs +24 -14
- package/package.json +31 -32
- package/server/index.js +82 -27
- package/server/index.js.map +2 -2
- package/dashboard/apple-touch-icon.png +0 -0
- package/dashboard/assets/index-BYHvURCl.css +0 -1
- package/dashboard/assets/index-Bur5ZNpv.js +0 -372
- package/dashboard/favicon-16.png +0 -0
- package/dashboard/favicon-192.png +0 -0
- package/dashboard/favicon-32.png +0 -0
- package/dashboard/favicon-512.png +0 -0
- package/dashboard/favicon.ico +0 -0
- package/dashboard/favicon.svg +0 -1
- package/dashboard/file.svg +0 -1
- package/dashboard/globe.svg +0 -1
- package/dashboard/index.html +0 -23
- package/dashboard/next.svg +0 -1
- package/dashboard/vercel.svg +0 -1
- package/dashboard/window.svg +0 -1
package/bin/gencow.mjs
CHANGED
|
@@ -303,7 +303,8 @@ function _ensureEsbuildConsistency() {
|
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
/**
|
|
306
|
-
* 모노레포 루트의 .
|
|
306
|
+
* 모노레포 루트의 .pnpmrc에 esbuild hoisting 차단 규칙을 추가.
|
|
307
|
+
* (.npmrc가 아닌 .pnpmrc — hoist-pattern은 pnpm 전용이므로 npm 경고 방지)
|
|
307
308
|
* 이미 존재하면 스킵.
|
|
308
309
|
*/
|
|
309
310
|
function _patchNpmrcForEsbuild(cwd) {
|
|
@@ -312,8 +313,9 @@ function _patchNpmrcForEsbuild(cwd) {
|
|
|
312
313
|
let dir = cwd;
|
|
313
314
|
for (let i = 0; i < 10; i++) {
|
|
314
315
|
if (existsSync(resolve(dir, "pnpm-workspace.yaml"))) {
|
|
315
|
-
|
|
316
|
-
const
|
|
316
|
+
// .pnpmrc 대상 (pnpm 전용 설정 → npm "Unknown project config" 경고 방지)
|
|
317
|
+
const pnpmrcPath = resolve(dir, ".pnpmrc");
|
|
318
|
+
const content = existsSync(pnpmrcPath) ? readFileSync(pnpmrcPath, "utf8") : "";
|
|
317
319
|
if (!content.includes("hoist-pattern[]=!@esbuild/*")) {
|
|
318
320
|
const patch = [
|
|
319
321
|
"",
|
|
@@ -323,8 +325,8 @@ function _patchNpmrcForEsbuild(cwd) {
|
|
|
323
325
|
"hoist-pattern[]=!esbuild",
|
|
324
326
|
"",
|
|
325
327
|
].join("\n");
|
|
326
|
-
appendFileSync(
|
|
327
|
-
info(".
|
|
328
|
+
appendFileSync(pnpmrcPath, patch);
|
|
329
|
+
info(".pnpmrc에 esbuild hoisting 차단 규칙을 추가했습니다.");
|
|
328
330
|
info("다음 pnpm install 시 영구 적용됩니다.");
|
|
329
331
|
}
|
|
330
332
|
return;
|
|
@@ -333,7 +335,7 @@ function _patchNpmrcForEsbuild(cwd) {
|
|
|
333
335
|
if (parent === dir) break;
|
|
334
336
|
dir = parent;
|
|
335
337
|
}
|
|
336
|
-
} catch { /* .
|
|
338
|
+
} catch { /* .pnpmrc 수정 실패 — 무시 (즉시 우회로 이번 실행은 문제 없음) */ }
|
|
337
339
|
}
|
|
338
340
|
|
|
339
341
|
function findServerRoot() {
|
|
@@ -1197,7 +1199,7 @@ ${hasPrompt ? `
|
|
|
1197
1199
|
|
|
1198
1200
|
if (!appId) {
|
|
1199
1201
|
error("Cloud app not found — gencow.json이 없거나 appId가 설정되지 않았습니다.");
|
|
1200
|
-
info(`${DIM}먼저 gencow
|
|
1202
|
+
info(`${DIM}먼저 gencow dev 를 실행하세요.${RESET}`);
|
|
1201
1203
|
info(`${DIM}로컬 서버에 seed하려면: gencow db:seed --local${RESET}`);
|
|
1202
1204
|
log("");
|
|
1203
1205
|
return;
|
|
@@ -1218,7 +1220,7 @@ ${hasPrompt ? `
|
|
|
1218
1220
|
if (!statusRes || !statusRes.ok) {
|
|
1219
1221
|
error(`Cloud app "${appId}" is not running`);
|
|
1220
1222
|
info(`${DIM}Dashboard → Apps → ${appId} → Resume 으로 앱을 시작하세요.${RESET}`);
|
|
1221
|
-
info(`${DIM}또는: gencow
|
|
1223
|
+
info(`${DIM}또는: gencow dev 로 앱 생성 후 시도하세요.${RESET}`);
|
|
1222
1224
|
log("");
|
|
1223
1225
|
return;
|
|
1224
1226
|
}
|
|
@@ -1236,7 +1238,7 @@ ${hasPrompt ? `
|
|
|
1236
1238
|
log(` ${DIM}export default async function seed(ctx) {${RESET}`);
|
|
1237
1239
|
log(` ${DIM} await ctx.db.insert(tasks).values([...]);${RESET}`);
|
|
1238
1240
|
log(` ${DIM}};${RESET}\n`);
|
|
1239
|
-
info(`${DIM}After creating seed.ts: gencow
|
|
1241
|
+
info(`${DIM}After creating seed.ts: gencow dev 실행 후 gencow db:seed${RESET}`);
|
|
1240
1242
|
} else if (!res.ok) {
|
|
1241
1243
|
error(`Cloud seed failed: ${data.error || "Unknown error"}`);
|
|
1242
1244
|
} else {
|
|
@@ -1555,7 +1557,7 @@ ${hasPrompt ? `
|
|
|
1555
1557
|
|
|
1556
1558
|
if (!appId) {
|
|
1557
1559
|
error("Cloud app not found — gencow.json이 없거나 appId가 설정되지 않았습니다.");
|
|
1558
|
-
info(`${DIM}먼저 gencow
|
|
1560
|
+
info(`${DIM}먼저 gencow dev 를 실행하세요.${RESET}`);
|
|
1559
1561
|
info(`${DIM}로컬 DB에 push하려면: gencow db:push --local${RESET}`);
|
|
1560
1562
|
log("");
|
|
1561
1563
|
return;
|
|
@@ -1564,7 +1566,7 @@ ${hasPrompt ? `
|
|
|
1564
1566
|
// ── Cloud 모드: Platform API를 통해 Cloud DB에 schema push ──
|
|
1565
1567
|
const creds = requireCreds();
|
|
1566
1568
|
log(`\n${BOLD}${CYAN}Gencow DB Push${RESET} ${DIM}(cloud: ${appId})${RESET}\n`);
|
|
1567
|
-
warn("명령어 실행 전 먼저 gencow
|
|
1569
|
+
warn("명령어 실행 전 먼저 gencow dev 를 통해 최신 코드가 배포되어 있어야 합니다.");
|
|
1568
1570
|
log("");
|
|
1569
1571
|
info("Pushing schema.ts → cloud database...");
|
|
1570
1572
|
|
|
@@ -1805,10 +1807,11 @@ ${BOLD}BaaS commands (login required):${RESET}
|
|
|
1805
1807
|
${GREEN}login${RESET} Login to Gencow Platform ${DIM}(browser → token)${RESET}
|
|
1806
1808
|
${GREEN}logout${RESET} Clear credentials
|
|
1807
1809
|
${GREEN}whoami${RESET} Show current user info
|
|
1808
|
-
${GREEN}
|
|
1809
|
-
${DIM}--
|
|
1810
|
-
${DIM}--
|
|
1811
|
-
|
|
1810
|
+
${GREEN}static [dir]${RESET} Deploy static files to dev ${DIM}(dist/, out/, build/)${RESET}
|
|
1811
|
+
${DIM}--no-backend Skip backend auto-deploy${RESET}
|
|
1812
|
+
${DIM}--force, -f Skip dependency audit${RESET}
|
|
1813
|
+
${GREEN}deploy${RESET} Deploy to production ${DIM}(Pro+ only)${RESET}
|
|
1814
|
+
${DIM}--static [dir] Deploy static files to production${RESET}
|
|
1812
1815
|
${DIM}--rollback Rollback to previous deployment${RESET}
|
|
1813
1816
|
${DIM}--force, -f Skip dependency audit${RESET}
|
|
1814
1817
|
${GREEN}env list${RESET} List cloud env vars ${DIM}(--prod for production)${RESET}
|
|
@@ -1985,14 +1988,55 @@ ${BOLD}Examples:${RESET}
|
|
|
1985
1988
|
}
|
|
1986
1989
|
},
|
|
1987
1990
|
|
|
1988
|
-
// ──
|
|
1991
|
+
// ── static — dev 정적 파일 배포 (독립 명령어) ─────────
|
|
1992
|
+
async static(...staticArgs) {
|
|
1993
|
+
const creds = requireCreds();
|
|
1994
|
+
|
|
1995
|
+
let staticDir = null;
|
|
1996
|
+
let forceDeploy = false;
|
|
1997
|
+
let noBackend = false;
|
|
1998
|
+
let appId = null;
|
|
1999
|
+
|
|
2000
|
+
for (let i = 0; i < staticArgs.length; i++) {
|
|
2001
|
+
const a = staticArgs[i];
|
|
2002
|
+
if (a === "--force" || a === "-f") forceDeploy = true;
|
|
2003
|
+
else if (a === "--no-backend") noBackend = true;
|
|
2004
|
+
else if (a === "--app" || a === "-a") appId = staticArgs[++i];
|
|
2005
|
+
else if (!a.startsWith("-")) {
|
|
2006
|
+
staticDir = a;
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
// gencow.json에서 appId 로드
|
|
2011
|
+
const gencowJsonPath = resolve(process.cwd(), "gencow.json");
|
|
2012
|
+
if (!appId && existsSync(gencowJsonPath)) {
|
|
2013
|
+
const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
|
|
2014
|
+
appId = gencowJson.appId || gencowJson.appName;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
// displayName
|
|
2018
|
+
let displayName = null;
|
|
2019
|
+
const configPath = resolve(process.cwd(), "gencow.config.ts");
|
|
2020
|
+
if (existsSync(configPath)) {
|
|
2021
|
+
const content = readFileSync(configPath, "utf8");
|
|
2022
|
+
const match = content.match(/name:\s*["']([^"']+)["']/);
|
|
2023
|
+
if (match) displayName = match[1];
|
|
2024
|
+
}
|
|
2025
|
+
if (!displayName) displayName = basename(process.cwd());
|
|
2026
|
+
|
|
2027
|
+
return await this._deployStatic(creds, appId, displayName, staticDir, gencowJsonPath, {
|
|
2028
|
+
forceDeploy, noBackend, envTarget: "dev",
|
|
2029
|
+
});
|
|
2030
|
+
},
|
|
2031
|
+
|
|
2032
|
+
// ── deploy — 프로덕션 배포 (Pro+ only) ───────────────
|
|
1989
2033
|
async deploy(...deployArgs) {
|
|
1990
2034
|
const creds = requireCreds();
|
|
1991
2035
|
|
|
1992
2036
|
// gencow.json에서 앱 ID 확인 (자동 생성된 유니크 ID)
|
|
1993
2037
|
let appId = null;
|
|
1994
2038
|
let displayName = null;
|
|
1995
|
-
let envTarget = "
|
|
2039
|
+
let envTarget = "prod"; // v0.1.120: deploy = prod 기본값
|
|
1996
2040
|
let staticDir = null; // --static 옵션
|
|
1997
2041
|
let isStatic = false;
|
|
1998
2042
|
let forceDeploy = false; // --force: skip dependency audit
|
|
@@ -2061,7 +2105,12 @@ ${BOLD}Examples:${RESET}
|
|
|
2061
2105
|
|
|
2062
2106
|
for (let i = 0; i < deployArgs.length; i++) {
|
|
2063
2107
|
const a = deployArgs[i];
|
|
2064
|
-
if (a === "--prod")
|
|
2108
|
+
if (a === "--prod") {
|
|
2109
|
+
// deprecated: deploy는 이미 prod 기본값
|
|
2110
|
+
warn(`${YELLOW}--prod 플래그는 더 이상 필요하지 않습니다.${RESET}`);
|
|
2111
|
+
info(`gencow deploy는 기본적으로 프로덕션에 배포합니다.`);
|
|
2112
|
+
envTarget = "prod";
|
|
2113
|
+
}
|
|
2065
2114
|
else if (a === "--force" || a === "-f") forceDeploy = true;
|
|
2066
2115
|
else if (a === "--no-backend") noBackend = true;
|
|
2067
2116
|
else if (a === "--rollback") isRollback = true;
|
|
@@ -2080,13 +2129,16 @@ ${BOLD}Examples:${RESET}
|
|
|
2080
2129
|
error(`알 수 없는 deploy 인자: "${a}"`);
|
|
2081
2130
|
log("");
|
|
2082
2131
|
info(`사용법: gencow deploy [옵션]`);
|
|
2083
|
-
info(` gencow deploy
|
|
2084
|
-
info(` gencow deploy --static 정적
|
|
2132
|
+
info(` gencow deploy 프로덕션 배포 (Pro+)`);
|
|
2133
|
+
info(` gencow deploy --static 프로덕션 정적 배포 (Pro+)`);
|
|
2085
2134
|
info(` gencow deploy --rollback 이전 버전으로 롤백`);
|
|
2086
|
-
info(` gencow deploy --prod 프로덕션 배포`);
|
|
2087
2135
|
info(` gencow deploy logs 서버 로그 조회`);
|
|
2088
2136
|
info(` gencow deploy status 앱 상태 확인`);
|
|
2089
2137
|
info(` gencow deploy --force 의존성 감사 스킵`);
|
|
2138
|
+
log("");
|
|
2139
|
+
info(`💡 개발 환경 배포:`);
|
|
2140
|
+
info(` gencow dev 백엔드 실시간 배포`);
|
|
2141
|
+
info(` gencow static [dir] 정적 파일 배포`);
|
|
2090
2142
|
process.exit(1);
|
|
2091
2143
|
}
|
|
2092
2144
|
}
|
|
@@ -2199,8 +2251,18 @@ ${BOLD}Examples:${RESET}
|
|
|
2199
2251
|
if (!createProdRes.ok) {
|
|
2200
2252
|
const errData = await createProdRes.json().catch(() => ({}));
|
|
2201
2253
|
if (createProdRes.status === 403) {
|
|
2202
|
-
|
|
2203
|
-
|
|
2254
|
+
log("");
|
|
2255
|
+
log(` ${RED}⛔ Production Deploy — Pro 플랜 이상 필요${RESET}`);
|
|
2256
|
+
log("");
|
|
2257
|
+
log(` ${DIM}gencow deploy는 프로덕션 환경에 배포하는 명령어입니다.${RESET}`);
|
|
2258
|
+
log("");
|
|
2259
|
+
log(` ${BOLD}💡 개발 환경 배포:${RESET}`);
|
|
2260
|
+
log(` ${GREEN}gencow dev${RESET} ${DIM}← 백엔드 실시간 배포 + 라이브 로그${RESET}`);
|
|
2261
|
+
log(` ${GREEN}gencow static dist/${RESET} ${DIM}← 정적 파일(dist/) 배포${RESET}`);
|
|
2262
|
+
log("");
|
|
2263
|
+
log(` ${BOLD}🚀 프로덕션 배포 잠금 해제:${RESET}`);
|
|
2264
|
+
log(` ${GREEN}gencow upgrade${RESET} ${DIM}← Pro 플랜으로 업그레이드${RESET}`);
|
|
2265
|
+
log("");
|
|
2204
2266
|
} else {
|
|
2205
2267
|
error(`Prod 앱 생성 실패: ${errData.error || createProdRes.statusText}`);
|
|
2206
2268
|
}
|
|
@@ -2635,7 +2697,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2635
2697
|
if (!targetDir || !existsSync(resolve(process.cwd(), targetDir))) {
|
|
2636
2698
|
error(`정적 파일 폴더를 찾을 수 없습니다.`);
|
|
2637
2699
|
info(`자동 감지 순서: ${AUTO_DETECT.join(", ")}`);
|
|
2638
|
-
info(`수동 지정: gencow
|
|
2700
|
+
info(`수동 지정: gencow static <dir>`);
|
|
2639
2701
|
process.exit(1);
|
|
2640
2702
|
}
|
|
2641
2703
|
|
|
@@ -3069,7 +3131,7 @@ ${BOLD}Examples:${RESET}
|
|
|
3069
3131
|
|
|
3070
3132
|
// --prod인데 prod 앱이 없는 경우
|
|
3071
3133
|
if (envTarget === "prod" && appId && !appId.endsWith("-prod")) {
|
|
3072
|
-
error("Prod 앱이 아직 없습니다. gencow deploy
|
|
3134
|
+
error("Prod 앱이 아직 없습니다. gencow deploy를 먼저 실행하세요.");
|
|
3073
3135
|
return;
|
|
3074
3136
|
}
|
|
3075
3137
|
|
package/core/index.js
CHANGED
|
@@ -1928,7 +1928,7 @@ function ownerRls(userIdColumn, options) {
|
|
|
1928
1928
|
"[ownerRls] userIdColumn must have a .name property. Ensure you pass a valid Drizzle column reference (e.g. t.userId)."
|
|
1929
1929
|
);
|
|
1930
1930
|
}
|
|
1931
|
-
const isOwner = sql`${userIdColumn} = current_setting('app.current_user_id')`;
|
|
1931
|
+
const isOwner = sql`${userIdColumn} = current_setting('app.current_user_id', true)`;
|
|
1932
1932
|
const meta = {
|
|
1933
1933
|
columnName: colName,
|
|
1934
1934
|
readPublic: options?.read === "public"
|
|
@@ -1966,6 +1966,10 @@ function createRlsDb(db, userId) {
|
|
|
1966
1966
|
// ../core/src/crud.ts
|
|
1967
1967
|
import { eq, ne, gt, gte, lt, lte, desc, asc, like, ilike, inArray, notInArray, or, and, count as drizzleCount, getTableName, getTableColumns } from "drizzle-orm";
|
|
1968
1968
|
import { getTableConfig } from "drizzle-orm/pg-core";
|
|
1969
|
+
var _ownerRlsTables = [];
|
|
1970
|
+
function getOwnerRlsTables() {
|
|
1971
|
+
return _ownerRlsTables;
|
|
1972
|
+
}
|
|
1969
1973
|
function detectIdType(column) {
|
|
1970
1974
|
const colType = column.dataType;
|
|
1971
1975
|
if (colType === "string") return v.string();
|
|
@@ -2001,6 +2005,10 @@ function detectOwnerMeta(table) {
|
|
|
2001
2005
|
registerOwnerRls(table, { columnName: colName, readPublic: false });
|
|
2002
2006
|
return { column: userIdCol, columnName: colName, propertyName: propName, readPublic: false };
|
|
2003
2007
|
}
|
|
2008
|
+
const tblName = getTableName(table);
|
|
2009
|
+
console.warn(
|
|
2010
|
+
`[crud] \u26A0\uFE0F Table "${tblName}" has ${config.policies.length} pgPolicy but no userId/user_id column found. ownerRls auto-isolation will NOT be applied. If you used ownerRls(), ensure the column is named 'userId' (JS) / 'user_id' (DB).`
|
|
2011
|
+
);
|
|
2004
2012
|
}
|
|
2005
2013
|
} catch {
|
|
2006
2014
|
}
|
|
@@ -2088,6 +2096,13 @@ function crud(table, options) {
|
|
|
2088
2096
|
const defaultOrderCol = createdAtCol || pk;
|
|
2089
2097
|
const userIdCol = anyTable["userId"];
|
|
2090
2098
|
const ownerMeta = detectOwnerMeta(table);
|
|
2099
|
+
if (ownerMeta && !_ownerRlsTables.some((t) => t.tableName === tableName)) {
|
|
2100
|
+
_ownerRlsTables.push({
|
|
2101
|
+
tableName,
|
|
2102
|
+
columnName: ownerMeta.columnName,
|
|
2103
|
+
readPublic: ownerMeta.readPublic
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2091
2106
|
if (ownerMeta && isPublic && !ownerMeta.readPublic) {
|
|
2092
2107
|
console.warn(
|
|
2093
2108
|
`[crud] \u26A0\uFE0F Table "${tableName}": ownerRls detected but public=true. CUD operations will still enforce ownerRls (auth required). Consider removing { public: true } or using ownerRls(col, { read: "public" }).`
|
|
@@ -2116,15 +2131,23 @@ function crud(table, options) {
|
|
|
2116
2131
|
}
|
|
2117
2132
|
return conditions.length > 0 ? and(...conditions) : void 0;
|
|
2118
2133
|
}
|
|
2134
|
+
async function inRlsOrPlainTx(db, fn) {
|
|
2135
|
+
if (typeof db?.transaction === "function") {
|
|
2136
|
+
return await db.transaction(fn);
|
|
2137
|
+
}
|
|
2138
|
+
return await fn(db);
|
|
2139
|
+
}
|
|
2119
2140
|
async function fetchListWithTotal(db, whereClause, userId) {
|
|
2120
2141
|
let effectiveWhere = whereClause;
|
|
2121
2142
|
if (ownerMeta && userId && !ownerMeta.readPublic) {
|
|
2122
2143
|
const ownerFilter = eq(ownerMeta.column, userId);
|
|
2123
2144
|
effectiveWhere = effectiveWhere ? and(effectiveWhere, ownerFilter) : ownerFilter;
|
|
2124
2145
|
}
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2146
|
+
return await inRlsOrPlainTx(db, async (tx) => {
|
|
2147
|
+
const data = await tx.select().from(anyTable).where(effectiveWhere).orderBy(desc(defaultOrderCol));
|
|
2148
|
+
const countResult = await tx.select({ count: drizzleCount() }).from(anyTable).where(effectiveWhere);
|
|
2149
|
+
return { data, total: Number(countResult[0]?.count ?? 0) };
|
|
2150
|
+
});
|
|
2128
2151
|
}
|
|
2129
2152
|
const enabledMethods = new Set(options?.methods ?? ["list", "get", "create", "update", "remove"]);
|
|
2130
2153
|
const listDef = !enabledMethods.has("list") ? void 0 : query(`${prefix}.list`, {
|
|
@@ -2155,12 +2178,14 @@ function crud(table, options) {
|
|
|
2155
2178
|
} else {
|
|
2156
2179
|
orderByClause = desc(defaultOrderCol);
|
|
2157
2180
|
}
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2181
|
+
return await inRlsOrPlainTx(ctx.db, async (tx) => {
|
|
2182
|
+
const results = await tx.select().from(anyTable).where(whereClause).orderBy(orderByClause).limit(limit).offset(offset);
|
|
2183
|
+
const countResult = await tx.select({ count: drizzleCount() }).from(anyTable).where(whereClause);
|
|
2184
|
+
return {
|
|
2185
|
+
data: results,
|
|
2186
|
+
total: Number(countResult[0]?.count ?? 0)
|
|
2187
|
+
};
|
|
2188
|
+
});
|
|
2164
2189
|
}
|
|
2165
2190
|
});
|
|
2166
2191
|
const getDef = !enabledMethods.has("get") ? void 0 : query(`${prefix}.get`, {
|
|
@@ -2177,8 +2202,10 @@ function crud(table, options) {
|
|
|
2177
2202
|
const sdField = anyTable[options.softDelete.field];
|
|
2178
2203
|
whereCond = and(whereCond, eq(sdField, null));
|
|
2179
2204
|
}
|
|
2180
|
-
|
|
2181
|
-
|
|
2205
|
+
return await inRlsOrPlainTx(ctx.db, async (tx) => {
|
|
2206
|
+
const [result] = await tx.select().from(anyTable).where(whereCond).limit(1);
|
|
2207
|
+
return result ?? null;
|
|
2208
|
+
});
|
|
2182
2209
|
}
|
|
2183
2210
|
});
|
|
2184
2211
|
const createDef = !enabledMethods.has("create") ? void 0 : mutation(`${prefix}.create`, {
|
|
@@ -2186,6 +2213,12 @@ function crud(table, options) {
|
|
|
2186
2213
|
handler: async (ctx, args) => {
|
|
2187
2214
|
const user = ownerMeta || !isPublic ? ctx.auth.requireAuth() : null;
|
|
2188
2215
|
let insertData = { ...args };
|
|
2216
|
+
if (ownerMeta && user) {
|
|
2217
|
+
const requestedOwner = insertData[ownerMeta.propertyName] ?? insertData[ownerMeta.columnName] ?? insertData.userId;
|
|
2218
|
+
if (requestedOwner != null && requestedOwner !== user.id) {
|
|
2219
|
+
throw new Error("Forbidden: cannot create resource for another user");
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2189
2222
|
if (ownerMeta && user) {
|
|
2190
2223
|
insertData[ownerMeta.propertyName] = user.id;
|
|
2191
2224
|
} else if (userIdCol && user && !insertData.userId) {
|
|
@@ -2194,7 +2227,10 @@ function crud(table, options) {
|
|
|
2194
2227
|
if (options?.hooks?.beforeCreate) {
|
|
2195
2228
|
insertData = await options.hooks.beforeCreate(insertData);
|
|
2196
2229
|
}
|
|
2197
|
-
const [result] = await
|
|
2230
|
+
const [result] = await inRlsOrPlainTx(
|
|
2231
|
+
ctx.db,
|
|
2232
|
+
async (tx) => tx.insert(anyTable).values(insertData).returning()
|
|
2233
|
+
);
|
|
2198
2234
|
if (useRealtime && enabledMethods.has("list")) {
|
|
2199
2235
|
const currentUserId = user?.id;
|
|
2200
2236
|
const listResult = await fetchListWithTotal(ctx.db, void 0, currentUserId);
|
|
@@ -2223,7 +2259,10 @@ function crud(table, options) {
|
|
|
2223
2259
|
if (options?.hooks?.beforeUpdate) {
|
|
2224
2260
|
updateData = await options.hooks.beforeUpdate(updateData);
|
|
2225
2261
|
}
|
|
2226
|
-
const [result] = await
|
|
2262
|
+
const [result] = await inRlsOrPlainTx(
|
|
2263
|
+
ctx.db,
|
|
2264
|
+
async (tx) => tx.update(anyTable).set(updateData).where(updateWhere).returning()
|
|
2265
|
+
);
|
|
2227
2266
|
if (useRealtime) {
|
|
2228
2267
|
const currentUserId = ownerMeta ? user?.id : void 0;
|
|
2229
2268
|
if (enabledMethods.has("list")) {
|
|
@@ -2245,12 +2284,14 @@ function crud(table, options) {
|
|
|
2245
2284
|
if (ownerMeta && user) {
|
|
2246
2285
|
deleteWhere = and(eq(pk, args.id), eq(ownerMeta.column, user.id));
|
|
2247
2286
|
}
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2287
|
+
await inRlsOrPlainTx(ctx.db, async (tx) => {
|
|
2288
|
+
if (options?.softDelete) {
|
|
2289
|
+
const sdField = options.softDelete.field;
|
|
2290
|
+
await tx.update(anyTable).set({ [sdField]: /* @__PURE__ */ new Date() }).where(deleteWhere);
|
|
2291
|
+
} else {
|
|
2292
|
+
await tx.delete(anyTable).where(deleteWhere);
|
|
2293
|
+
}
|
|
2294
|
+
});
|
|
2254
2295
|
if (useRealtime && enabledMethods.has("list")) {
|
|
2255
2296
|
const currentUserId = ownerMeta ? user?.id : void 0;
|
|
2256
2297
|
const listResult = await fetchListWithTotal(ctx.db, void 0, currentUserId);
|
|
@@ -2279,6 +2320,7 @@ export {
|
|
|
2279
2320
|
deregisterClient,
|
|
2280
2321
|
crud as gencowCrud,
|
|
2281
2322
|
getOwnerRlsMeta,
|
|
2323
|
+
getOwnerRlsTables,
|
|
2282
2324
|
getQueryDef,
|
|
2283
2325
|
getQueryHandler,
|
|
2284
2326
|
getRegisteredHttpActions,
|
|
@@ -252,8 +252,9 @@ describe("buildAiPrompt — AI Vibe-Coding 프롬프트", () => {
|
|
|
252
252
|
|
|
253
253
|
it("배포 규칙 포함", () => {
|
|
254
254
|
const md = buildAiPrompt(SIMPLE_API, ["tasks"]);
|
|
255
|
+
expect(md).toContain("npx gencow dev");
|
|
256
|
+
expect(md).toContain("npx gencow static");
|
|
255
257
|
expect(md).toContain("npx gencow deploy");
|
|
256
|
-
expect(md).toContain("--static");
|
|
257
258
|
expect(md).toContain("--no-backend");
|
|
258
259
|
expect(md).toContain("백엔드가 감지되면");
|
|
259
260
|
});
|
package/lib/readme-codegen.mjs
CHANGED
|
@@ -232,7 +232,7 @@ export function buildAuthSection() {
|
|
|
232
232
|
md += `\`\`\`typescript\n`;
|
|
233
233
|
md += `const { signIn, useAuth } = createAuthClient(import.meta.env.VITE_API_URL);\n`;
|
|
234
234
|
md += `\`\`\`\n\n`;
|
|
235
|
-
md += `> ⚠️ \`VITE_API_URL\`은 \`.env\`에 설정하세요. \`gencow init\` / \`gencow
|
|
235
|
+
md += `> ⚠️ \`VITE_API_URL\`은 \`.env\`에 설정하세요. \`gencow init\` / \`gencow dev\` 시 자동으로 설정됩니다.\n\n`;
|
|
236
236
|
return md;
|
|
237
237
|
}
|
|
238
238
|
|
|
@@ -334,10 +334,12 @@ export function buildAiPrompt(apiObj, namespaces) {
|
|
|
334
334
|
md += `- TypeScript 타입을 최대한 활용하고, 로딩/에러 상태도 처리해줘.\n`;
|
|
335
335
|
md += `- import { ai } from "./ai"; + ai.chat()을 사용해서 AI를 호출하고, OpenAI SDK를 직접 설치하지 마.\n`;
|
|
336
336
|
md += `배포 규칙:\n`;
|
|
337
|
-
md += `-
|
|
338
|
-
md += `-
|
|
337
|
+
md += `- 개발: \`npx gencow dev\` (실시간 배포 — gencow/ 변경 감지 → 자동 배포)\n`;
|
|
338
|
+
md += `- 정적 파일 배포: \`npx gencow static dist/\` (dev 환경, 1회성)\n`;
|
|
339
|
+
md += `- 프로덕션: \`npx gencow deploy\` (Pro+ only, prod 환경)\n`;
|
|
340
|
+
md += `- 풀스택 정적: VITE_API_URL=https://{앱ID}.{도메인} npm run build 후 \`npx gencow static dist/\`\n`;
|
|
339
341
|
md += ` → 백엔드가 감지되면 자동으로 백엔드 먼저 배포 후 프론트엔드 배포\n`;
|
|
340
|
-
md += `- 프론트엔드만 배포: \`npx gencow
|
|
342
|
+
md += `- 프론트엔드만 배포: \`npx gencow static --no-backend dist/\`\n`;
|
|
341
343
|
md += `- 환경변수는 \`npx gencow env set KEY=VALUE\`로 클라우드에 설정해. 즉시 반영 (재시작 불필요).\n`;
|
|
342
344
|
md += `- process.env.KEY는 반드시 handler 내부에서 접근해 (모듈 상단 캐싱 시 hot-reload 안 됨).\n`;
|
|
343
345
|
md += `- .env 파일은 로컬 개발 전용이야. 클라우드에는 gencow env push로 올려.\n`;
|
|
@@ -469,8 +471,11 @@ export function buildDeploySection() {
|
|
|
469
471
|
md += `\`\`\`bash\n`;
|
|
470
472
|
md += `# 1. 로그인 (최초 1회)\n`;
|
|
471
473
|
md += `npx gencow login\n\n`;
|
|
472
|
-
md += `# 2.
|
|
473
|
-
|
|
474
|
+
md += `# 2. 개발 (실시간 배포)
|
|
475
|
+
`;
|
|
476
|
+
md += `npx gencow dev\n\n`;
|
|
477
|
+
md += `# 3. 배포 후 URL 확인\n`;
|
|
478
|
+
md += `# → https://{앱이름}.{도메인}\n\n`;
|
|
474
479
|
md += `# 3. 환경변수 설정 (필요 시)\n`;
|
|
475
480
|
md += `npx gencow env set DATABASE_URL=postgres://... # 클라우드에 설정\n`;
|
|
476
481
|
md += `npx gencow env list # 클라우드 환경변수 목록\n`;
|
|
@@ -499,23 +504,28 @@ export function buildDeploySection() {
|
|
|
499
504
|
|
|
500
505
|
// 상세 배포 가이드
|
|
501
506
|
md += `---\n\n## 🚀 배포\n\n`;
|
|
502
|
-
md += `### 백엔드 API
|
|
507
|
+
md += `### 백엔드 API 개발 (실시간)\n`;
|
|
503
508
|
md += `\`\`\`bash\n`;
|
|
504
|
-
md += `gencow
|
|
509
|
+
md += `gencow dev # Watch + 자동 배포 (schema 변경 시 drizzle-kit generate 자동)\n`;
|
|
505
510
|
md += `\`\`\`\n\n`;
|
|
506
511
|
md += `### 프론트엔드 배포 (frontend/ 있는 경우)\n`;
|
|
507
512
|
md += `\`\`\`bash\n`;
|
|
508
513
|
md += `# 1. 백엔드 URL을 환경변수로 빌드\n`;
|
|
509
514
|
md += `cd frontend\n`;
|
|
510
515
|
md += `VITE_API_URL=https://{앱ID}.{도메인} npm run build\n\n`;
|
|
511
|
-
md += `# 2.
|
|
512
|
-
md += `gencow
|
|
516
|
+
md += `# 2. 정적 배포 — 백엔드가 감지되면 자동으로 백엔드 먼저 배포 후 프론트엔드 배포\n`;
|
|
517
|
+
md += `gencow static dist/\n`;
|
|
513
518
|
md += `\`\`\`\n\n`;
|
|
514
519
|
md += `> 💡 동일 프로젝트에 \`gencow/\` 폴더가 있으면 백엔드를 자동 감지하여 백엔드 → 프론트엔드 순서로 배포합니다.\n`;
|
|
515
|
-
md += `> 프론트엔드만 배포하려면: \`gencow
|
|
520
|
+
md += `> 프론트엔드만 배포하려면: \`gencow static --no-backend dist/\`\n\n`;
|
|
516
521
|
md += `### 정적 사이트 전용 (API 없는 경우)\n`;
|
|
517
522
|
md += `\`\`\`bash\n`;
|
|
518
|
-
md += `gencow
|
|
523
|
+
md += `gencow static dist/ # 순수 HTML/CSS/JS만 배포\n`;
|
|
524
|
+
md += `\`\`\`\n\n`;
|
|
525
|
+
md += `### 프로덕션 배포 (Pro+ only)\n`;
|
|
526
|
+
md += `\`\`\`bash\n`;
|
|
527
|
+
md += `gencow deploy # 백엔드를 프로덕션에 배포\n`;
|
|
528
|
+
md += `gencow deploy --static dist/ # 정적 파일을 프로덕션에 배포\n`;
|
|
519
529
|
md += `\`\`\`\n\n`;
|
|
520
530
|
md += `> ⚠️ 프론트엔드에서 API를 호출하려면 빌드 시 \`VITE_API_URL\`을 반드시 설정하세요.\n\n`;
|
|
521
531
|
|
|
@@ -532,7 +542,7 @@ export function buildDeploySection() {
|
|
|
532
542
|
md += `| \`hono\` | HTTP 프레임워크 |\n`;
|
|
533
543
|
md += `| \`ai\`, \`@ai-sdk/*\` | AI SDK |\n`;
|
|
534
544
|
md += `| \`zod\` | 밸리데이션 |\n\n`;
|
|
535
|
-
md += `> 📦 langfuse, axios, cheerio 등 추가 패키지도 \`npm install\` 후
|
|
545
|
+
md += `> 📦 langfuse, axios, cheerio 등 추가 패키지도 \`npm install\` 후 배포하면 자동 설치됩니다.\n`;
|
|
536
546
|
md += `> ⛔ \`child_process\`, \`vm\`, \`os\`, \`cluster\`, \`worker_threads\` 모듈은 보안상 차단됩니다.\n\n`;
|
|
537
547
|
md += `### CORS 설정\n\n`;
|
|
538
548
|
md += `- \`*.{BASE_DOMAIN}\` 서브도메인 간 요청은 **자동 허용**됩니다.\n`;
|
|
@@ -593,7 +603,7 @@ export function buildDevTips() {
|
|
|
593
603
|
md += `- \`gencow/\` 폴더 내 파일을 수정하면 \`api.ts\`와 이 README가 **자동으로 재생성**됩니다.\n`;
|
|
594
604
|
md += `- 스키마 변경 후:\n`;
|
|
595
605
|
md += ` - 로컬 개발 + 배포: \`gencow dev\` 실행 시 자동으로 \`drizzle-kit generate\` 실행 → \`gencow/migrations/\` 생성\n`;
|
|
596
|
-
md += ` - \`gencow deploy
|
|
606
|
+
md += ` - \`gencow deploy\`(프로덕션)도 번들링 전 자동으로 \`drizzle-kit generate\` 실행\n`;
|
|
597
607
|
md += ` - ⚠️ drizzle.config.ts 없으면 generate 실패 경고 출력 (deploy는 계속 진행, 하지만 migrations/ 없으면 플랫폼이 스키마 스킵)\n`;
|
|
598
608
|
md += `- MCP 서버를 사용하면 AI가 이 구조를 자동으로 인식합니다.\n`;
|
|
599
609
|
md += `- 로컬 개발: \`gencow dev\` — 로컬 서버 시작\n`;
|
package/package.json
CHANGED
|
@@ -1,33 +1,32 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
2
|
+
"name": "gencow",
|
|
3
|
+
"version": "0.1.121",
|
|
4
|
+
"description": "Gencow — AI Backend Engine",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"gencow": "./bin/gencow.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"lib/",
|
|
12
|
+
"templates/",
|
|
13
|
+
"server/",
|
|
14
|
+
"core/",
|
|
15
|
+
"scripts/",
|
|
16
|
+
"dashboard/"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
20
|
+
"open": "^10.1.0",
|
|
21
|
+
"tar": "^7",
|
|
22
|
+
"ws": "^8.19.0",
|
|
23
|
+
"zod": "^4.3.6"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^25",
|
|
27
|
+
"esbuild": "^0.27.3"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "node scripts/bundle-server.mjs"
|
|
31
|
+
}
|
|
32
|
+
}
|