gencow 0.1.124 → 0.1.126
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 +157 -36
- package/core/index.js +192 -5
- package/lib/readme-codegen.mjs +5 -4
- package/package.json +1 -1
- package/server/index.js +243 -26
- package/server/index.js.map +3 -3
package/bin/gencow.mjs
CHANGED
|
@@ -677,6 +677,11 @@ function cleanupOldBackups(backupDir, keep = 3) {
|
|
|
677
677
|
} catch { /* ignore */ }
|
|
678
678
|
}
|
|
679
679
|
|
|
680
|
+
function isLocalDbTarget(args) {
|
|
681
|
+
if (process.env.GENCOW_LOCAL === "1") return true;
|
|
682
|
+
return Array.isArray(args) && args.includes("--local");
|
|
683
|
+
}
|
|
684
|
+
|
|
680
685
|
// ─── Commands ─────────────────────────────────────────────
|
|
681
686
|
|
|
682
687
|
const commands = {
|
|
@@ -783,8 +788,8 @@ const commands = {
|
|
|
783
788
|
"@gencow/core": "latest",
|
|
784
789
|
"@electric-sql/pglite": "^0.3.15",
|
|
785
790
|
"better-auth": "^1.5.1",
|
|
786
|
-
"drizzle-orm": "^0.
|
|
787
|
-
"drizzle-kit": "^0.31.
|
|
791
|
+
"drizzle-orm": "^0.45.1",
|
|
792
|
+
"drizzle-kit": "^0.31.4",
|
|
788
793
|
"postgres": "^3.4.8",
|
|
789
794
|
};
|
|
790
795
|
const pkgJsonPath = resolve(projectDir, "package.json");
|
|
@@ -1049,9 +1054,81 @@ ${hasPrompt ? `
|
|
|
1049
1054
|
},
|
|
1050
1055
|
|
|
1051
1056
|
// ── db:reset ─────────────────────────────────────────
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1057
|
+
// TRUNCATE all user tables + seed.ts 실행.
|
|
1058
|
+
// 로컬 전용: Cloud DB reset은 위험하므로 CLI에서 미지원 (Dashboard에서만 가능)
|
|
1059
|
+
// 사용: gencow db:reset --local
|
|
1060
|
+
async "db:reset"(...args) {
|
|
1061
|
+
const config = loadConfig();
|
|
1062
|
+
const cwd = process.cwd();
|
|
1063
|
+
const isLocal = isLocalDbTarget(args);
|
|
1064
|
+
|
|
1065
|
+
if (!isLocal) {
|
|
1066
|
+
error("db:reset only applies to the local database.");
|
|
1067
|
+
info(`${DIM}Cloud DB reset is only available via Dashboard (data protection).${RESET}`);
|
|
1068
|
+
info(`${DIM}For local DB: set ${GREEN}GENCOW_LOCAL=1${RESET}${DIM} and run this command again.${RESET}`);
|
|
1069
|
+
log("");
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const port = process.env.PORT || config.port || 5456;
|
|
1074
|
+
|
|
1075
|
+
log(`\n${BOLD}${CYAN}Gencow DB Reset${RESET}\n`);
|
|
1076
|
+
|
|
1077
|
+
let serverAlive = false;
|
|
1078
|
+
try {
|
|
1079
|
+
const res = await fetch(`http://localhost:${port}/_admin/status`, { signal: AbortSignal.timeout(2000) });
|
|
1080
|
+
serverAlive = res.ok;
|
|
1081
|
+
} catch { /* 서버 꺼져있음 */ }
|
|
1082
|
+
|
|
1083
|
+
if (serverAlive) {
|
|
1084
|
+
info(`Server detected on :${port} — using API reset (TRUNCATE + seed)`);
|
|
1085
|
+
log("");
|
|
1086
|
+
|
|
1087
|
+
try {
|
|
1088
|
+
const res = await fetch(`http://localhost:${port}/_admin/reset`, {
|
|
1089
|
+
method: "POST",
|
|
1090
|
+
headers: { "Content-Type": "application/json" },
|
|
1091
|
+
body: JSON.stringify({ confirm: "delete all data" }),
|
|
1092
|
+
});
|
|
1093
|
+
const data = await res.json();
|
|
1094
|
+
|
|
1095
|
+
if (!res.ok) {
|
|
1096
|
+
error(`Reset failed: ${data.error || "Unknown error"}`);
|
|
1097
|
+
log("");
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
success(`TRUNCATE done: ${data.truncated?.length || 0} tables`);
|
|
1102
|
+
if (data.truncated?.length > 0) {
|
|
1103
|
+
info(` ${DIM}${data.truncated.join(", ")}${RESET}`);
|
|
1104
|
+
}
|
|
1105
|
+
if (data.seeded) {
|
|
1106
|
+
success(`Seed completed ✓`);
|
|
1107
|
+
} else {
|
|
1108
|
+
info(`${DIM}No seed.ts found — skipping${RESET}`);
|
|
1109
|
+
}
|
|
1110
|
+
} catch (e) {
|
|
1111
|
+
error(`Server connection failed: ${e.message}`);
|
|
1112
|
+
}
|
|
1113
|
+
} else {
|
|
1114
|
+
const dbPath = resolve(cwd, config.db.url);
|
|
1115
|
+
if (existsSync(dbPath)) {
|
|
1116
|
+
const backupDir = resolve(cwd, ".gencow", "backups");
|
|
1117
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1118
|
+
const backupPath = resolve(backupDir, ts);
|
|
1119
|
+
mkdirSync(backupPath, { recursive: true });
|
|
1120
|
+
cpSync(dbPath, backupPath, { recursive: true });
|
|
1121
|
+
success(`Backup created: .gencow/backups/${ts}`);
|
|
1122
|
+
|
|
1123
|
+
cleanupOldBackups(backupDir, 3);
|
|
1124
|
+
|
|
1125
|
+
rmSync(dbPath, { recursive: true, force: true });
|
|
1126
|
+
success("DB deleted — restart with gencow dev:local to create a new one");
|
|
1127
|
+
} else {
|
|
1128
|
+
info("DB does not exist yet. Run gencow dev:local to get started.");
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
log("");
|
|
1055
1132
|
},
|
|
1056
1133
|
|
|
1057
1134
|
// ── db:seed ──────────────────────────────────────────
|
|
@@ -1059,11 +1136,48 @@ ${hasPrompt ? `
|
|
|
1059
1136
|
// Cloud-first: 기본=Cloud, --local=로컬
|
|
1060
1137
|
async "db:seed"(...args) {
|
|
1061
1138
|
const config = loadConfig();
|
|
1062
|
-
const isLocal = args
|
|
1139
|
+
const isLocal = isLocalDbTarget(args);
|
|
1063
1140
|
|
|
1064
1141
|
if (isLocal) {
|
|
1065
|
-
|
|
1066
|
-
|
|
1142
|
+
const port = process.env.PORT || config.port || 5456;
|
|
1143
|
+
|
|
1144
|
+
log(`\n${BOLD}${CYAN}Gencow DB Seed${RESET} ${DIM}(local)${RESET}\n`);
|
|
1145
|
+
|
|
1146
|
+
try {
|
|
1147
|
+
const statusRes = await fetch(`http://localhost:${port}/_admin/status`, { signal: AbortSignal.timeout(2000) });
|
|
1148
|
+
if (!statusRes.ok) throw new Error("Server not ready");
|
|
1149
|
+
} catch {
|
|
1150
|
+
error(`Server not running on :${port}`);
|
|
1151
|
+
info(`${DIM}Start the local server first: ${GREEN}gencow dev:local${RESET}`);
|
|
1152
|
+
log("");
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
try {
|
|
1157
|
+
const res = await fetch(`http://localhost:${port}/_admin/seed`, {
|
|
1158
|
+
method: "POST",
|
|
1159
|
+
headers: { "Content-Type": "application/json" },
|
|
1160
|
+
});
|
|
1161
|
+
const data = await res.json();
|
|
1162
|
+
|
|
1163
|
+
if (res.status === 404) {
|
|
1164
|
+
warn("gencow/seed.ts not found");
|
|
1165
|
+
info(`\n ${DIM}Create gencow/seed.ts:${RESET}\n`);
|
|
1166
|
+
log(` ${DIM}import { tasks } from "./schema";${RESET}`);
|
|
1167
|
+
log(` ${DIM}export default async function seed(ctx) {${RESET}`);
|
|
1168
|
+
log(` ${DIM} await ctx.db.insert(tasks).values([${RESET}`);
|
|
1169
|
+
log(` ${DIM} { title: "Hello World" },${RESET}`);
|
|
1170
|
+
log(` ${DIM} ]);${RESET}`);
|
|
1171
|
+
log(` ${DIM}};${RESET}`);
|
|
1172
|
+
} else if (!res.ok) {
|
|
1173
|
+
error(`Seed failed: ${data.error || "Unknown error"}`);
|
|
1174
|
+
} else {
|
|
1175
|
+
success("Seed completed ✓");
|
|
1176
|
+
}
|
|
1177
|
+
} catch (e) {
|
|
1178
|
+
error(`Server connection failed: ${e.message}`);
|
|
1179
|
+
}
|
|
1180
|
+
log("");
|
|
1067
1181
|
return;
|
|
1068
1182
|
}
|
|
1069
1183
|
|
|
@@ -1198,15 +1312,11 @@ ${hasPrompt ? `
|
|
|
1198
1312
|
if (devArgs.includes("--verbose")) {
|
|
1199
1313
|
process.env.GENCOW_VERBOSE = "true";
|
|
1200
1314
|
}
|
|
1201
|
-
|
|
1202
|
-
if (
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
info(`${DIM}Use cloud mode (default): gencow dev${RESET}\n`);
|
|
1206
|
-
return;
|
|
1315
|
+
const useLocal = process.env.GENCOW_LOCAL === "1" || devArgs.includes("--local");
|
|
1316
|
+
if (useLocal) {
|
|
1317
|
+
const forwarded = devArgs.filter(a => a !== "--local");
|
|
1318
|
+
return commands["dev:local"](...forwarded);
|
|
1207
1319
|
}
|
|
1208
|
-
// --cloud 하위 호환 (이미 기본이므로 그대로 진행)
|
|
1209
|
-
// 기본 = 클라우드 모드
|
|
1210
1320
|
return commands["dev:cloud"](...devArgs);
|
|
1211
1321
|
},
|
|
1212
1322
|
|
|
@@ -1422,11 +1532,20 @@ ${hasPrompt ? `
|
|
|
1422
1532
|
// Cloud-first: 기본=Cloud, --local=로컬
|
|
1423
1533
|
async "db:push"(...args) {
|
|
1424
1534
|
const config = loadConfig();
|
|
1425
|
-
const isLocal = args
|
|
1535
|
+
const isLocal = isLocalDbTarget(args);
|
|
1426
1536
|
|
|
1427
1537
|
if (isLocal) {
|
|
1428
|
-
|
|
1429
|
-
|
|
1538
|
+
let hasDb = !!process.env.DATABASE_URL;
|
|
1539
|
+
if (!hasDb) {
|
|
1540
|
+
const envPath = resolve(process.cwd(), ".env");
|
|
1541
|
+
if (existsSync(envPath)) hasDb = readFileSync(envPath, "utf8").includes("DATABASE_URL=");
|
|
1542
|
+
}
|
|
1543
|
+
const targetDb = hasDb ? "local PG" : "local fallback: PGlite";
|
|
1544
|
+
log(`\n${BOLD}${CYAN}Gencow DB Push${RESET} ${DIM}(${targetDb})${RESET}\n`);
|
|
1545
|
+
info("Pushing schema.ts → database (no migration files)...");
|
|
1546
|
+
runInServer("pnpm db:push --force", buildEnv(config));
|
|
1547
|
+
success("Schema pushed!");
|
|
1548
|
+
log(` ${DIM}Tables are in sync with schema.ts${RESET}\n`);
|
|
1430
1549
|
return;
|
|
1431
1550
|
}
|
|
1432
1551
|
|
|
@@ -1630,7 +1749,8 @@ ${BOLD}Commands (login required):${RESET}
|
|
|
1630
1749
|
${GREEN}static [dir]${RESET} Deploy static files ${DIM}(dist/, out/, build/)${RESET}
|
|
1631
1750
|
${DIM}--prod Deploy to production app${RESET}
|
|
1632
1751
|
${DIM}--force, -f Skip dependency audit${RESET}
|
|
1633
|
-
${GREEN}deploy${RESET} Deploy backend to
|
|
1752
|
+
${GREEN}deploy${RESET} Deploy backend to cloud ${DIM}(dev by default)${RESET}
|
|
1753
|
+
${DIM}--prod Deploy to production (Pro+ only)${RESET}
|
|
1634
1754
|
${DIM}--rollback Rollback to previous deployment${RESET}
|
|
1635
1755
|
${DIM}--force, -f Skip dependency audit${RESET}
|
|
1636
1756
|
${GREEN}env list${RESET} List cloud env vars ${DIM}(--prod for production)${RESET}
|
|
@@ -1656,7 +1776,7 @@ ${BOLD}App management:${RESET}
|
|
|
1656
1776
|
${GREEN}app status${RESET} Show app status
|
|
1657
1777
|
|
|
1658
1778
|
${DIM}Tip: Most commands support --prod to target the production app.${RESET}
|
|
1659
|
-
${DIM} e.g. gencow
|
|
1779
|
+
${DIM} e.g. gencow deploy --prod, gencow env list --prod${RESET}
|
|
1660
1780
|
|
|
1661
1781
|
${BOLD}Examples:${RESET}
|
|
1662
1782
|
${DIM}# New project:${RESET}
|
|
@@ -1665,9 +1785,11 @@ ${BOLD}Examples:${RESET}
|
|
|
1665
1785
|
${DIM}# Initialize in current directory:${RESET}
|
|
1666
1786
|
gencow init . --force
|
|
1667
1787
|
|
|
1668
|
-
${DIM}# Deploy to
|
|
1669
|
-
gencow login
|
|
1788
|
+
${DIM}# Deploy to dev:${RESET}
|
|
1670
1789
|
gencow deploy
|
|
1790
|
+
|
|
1791
|
+
${DIM}# Deploy to production (Pro+ only):${RESET}
|
|
1792
|
+
gencow deploy --prod
|
|
1671
1793
|
`);
|
|
1672
1794
|
},
|
|
1673
1795
|
|
|
@@ -1865,14 +1987,14 @@ ${BOLD}Examples:${RESET}
|
|
|
1865
1987
|
});
|
|
1866
1988
|
},
|
|
1867
1989
|
|
|
1868
|
-
// ── deploy —
|
|
1990
|
+
// ── deploy — 클라우드 배포 (dev 기본, --prod로 프로덕션) ──
|
|
1869
1991
|
async deploy(...deployArgs) {
|
|
1870
1992
|
const creds = requireCreds();
|
|
1871
1993
|
|
|
1872
1994
|
// gencow.json에서 앱 ID 확인 (자동 생성된 유니크 ID)
|
|
1873
1995
|
let appId = null;
|
|
1874
1996
|
let displayName = null;
|
|
1875
|
-
let envTarget = "
|
|
1997
|
+
let envTarget = "dev"; // v0.1.125: deploy = dev 기본값, --prod로 prod
|
|
1876
1998
|
let forceDeploy = false; // --force: skip dependency audit
|
|
1877
1999
|
let isRollback = false; // --rollback: rollback to previous deploy
|
|
1878
2000
|
|
|
@@ -1939,9 +2061,6 @@ ${BOLD}Examples:${RESET}
|
|
|
1939
2061
|
for (let i = 0; i < deployArgs.length; i++) {
|
|
1940
2062
|
const a = deployArgs[i];
|
|
1941
2063
|
if (a === "--prod") {
|
|
1942
|
-
// deprecated: deploy는 이미 prod 기본값
|
|
1943
|
-
warn(`${YELLOW}--prod flag is no longer needed.${RESET}`);
|
|
1944
|
-
info(`gencow deploy targets production by default.`);
|
|
1945
2064
|
envTarget = "prod";
|
|
1946
2065
|
}
|
|
1947
2066
|
else if (a === "--force" || a === "-f") forceDeploy = true;
|
|
@@ -1960,7 +2079,8 @@ ${BOLD}Examples:${RESET}
|
|
|
1960
2079
|
error(`Unknown deploy argument: "${a}"`);
|
|
1961
2080
|
log("");
|
|
1962
2081
|
info(`Usage: gencow deploy [options]`);
|
|
1963
|
-
info(` gencow deploy
|
|
2082
|
+
info(` gencow deploy Dev backend deploy`);
|
|
2083
|
+
info(` gencow deploy --prod Production backend deploy (Pro+)`);
|
|
1964
2084
|
info(` gencow deploy --rollback Rollback to previous version`);
|
|
1965
2085
|
info(` gencow deploy logs View server logs`);
|
|
1966
2086
|
info(` gencow deploy status Check app status`);
|
|
@@ -1996,15 +2116,15 @@ ${BOLD}Examples:${RESET}
|
|
|
1996
2116
|
|
|
1997
2117
|
// ── Rollback 분기 ─────────────────────────────────────
|
|
1998
2118
|
if (isRollback) {
|
|
1999
|
-
//
|
|
2000
|
-
const rollbackTarget = prodAppId || appId;
|
|
2119
|
+
// --prod: prod 앱 롤백, 기본: dev 앱 롤백
|
|
2120
|
+
const rollbackTarget = envTarget === "prod" ? (prodAppId || appId) : appId;
|
|
2001
2121
|
if (!rollbackTarget) {
|
|
2002
2122
|
error("App not found. Run from the project root with gencow.json.");
|
|
2003
2123
|
process.exit(1);
|
|
2004
2124
|
}
|
|
2005
2125
|
|
|
2006
2126
|
log(`\n${BOLD}${CYAN}Gencow Rollback${RESET}\n`);
|
|
2007
|
-
info(`App: ${rollbackTarget}${
|
|
2127
|
+
info(`App: ${rollbackTarget}${envTarget === "prod" ? " (prod)" : ""}`);
|
|
2008
2128
|
log("");
|
|
2009
2129
|
|
|
2010
2130
|
const rollbackStartTime = Date.now();
|
|
@@ -2078,11 +2198,12 @@ ${BOLD}Examples:${RESET}
|
|
|
2078
2198
|
log("");
|
|
2079
2199
|
log(` ${RED}⛔ Production Deploy — Pro plan or higher required${RESET}`);
|
|
2080
2200
|
log("");
|
|
2081
|
-
log(` ${DIM}gencow deploy is for production deployment.${RESET}`);
|
|
2201
|
+
log(` ${DIM}gencow deploy --prod is for production deployment.${RESET}`);
|
|
2082
2202
|
log("");
|
|
2083
2203
|
log(` ${BOLD}💡 Dev environment deploy:${RESET}`);
|
|
2084
|
-
log(` ${GREEN}gencow
|
|
2085
|
-
log(` ${GREEN}gencow
|
|
2204
|
+
log(` ${GREEN}gencow deploy${RESET} ${DIM}← Deploy backend to dev${RESET}`);
|
|
2205
|
+
log(` ${GREEN}gencow dev${RESET} ${DIM}← Real-time backend + live logs${RESET}`);
|
|
2206
|
+
log(` ${GREEN}gencow static dist/${RESET} ${DIM}← Static file (dist/) deploy${RESET}`);
|
|
2086
2207
|
log("");
|
|
2087
2208
|
log(` ${BOLD}🚀 Unlock production deploy:${RESET}`);
|
|
2088
2209
|
log(` ${GREEN}gencow upgrade${RESET} ${DIM}← Upgrade to Pro plan${RESET}`);
|
|
@@ -2124,7 +2245,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2124
2245
|
} else {
|
|
2125
2246
|
info(`App: new (auto-generated ID)`);
|
|
2126
2247
|
}
|
|
2127
|
-
info(`Env: ${envTarget}`);
|
|
2248
|
+
info(`Env: ${envTarget}${envTarget === "prod" ? ` ${YELLOW}(production)${RESET}` : ""}`);
|
|
2128
2249
|
info(`Format: tar.gz`);
|
|
2129
2250
|
log("");
|
|
2130
2251
|
|
package/core/index.js
CHANGED
|
@@ -1979,22 +1979,209 @@ function ownerRls(userIdColumn, options) {
|
|
|
1979
1979
|
}
|
|
1980
1980
|
|
|
1981
1981
|
// ../core/src/rls-db.ts
|
|
1982
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1982
1983
|
import { sql as sql2 } from "drizzle-orm";
|
|
1983
|
-
|
|
1984
|
+
var gucNameRe = /^app\.[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)*$/;
|
|
1985
|
+
var RESERVED_VARS_KEYS = /* @__PURE__ */ new Set([
|
|
1986
|
+
"app.current_user_id",
|
|
1987
|
+
"app.current_user_role",
|
|
1988
|
+
"app.tenant_id"
|
|
1989
|
+
]);
|
|
1990
|
+
function assertSafeGucName(key) {
|
|
1991
|
+
if (!gucNameRe.test(key)) {
|
|
1992
|
+
throw new Error(
|
|
1993
|
+
`createRlsDb: GUC name "${key}" is invalid \u2014 use lowercase app.* names (e.g. app.org_id)`
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
function rlsSetConfigPairs(rls) {
|
|
1998
|
+
const pairs = [["app.current_user_id", rls.userId]];
|
|
1999
|
+
if (rls.role !== void 0) {
|
|
2000
|
+
pairs.push(["app.current_user_role", rls.role]);
|
|
2001
|
+
}
|
|
2002
|
+
if (rls.tenantId !== void 0) {
|
|
2003
|
+
pairs.push(["app.tenant_id", rls.tenantId]);
|
|
2004
|
+
}
|
|
2005
|
+
if (rls.vars) {
|
|
2006
|
+
for (const [key, value] of Object.entries(rls.vars)) {
|
|
2007
|
+
assertSafeGucName(key);
|
|
2008
|
+
if (RESERVED_VARS_KEYS.has(key)) {
|
|
2009
|
+
throw new Error(
|
|
2010
|
+
`createRlsDb: vars must not set "${key}" \u2014 use userId, role, or tenantId on the context object`
|
|
2011
|
+
);
|
|
2012
|
+
}
|
|
2013
|
+
pairs.push([key, value]);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
return pairs;
|
|
2017
|
+
}
|
|
2018
|
+
async function forEachSetConfig(rls, setOne) {
|
|
2019
|
+
for (const [name, value] of rlsSetConfigPairs(rls)) {
|
|
2020
|
+
await setOne(name, value);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
var rlsExecClient = new AsyncLocalStorage();
|
|
2024
|
+
function isDrizzleTransactionDb(db) {
|
|
2025
|
+
const d = db;
|
|
2026
|
+
if (typeof d?.rollback === "function") {
|
|
2027
|
+
return true;
|
|
2028
|
+
}
|
|
2029
|
+
const name = d?.constructor?.name;
|
|
2030
|
+
return typeof name === "string" && name.includes("Transaction");
|
|
2031
|
+
}
|
|
2032
|
+
async function execSetConfig(client, name, value) {
|
|
2033
|
+
if (typeof client?.unsafe === "function") {
|
|
2034
|
+
await client.unsafe(`select set_config($1::text, $2::text, true)`, [name, value]);
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
if (typeof client?.query === "function") {
|
|
2038
|
+
await client.query(`select set_config($1::text, $2::text, true)`, [name, value]);
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
throw new Error(
|
|
2042
|
+
"createRlsDb: unsupported SQL driver (expected Bun SQL, node-pg client/pool, or PGlite)"
|
|
2043
|
+
);
|
|
2044
|
+
}
|
|
2045
|
+
async function applyRlsSessionVars(client, rls) {
|
|
2046
|
+
await forEachSetConfig(rls, (name, value) => execSetConfig(client, name, value));
|
|
2047
|
+
}
|
|
2048
|
+
async function injectRlsVarsOnTx(tx, rls) {
|
|
2049
|
+
await forEachSetConfig(
|
|
2050
|
+
rls,
|
|
2051
|
+
(name, value) => tx.execute(sql2`SELECT set_config(${name}, ${value}, true)`)
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
2054
|
+
async function withRlsLeasedConnection(leased, rls, fn) {
|
|
2055
|
+
try {
|
|
2056
|
+
await leased.query("begin");
|
|
2057
|
+
await applyRlsSessionVars(leased, rls);
|
|
2058
|
+
const result = await rlsExecClient.run(leased, () => fn(leased));
|
|
2059
|
+
await leased.query("commit");
|
|
2060
|
+
return result;
|
|
2061
|
+
} catch (e) {
|
|
2062
|
+
try {
|
|
2063
|
+
await leased.query("rollback");
|
|
2064
|
+
} catch {
|
|
2065
|
+
}
|
|
2066
|
+
throw e;
|
|
2067
|
+
} finally {
|
|
2068
|
+
leased.release();
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
async function withRlsConnection(session, rls, reuseOuterConnection, fn) {
|
|
2072
|
+
if (reuseOuterConnection) {
|
|
2073
|
+
const c2 = session.client;
|
|
2074
|
+
return rlsExecClient.run(c2, async () => {
|
|
2075
|
+
await applyRlsSessionVars(c2, rls);
|
|
2076
|
+
return fn(c2);
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2079
|
+
const c = session.client;
|
|
2080
|
+
const runInner = async (client) => {
|
|
2081
|
+
await applyRlsSessionVars(client, rls);
|
|
2082
|
+
return rlsExecClient.run(client, () => fn(client));
|
|
2083
|
+
};
|
|
2084
|
+
if (typeof c.begin === "function") {
|
|
2085
|
+
return c.begin(runInner);
|
|
2086
|
+
}
|
|
2087
|
+
if (typeof c.transaction === "function") {
|
|
2088
|
+
return c.transaction(runInner);
|
|
2089
|
+
}
|
|
2090
|
+
if (typeof c.connect === "function" && typeof c.query === "function") {
|
|
2091
|
+
const leased = await c.connect();
|
|
2092
|
+
return withRlsLeasedConnection(leased, rls, fn);
|
|
2093
|
+
}
|
|
2094
|
+
return runInner(c);
|
|
2095
|
+
}
|
|
2096
|
+
function wrapPreparedQuery(pq, session, rls, reuseOuterConnection) {
|
|
2097
|
+
const origExecute = pq.execute.bind(pq);
|
|
2098
|
+
const origAll = pq.all.bind(pq);
|
|
2099
|
+
pq.execute = async (placeholderValues) => {
|
|
2100
|
+
const active = rlsExecClient.getStore();
|
|
2101
|
+
if (active) {
|
|
2102
|
+
const prev = pq.client;
|
|
2103
|
+
pq.client = active;
|
|
2104
|
+
try {
|
|
2105
|
+
return await origExecute(placeholderValues);
|
|
2106
|
+
} finally {
|
|
2107
|
+
pq.client = prev;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
return withRlsConnection(session, rls, reuseOuterConnection, async (client) => {
|
|
2111
|
+
const prev = pq.client;
|
|
2112
|
+
pq.client = client;
|
|
2113
|
+
try {
|
|
2114
|
+
return await origExecute(placeholderValues);
|
|
2115
|
+
} finally {
|
|
2116
|
+
pq.client = prev;
|
|
2117
|
+
}
|
|
2118
|
+
});
|
|
2119
|
+
};
|
|
2120
|
+
pq.all = async (placeholderValues) => {
|
|
2121
|
+
const active = rlsExecClient.getStore();
|
|
2122
|
+
if (active) {
|
|
2123
|
+
const prev = pq.client;
|
|
2124
|
+
pq.client = active;
|
|
2125
|
+
try {
|
|
2126
|
+
return await origAll(placeholderValues);
|
|
2127
|
+
} finally {
|
|
2128
|
+
pq.client = prev;
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
return withRlsConnection(session, rls, reuseOuterConnection, async (client) => {
|
|
2132
|
+
const prev = pq.client;
|
|
2133
|
+
pq.client = client;
|
|
2134
|
+
try {
|
|
2135
|
+
return await origAll(placeholderValues);
|
|
2136
|
+
} finally {
|
|
2137
|
+
pq.client = prev;
|
|
2138
|
+
}
|
|
2139
|
+
});
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
function wrapSession(session, rls, reuseOuterConnection) {
|
|
2143
|
+
return new Proxy(session, {
|
|
2144
|
+
get(sTarget, sProp, sRecv) {
|
|
2145
|
+
if (sProp === "prepareQuery") {
|
|
2146
|
+
return (...args) => {
|
|
2147
|
+
const pq = sTarget.prepareQuery(...args);
|
|
2148
|
+
wrapPreparedQuery(pq, sTarget, rls, reuseOuterConnection);
|
|
2149
|
+
return pq;
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
const v2 = Reflect.get(sTarget, sProp, sRecv);
|
|
2153
|
+
return typeof v2 === "function" ? v2.bind(sTarget) : v2;
|
|
2154
|
+
}
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
function createRlsDb(db, rls) {
|
|
2158
|
+
const reuseOuterConnection = isDrizzleTransactionDb(db);
|
|
2159
|
+
const baseSession = db.session;
|
|
2160
|
+
const wrappedSession = wrapSession(baseSession, rls, reuseOuterConnection);
|
|
1984
2161
|
return new Proxy(db, {
|
|
1985
2162
|
get(target, prop, receiver) {
|
|
2163
|
+
if (prop === "session") {
|
|
2164
|
+
return wrappedSession;
|
|
2165
|
+
}
|
|
2166
|
+
if (prop === "_") {
|
|
2167
|
+
const inner = target._;
|
|
2168
|
+
return new Proxy(inner, {
|
|
2169
|
+
get(i, p, r) {
|
|
2170
|
+
if (p === "session") return wrappedSession;
|
|
2171
|
+
return Reflect.get(i, p, r);
|
|
2172
|
+
}
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
1986
2175
|
if (prop === "transaction") {
|
|
1987
2176
|
return async (callback, ...rest) => {
|
|
1988
2177
|
return await target.transaction(async (tx) => {
|
|
1989
|
-
await tx
|
|
1990
|
-
sql2`SELECT set_config('app.current_user_id', ${userId}, true)`
|
|
1991
|
-
);
|
|
2178
|
+
await injectRlsVarsOnTx(tx, rls);
|
|
1992
2179
|
return await callback(tx);
|
|
1993
2180
|
}, ...rest);
|
|
1994
2181
|
};
|
|
1995
2182
|
}
|
|
1996
2183
|
const value = Reflect.get(target, prop, receiver);
|
|
1997
|
-
return typeof value === "function" ? value.bind(
|
|
2184
|
+
return typeof value === "function" ? value.bind(receiver) : value;
|
|
1998
2185
|
}
|
|
1999
2186
|
});
|
|
2000
2187
|
}
|
package/lib/readme-codegen.mjs
CHANGED
|
@@ -336,7 +336,7 @@ export function buildAiPrompt(apiObj, namespaces) {
|
|
|
336
336
|
md += `배포 규칙:\n`;
|
|
337
337
|
md += `- 개발: \`npx gencow dev\` (실시간 배포 — gencow/ 변경 감지 → 자동 배포)\n`;
|
|
338
338
|
md += `- 정적 파일 배포: \`npx gencow static dist/\` (dev 환경, 1회성)\n`;
|
|
339
|
-
md += `- 프로덕션: \`npx gencow deploy\` (Pro+ only, prod 환경)\n`;
|
|
339
|
+
md += `- 프로덕션: \`npx gencow deploy --prod\` (Pro+ only, prod 환경)\n`;
|
|
340
340
|
md += `- 풀스택 정적: VITE_API_URL=https://{앱ID}.{도메인} npm run build 후 \`npx gencow static dist/\`\n`;
|
|
341
341
|
md += ` → 백엔드가 감지되면 자동으로 백엔드 먼저 배포 후 프론트엔드 배포\n`;
|
|
342
342
|
md += `- 프론트엔드만 배포: \`npx gencow static --no-backend dist/\`\n`;
|
|
@@ -524,8 +524,9 @@ export function buildDeploySection() {
|
|
|
524
524
|
md += `\`\`\`\n\n`;
|
|
525
525
|
md += `### 프로덕션 배포 (Pro+ only)\n`;
|
|
526
526
|
md += `\`\`\`bash\n`;
|
|
527
|
-
md += `gencow deploy # 백엔드를
|
|
528
|
-
md += `gencow deploy --
|
|
527
|
+
md += `gencow deploy # 백엔드를 dev에 1회성 배포\n`;
|
|
528
|
+
md += `gencow deploy --prod # 백엔드를 프로덕션에 배포 (Pro+)\n`;
|
|
529
|
+
md += `gencow static --prod dist/ # 정적 파일을 프로덕션에 배포\n`;
|
|
529
530
|
md += `\`\`\`\n\n`;
|
|
530
531
|
md += `> ⚠️ 프론트엔드에서 API를 호출하려면 빌드 시 \`VITE_API_URL\`을 반드시 설정하세요.\n\n`;
|
|
531
532
|
|
|
@@ -603,7 +604,7 @@ export function buildDevTips() {
|
|
|
603
604
|
md += `- \`gencow/\` 폴더 내 파일을 수정하면 \`api.ts\`와 이 README가 **자동으로 재생성**됩니다.\n`;
|
|
604
605
|
md += `- 스키마 변경 후:\n`;
|
|
605
606
|
md += ` - 로컬 개발 + 배포: \`gencow dev\` 실행 시 자동으로 \`drizzle-kit generate\` 실행 → \`gencow/migrations/\` 생성\n`;
|
|
606
|
-
md += ` - \`gencow deploy
|
|
607
|
+
md += ` - \`gencow deploy\`도 번들링 전 자동으로 \`drizzle-kit generate\` 실행\n`;
|
|
607
608
|
md += ` - ⚠️ drizzle.config.ts 없으면 generate 실패 경고 출력 (deploy는 계속 진행, 하지만 migrations/ 없으면 플랫폼이 스키마 스킵)\n`;
|
|
608
609
|
md += `- MCP 서버를 사용하면 AI가 이 구조를 자동으로 인식합니다.\n`;
|
|
609
610
|
md += `- 로컬 개발: \`gencow dev\` — 로컬 서버 시작\n`;
|