gencow 0.1.80 → 0.1.82
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 +105 -11
- package/core/index.js +325 -0
- package/dashboard/assets/{index-CLmqVsl3.js → index-RH5HoiTX.js} +57 -57
- package/dashboard/index.html +1 -1
- package/lib/deploy-auditor.mjs +3 -1
- package/lib/readme-codegen.mjs +9 -1
- package/package.json +1 -1
- package/server/index.js +5310 -25372
- package/server/index.js.map +4 -4
- package/templates/SECURITY.md +74 -50
package/bin/gencow.mjs
CHANGED
|
@@ -2,16 +2,14 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* @gencow/cli — Gencow CLI
|
|
4
4
|
*
|
|
5
|
-
* Like `npx convex dev` but for Gencow projects.
|
|
6
|
-
*
|
|
7
5
|
* Commands:
|
|
8
6
|
* gencow dev — push schema → start server with hot-reload
|
|
9
|
-
* gencow db:push — push schema.ts changes to DB (
|
|
7
|
+
* gencow db:push — push schema.ts changes to DB (instant, no migration files)
|
|
10
8
|
* gencow db:generate — generate SQL migration files from schema.ts
|
|
11
9
|
* gencow db:migrate — apply pending migrations
|
|
12
10
|
* gencow db:reset — backup + reset DB (safe reset)
|
|
13
11
|
* gencow db:restore — restore DB from backup
|
|
14
|
-
* gencow db:studio — open Drizzle Studio (
|
|
12
|
+
* gencow db:studio — open Drizzle Studio (visual DB browser)
|
|
15
13
|
* gencow dashboard — open /_admin in browser
|
|
16
14
|
* gencow backup — manage database backups (list/create/restore/delete/download)
|
|
17
15
|
* gencow help — show this help
|
|
@@ -1351,7 +1349,7 @@ ${hasPrompt ? `
|
|
|
1351
1349
|
async "db:studio"() {
|
|
1352
1350
|
const config = loadConfig();
|
|
1353
1351
|
log(`\n${BOLD}${CYAN}Gencow DB Studio${RESET}\n`);
|
|
1354
|
-
info("Opening Drizzle Studio
|
|
1352
|
+
info("Opening Drizzle Studio...");
|
|
1355
1353
|
runInServer("pnpm db:studio", buildEnv(config));
|
|
1356
1354
|
},
|
|
1357
1355
|
|
|
@@ -1739,12 +1737,14 @@ ${BOLD}Examples:${RESET}
|
|
|
1739
1737
|
}
|
|
1740
1738
|
|
|
1741
1739
|
// 1-0. Pre-deploy dependency audit (informational — user deps auto-installed)
|
|
1740
|
+
// + Phase B: filter package.json to backend-only deps
|
|
1741
|
+
let auditResult = null;
|
|
1742
1742
|
if (!forceDeploy) {
|
|
1743
1743
|
try {
|
|
1744
1744
|
const { auditDeployDependencies, formatAuditError } = await import("../lib/deploy-auditor.mjs");
|
|
1745
1745
|
const entryPoint = resolve(process.cwd(), "gencow", "index.ts");
|
|
1746
1746
|
if (existsSync(entryPoint)) {
|
|
1747
|
-
|
|
1747
|
+
auditResult = await auditDeployDependencies(entryPoint);
|
|
1748
1748
|
const auditMsg = formatAuditError(auditResult);
|
|
1749
1749
|
if (auditMsg) log(auditMsg);
|
|
1750
1750
|
}
|
|
@@ -1753,13 +1753,61 @@ ${BOLD}Examples:${RESET}
|
|
|
1753
1753
|
}
|
|
1754
1754
|
}
|
|
1755
1755
|
|
|
1756
|
+
// Generate filtered package.json with only backend runtime deps
|
|
1757
|
+
let useFilteredPkg = false;
|
|
1758
|
+
const filteredPkgPath = resolve(process.cwd(), ".gencow", "deploy-package.json");
|
|
1759
|
+
if (auditResult?.runtimeDeps?.length >= 0) {
|
|
1760
|
+
try {
|
|
1761
|
+
const projectPkg = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf8"));
|
|
1762
|
+
const allDeps = { ...projectPkg.dependencies, ...projectPkg.devDependencies };
|
|
1763
|
+
const runtimeDeps = {};
|
|
1764
|
+
for (const dep of auditResult.runtimeDeps) {
|
|
1765
|
+
if (allDeps[dep]) runtimeDeps[dep] = allDeps[dep];
|
|
1766
|
+
}
|
|
1767
|
+
const filteredPkg = { name: projectPkg.name || "gencow-app", dependencies: runtimeDeps };
|
|
1768
|
+
writeFileSync(filteredPkgPath, JSON.stringify(filteredPkg, null, 2));
|
|
1769
|
+
useFilteredPkg = true;
|
|
1770
|
+
|
|
1771
|
+
const totalDeps = Object.keys(allDeps).length;
|
|
1772
|
+
const runtimeCount = Object.keys(runtimeDeps).length;
|
|
1773
|
+
if (totalDeps > runtimeCount) {
|
|
1774
|
+
info(`${DIM}package.json 필터링: ${runtimeCount}/${totalDeps} 패키지만 서버에 설치됩니다.${RESET}`);
|
|
1775
|
+
}
|
|
1776
|
+
} catch {
|
|
1777
|
+
// Fallback: use original package.json
|
|
1778
|
+
useFilteredPkg = false;
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1756
1782
|
try {
|
|
1757
|
-
|
|
1783
|
+
if (useFilteredPkg) {
|
|
1784
|
+
// macOS-safe: stage files in temp dir with filtered package.json
|
|
1785
|
+
const otherFiles = filesToPack.filter(f => f !== "package.json");
|
|
1786
|
+
const tmpDir = resolve(process.cwd(), ".gencow", "deploy-staging");
|
|
1787
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
1788
|
+
writeFileSync(resolve(tmpDir, "package.json"), readFileSync(filteredPkgPath, "utf8"));
|
|
1789
|
+
for (const f of otherFiles) {
|
|
1790
|
+
const src = resolve(process.cwd(), f);
|
|
1791
|
+
const dst = resolve(tmpDir, f);
|
|
1792
|
+
if (f.endsWith("/")) {
|
|
1793
|
+
exec(`cp -r "${src}" "${dst}"`, { cwd: process.cwd() });
|
|
1794
|
+
} else if (existsSync(src)) {
|
|
1795
|
+
exec(`cp "${src}" "${dst}"`, { cwd: process.cwd() });
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
exec(`tar -czf "${tmpBundle}" .`, { cwd: tmpDir });
|
|
1799
|
+
exec(`rm -rf "${tmpDir}"`, { cwd: process.cwd() });
|
|
1800
|
+
} else {
|
|
1801
|
+
exec(`tar -czf "${tmpBundle}" ${filesToPack.join(" ")}`, { cwd: process.cwd() });
|
|
1802
|
+
}
|
|
1758
1803
|
} catch (e) {
|
|
1759
1804
|
error(`패키징 실패: ${e.message}`);
|
|
1760
1805
|
process.exit(1);
|
|
1761
1806
|
}
|
|
1762
1807
|
|
|
1808
|
+
// Clean up temp filtered package.json
|
|
1809
|
+
try { if (useFilteredPkg) unlinkSync(filteredPkgPath); } catch {}
|
|
1810
|
+
|
|
1763
1811
|
const bundleSize = statSync(tmpBundle).size;
|
|
1764
1812
|
success(`번들 생성: ${(bundleSize / 1024).toFixed(1)} KB`);
|
|
1765
1813
|
|
|
@@ -2011,13 +2059,14 @@ ${BOLD}Examples:${RESET}
|
|
|
2011
2059
|
if (shouldDeployBackend) {
|
|
2012
2060
|
log(` ${BOLD}── 백엔드 배포 ──────────────────────${RESET}\n`);
|
|
2013
2061
|
|
|
2014
|
-
// 1-0. Pre-deploy dependency audit
|
|
2062
|
+
// 1-0. Pre-deploy dependency audit + Phase B: filter package.json
|
|
2063
|
+
let auditResult = null;
|
|
2015
2064
|
if (!forceDeploy) {
|
|
2016
2065
|
try {
|
|
2017
2066
|
const { auditDeployDependencies, formatAuditError } = await import("../lib/deploy-auditor.mjs");
|
|
2018
2067
|
const entryPoint = resolve(backendRoot, "gencow", "index.ts");
|
|
2019
2068
|
if (existsSync(entryPoint)) {
|
|
2020
|
-
|
|
2069
|
+
auditResult = await auditDeployDependencies(entryPoint);
|
|
2021
2070
|
const auditMsg = formatAuditError(auditResult);
|
|
2022
2071
|
if (auditMsg) log(auditMsg);
|
|
2023
2072
|
}
|
|
@@ -2037,12 +2086,57 @@ ${BOLD}Examples:${RESET}
|
|
|
2037
2086
|
if (existsSync(resolve(backendRoot, "package-lock.json"))) backendFiles.push("package-lock.json");
|
|
2038
2087
|
if (existsSync(resolve(backendRoot, "tsconfig.json"))) backendFiles.push("tsconfig.json");
|
|
2039
2088
|
|
|
2089
|
+
// Generate filtered package.json with only backend runtime deps
|
|
2090
|
+
let useFilteredPkg = false;
|
|
2091
|
+
const filteredPkgPath = resolve(backendRoot, ".gencow", "deploy-package.json");
|
|
2092
|
+
if (auditResult?.runtimeDeps?.length >= 0) {
|
|
2093
|
+
try {
|
|
2094
|
+
const projectPkg = JSON.parse(readFileSync(resolve(backendRoot, "package.json"), "utf8"));
|
|
2095
|
+
const allDeps = { ...projectPkg.dependencies, ...projectPkg.devDependencies };
|
|
2096
|
+
const runtimeDeps = {};
|
|
2097
|
+
for (const dep of auditResult.runtimeDeps) {
|
|
2098
|
+
if (allDeps[dep]) runtimeDeps[dep] = allDeps[dep];
|
|
2099
|
+
}
|
|
2100
|
+
const filteredPkg = { name: projectPkg.name || "gencow-app", dependencies: runtimeDeps };
|
|
2101
|
+
writeFileSync(filteredPkgPath, JSON.stringify(filteredPkg, null, 2));
|
|
2102
|
+
useFilteredPkg = true;
|
|
2103
|
+
|
|
2104
|
+
const totalDeps = Object.keys(allDeps).length;
|
|
2105
|
+
const runtimeCount = Object.keys(runtimeDeps).length;
|
|
2106
|
+
if (totalDeps > runtimeCount) {
|
|
2107
|
+
info(`${DIM}package.json 필터링: ${runtimeCount}/${totalDeps} 패키지만 서버에 설치됩니다.${RESET}`);
|
|
2108
|
+
}
|
|
2109
|
+
} catch {
|
|
2110
|
+
useFilteredPkg = false;
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2040
2114
|
try {
|
|
2041
|
-
|
|
2115
|
+
if (useFilteredPkg) {
|
|
2116
|
+
const otherFiles = backendFiles.filter(f => f !== "package.json");
|
|
2117
|
+
// macOS-safe: stage files in temp dir
|
|
2118
|
+
const tmpDir = resolve(backendRoot, ".gencow", "deploy-staging");
|
|
2119
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
2120
|
+
writeFileSync(resolve(tmpDir, "package.json"), readFileSync(filteredPkgPath, "utf8"));
|
|
2121
|
+
for (const f of otherFiles) {
|
|
2122
|
+
const src = resolve(backendRoot, f);
|
|
2123
|
+
const dst = resolve(tmpDir, f);
|
|
2124
|
+
if (f.endsWith("/")) {
|
|
2125
|
+
exec(`cp -r "${src}" "${dst}"`, { cwd: backendRoot });
|
|
2126
|
+
} else if (existsSync(src)) {
|
|
2127
|
+
exec(`cp "${src}" "${dst}"`, { cwd: backendRoot });
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
exec(`tar -czf "${tmpBackendBundle}" .`, { cwd: tmpDir });
|
|
2131
|
+
exec(`rm -rf "${tmpDir}"`, { cwd: backendRoot });
|
|
2132
|
+
} else {
|
|
2133
|
+
exec(`tar -czf "${tmpBackendBundle}" ${backendFiles.join(" ")}`, { cwd: backendRoot });
|
|
2134
|
+
}
|
|
2042
2135
|
} catch (e) {
|
|
2043
2136
|
error(`백엔드 패키징 실패: ${e.message}`);
|
|
2044
2137
|
process.exit(1);
|
|
2045
2138
|
}
|
|
2139
|
+
try { if (useFilteredPkg) unlinkSync(filteredPkgPath); } catch {}
|
|
2046
2140
|
|
|
2047
2141
|
const backendBundleSize = statSync(tmpBackendBundle).size;
|
|
2048
2142
|
success(`백엔드 번들 생성: ${(backendBundleSize / 1024).toFixed(1)} KB`);
|
|
@@ -2837,7 +2931,7 @@ process.exit(0);
|
|
|
2837
2931
|
// Save current app to creds
|
|
2838
2932
|
saveCreds({ ...creds, currentApp: name });
|
|
2839
2933
|
|
|
2840
|
-
// ── Scaffold local project files
|
|
2934
|
+
// ── Scaffold local project files ────────────────────
|
|
2841
2935
|
const cwd = process.cwd();
|
|
2842
2936
|
const config = loadConfig();
|
|
2843
2937
|
|
package/core/index.js
CHANGED
|
@@ -1822,23 +1822,348 @@ function intervalToPattern(options) {
|
|
|
1822
1822
|
function defineAuth(config) {
|
|
1823
1823
|
return config;
|
|
1824
1824
|
}
|
|
1825
|
+
|
|
1826
|
+
// ../core/src/table.ts
|
|
1827
|
+
import { pgTable } from "drizzle-orm/pg-core";
|
|
1828
|
+
import { eq } from "drizzle-orm";
|
|
1829
|
+
function isOwnerFilter(opts) {
|
|
1830
|
+
return "_ownerColumn" in opts;
|
|
1831
|
+
}
|
|
1832
|
+
if (!globalThis.__gencow_tableAccessRegistry) {
|
|
1833
|
+
globalThis.__gencow_tableAccessRegistry = /* @__PURE__ */ new Map();
|
|
1834
|
+
}
|
|
1835
|
+
var tableAccessRegistry = globalThis.__gencow_tableAccessRegistry;
|
|
1836
|
+
function gencowTable(name, columns, options) {
|
|
1837
|
+
if (!options || typeof options.filter !== "function" && !isOwnerFilter(options)) {
|
|
1838
|
+
throw new Error(
|
|
1839
|
+
`[gencow] gencowTable("${name}") requires a filter option. Use ownerFilter("userId") for simple user isolation, or { filter: () => true } for public tables.`
|
|
1840
|
+
);
|
|
1841
|
+
}
|
|
1842
|
+
const table = pgTable(name, columns);
|
|
1843
|
+
let filter;
|
|
1844
|
+
if (isOwnerFilter(options)) {
|
|
1845
|
+
const columnName = options._ownerColumn;
|
|
1846
|
+
const col = table[columnName];
|
|
1847
|
+
if (!col) {
|
|
1848
|
+
throw new Error(
|
|
1849
|
+
`[gencow] ownerFilter("${columnName}"): column "${columnName}" not found on table "${name}". Available columns: ${Object.keys(table).filter((k) => !k.startsWith("_") && !k.startsWith("$")).join(", ")}`
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1852
|
+
filter = (ctx) => {
|
|
1853
|
+
const user = ctx.auth.requireAuth();
|
|
1854
|
+
return eq(col, user.id);
|
|
1855
|
+
};
|
|
1856
|
+
} else {
|
|
1857
|
+
filter = options.filter;
|
|
1858
|
+
}
|
|
1859
|
+
tableAccessRegistry.set(table, {
|
|
1860
|
+
filter,
|
|
1861
|
+
fieldAccess: options.fieldAccess,
|
|
1862
|
+
tableName: name
|
|
1863
|
+
});
|
|
1864
|
+
return table;
|
|
1865
|
+
}
|
|
1866
|
+
function ownerFilter(columnName = "userId") {
|
|
1867
|
+
return {
|
|
1868
|
+
_ownerColumn: columnName,
|
|
1869
|
+
// Placeholder filter — replaced by gencowTable()
|
|
1870
|
+
filter: () => {
|
|
1871
|
+
throw new Error("[gencow] ownerFilter placeholder should not be called directly");
|
|
1872
|
+
}
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
function getTableAccessMeta(table) {
|
|
1876
|
+
return tableAccessRegistry.get(table);
|
|
1877
|
+
}
|
|
1878
|
+
function isGencowTable(table) {
|
|
1879
|
+
return tableAccessRegistry.has(table);
|
|
1880
|
+
}
|
|
1881
|
+
function getAllGencowTables() {
|
|
1882
|
+
return new Map(tableAccessRegistry);
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
// ../core/src/scoped-db.ts
|
|
1886
|
+
import { and } from "drizzle-orm";
|
|
1887
|
+
function createScopedDb(db, ctx) {
|
|
1888
|
+
return new Proxy(db, {
|
|
1889
|
+
get(target, prop) {
|
|
1890
|
+
const propStr = typeof prop === "string" ? prop : "";
|
|
1891
|
+
if (propStr === "execute") {
|
|
1892
|
+
return () => {
|
|
1893
|
+
throw new Error(
|
|
1894
|
+
"[gencow] ctx.db.execute() is not allowed. Use ctx.db.select().from(table) for type-safe queries with automatic access control. If you need raw SQL, use ctx.unsafeDb.execute()."
|
|
1895
|
+
);
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
if (propStr === "$client" || propStr === "_") {
|
|
1899
|
+
throw new Error(
|
|
1900
|
+
`[gencow] ctx.db.${propStr} is not allowed. Direct database client access bypasses access control. Use ctx.unsafeDb if you need direct access.`
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1903
|
+
if (propStr === "select") {
|
|
1904
|
+
return (...selectArgs) => {
|
|
1905
|
+
const selectResult = target.select(...selectArgs);
|
|
1906
|
+
return wrapSelectChain(selectResult, ctx);
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1909
|
+
if (propStr === "update") {
|
|
1910
|
+
return (table) => {
|
|
1911
|
+
const updateResult = target.update(table);
|
|
1912
|
+
return wrapWriteChain(updateResult, table, ctx);
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
if (propStr === "delete") {
|
|
1916
|
+
return (table) => {
|
|
1917
|
+
const deleteResult = target.delete(table);
|
|
1918
|
+
return wrapWriteChain(deleteResult, table, ctx);
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
if (propStr === "query") {
|
|
1922
|
+
return wrapRelationalQuery(target.query, ctx);
|
|
1923
|
+
}
|
|
1924
|
+
const value = target[prop];
|
|
1925
|
+
if (typeof value === "function") {
|
|
1926
|
+
return value.bind(target);
|
|
1927
|
+
}
|
|
1928
|
+
return value;
|
|
1929
|
+
}
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
function wrapSelectChain(selectResult, ctx) {
|
|
1933
|
+
return new Proxy(selectResult, {
|
|
1934
|
+
get(target, prop) {
|
|
1935
|
+
const propStr = typeof prop === "string" ? prop : "";
|
|
1936
|
+
if (propStr === "from") {
|
|
1937
|
+
return (table, ...restArgs) => {
|
|
1938
|
+
const fromResult = target.from(table, ...restArgs);
|
|
1939
|
+
const meta = getTableAccessMeta(table);
|
|
1940
|
+
if (meta) {
|
|
1941
|
+
return wrapFromChain(fromResult, ctx, [{ table, meta }]);
|
|
1942
|
+
}
|
|
1943
|
+
return wrapFromChain(fromResult, ctx, []);
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
const value = target[prop];
|
|
1947
|
+
if (typeof value === "function") {
|
|
1948
|
+
return value.bind(target);
|
|
1949
|
+
}
|
|
1950
|
+
return value;
|
|
1951
|
+
}
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
function wrapFromChain(chain, ctx, pendingFilters) {
|
|
1955
|
+
return new Proxy(chain, {
|
|
1956
|
+
get(target, prop) {
|
|
1957
|
+
const propStr = typeof prop === "string" ? prop : "";
|
|
1958
|
+
if (["leftJoin", "rightJoin", "innerJoin", "fullJoin"].includes(propStr)) {
|
|
1959
|
+
return (joinTable, ...joinArgs) => {
|
|
1960
|
+
const joinResult = target[propStr](joinTable, ...joinArgs);
|
|
1961
|
+
const joinMeta = getTableAccessMeta(joinTable);
|
|
1962
|
+
const newFilters = joinMeta ? [...pendingFilters, { table: joinTable, meta: joinMeta }] : pendingFilters;
|
|
1963
|
+
return wrapFromChain(joinResult, ctx, newFilters);
|
|
1964
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
if (propStr === "where") {
|
|
1967
|
+
return (...whereArgs) => {
|
|
1968
|
+
const combinedFilter = buildCombinedFilter(pendingFilters, ctx);
|
|
1969
|
+
if (combinedFilter) {
|
|
1970
|
+
const userWhere = whereArgs[0];
|
|
1971
|
+
const merged = userWhere ? and(userWhere, combinedFilter) : combinedFilter;
|
|
1972
|
+
const result = target.where(merged);
|
|
1973
|
+
return wrapFromChain(result, ctx, []);
|
|
1974
|
+
}
|
|
1975
|
+
return wrapFromChain(target.where(...whereArgs), ctx, []);
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
if (propStr === "then" || propStr === "execute") {
|
|
1979
|
+
if (pendingFilters.length > 0) {
|
|
1980
|
+
const combinedFilter = buildCombinedFilter(pendingFilters, ctx);
|
|
1981
|
+
if (combinedFilter) {
|
|
1982
|
+
const filtered = target.where(combinedFilter);
|
|
1983
|
+
return filtered[prop].bind(filtered);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
const value2 = target[prop];
|
|
1987
|
+
return typeof value2 === "function" ? value2.bind(target) : value2;
|
|
1988
|
+
}
|
|
1989
|
+
const value = target[prop];
|
|
1990
|
+
if (typeof value === "function") {
|
|
1991
|
+
return (...args) => {
|
|
1992
|
+
const result = value.apply(target, args);
|
|
1993
|
+
if (result && typeof result === "object" && typeof result.then === "function") {
|
|
1994
|
+
return wrapFromChain(result, ctx, pendingFilters);
|
|
1995
|
+
}
|
|
1996
|
+
if (result && typeof result === "object" && "where" in result) {
|
|
1997
|
+
return wrapFromChain(result, ctx, pendingFilters);
|
|
1998
|
+
}
|
|
1999
|
+
return result;
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
return value;
|
|
2003
|
+
}
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
function wrapWriteChain(chain, table, ctx) {
|
|
2007
|
+
const meta = getTableAccessMeta(table);
|
|
2008
|
+
if (!meta) {
|
|
2009
|
+
return chain;
|
|
2010
|
+
}
|
|
2011
|
+
return new Proxy(chain, {
|
|
2012
|
+
get(target, prop) {
|
|
2013
|
+
const propStr = typeof prop === "string" ? prop : "";
|
|
2014
|
+
if (propStr === "where") {
|
|
2015
|
+
return (...whereArgs) => {
|
|
2016
|
+
const filterResult = evaluateFilterSync(meta, ctx);
|
|
2017
|
+
if (typeof filterResult === "boolean") {
|
|
2018
|
+
if (!filterResult) {
|
|
2019
|
+
return target.where(whereArgs[0]);
|
|
2020
|
+
}
|
|
2021
|
+
return target.where(...whereArgs);
|
|
2022
|
+
}
|
|
2023
|
+
const userWhere = whereArgs[0];
|
|
2024
|
+
const merged = userWhere ? and(userWhere, filterResult) : filterResult;
|
|
2025
|
+
return target.where(merged);
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
if (propStr === "then" || propStr === "execute" || propStr === "returning") {
|
|
2029
|
+
const filterResult = evaluateFilterSync(meta, ctx);
|
|
2030
|
+
if (filterResult && typeof filterResult !== "boolean") {
|
|
2031
|
+
const filtered = target.where(filterResult);
|
|
2032
|
+
const value3 = filtered[prop];
|
|
2033
|
+
return typeof value3 === "function" ? value3.bind(filtered) : value3;
|
|
2034
|
+
}
|
|
2035
|
+
const value2 = target[prop];
|
|
2036
|
+
return typeof value2 === "function" ? value2.bind(target) : value2;
|
|
2037
|
+
}
|
|
2038
|
+
const value = target[prop];
|
|
2039
|
+
if (typeof value === "function") {
|
|
2040
|
+
return value.bind(target);
|
|
2041
|
+
}
|
|
2042
|
+
return value;
|
|
2043
|
+
}
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
function wrapRelationalQuery(queryObj, ctx) {
|
|
2047
|
+
if (!queryObj) return queryObj;
|
|
2048
|
+
return new Proxy(queryObj, {
|
|
2049
|
+
get(target, tableName) {
|
|
2050
|
+
const tableProxy = target[tableName];
|
|
2051
|
+
if (!tableProxy || typeof tableProxy !== "object") return tableProxy;
|
|
2052
|
+
return new Proxy(tableProxy, {
|
|
2053
|
+
get(tableTarget, method) {
|
|
2054
|
+
const methodStr = typeof method === "string" ? method : "";
|
|
2055
|
+
if (methodStr === "findMany" || methodStr === "findFirst") {
|
|
2056
|
+
return (args = {}) => {
|
|
2057
|
+
const meta = findMetaByTableName(String(tableName));
|
|
2058
|
+
if (meta) {
|
|
2059
|
+
const filterResult = evaluateFilterSync(meta, ctx);
|
|
2060
|
+
if (filterResult && typeof filterResult !== "boolean") {
|
|
2061
|
+
args.where = args.where ? and(args.where, filterResult) : filterResult;
|
|
2062
|
+
} else if (filterResult === false) {
|
|
2063
|
+
args.where = args.where;
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
return tableTarget[method](args);
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
const value = tableTarget[method];
|
|
2070
|
+
if (typeof value === "function") {
|
|
2071
|
+
return value.bind(tableTarget);
|
|
2072
|
+
}
|
|
2073
|
+
return value;
|
|
2074
|
+
}
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2079
|
+
function buildCombinedFilter(pendingFilters, ctx) {
|
|
2080
|
+
const sqlConditions = [];
|
|
2081
|
+
for (const { meta } of pendingFilters) {
|
|
2082
|
+
const result = evaluateFilterSync(meta, ctx);
|
|
2083
|
+
if (result === false) {
|
|
2084
|
+
const { sql: sqlTag } = __require("drizzle-orm");
|
|
2085
|
+
return sqlTag`1 = 0`;
|
|
2086
|
+
}
|
|
2087
|
+
if (result === true) {
|
|
2088
|
+
continue;
|
|
2089
|
+
}
|
|
2090
|
+
if (result) {
|
|
2091
|
+
sqlConditions.push(result);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
if (sqlConditions.length === 0) return null;
|
|
2095
|
+
if (sqlConditions.length === 1) return sqlConditions[0];
|
|
2096
|
+
return and(...sqlConditions) ?? null;
|
|
2097
|
+
}
|
|
2098
|
+
function evaluateFilterSync(meta, ctx) {
|
|
2099
|
+
const result = meta.filter(ctx);
|
|
2100
|
+
if (result instanceof Promise) {
|
|
2101
|
+
throw new Error(
|
|
2102
|
+
`[gencow] Async filter on table "${meta.tableName}" is not supported in synchronous context. Use synchronous filters for schema-level access control.`
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
return result;
|
|
2106
|
+
}
|
|
2107
|
+
function findMetaByTableName(name) {
|
|
2108
|
+
for (const [, meta] of globalThis.__gencow_tableAccessRegistry || []) {
|
|
2109
|
+
if (meta.tableName === name) return meta;
|
|
2110
|
+
}
|
|
2111
|
+
return void 0;
|
|
2112
|
+
}
|
|
2113
|
+
function applyFieldAccess(result, table, ctx) {
|
|
2114
|
+
const meta = getTableAccessMeta(table);
|
|
2115
|
+
if (!meta?.fieldAccess) return result;
|
|
2116
|
+
const fieldAccess = meta.fieldAccess;
|
|
2117
|
+
const maskedFields = [];
|
|
2118
|
+
for (const [field, rule] of Object.entries(fieldAccess)) {
|
|
2119
|
+
try {
|
|
2120
|
+
if (!rule.read(ctx)) {
|
|
2121
|
+
maskedFields.push(field);
|
|
2122
|
+
}
|
|
2123
|
+
} catch {
|
|
2124
|
+
maskedFields.push(field);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
if (maskedFields.length === 0) return result;
|
|
2128
|
+
const maskRow = (row) => {
|
|
2129
|
+
if (!row || typeof row !== "object") return row;
|
|
2130
|
+
const masked = { ...row };
|
|
2131
|
+
for (const field of maskedFields) {
|
|
2132
|
+
if (field in masked) {
|
|
2133
|
+
masked[field] = null;
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
return masked;
|
|
2137
|
+
};
|
|
2138
|
+
if (Array.isArray(result)) {
|
|
2139
|
+
return result.map(maskRow);
|
|
2140
|
+
}
|
|
2141
|
+
return maskRow(result);
|
|
2142
|
+
}
|
|
1825
2143
|
export {
|
|
1826
2144
|
GencowValidationError,
|
|
2145
|
+
applyFieldAccess,
|
|
1827
2146
|
buildRealtimeCtx,
|
|
1828
2147
|
createScheduler,
|
|
2148
|
+
createScopedDb,
|
|
1829
2149
|
cronJobs,
|
|
1830
2150
|
defineAuth,
|
|
1831
2151
|
deregisterClient,
|
|
2152
|
+
gencowTable,
|
|
2153
|
+
getAllGencowTables,
|
|
1832
2154
|
getQueryDef,
|
|
1833
2155
|
getQueryHandler,
|
|
1834
2156
|
getRegisteredHttpActions,
|
|
1835
2157
|
getRegisteredMutations,
|
|
1836
2158
|
getRegisteredQueries,
|
|
1837
2159
|
getSchedulerInfo,
|
|
2160
|
+
getTableAccessMeta,
|
|
1838
2161
|
handleWsMessage,
|
|
1839
2162
|
httpAction,
|
|
1840
2163
|
invalidateQueries,
|
|
2164
|
+
isGencowTable,
|
|
1841
2165
|
mutation,
|
|
2166
|
+
ownerFilter,
|
|
1842
2167
|
parseArgs,
|
|
1843
2168
|
query,
|
|
1844
2169
|
registerClient,
|