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 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
- return await this._deployStatic(creds, appId, displayName, staticDir, gencowJsonPath, { forceDeploy, noBackend, envTarget });
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
- return [
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
- const data = await db.select().from(anyTable).where(whereClause).orderBy(desc(defaultOrderCol));
2061
- const countResult = await db.select({ count: drizzleCount() }).from(anyTable).where(whereClause);
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
- if (!isPublic) ctx.auth.requireAuth();
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
- const whereClause = buildWhereConditions(args);
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
- if (!isPublic) ctx.auth.requireAuth();
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 ? null : ctx.auth.requireAuth();
2187
+ const user = ownerMeta || !isPublic ? ctx.auth.requireAuth() : null;
2114
2188
  let insertData = { ...args };
2115
- if (userIdCol && user && !insertData.userId) {
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 listResult = await fetchListWithTotal(ctx.db);
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
- if (!isPublic) ctx.auth.requireAuth();
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(eq(pk, id)).returning();
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
- if (!isPublic) ctx.auth.requireAuth();
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(eq(pk, args.id));
2250
+ await ctx.db.update(anyTable).set({ [sdField]: /* @__PURE__ */ new Date() }).where(deleteWhere);
2161
2251
  } else {
2162
- await ctx.db.delete(anyTable).where(eq(pk, args.id));
2252
+ await ctx.db.delete(anyTable).where(deleteWhere);
2163
2253
  }
2164
2254
  if (useRealtime && enabledMethods.has("list")) {
2165
- const listResult = await fetchListWithTotal(ctx.db);
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,