gencow 0.1.125 → 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 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.44.0",
787
- "drizzle-kit": "^0.31.0",
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
- async "db:reset"() {
1053
- info(`db:reset is ${BOLD}coming soon${RESET}.`);
1054
- info(`${DIM}Use Gencow Dashboard for cloud DB management.${RESET}\n`);
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.includes("--local");
1139
+ const isLocal = isLocalDbTarget(args);
1063
1140
 
1064
1141
  if (isLocal) {
1065
- info(`Local db:seed is ${BOLD}coming soon${RESET}.`);
1066
- info(`${DIM}Use cloud mode (default): gencow db:seed${RESET}\n`);
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
- // --local 플래그: coming soon
1202
- if (devArgs.includes("--local")) {
1203
- log(`\n${BOLD}${CYAN}Gencow Dev${RESET} ${DIM}(local)${RESET}\n`);
1204
- info(`Local development mode is ${BOLD}coming soon${RESET}.`);
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.includes("--local");
1535
+ const isLocal = isLocalDbTarget(args);
1426
1536
 
1427
1537
  if (isLocal) {
1428
- info(`Local db:push is ${BOLD}coming soon${RESET}.`);
1429
- info(`${DIM}Use cloud mode (default): gencow db:push${RESET}\n`);
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
 
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
- function createRlsDb(db, userId) {
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.execute(
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(target) : value;
2184
+ return typeof value === "function" ? value.bind(receiver) : value;
1998
2185
  }
1999
2186
  });
2000
2187
  }