gencow 0.1.118 → 0.1.119
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 +274 -18
- package/core/index.js +111 -18
- package/dashboard/assets/index-Bur5ZNpv.js +372 -0
- package/dashboard/index.html +1 -1
- package/package.json +1 -1
- package/server/index.js +508 -43
- package/server/index.js.map +3 -3
- package/dashboard/assets/index-C26b7MIs.js +0 -372
package/bin/gencow.mjs
CHANGED
|
@@ -1808,15 +1808,20 @@ ${BOLD}BaaS commands (login required):${RESET}
|
|
|
1808
1808
|
${GREEN}deploy${RESET} Bundle gencow/ and deploy to platform
|
|
1809
1809
|
${DIM}--static [dir] Deploy static files (dist/, out/, build/)${RESET}
|
|
1810
1810
|
${DIM}--no-backend Skip backend auto-deploy in --static mode${RESET}
|
|
1811
|
+
${DIM}--prod Deploy to production app (Pro+, auto-creates)${RESET}
|
|
1812
|
+
${DIM}--rollback Rollback to previous deployment${RESET}
|
|
1811
1813
|
${DIM}--force, -f Skip dependency audit${RESET}
|
|
1812
|
-
${GREEN}env list${RESET} List cloud env vars
|
|
1814
|
+
${GREEN}env list${RESET} List cloud env vars ${DIM}(--prod for production)${RESET}
|
|
1813
1815
|
${GREEN}env set K=V${RESET} Set cloud env var ${DIM}(hot-reload, no restart)${RESET}
|
|
1814
1816
|
${GREEN}env unset KEY${RESET} Remove cloud env var
|
|
1815
|
-
${GREEN}env push${RESET} Push .env to cloud
|
|
1817
|
+
${GREEN}env push${RESET} Push .env to cloud ${DIM}(--prod reads .env.production)${RESET}
|
|
1816
1818
|
${GREEN}files upload${RESET} Upload files to app storage ${DIM}(--recursive for dirs)${RESET}
|
|
1817
1819
|
${GREEN}files list${RESET} List uploaded files
|
|
1818
1820
|
${GREEN}files delete${RESET} Delete uploaded file ${DIM}(--yes to skip confirm)${RESET}
|
|
1819
1821
|
${GREEN}files url${RESET} Get serving URL for a file
|
|
1822
|
+
${GREEN}config set${RESET} Set app config ${DIM}(image.maxWidth, image.quality)${RESET}
|
|
1823
|
+
${GREEN}config get${RESET} Show current app config
|
|
1824
|
+
${GREEN}config reset${RESET} Reset app config to tier defaults
|
|
1820
1825
|
${GREEN}domain set${RESET} Connect custom domain ${DIM}(myapp.com)${RESET}
|
|
1821
1826
|
${GREEN}domain status${RESET} Check domain DNS/TLS status
|
|
1822
1827
|
${GREEN}domain remove${RESET} Disconnect custom domain
|
|
@@ -1992,6 +1997,7 @@ ${BOLD}Examples:${RESET}
|
|
|
1992
1997
|
let isStatic = false;
|
|
1993
1998
|
let forceDeploy = false; // --force: skip dependency audit
|
|
1994
1999
|
let noBackend = false; // --no-backend: skip backend auto-deploy in --static mode
|
|
2000
|
+
let isRollback = false; // --rollback: rollback to previous deploy
|
|
1995
2001
|
|
|
1996
2002
|
// ── subcommand 분기 (logs / status) ───────────────────
|
|
1997
2003
|
const knownSubcmds = new Set(["logs", "status"]);
|
|
@@ -2058,6 +2064,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2058
2064
|
if (a === "--prod") envTarget = "prod";
|
|
2059
2065
|
else if (a === "--force" || a === "-f") forceDeploy = true;
|
|
2060
2066
|
else if (a === "--no-backend") noBackend = true;
|
|
2067
|
+
else if (a === "--rollback") isRollback = true;
|
|
2061
2068
|
else if (a === "--app" || a === "-a") appId = deployArgs[++i];
|
|
2062
2069
|
else if (a === "--static") {
|
|
2063
2070
|
isStatic = true;
|
|
@@ -2075,6 +2082,7 @@ ${BOLD}Examples:${RESET}
|
|
|
2075
2082
|
info(`사용법: gencow deploy [옵션]`);
|
|
2076
2083
|
info(` gencow deploy 백엔드 배포`);
|
|
2077
2084
|
info(` gencow deploy --static 정적 파일 배포`);
|
|
2085
|
+
info(` gencow deploy --rollback 이전 버전으로 롤백`);
|
|
2078
2086
|
info(` gencow deploy --prod 프로덕션 배포`);
|
|
2079
2087
|
info(` gencow deploy logs 서버 로그 조회`);
|
|
2080
2088
|
info(` gencow deploy status 앱 상태 확인`);
|
|
@@ -2083,11 +2091,16 @@ ${BOLD}Examples:${RESET}
|
|
|
2083
2091
|
}
|
|
2084
2092
|
}
|
|
2085
2093
|
|
|
2086
|
-
// gencow.json에서 appId 로드
|
|
2094
|
+
// gencow.json에서 appId + prodApp 로드
|
|
2087
2095
|
const gencowJsonPath = resolve(process.cwd(), "gencow.json");
|
|
2096
|
+
let prodAppId = null;
|
|
2088
2097
|
if (!appId && existsSync(gencowJsonPath)) {
|
|
2089
2098
|
const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
|
|
2090
2099
|
appId = gencowJson.appId || gencowJson.appName; // 하위 호환
|
|
2100
|
+
prodAppId = gencowJson.prodApp || null;
|
|
2101
|
+
} else if (existsSync(gencowJsonPath)) {
|
|
2102
|
+
const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
|
|
2103
|
+
prodAppId = gencowJson.prodApp || null;
|
|
2091
2104
|
}
|
|
2092
2105
|
|
|
2093
2106
|
// 프로젝트명은 display용으로만 사용 (appId가 아님)
|
|
@@ -2099,9 +2112,124 @@ ${BOLD}Examples:${RESET}
|
|
|
2099
2112
|
}
|
|
2100
2113
|
if (!displayName) displayName = basename(process.cwd());
|
|
2101
2114
|
|
|
2115
|
+
// ── Rollback 분기 ─────────────────────────────────────
|
|
2116
|
+
if (isRollback) {
|
|
2117
|
+
// prodApp이 있으면 prod 대상 롤백, 없으면 dev 대상 롤백
|
|
2118
|
+
const rollbackTarget = prodAppId || appId;
|
|
2119
|
+
if (!rollbackTarget) {
|
|
2120
|
+
error("앱 ID를 찾을 수 없습니다. gencow.json이 있는 프로젝트 루트에서 실행하세요.");
|
|
2121
|
+
process.exit(1);
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
log(`\n${BOLD}${CYAN}Gencow Rollback${RESET}\n`);
|
|
2125
|
+
info(`앱: ${rollbackTarget}${prodAppId ? " (prod)" : ""}`);
|
|
2126
|
+
log("");
|
|
2127
|
+
|
|
2128
|
+
const rollbackStartTime = Date.now();
|
|
2129
|
+
const spinnerFrames = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
|
|
2130
|
+
let spinnerIdx = 0;
|
|
2131
|
+
const spinner = setInterval(() => {
|
|
2132
|
+
const elapsed = ((Date.now() - rollbackStartTime) / 1000).toFixed(0);
|
|
2133
|
+
process.stdout.write(`\r ${CYAN}${spinnerFrames[spinnerIdx++ % spinnerFrames.length]}${RESET} 롤백 중... ${DIM}(${elapsed}s)${RESET} `);
|
|
2134
|
+
}, 120);
|
|
2135
|
+
|
|
2136
|
+
const rollbackRes = await platformFetch(creds, `/platform/apps/${rollbackTarget}/rollback`, {
|
|
2137
|
+
method: "POST",
|
|
2138
|
+
headers: { "Content-Type": "application/json" },
|
|
2139
|
+
});
|
|
2140
|
+
|
|
2141
|
+
clearInterval(spinner);
|
|
2142
|
+
process.stdout.write("\r" + " ".repeat(50) + "\r");
|
|
2143
|
+
|
|
2144
|
+
if (!rollbackRes.ok) {
|
|
2145
|
+
const errData = await rollbackRes.json().catch(() => ({}));
|
|
2146
|
+
error(`롤백 실패: ${errData.error || rollbackRes.statusText}`);
|
|
2147
|
+
process.exit(1);
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
const rollbackData = await rollbackRes.json();
|
|
2151
|
+
const rollbackElapsed = ((Date.now() - rollbackStartTime) / 1000).toFixed(1);
|
|
2152
|
+
|
|
2153
|
+
log("");
|
|
2154
|
+
success(`🔄 롤백 완료! (${rollbackElapsed}s)`);
|
|
2155
|
+
info(`롤백: #${rollbackData.rolledBackFrom} → #${rollbackData.rolledBackTo}`);
|
|
2156
|
+
info(`번들: ${rollbackData.bundleHash}`);
|
|
2157
|
+
info(`URL: ${rollbackData.url}`);
|
|
2158
|
+
log("");
|
|
2159
|
+
warn(`ℹ️ 코드만 롤백되었습니다. 데이터베이스는 변경되지 않았습니다.`);
|
|
2160
|
+
log("");
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2102
2164
|
// ── Static Deploy 분기 ────────────────────────────────
|
|
2103
2165
|
if (isStatic) {
|
|
2104
|
-
|
|
2166
|
+
// prod 모드면 prodApp 대상으로 정적 배포
|
|
2167
|
+
const staticTarget = (envTarget === "prod" && prodAppId) ? prodAppId : appId;
|
|
2168
|
+
return await this._deployStatic(creds, staticTarget, displayName, staticDir, gencowJsonPath, { forceDeploy, noBackend, envTarget });
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
// ── Prod 앱 자동 생성 + 배포 대상 전환 (Pro+) ──────────
|
|
2172
|
+
if (envTarget === "prod" && appId) {
|
|
2173
|
+
if (!prodAppId) {
|
|
2174
|
+
// prod 앱이 없음 — 첫 프로덕션 배포
|
|
2175
|
+
const ciMode = process.env.CI === "true" || deployArgs.includes("--yes");
|
|
2176
|
+
if (!ciMode) {
|
|
2177
|
+
const { createInterface } = await import("readline");
|
|
2178
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2179
|
+
log("");
|
|
2180
|
+
log(` ${BOLD}🚀 First production deployment!${RESET}`);
|
|
2181
|
+
log(` This will create a production app: ${CYAN}${appId}-prod${RESET}`);
|
|
2182
|
+
log("");
|
|
2183
|
+
const answer = await new Promise(resolve => {
|
|
2184
|
+
rl.question(` ${YELLOW}⚠${RESET} Proceed? (y/N) `, resolve);
|
|
2185
|
+
});
|
|
2186
|
+
rl.close();
|
|
2187
|
+
if (answer.toLowerCase() !== "y") {
|
|
2188
|
+
info("배포 취소됨.");
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
info("Prod 앱 생성 중...");
|
|
2194
|
+
const createProdRes = await platformFetch(creds, `/platform/apps/${appId}/create-prod`, {
|
|
2195
|
+
method: "POST",
|
|
2196
|
+
headers: { "Content-Type": "application/json" },
|
|
2197
|
+
});
|
|
2198
|
+
|
|
2199
|
+
if (!createProdRes.ok) {
|
|
2200
|
+
const errData = await createProdRes.json().catch(() => ({}));
|
|
2201
|
+
if (createProdRes.status === 403) {
|
|
2202
|
+
error(errData.error || "Production deploys require Pro plan or higher.");
|
|
2203
|
+
info("Run: gencow upgrade");
|
|
2204
|
+
} else {
|
|
2205
|
+
error(`Prod 앱 생성 실패: ${errData.error || createProdRes.statusText}`);
|
|
2206
|
+
}
|
|
2207
|
+
process.exit(1);
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
const createProdData = await createProdRes.json();
|
|
2211
|
+
prodAppId = createProdData.prodApp;
|
|
2212
|
+
|
|
2213
|
+
// gencow.json 업데이트
|
|
2214
|
+
const gencowJson = existsSync(gencowJsonPath)
|
|
2215
|
+
? JSON.parse(readFileSync(gencowJsonPath, "utf8"))
|
|
2216
|
+
: {};
|
|
2217
|
+
gencowJson.prodApp = prodAppId;
|
|
2218
|
+
writeFileSync(gencowJsonPath, JSON.stringify(gencowJson, null, 2));
|
|
2219
|
+
|
|
2220
|
+
if (createProdData.alreadyExists) {
|
|
2221
|
+
info(`Prod 앱 확인: ${prodAppId}`);
|
|
2222
|
+
} else {
|
|
2223
|
+
success(`Prod 앱 생성 완료: ${prodAppId}`);
|
|
2224
|
+
info(`URL: ${createProdData.url}`);
|
|
2225
|
+
// 프로비저닝 대기
|
|
2226
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
// 배포 대상을 prod 앱으로 전환
|
|
2231
|
+
appId = prodAppId;
|
|
2232
|
+
info(`배포 대상: ${CYAN}${appId}${RESET} (production)`);
|
|
2105
2233
|
}
|
|
2106
2234
|
|
|
2107
2235
|
log(`\n${BOLD}${CYAN}Gencow Deploy${RESET}\n`);
|
|
@@ -2114,20 +2242,6 @@ ${BOLD}Examples:${RESET}
|
|
|
2114
2242
|
info(`포맷: tar.gz`);
|
|
2115
2243
|
log("");
|
|
2116
2244
|
|
|
2117
|
-
// 프로덕션 배포 확인
|
|
2118
|
-
if (envTarget === "prod") {
|
|
2119
|
-
const { createInterface } = await import("readline");
|
|
2120
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2121
|
-
const answer = await new Promise(resolve => {
|
|
2122
|
-
rl.question(` ${YELLOW}⚠${RESET} 프로덕션 배포를 진행하시겠습니까? (y/N) `, resolve);
|
|
2123
|
-
});
|
|
2124
|
-
rl.close();
|
|
2125
|
-
if (answer.toLowerCase() !== "y") {
|
|
2126
|
-
info("배포 취소됨.");
|
|
2127
|
-
return;
|
|
2128
|
-
}
|
|
2129
|
-
}
|
|
2130
|
-
|
|
2131
2245
|
// 0-1. drizzle-kit generate 자동 실행 (번들링 전 migrations/ 최신화)
|
|
2132
2246
|
{
|
|
2133
2247
|
const { execSync: execGen } = await import("child_process");
|
|
@@ -2946,9 +3060,19 @@ ${BOLD}Examples:${RESET}
|
|
|
2946
3060
|
if (existsSync(gencowJsonPath)) {
|
|
2947
3061
|
const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
|
|
2948
3062
|
appId = gencowJson.appId || gencowJson.appName; // 하위 호환
|
|
3063
|
+
// --prod 시 prod 앱으로 대상 전환
|
|
3064
|
+
if (envTarget === "prod" && gencowJson.prodApp) {
|
|
3065
|
+
appId = gencowJson.prodApp;
|
|
3066
|
+
}
|
|
2949
3067
|
}
|
|
2950
3068
|
}
|
|
2951
3069
|
|
|
3070
|
+
// --prod인데 prod 앱이 없는 경우
|
|
3071
|
+
if (envTarget === "prod" && appId && !appId.endsWith("-prod")) {
|
|
3072
|
+
error("Prod 앱이 아직 없습니다. gencow deploy --prod를 먼저 실행하세요.");
|
|
3073
|
+
return;
|
|
3074
|
+
}
|
|
3075
|
+
|
|
2952
3076
|
if (!appId) {
|
|
2953
3077
|
error("앱 ID를 찾을 수 없습니다. gencow deploy를 먼저 실행하세요.");
|
|
2954
3078
|
return;
|
|
@@ -3083,6 +3207,138 @@ ${BOLD}Examples:${RESET}
|
|
|
3083
3207
|
}
|
|
3084
3208
|
},
|
|
3085
3209
|
|
|
3210
|
+
// ── config ────────────────────────────────────────────
|
|
3211
|
+
async config(...configArgs) {
|
|
3212
|
+
const creds = requireCreds();
|
|
3213
|
+
const subCmd = configArgs[0] || "help";
|
|
3214
|
+
const restArgs = configArgs.slice(1);
|
|
3215
|
+
|
|
3216
|
+
// 앱 ID 결정
|
|
3217
|
+
let appId = null;
|
|
3218
|
+
for (let i = 0; i < restArgs.length; i++) {
|
|
3219
|
+
if (restArgs[i] === "--app" || restArgs[i] === "-a") appId = restArgs[++i];
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
if (!appId) {
|
|
3223
|
+
const gencowJsonPath = resolve(process.cwd(), "gencow.json");
|
|
3224
|
+
if (existsSync(gencowJsonPath)) {
|
|
3225
|
+
const gencowJson = JSON.parse(readFileSync(gencowJsonPath, "utf8"));
|
|
3226
|
+
appId = gencowJson.appId || gencowJson.appName;
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
if (subCmd === "help" || subCmd === "--help" || subCmd === "-h") {
|
|
3231
|
+
log(`\n${BOLD}${CYAN}gencow config${RESET} — 앱 설정 관리\n`);
|
|
3232
|
+
log(` ${CYAN}set${RESET} image.maxWidth <값> Auto WebP 최대 폭 (px)`);
|
|
3233
|
+
log(` ${CYAN}set${RESET} image.quality <값> Auto WebP 품질 (1-100)`);
|
|
3234
|
+
log(` ${CYAN}get${RESET} image 현재 이미지 설정 조회`);
|
|
3235
|
+
log(` ${CYAN}reset${RESET} image 이미지 설정 초기화 (Tier 기본값)\n`);
|
|
3236
|
+
log(` ${DIM}옵션:${RESET}`);
|
|
3237
|
+
log(` ${DIM}--app, -a <앱이름> 대상 앱 지정 (기본: gencow.json)${RESET}\n`);
|
|
3238
|
+
return;
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
if (!appId) {
|
|
3242
|
+
error("앱 ID를 찾을 수 없습니다. gencow deploy를 먼저 실행하세요.");
|
|
3243
|
+
return;
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
switch (subCmd) {
|
|
3247
|
+
case "set": {
|
|
3248
|
+
const key = restArgs.find((a, i) => !a.startsWith("-") && !(restArgs[i - 1] === "--app" || restArgs[i - 1] === "-a"));
|
|
3249
|
+
const val = restArgs[restArgs.indexOf(key) + 1];
|
|
3250
|
+
|
|
3251
|
+
if (!key || val === undefined) {
|
|
3252
|
+
error("사용법: gencow config set image.maxWidth <값>");
|
|
3253
|
+
return;
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
const imageConfig = {};
|
|
3257
|
+
if (key === "image.maxWidth" || key === "image.max-width") {
|
|
3258
|
+
const v = parseInt(val);
|
|
3259
|
+
if (isNaN(v) || v < 0 || v > 10000) {
|
|
3260
|
+
error("maxWidth는 0~10000 사이의 값이어야 합니다. (0 = 리셋)");
|
|
3261
|
+
return;
|
|
3262
|
+
}
|
|
3263
|
+
imageConfig.autoMaxWidth = v;
|
|
3264
|
+
} else if (key === "image.quality") {
|
|
3265
|
+
const v = parseInt(val);
|
|
3266
|
+
if (isNaN(v) || v < 0 || v > 100) {
|
|
3267
|
+
error("quality는 0~100 사이의 값이어야 합니다. (0 = 리셋)");
|
|
3268
|
+
return;
|
|
3269
|
+
}
|
|
3270
|
+
imageConfig.autoQuality = v;
|
|
3271
|
+
} else {
|
|
3272
|
+
error(`알 수 없는 설정 키: ${key}`);
|
|
3273
|
+
info("사용 가능: image.maxWidth, image.quality");
|
|
3274
|
+
return;
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
const res = await rpcMutation(creds, "apps.updateImageConfig", { name: appId, imageConfig });
|
|
3278
|
+
if (res.ok) {
|
|
3279
|
+
const data = await res.json();
|
|
3280
|
+
success(`${key} = ${val} 설정 완료`);
|
|
3281
|
+
if (data.imageConfig) {
|
|
3282
|
+
info(`현재 설정: ${JSON.stringify(data.imageConfig)}`);
|
|
3283
|
+
}
|
|
3284
|
+
info(`${DIM}※ 다음 이미지 요청부터 적용됩니다. 기존 캐시는 별도 키로 저장됩니다.${RESET}`);
|
|
3285
|
+
} else {
|
|
3286
|
+
const errData = await res.json().catch(() => ({}));
|
|
3287
|
+
error(`설정 실패: ${errData.error || res.statusText}`);
|
|
3288
|
+
}
|
|
3289
|
+
break;
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
case "get": {
|
|
3293
|
+
const key = restArgs.find(a => !a.startsWith("-"));
|
|
3294
|
+
if (key && key !== "image") {
|
|
3295
|
+
error(`알 수 없는 설정 키: ${key}`);
|
|
3296
|
+
info("사용 가능: image");
|
|
3297
|
+
return;
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
const res = await rpcQuery(creds, "apps.getImageConfig", { name: appId });
|
|
3301
|
+
if (res.ok) {
|
|
3302
|
+
const config = await res.json();
|
|
3303
|
+
log(`\n${BOLD}${CYAN}이미지 설정${RESET} — ${appId}\n`);
|
|
3304
|
+
if (Object.keys(config).length === 0) {
|
|
3305
|
+
info("커스텀 설정 없음 (Tier 기본값 사용)");
|
|
3306
|
+
} else {
|
|
3307
|
+
if (config.autoMaxWidth) log(` ${GREEN}autoMaxWidth${RESET} ${config.autoMaxWidth} px`);
|
|
3308
|
+
if (config.autoQuality) log(` ${GREEN}autoQuality${RESET} ${config.autoQuality}`);
|
|
3309
|
+
}
|
|
3310
|
+
log("");
|
|
3311
|
+
} else {
|
|
3312
|
+
error("설정 조회 실패");
|
|
3313
|
+
}
|
|
3314
|
+
break;
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
case "reset": {
|
|
3318
|
+
const key = restArgs.find(a => !a.startsWith("-"));
|
|
3319
|
+
if (key && key !== "image") {
|
|
3320
|
+
error(`알 수 없는 설정 키: ${key}`);
|
|
3321
|
+
return;
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
// autoMaxWidth=0 + autoQuality=0 → 둘 다 리셋
|
|
3325
|
+
const res = await rpcMutation(creds, "apps.updateImageConfig", {
|
|
3326
|
+
name: appId,
|
|
3327
|
+
imageConfig: { autoMaxWidth: 0, autoQuality: 0 },
|
|
3328
|
+
});
|
|
3329
|
+
if (res.ok) {
|
|
3330
|
+
success("이미지 설정 초기화 완료 (Tier 기본값 사용)");
|
|
3331
|
+
} else {
|
|
3332
|
+
error("초기화 실패");
|
|
3333
|
+
}
|
|
3334
|
+
break;
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
default:
|
|
3338
|
+
error(`알 수 없는 하위 명령: ${subCmd}`);
|
|
3339
|
+
info("사용법: gencow config [set|get|reset] ...");
|
|
3340
|
+
}
|
|
3341
|
+
},
|
|
3086
3342
|
|
|
3087
3343
|
// ── files ─────────────────────────────────────────────
|
|
3088
3344
|
async files(...filesArgs) {
|
package/core/index.js
CHANGED
|
@@ -1914,14 +1914,32 @@ function defineAuth(config) {
|
|
|
1914
1914
|
// ../core/src/rls.ts
|
|
1915
1915
|
import { sql } from "drizzle-orm";
|
|
1916
1916
|
import { pgPolicy } from "drizzle-orm/pg-core";
|
|
1917
|
+
var _ownerRlsRegistry = /* @__PURE__ */ new WeakMap();
|
|
1918
|
+
function getOwnerRlsMeta(table) {
|
|
1919
|
+
return _ownerRlsRegistry.get(table);
|
|
1920
|
+
}
|
|
1921
|
+
function registerOwnerRls(table, meta) {
|
|
1922
|
+
_ownerRlsRegistry.set(table, meta);
|
|
1923
|
+
}
|
|
1917
1924
|
function ownerRls(userIdColumn, options) {
|
|
1925
|
+
const colName = userIdColumn.name;
|
|
1926
|
+
if (!colName) {
|
|
1927
|
+
throw new Error(
|
|
1928
|
+
"[ownerRls] userIdColumn must have a .name property. Ensure you pass a valid Drizzle column reference (e.g. t.userId)."
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1918
1931
|
const isOwner = sql`${userIdColumn} = current_setting('app.current_user_id')`;
|
|
1919
|
-
|
|
1932
|
+
const meta = {
|
|
1933
|
+
columnName: colName,
|
|
1934
|
+
readPublic: options?.read === "public"
|
|
1935
|
+
};
|
|
1936
|
+
const policies = [
|
|
1920
1937
|
pgPolicy("rls-select", { for: "select", using: options?.read === "public" ? sql`true` : isOwner }),
|
|
1921
1938
|
pgPolicy("rls-insert", { for: "insert", withCheck: isOwner }),
|
|
1922
1939
|
pgPolicy("rls-update", { for: "update", using: isOwner, withCheck: isOwner }),
|
|
1923
1940
|
pgPolicy("rls-delete", { for: "delete", using: isOwner })
|
|
1924
1941
|
];
|
|
1942
|
+
return policies;
|
|
1925
1943
|
}
|
|
1926
1944
|
|
|
1927
1945
|
// ../core/src/rls-db.ts
|
|
@@ -1946,12 +1964,48 @@ function createRlsDb(db, userId) {
|
|
|
1946
1964
|
}
|
|
1947
1965
|
|
|
1948
1966
|
// ../core/src/crud.ts
|
|
1949
|
-
import { eq, ne, gt, gte, lt, lte, desc, asc, like, ilike, inArray, notInArray, or, and, count as drizzleCount, getTableName } from "drizzle-orm";
|
|
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
|
+
import { getTableConfig } from "drizzle-orm/pg-core";
|
|
1950
1969
|
function detectIdType(column) {
|
|
1951
1970
|
const colType = column.dataType;
|
|
1952
1971
|
if (colType === "string") return v.string();
|
|
1953
1972
|
return v.number();
|
|
1954
1973
|
}
|
|
1974
|
+
function resolvePropertyName(table, columnName) {
|
|
1975
|
+
try {
|
|
1976
|
+
const columns = getTableColumns(table);
|
|
1977
|
+
for (const [propName, col] of Object.entries(columns)) {
|
|
1978
|
+
if (col.name === columnName) return propName;
|
|
1979
|
+
}
|
|
1980
|
+
} catch {
|
|
1981
|
+
}
|
|
1982
|
+
return columnName;
|
|
1983
|
+
}
|
|
1984
|
+
function detectOwnerMeta(table) {
|
|
1985
|
+
const anyTable = table;
|
|
1986
|
+
const meta = getOwnerRlsMeta(table);
|
|
1987
|
+
if (meta) {
|
|
1988
|
+
const propName = resolvePropertyName(table, meta.columnName);
|
|
1989
|
+
const col = anyTable[propName];
|
|
1990
|
+
if (col) {
|
|
1991
|
+
return { column: col, columnName: meta.columnName, propertyName: propName, readPublic: meta.readPublic };
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
try {
|
|
1995
|
+
const config = getTableConfig(table);
|
|
1996
|
+
if (config.policies && config.policies.length > 0) {
|
|
1997
|
+
const propName = anyTable["userId"] ? "userId" : anyTable["user_id"] ? "user_id" : null;
|
|
1998
|
+
if (propName) {
|
|
1999
|
+
const userIdCol = anyTable[propName];
|
|
2000
|
+
const colName = userIdCol.name || "user_id";
|
|
2001
|
+
registerOwnerRls(table, { columnName: colName, readPublic: false });
|
|
2002
|
+
return { column: userIdCol, columnName: colName, propertyName: propName, readPublic: false };
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
} catch {
|
|
2006
|
+
}
|
|
2007
|
+
return null;
|
|
2008
|
+
}
|
|
1955
2009
|
var MAX_FILTER_DEPTH = 5;
|
|
1956
2010
|
var FILTER_OPS = ["eq", "ne", "gt", "gte", "lt", "lte", "in", "nin", "like", "ilike"];
|
|
1957
2011
|
function isValidFilterOp(op) {
|
|
@@ -2033,6 +2087,12 @@ function crud(table, options) {
|
|
|
2033
2087
|
const createdAtCol = anyTable["createdAt"];
|
|
2034
2088
|
const defaultOrderCol = createdAtCol || pk;
|
|
2035
2089
|
const userIdCol = anyTable["userId"];
|
|
2090
|
+
const ownerMeta = detectOwnerMeta(table);
|
|
2091
|
+
if (ownerMeta && isPublic && !ownerMeta.readPublic) {
|
|
2092
|
+
console.warn(
|
|
2093
|
+
`[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" }).`
|
|
2094
|
+
);
|
|
2095
|
+
}
|
|
2036
2096
|
function buildWhereConditions(args) {
|
|
2037
2097
|
const conditions = [];
|
|
2038
2098
|
if (options?.softDelete) {
|
|
@@ -2056,9 +2116,14 @@ function crud(table, options) {
|
|
|
2056
2116
|
}
|
|
2057
2117
|
return conditions.length > 0 ? and(...conditions) : void 0;
|
|
2058
2118
|
}
|
|
2059
|
-
async function fetchListWithTotal(db, whereClause) {
|
|
2060
|
-
|
|
2061
|
-
|
|
2119
|
+
async function fetchListWithTotal(db, whereClause, userId) {
|
|
2120
|
+
let effectiveWhere = whereClause;
|
|
2121
|
+
if (ownerMeta && userId && !ownerMeta.readPublic) {
|
|
2122
|
+
const ownerFilter = eq(ownerMeta.column, userId);
|
|
2123
|
+
effectiveWhere = effectiveWhere ? and(effectiveWhere, ownerFilter) : ownerFilter;
|
|
2124
|
+
}
|
|
2125
|
+
const data = await db.select().from(anyTable).where(effectiveWhere).orderBy(desc(defaultOrderCol));
|
|
2126
|
+
const countResult = await db.select({ count: drizzleCount() }).from(anyTable).where(effectiveWhere);
|
|
2062
2127
|
return { data, total: Number(countResult[0]?.count ?? 0) };
|
|
2063
2128
|
}
|
|
2064
2129
|
const enabledMethods = new Set(options?.methods ?? ["list", "get", "create", "update", "remove"]);
|
|
@@ -2073,11 +2138,16 @@ function crud(table, options) {
|
|
|
2073
2138
|
filters: v.optional(v.any())
|
|
2074
2139
|
},
|
|
2075
2140
|
handler: async (ctx, args) => {
|
|
2076
|
-
|
|
2141
|
+
const needsAuth = !isPublic || ownerMeta && !ownerMeta.readPublic;
|
|
2142
|
+
const user = needsAuth ? ctx.auth.requireAuth() : null;
|
|
2077
2143
|
const page = Math.max(1, args?.page || 1);
|
|
2078
2144
|
const limit = Math.min(Math.max(1, args?.limit || defaultLimit), maxLimit);
|
|
2079
2145
|
const offset = (page - 1) * limit;
|
|
2080
|
-
|
|
2146
|
+
let whereClause = buildWhereConditions(args);
|
|
2147
|
+
if (ownerMeta && !ownerMeta.readPublic && user) {
|
|
2148
|
+
const ownerFilter = eq(ownerMeta.column, user.id);
|
|
2149
|
+
whereClause = whereClause ? and(whereClause, ownerFilter) : ownerFilter;
|
|
2150
|
+
}
|
|
2081
2151
|
let orderByClause;
|
|
2082
2152
|
if (args?.orderBy && anyTable[args.orderBy]) {
|
|
2083
2153
|
const col = anyTable[args.orderBy];
|
|
@@ -2097,8 +2167,12 @@ function crud(table, options) {
|
|
|
2097
2167
|
public: isPublic,
|
|
2098
2168
|
args: { id: idValidator },
|
|
2099
2169
|
handler: async (ctx, args) => {
|
|
2100
|
-
|
|
2170
|
+
const needsAuth = !isPublic || ownerMeta && !ownerMeta.readPublic;
|
|
2171
|
+
const user = needsAuth ? ctx.auth.requireAuth() : null;
|
|
2101
2172
|
let whereCond = eq(pk, args.id);
|
|
2173
|
+
if (ownerMeta && !ownerMeta.readPublic && user) {
|
|
2174
|
+
whereCond = and(whereCond, eq(ownerMeta.column, user.id));
|
|
2175
|
+
}
|
|
2102
2176
|
if (options?.softDelete) {
|
|
2103
2177
|
const sdField = anyTable[options.softDelete.field];
|
|
2104
2178
|
whereCond = and(whereCond, eq(sdField, null));
|
|
@@ -2110,9 +2184,11 @@ function crud(table, options) {
|
|
|
2110
2184
|
const createDef = !enabledMethods.has("create") ? void 0 : mutation(`${prefix}.create`, {
|
|
2111
2185
|
public: isPublic,
|
|
2112
2186
|
handler: async (ctx, args) => {
|
|
2113
|
-
const user = isPublic ?
|
|
2187
|
+
const user = ownerMeta || !isPublic ? ctx.auth.requireAuth() : null;
|
|
2114
2188
|
let insertData = { ...args };
|
|
2115
|
-
if (
|
|
2189
|
+
if (ownerMeta && user) {
|
|
2190
|
+
insertData[ownerMeta.propertyName] = user.id;
|
|
2191
|
+
} else if (userIdCol && user && !insertData.userId) {
|
|
2116
2192
|
insertData.userId = user.id;
|
|
2117
2193
|
}
|
|
2118
2194
|
if (options?.hooks?.beforeCreate) {
|
|
@@ -2120,7 +2196,8 @@ function crud(table, options) {
|
|
|
2120
2196
|
}
|
|
2121
2197
|
const [result] = await ctx.db.insert(anyTable).values(insertData).returning();
|
|
2122
2198
|
if (useRealtime && enabledMethods.has("list")) {
|
|
2123
|
-
const
|
|
2199
|
+
const currentUserId = user?.id;
|
|
2200
|
+
const listResult = await fetchListWithTotal(ctx.db, void 0, currentUserId);
|
|
2124
2201
|
ctx.realtime.emit(`${prefix}.list`, listResult);
|
|
2125
2202
|
}
|
|
2126
2203
|
return result;
|
|
@@ -2129,19 +2206,28 @@ function crud(table, options) {
|
|
|
2129
2206
|
const updateDef = !enabledMethods.has("update") ? void 0 : mutation(`${prefix}.update`, {
|
|
2130
2207
|
public: isPublic,
|
|
2131
2208
|
handler: async (ctx, args) => {
|
|
2132
|
-
|
|
2209
|
+
const user = ownerMeta || !isPublic ? ctx.auth.requireAuth() : null;
|
|
2133
2210
|
const { id, ...updates } = args;
|
|
2134
2211
|
let updateData = { ...updates };
|
|
2212
|
+
let updateWhere = eq(pk, id);
|
|
2213
|
+
if (ownerMeta && user) {
|
|
2214
|
+
updateWhere = and(eq(pk, id), eq(ownerMeta.column, user.id));
|
|
2215
|
+
delete updateData[ownerMeta.propertyName];
|
|
2216
|
+
if (ownerMeta.propertyName !== ownerMeta.columnName) {
|
|
2217
|
+
delete updateData[ownerMeta.columnName];
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2135
2220
|
if (anyTable["updatedAt"]) {
|
|
2136
2221
|
updateData.updatedAt = /* @__PURE__ */ new Date();
|
|
2137
2222
|
}
|
|
2138
2223
|
if (options?.hooks?.beforeUpdate) {
|
|
2139
2224
|
updateData = await options.hooks.beforeUpdate(updateData);
|
|
2140
2225
|
}
|
|
2141
|
-
const [result] = await ctx.db.update(anyTable).set(updateData).where(
|
|
2226
|
+
const [result] = await ctx.db.update(anyTable).set(updateData).where(updateWhere).returning();
|
|
2142
2227
|
if (useRealtime) {
|
|
2228
|
+
const currentUserId = ownerMeta ? user?.id : void 0;
|
|
2143
2229
|
if (enabledMethods.has("list")) {
|
|
2144
|
-
const listResult = await fetchListWithTotal(ctx.db);
|
|
2230
|
+
const listResult = await fetchListWithTotal(ctx.db, void 0, currentUserId);
|
|
2145
2231
|
ctx.realtime.emit(`${prefix}.list`, listResult);
|
|
2146
2232
|
}
|
|
2147
2233
|
if (enabledMethods.has("get")) {
|
|
@@ -2154,15 +2240,20 @@ function crud(table, options) {
|
|
|
2154
2240
|
const removeDef = !enabledMethods.has("remove") ? void 0 : mutation(`${prefix}.remove`, {
|
|
2155
2241
|
public: isPublic,
|
|
2156
2242
|
handler: async (ctx, args) => {
|
|
2157
|
-
|
|
2243
|
+
const user = ownerMeta || !isPublic ? ctx.auth.requireAuth() : null;
|
|
2244
|
+
let deleteWhere = eq(pk, args.id);
|
|
2245
|
+
if (ownerMeta && user) {
|
|
2246
|
+
deleteWhere = and(eq(pk, args.id), eq(ownerMeta.column, user.id));
|
|
2247
|
+
}
|
|
2158
2248
|
if (options?.softDelete) {
|
|
2159
2249
|
const sdField = options.softDelete.field;
|
|
2160
|
-
await ctx.db.update(anyTable).set({ [sdField]: /* @__PURE__ */ new Date() }).where(
|
|
2250
|
+
await ctx.db.update(anyTable).set({ [sdField]: /* @__PURE__ */ new Date() }).where(deleteWhere);
|
|
2161
2251
|
} else {
|
|
2162
|
-
await ctx.db.delete(anyTable).where(
|
|
2252
|
+
await ctx.db.delete(anyTable).where(deleteWhere);
|
|
2163
2253
|
}
|
|
2164
2254
|
if (useRealtime && enabledMethods.has("list")) {
|
|
2165
|
-
const
|
|
2255
|
+
const currentUserId = ownerMeta ? user?.id : void 0;
|
|
2256
|
+
const listResult = await fetchListWithTotal(ctx.db, void 0, currentUserId);
|
|
2166
2257
|
ctx.realtime.emit(`${prefix}.list`, listResult);
|
|
2167
2258
|
}
|
|
2168
2259
|
return { success: true };
|
|
@@ -2187,6 +2278,7 @@ export {
|
|
|
2187
2278
|
defineAuth,
|
|
2188
2279
|
deregisterClient,
|
|
2189
2280
|
crud as gencowCrud,
|
|
2281
|
+
getOwnerRlsMeta,
|
|
2190
2282
|
getQueryDef,
|
|
2191
2283
|
getQueryHandler,
|
|
2192
2284
|
getRegisteredHttpActions,
|
|
@@ -2201,6 +2293,7 @@ export {
|
|
|
2201
2293
|
parseFilterNode,
|
|
2202
2294
|
query,
|
|
2203
2295
|
registerClient,
|
|
2296
|
+
registerOwnerRls,
|
|
2204
2297
|
subscribe,
|
|
2205
2298
|
unsubscribe,
|
|
2206
2299
|
v,
|