gencow 0.1.81 → 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 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 (Convex-like, no migration files)
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 (like Convex dashboard)
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 (Convex Dashboard equivalent)...");
1352
+ info("Opening Drizzle Studio...");
1355
1353
  runInServer("pnpm db:studio", buildEnv(config));
1356
1354
  },
1357
1355
 
@@ -2933,7 +2931,7 @@ process.exit(0);
2933
2931
  // Save current app to creds
2934
2932
  saveCreds({ ...creds, currentApp: name });
2935
2933
 
2936
- // ── Scaffold local project files (Convex-like) ──────────
2934
+ // ── Scaffold local project files ────────────────────
2937
2935
  const cwd = process.cwd();
2938
2936
  const config = loadConfig();
2939
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,