@palbase/backend 24.3.0 → 25.0.0

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.
Files changed (61) hide show
  1. package/dist/bin/palbase-backend.cjs +101 -60
  2. package/dist/bin/palbase-backend.cjs.map +1 -1
  3. package/dist/bin/palbase-backend.js +17 -13
  4. package/dist/bin/palbase-backend.js.map +1 -1
  5. package/dist/{chunk-EIXCY4SS.js → chunk-43A3KGWL.js} +80 -49
  6. package/dist/chunk-43A3KGWL.js.map +1 -0
  7. package/dist/{chunk-ERDL5VAE.js → chunk-5CMLOAEF.js} +2 -2
  8. package/dist/chunk-OEQBHE2Z.js +825 -0
  9. package/dist/chunk-OEQBHE2Z.js.map +1 -0
  10. package/dist/{chunk-7Z6MGMXQ.js → chunk-XJ2RSHEU.js} +11 -5
  11. package/dist/chunk-XJ2RSHEU.js.map +1 -0
  12. package/dist/{chunk-UWSYTUGM.js → chunk-ZQRWW37O.js} +44 -1
  13. package/dist/chunk-ZQRWW37O.js.map +1 -0
  14. package/dist/db/env.cjs.map +1 -1
  15. package/dist/db/env.d.cts +29 -13
  16. package/dist/db/env.d.ts +29 -13
  17. package/dist/db/index.cjs +212 -111
  18. package/dist/db/index.cjs.map +1 -1
  19. package/dist/db/index.d.cts +1 -1
  20. package/dist/db/index.d.ts +1 -1
  21. package/dist/db/index.js +11 -1
  22. package/dist/engine/index.cjs +87 -50
  23. package/dist/engine/index.cjs.map +1 -1
  24. package/dist/engine/index.d.cts +2 -2
  25. package/dist/engine/index.d.ts +2 -2
  26. package/dist/engine/index.js +3 -3
  27. package/dist/{index-DEneI8Mn.d.ts → index-BF1f0DfA.d.ts} +5 -2
  28. package/dist/{index-C-ALG22n.d.cts → index-CoaDN9dL.d.cts} +5 -2
  29. package/dist/{index-BTMYod_l.d.ts → index-Ct1iiB4N.d.ts} +203 -61
  30. package/dist/{index-BLAbr9ZH.d.cts → index-CwaWRhyc.d.cts} +203 -61
  31. package/dist/index.cjs +550 -297
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +122 -20
  34. package/dist/index.d.ts +122 -20
  35. package/dist/index.js +164 -217
  36. package/dist/index.js.map +1 -1
  37. package/dist/openapi/index.cjs +100 -36
  38. package/dist/openapi/index.cjs.map +1 -1
  39. package/dist/openapi/index.js +59 -2
  40. package/dist/openapi/index.js.map +1 -1
  41. package/docs/README.md +64 -31
  42. package/docs/endpoints.md +25 -28
  43. package/docs/llms-full.txt +399 -153
  44. package/docs/schema.md +272 -91
  45. package/docs/services.md +39 -4
  46. package/package.json +1 -1
  47. package/template/AGENTS.md +119 -314
  48. package/template/CLAUDE.md +13 -0
  49. package/template/controllers/notes.controller.ts +6 -13
  50. package/template/db/public.ts +38 -0
  51. package/template/models/notes/create.ts +38 -0
  52. package/template/package.json +6 -3
  53. package/template/services/note.service.test.ts +45 -0
  54. package/template/services/note.service.ts +2 -2
  55. package/dist/chunk-7Z6MGMXQ.js.map +0 -1
  56. package/dist/chunk-D5CQES25.js +0 -556
  57. package/dist/chunk-D5CQES25.js.map +0 -1
  58. package/dist/chunk-EIXCY4SS.js.map +0 -1
  59. package/dist/chunk-UWSYTUGM.js.map +0 -1
  60. package/template/db/schema.ts +0 -35
  61. /package/dist/{chunk-ERDL5VAE.js.map → chunk-5CMLOAEF.js.map} +0 -0
package/dist/index.cjs CHANGED
@@ -42,6 +42,12 @@ __export(src_exports, {
42
42
  NotFound: () => NotFound,
43
43
  Notifications: () => Notifications,
44
44
  On: () => On,
45
+ OnAuthorize: () => OnAuthorize,
46
+ OnEmpty: () => OnEmpty,
47
+ OnFirst: () => OnFirst,
48
+ OnJoin: () => OnJoin,
49
+ OnLeave: () => OnLeave,
50
+ OnMessage: () => OnMessage,
45
51
  OptionalUser: () => OptionalUser,
46
52
  PALBASE_EXTENSIONS: () => PALBASE_EXTENSIONS,
47
53
  PalError: () => PalError,
@@ -56,11 +62,13 @@ __export(src_exports, {
56
62
  Realtime: () => Realtime,
57
63
  Req: () => Req,
58
64
  RequestId: () => RequestId,
65
+ Room: () => Room,
59
66
  Secrets: () => Secrets,
60
67
  Signal: () => Signal,
61
68
  Sse: () => Sse,
62
69
  SseOut: () => SseOut,
63
70
  Storage: () => Storage,
71
+ TABLE_META: () => TABLE_META,
64
72
  TooManyRequests: () => TooManyRequests,
65
73
  TraceId: () => TraceId,
66
74
  TxPlanError: () => TxPlanError,
@@ -89,6 +97,7 @@ __export(src_exports, {
89
97
  defineError: () => defineError,
90
98
  defineMiddleware: () => defineMiddleware,
91
99
  defineSchema: () => defineSchema,
100
+ defineTable: () => defineTable,
92
101
  enumType: () => enumType,
93
102
  getErrorRegistry: () => getErrorRegistry,
94
103
  getHookConfig: () => getHookConfig,
@@ -97,6 +106,7 @@ __export(src_exports, {
97
106
  getRoutes: () => getRoutes,
98
107
  getWebhookConfig: () => getWebhookConfig,
99
108
  inc: () => inc,
109
+ installationRef: () => installationRef,
100
110
  integer: () => integer,
101
111
  isPalbaseExtension: () => isPalbaseExtension,
102
112
  jsonb: () => jsonb,
@@ -109,6 +119,7 @@ __export(src_exports, {
109
119
  onShutdown: () => onShutdown,
110
120
  onStart: () => onStart,
111
121
  openai: () => openai,
122
+ ownedByUser: () => ownedByUser,
112
123
  ownerOnly: () => ownerOnly,
113
124
  policy: () => policy,
114
125
  publicChannel: () => publicChannel,
@@ -118,6 +129,7 @@ __export(src_exports, {
118
129
  text: () => text,
119
130
  timestamp: () => timestamp,
120
131
  toSchemaJSON: () => toSchemaJSON,
132
+ userRef: () => userRef,
121
133
  uuid: () => uuid,
122
134
  vector: () => vector,
123
135
  z: () => import_zod2.z
@@ -639,13 +651,13 @@ function makeServiceProxy(key) {
639
651
  };
640
652
  return new Proxy({}, handler);
641
653
  }
642
- function makeTablesAccessor(ops) {
643
- const tablesProxy = new Proxy(
654
+ function makeTableProxy(ops, prefix) {
655
+ return new Proxy(
644
656
  {},
645
657
  {
646
658
  get(_t, prop) {
647
659
  if (typeof prop !== "string") return void 0;
648
- const name = prop;
660
+ const name = `${prefix}${prop}`;
649
661
  return {
650
662
  insert: (data) => ops().insert(name, data),
651
663
  update: (id, data) => ops().update(name, id, data),
@@ -662,7 +674,12 @@ function makeTablesAccessor(ops) {
662
674
  }
663
675
  }
664
676
  );
665
- return tablesProxy;
677
+ }
678
+ function makeTablesAccessor(ops) {
679
+ return makeTableProxy(ops, "");
680
+ }
681
+ function makeSchemaAccessor(ops, schema) {
682
+ return { tables: makeTableProxy(ops, `${String(schema)}.`) };
666
683
  }
667
684
  var rawDatabase = makeServiceProxy("Database");
668
685
  function makeTypedSurface(raw2) {
@@ -689,6 +706,7 @@ function makeTypedSurface(raw2) {
689
706
  // as one on the request's, and each is bound to its own connection.
690
707
  attempt: (fn) => raw2.attempt(fn),
691
708
  tables: makeTablesAccessor(() => reco),
709
+ schema: (name) => makeSchemaAccessor(() => reco, name),
692
710
  transaction(fn) {
693
711
  const builder = new TxPlanBuilder();
694
712
  return runTxPlan(raw2, makeTxTablesAccessor(builder), builder, fn);
@@ -1669,198 +1687,6 @@ function buildModuleClients(cfg) {
1669
1687
  };
1670
1688
  }
1671
1689
 
1672
- // src/db/policy.ts
1673
- function quote(name) {
1674
- return `"${name.replace(/"/g, '""')}"`;
1675
- }
1676
- var PolicyBuilder = class {
1677
- _def;
1678
- constructor(name) {
1679
- this._def = {
1680
- name,
1681
- command: "all",
1682
- roles: ["authenticated"],
1683
- using: null,
1684
- withCheck: null,
1685
- permissive: true
1686
- };
1687
- }
1688
- /** Restrict the policy to a single SQL command (default `"all"`). */
1689
- for(command) {
1690
- this._def.command = command;
1691
- return this;
1692
- }
1693
- /**
1694
- * Set the DB roles the policy applies to (the `TO` clause), replacing any
1695
- * previously-set roles. Call with no arguments to target PUBLIC (all roles).
1696
- *
1697
- * @example
1698
- * policy("p").to("authenticated")
1699
- * policy("p").to("authenticated", "service_role")
1700
- * policy("p").to() // PUBLIC
1701
- */
1702
- to(...roles) {
1703
- this._def.roles = roles;
1704
- return this;
1705
- }
1706
- /** Set the `USING (...)` row-visibility expression (raw SQL). */
1707
- using(sqlExpr) {
1708
- this._def.using = sqlExpr;
1709
- return this;
1710
- }
1711
- /**
1712
- * "Rows of THIS table whose owner the caller is a member of" — the membership
1713
- * pattern, written so it cannot recurse.
1714
- *
1715
- * THE TRAP IT EXISTS FOR. Written by hand, membership policies point at each
1716
- * other: `channels` is visible to members, so its policy reads
1717
- * `channel_members`; `channel_members` is visible to members, so its policy
1718
- * reads `channels`. Postgres refuses the pair at query time with `infinite
1719
- * recursion detected in policy for relation ...`, and the error names the
1720
- * relation but not the cycle. The way out is asymmetry — the MEMBERSHIP table
1721
- * is protected by `user_id = auth.uid()` and nothing else, and every other
1722
- * table subqueries INTO it. That shape was in the platform's own schema and
1723
- * written down nowhere; a customer recovered it by reading that schema.
1724
- *
1725
- * `(select auth.uid())` rather than a bare call: the scalar subquery is
1726
- * evaluated ONCE per statement instead of per row.
1727
- *
1728
- * @example
1729
- * // channels: visible to members. The membership table gets the simple one.
1730
- * policy("member_read").for("select").to("authenticated")
1731
- * .memberOf("channel_members", "channel_id")
1732
- * // → id IN (SELECT "channel_id" FROM "channel_members"
1733
- * // WHERE "user_id" = (select auth.uid()))
1734
- */
1735
- memberOf(membershipTable, foreignKey, options = {}) {
1736
- if (this._def.using !== null) {
1737
- throw new Error(
1738
- `policy(${this._def.name}).memberOf(): this policy already has a using() expression. Write one or the other \u2014 memberOf IS the using expression.`
1739
- );
1740
- }
1741
- const column = options.column ?? "id";
1742
- const userColumn = options.userColumn ?? "user_id";
1743
- this._def.using = `${quote(column)} IN (SELECT ${quote(foreignKey)} FROM ${quote(membershipTable)} WHERE ${quote(userColumn)} = (select auth.uid()))`;
1744
- return this;
1745
- }
1746
- /** Set the `WITH CHECK (...)` write-validation expression (raw SQL). */
1747
- withCheck(sqlExpr) {
1748
- this._def.withCheck = sqlExpr;
1749
- return this;
1750
- }
1751
- /** Set the policy mode: `"permissive"` (default, OR-combined) or
1752
- * `"restrictive"` (AND-combined). */
1753
- as(mode) {
1754
- this._def.permissive = mode === "permissive";
1755
- return this;
1756
- }
1757
- };
1758
- function policy(name) {
1759
- return new PolicyBuilder(name);
1760
- }
1761
-
1762
- // src/db/schema.ts
1763
- function toPolicyDef(p) {
1764
- return p instanceof PolicyBuilder ? p._def : p;
1765
- }
1766
- function defineSchema(input) {
1767
- const tables = {};
1768
- for (const name of Object.keys(input.tables)) {
1769
- const table = input.tables[name];
1770
- if (table === void 0) continue;
1771
- const policies = (table.policies ?? []).map(toPolicyDef);
1772
- const rls = policies.length > 0 || table.rls !== false;
1773
- const tableDef = {
1774
- name,
1775
- columns: table.columns,
1776
- rls,
1777
- policies
1778
- };
1779
- if (table.primaryKey !== void 0) tableDef.primaryKey = table.primaryKey;
1780
- if (table.unique !== void 0) tableDef.unique = table.unique;
1781
- if (table.raw !== void 0 && table.raw.length > 0) tableDef.raw = table.raw.slice();
1782
- if (table.checks !== void 0 && table.checks.length > 0) tableDef.checks = table.checks.slice();
1783
- if (table.indexes !== void 0 && table.indexes.length > 0) tableDef.indexes = table.indexes.slice();
1784
- if (table.search !== void 0) {
1785
- const s = table.search;
1786
- if (s.from !== void 0) {
1787
- if (s.vector !== void 0) {
1788
- throw new Error(
1789
- `table ${String(name)}: search beyan\u0131nda tek bi\xE7im kullan\u0131n \u2014 'from' (yeni) ile 'vector' (eski) birlikte olamaz`
1790
- );
1791
- }
1792
- if (s.from.length === 0) {
1793
- throw new Error(`table ${String(name)}: search.from bo\u015F olamaz`);
1794
- }
1795
- if (s.model === void 0) {
1796
- throw new Error(
1797
- `table ${String(name)}: search.from model'siz anlams\u0131z \u2014 auto-embed i\xE7in model verin (BYO i\xE7in eski 'vector' bi\xE7imini kullan\u0131n)`
1798
- );
1799
- }
1800
- if (Array.isArray(s.text) && s.text.length === 0) {
1801
- throw new Error(`table ${String(name)}: search.text bo\u015F dizi olamaz \u2014 FTS istemiyorsan text: false yaz\u0131n`);
1802
- }
1803
- } else {
1804
- if (typeof s.text === "boolean") {
1805
- throw new Error(`table ${String(name)}: text:${String(s.text)} yaln\u0131z yeni bi\xE7imde ('from' ile) ge\xE7erli`);
1806
- }
1807
- if (s.text === void 0 && s.vector === void 0) {
1808
- throw new Error(
1809
- `table ${String(name)}: search beyan\u0131 bo\u015F \u2014 en az bir kol (text ya da vector) verin, yoksa alan\u0131 hi\xE7 yazmay\u0131n`
1810
- );
1811
- }
1812
- if (s.text !== void 0 && s.text.length === 0) {
1813
- throw new Error(`table ${String(name)}: search.text bo\u015F olamaz \u2014 FTS kolu istemiyorsan alan\u0131 hi\xE7 yazma`);
1814
- }
1815
- const legs = s.vector === void 0 ? [] : Array.isArray(s.vector) ? s.vector : [s.vector];
1816
- for (const leg of legs) {
1817
- if (leg.model !== void 0 && (leg.from === void 0 || leg.from.length === 0)) {
1818
- throw new Error(`table ${String(name)}: search.vector.model beyan edildi ama 'from' yok \u2014 embed kayna\u011F\u0131 kolonlar zorunlu (C-2)`);
1819
- }
1820
- if (leg.model === void 0 && leg.from !== void 0) {
1821
- throw new Error(`table ${String(name)}: search.vector.from model'siz anlams\u0131z \u2014 auto-embed i\xE7in model verin`);
1822
- }
1823
- }
1824
- }
1825
- tableDef.search = s;
1826
- }
1827
- if (table.memory !== void 0) {
1828
- if (table.memory.from.length === 0) {
1829
- throw new Error(`table ${String(name)}: memory.from bo\u015F olamaz`);
1830
- }
1831
- tableDef.memory = table.memory;
1832
- }
1833
- tables[name] = tableDef;
1834
- }
1835
- for (const [name, def] of Object.entries(tables)) {
1836
- const m = def.memory;
1837
- if (m === void 0) continue;
1838
- const target = Object.values(tables).find((t) => t.name === m.into);
1839
- if (target === void 0) {
1840
- throw new Error(`table ${name}: memory.into "${m.into}" \u015Femada declared de\u011Fil`);
1841
- }
1842
- const subject = m.subject ?? "owner";
1843
- if (!(subject in def.columns)) {
1844
- throw new Error(`table ${name}: memory.subject "${subject}" kolonu kaynak tabloda yok`);
1845
- }
1846
- if (!(subject in target.columns)) {
1847
- throw new Error(`table ${name}: memory.subject "${subject}" kolonu hedef "${m.into}" tablosunda yok`);
1848
- }
1849
- const factCol = target.columns["fact"];
1850
- const factDef = factCol !== void 0 && "_def" in factCol ? factCol._def : factCol;
1851
- if (factDef === void 0 || factDef.type !== "text") {
1852
- throw new Error(`table ${name}: memory.into "${m.into}" tablosunda "fact" (text) kolonu zorunlu`);
1853
- }
1854
- for (const c of m.from) {
1855
- if (!(c in def.columns)) {
1856
- throw new Error(`table ${name}: memory.from kolonu "${c}" kaynak tabloda yok`);
1857
- }
1858
- }
1859
- }
1860
- const extensions = [...new Set(input.extensions ?? [])];
1861
- return { tables, extensions };
1862
- }
1863
-
1864
1690
  // src/db/schema-json.ts
1865
1691
  function defOf(column) {
1866
1692
  return "_def" in column ? column._def : column;
@@ -1969,13 +1795,14 @@ function memoryToJSON(m) {
1969
1795
  extract: { provider: m.extract.provider, model: m.extract.model }
1970
1796
  };
1971
1797
  }
1972
- function tableToJSON(table) {
1798
+ function tableToJSON(table, schemaName) {
1973
1799
  const columns = {};
1974
1800
  for (const [name, column] of Object.entries(table.columns)) {
1975
1801
  columns[name] = columnToJSON(column);
1976
1802
  }
1977
1803
  const out = {
1978
1804
  name: table.name,
1805
+ schema: schemaName,
1979
1806
  columns,
1980
1807
  // Read, not re-derived. `defineSchema` already resolves the fail-closed
1981
1808
  // default (RLS on unless the author wrote `rls: false`, and forced on by any
@@ -2014,13 +1841,296 @@ function tableToJSON(table) {
2014
1841
  }
2015
1842
  return out;
2016
1843
  }
2017
- function toSchemaJSON(schema) {
1844
+ function qualifiedTableKey(schemaName, tableName) {
1845
+ return schemaName === "public" ? tableName : `${schemaName}.${tableName}`;
1846
+ }
1847
+ function toSchemaJSON(schemas) {
2018
1848
  const tables = {};
2019
- for (const table of Object.values(schema.tables)) {
2020
- const json = tableToJSON(table);
2021
- tables[json.name] = json;
1849
+ const extensions = [];
1850
+ const meta = [];
1851
+ const seen = /* @__PURE__ */ new Set();
1852
+ for (const schema of schemas) {
1853
+ if (seen.has(schema.name)) {
1854
+ throw new Error(`two schemas declare the name "${schema.name}" \u2014 schema names must be unique`);
1855
+ }
1856
+ seen.add(schema.name);
1857
+ for (const table of Object.values(schema.tables)) {
1858
+ const json = tableToJSON(table, schema.name);
1859
+ tables[qualifiedTableKey(schema.name, json.name)] = json;
1860
+ }
1861
+ extensions.push(...schema.extensions ?? []);
1862
+ meta.push({ name: schema.name, exposed: schema.exposed });
2022
1863
  }
2023
- return { tables, extensions: [...new Set(schema.extensions ?? [])] };
1864
+ return { tables, extensions: [...new Set(extensions)], schemas: meta };
1865
+ }
1866
+
1867
+ // src/db/policy.ts
1868
+ function quote(name) {
1869
+ return `"${name.replace(/"/g, '""')}"`;
1870
+ }
1871
+ var PolicyBuilder = class {
1872
+ _def;
1873
+ constructor(name) {
1874
+ this._def = {
1875
+ name,
1876
+ command: "all",
1877
+ roles: ["authenticated"],
1878
+ using: null,
1879
+ withCheck: null,
1880
+ permissive: true
1881
+ };
1882
+ }
1883
+ /** Restrict the policy to a single SQL command (default `"all"`). */
1884
+ for(command) {
1885
+ this._def.command = command;
1886
+ return this;
1887
+ }
1888
+ /**
1889
+ * Set the DB roles the policy applies to (the `TO` clause), replacing any
1890
+ * previously-set roles. Call with no arguments to target PUBLIC (all roles).
1891
+ *
1892
+ * @example
1893
+ * policy("p").to("authenticated")
1894
+ * policy("p").to("authenticated", "service_role")
1895
+ * policy("p").to() // PUBLIC
1896
+ */
1897
+ to(...roles) {
1898
+ this._def.roles = roles;
1899
+ return this;
1900
+ }
1901
+ /** Set the `USING (...)` row-visibility expression (raw SQL). */
1902
+ using(sqlExpr) {
1903
+ this._def.using = sqlExpr;
1904
+ return this;
1905
+ }
1906
+ /**
1907
+ * "Rows of THIS table whose owner the caller is a member of" — the membership
1908
+ * pattern, written so it cannot recurse.
1909
+ *
1910
+ * THE TRAP IT EXISTS FOR. Written by hand, membership policies point at each
1911
+ * other: `channels` is visible to members, so its policy reads
1912
+ * `channel_members`; `channel_members` is visible to members, so its policy
1913
+ * reads `channels`. Postgres refuses the pair at query time with `infinite
1914
+ * recursion detected in policy for relation ...`, and the error names the
1915
+ * relation but not the cycle. The way out is asymmetry — the MEMBERSHIP table
1916
+ * is protected by `user_id = auth.uid()` and nothing else, and every other
1917
+ * table subqueries INTO it. That shape was in the platform's own schema and
1918
+ * written down nowhere; a customer recovered it by reading that schema.
1919
+ *
1920
+ * `(select auth.uid())` rather than a bare call: the scalar subquery is
1921
+ * evaluated ONCE per statement instead of per row.
1922
+ *
1923
+ * @example
1924
+ * // channels: visible to members. The membership table gets the simple one.
1925
+ * policy("member_read").for("select").to("authenticated")
1926
+ * .memberOf("channel_members", "channel_id")
1927
+ * // → id IN (SELECT "channel_id" FROM "channel_members"
1928
+ * // WHERE "user_id" = (select auth.uid()))
1929
+ */
1930
+ memberOf(membershipTable, foreignKey, options = {}) {
1931
+ if (this._def.using !== null) {
1932
+ throw new Error(
1933
+ `policy(${this._def.name}).memberOf(): this policy already has a using() expression. Write one or the other \u2014 memberOf IS the using expression.`
1934
+ );
1935
+ }
1936
+ const column = options.column ?? "id";
1937
+ const userColumn = options.userColumn ?? "user_id";
1938
+ this._def.using = `${quote(column)} IN (SELECT ${quote(foreignKey)} FROM ${quote(membershipTable)} WHERE ${quote(userColumn)} = (select auth.uid()))`;
1939
+ return this;
1940
+ }
1941
+ /** Set the `WITH CHECK (...)` write-validation expression (raw SQL). */
1942
+ withCheck(sqlExpr) {
1943
+ this._def.withCheck = sqlExpr;
1944
+ return this;
1945
+ }
1946
+ /** Set the policy mode: `"permissive"` (default, OR-combined) or
1947
+ * `"restrictive"` (AND-combined). */
1948
+ as(mode) {
1949
+ this._def.permissive = mode === "permissive";
1950
+ return this;
1951
+ }
1952
+ };
1953
+ function policy(name) {
1954
+ return new PolicyBuilder(name);
1955
+ }
1956
+
1957
+ // src/db/schema.ts
1958
+ function toPolicyDef(p) {
1959
+ return p instanceof PolicyBuilder ? p._def : p;
1960
+ }
1961
+ var TABLE_META = /* @__PURE__ */ Symbol.for("palbase.table.meta");
1962
+ function defineTable(name, input) {
1963
+ if (name.trim() === "") {
1964
+ throw new Error("defineTable(name, \u2026): name must not be empty");
1965
+ }
1966
+ const policies = (input.policies ?? []).map(toPolicyDef);
1967
+ const rls = policies.length > 0 || input.rls !== false;
1968
+ const def = {
1969
+ name,
1970
+ columns: input.columns,
1971
+ rls,
1972
+ policies
1973
+ };
1974
+ if (input.primaryKey !== void 0) def.primaryKey = input.primaryKey;
1975
+ if (input.unique !== void 0) def.unique = input.unique;
1976
+ if (input.raw !== void 0 && input.raw.length > 0) def.raw = input.raw.slice();
1977
+ if (input.checks !== void 0 && input.checks.length > 0) def.checks = input.checks.slice();
1978
+ if (input.indexes !== void 0 && input.indexes.length > 0) def.indexes = input.indexes.slice();
1979
+ if (input.search !== void 0) {
1980
+ const s = input.search;
1981
+ if (s.from !== void 0) {
1982
+ if (s.vector !== void 0) {
1983
+ throw new Error(
1984
+ `table ${name}: search beyan\u0131nda tek bi\xE7im kullan\u0131n \u2014 'from' (yeni) ile 'vector' (eski) birlikte olamaz`
1985
+ );
1986
+ }
1987
+ if (s.from.length === 0) {
1988
+ throw new Error(`table ${name}: search.from bo\u015F olamaz`);
1989
+ }
1990
+ if (s.model === void 0) {
1991
+ throw new Error(
1992
+ `table ${name}: search.from model'siz anlams\u0131z \u2014 auto-embed i\xE7in model verin (BYO i\xE7in eski 'vector' bi\xE7imini kullan\u0131n)`
1993
+ );
1994
+ }
1995
+ if (Array.isArray(s.text) && s.text.length === 0) {
1996
+ throw new Error(`table ${name}: search.text bo\u015F dizi olamaz \u2014 FTS istemiyorsan text: false yaz\u0131n`);
1997
+ }
1998
+ } else {
1999
+ if (typeof s.text === "boolean") {
2000
+ throw new Error(`table ${name}: text:${String(s.text)} yaln\u0131z yeni bi\xE7imde ('from' ile) ge\xE7erli`);
2001
+ }
2002
+ if (s.text === void 0 && s.vector === void 0) {
2003
+ throw new Error(
2004
+ `table ${name}: search beyan\u0131 bo\u015F \u2014 en az bir kol (text ya da vector) verin, yoksa alan\u0131 hi\xE7 yazmay\u0131n`
2005
+ );
2006
+ }
2007
+ if (s.text !== void 0 && s.text.length === 0) {
2008
+ throw new Error(`table ${name}: search.text bo\u015F olamaz \u2014 FTS kolu istemiyorsan alan\u0131 hi\xE7 yazma`);
2009
+ }
2010
+ const legs = s.vector === void 0 ? [] : Array.isArray(s.vector) ? s.vector : [s.vector];
2011
+ for (const leg of legs) {
2012
+ if (leg.model !== void 0 && (leg.from === void 0 || leg.from.length === 0)) {
2013
+ throw new Error(`table ${name}: search.vector.model beyan edildi ama 'from' yok \u2014 embed kayna\u011F\u0131 kolonlar zorunlu (C-2)`);
2014
+ }
2015
+ if (leg.model === void 0 && leg.from !== void 0) {
2016
+ throw new Error(`table ${name}: search.vector.from model'siz anlams\u0131z \u2014 auto-embed i\xE7in model verin`);
2017
+ }
2018
+ }
2019
+ }
2020
+ def.search = s;
2021
+ }
2022
+ if (input.memory !== void 0) {
2023
+ if (input.memory.from.length === 0) {
2024
+ throw new Error(`table ${name}: memory.from bo\u015F olamaz`);
2025
+ }
2026
+ def.memory = input.memory;
2027
+ }
2028
+ for (const b of Object.values(input.columns)) {
2029
+ b._def.ownerTable = def;
2030
+ }
2031
+ const handle = { ...input.columns };
2032
+ Object.defineProperty(handle, TABLE_META, { value: def, enumerable: false });
2033
+ return handle;
2034
+ }
2035
+ var declaringSchemaOf = /* @__PURE__ */ new WeakMap();
2036
+ function resolveReferences(tables, schemaName) {
2037
+ for (const table of Object.values(tables)) {
2038
+ for (const [colName, builder] of Object.entries(table.columns)) {
2039
+ const def = builder._def;
2040
+ if (def.selfRefColumn !== void 0) {
2041
+ if (!(def.selfRefColumn in table.columns)) {
2042
+ throw new Error(
2043
+ `table "${table.name}" column "${colName}": selfReferences("${def.selfRefColumn}") \u2014 no such column on this table`
2044
+ );
2045
+ }
2046
+ def.references = {
2047
+ table: qualifiedTableKey(schemaName, table.name),
2048
+ column: def.selfRefColumn
2049
+ };
2050
+ continue;
2051
+ }
2052
+ if (def.referencesThunk === void 0) continue;
2053
+ const target = def.referencesThunk();
2054
+ const owner = target?._def?.ownerTable;
2055
+ if (owner === void 0) {
2056
+ throw new Error(
2057
+ `table "${table.name}" column "${colName}": references(() => \u2026) must point at a column of a table declared with defineTable(...)`
2058
+ );
2059
+ }
2060
+ const targetColumn = Object.entries(owner.columns).find(
2061
+ ([, b]) => b._def === target._def
2062
+ )?.[0];
2063
+ if (targetColumn === void 0) {
2064
+ throw new Error(
2065
+ `table "${table.name}" column "${colName}": the referenced column was not found on table "${owner.name}"`
2066
+ );
2067
+ }
2068
+ const targetSchema = declaringSchemaOf.get(owner);
2069
+ if (targetSchema === void 0) {
2070
+ throw new Error(
2071
+ `table "${table.name}" column "${colName}": references(() => \u2026) points at table "${owner.name}", which no defineSchema(...) has claimed yet \u2014 a foreign key cannot be written without its target's schema. Declare that table's schema first (one file per schema: db/public.ts, db/billing.ts \u2026, imported by this one).`
2072
+ );
2073
+ }
2074
+ def.references = {
2075
+ table: qualifiedTableKey(targetSchema, owner.name),
2076
+ column: targetColumn
2077
+ };
2078
+ }
2079
+ }
2080
+ }
2081
+ function defineSchema(name, input) {
2082
+ if (typeof name !== "string") {
2083
+ throw new Error(
2084
+ `defineSchema(...) takes the schema NAME first: defineSchema("public", { tables: [todos, lists] }). The one-argument form defineSchema({ tables: { \u2026 } }) is gone \u2014 a schema that does not say its own name cannot be told apart from another schema's table of the same name, which is what a cross-schema foreign key has to do. Migrate db/public.ts (one file per schema: db/public.ts, db/billing.ts \u2026): name the schema, and lift each table key onto the table itself with defineTable("todos", { columns: { \u2026 } }), then list the tables in the array.`
2085
+ );
2086
+ }
2087
+ if (name.trim() === "") {
2088
+ throw new Error("defineSchema(name, \u2026): name must not be empty");
2089
+ }
2090
+ const tables = {};
2091
+ for (const handle of input.tables) {
2092
+ const table = handle[TABLE_META];
2093
+ if (tables[table.name] !== void 0) {
2094
+ throw new Error(
2095
+ `defineSchema("${name}"): two tables declare the name "${table.name}" \u2014 table names must be unique within a schema`
2096
+ );
2097
+ }
2098
+ tables[table.name] = table;
2099
+ declaringSchemaOf.set(table, name);
2100
+ }
2101
+ resolveReferences(tables, name);
2102
+ for (const [name2, def] of Object.entries(tables)) {
2103
+ const m = def.memory;
2104
+ if (m === void 0) continue;
2105
+ const target = Object.values(tables).find((t) => t.name === m.into);
2106
+ if (target === void 0) {
2107
+ throw new Error(`table ${name2}: memory.into "${m.into}" \u015Femada declared de\u011Fil`);
2108
+ }
2109
+ const subject = m.subject ?? "owner";
2110
+ if (!(subject in def.columns)) {
2111
+ throw new Error(`table ${name2}: memory.subject "${subject}" kolonu kaynak tabloda yok`);
2112
+ }
2113
+ if (!(subject in target.columns)) {
2114
+ throw new Error(`table ${name2}: memory.subject "${subject}" kolonu hedef "${m.into}" tablosunda yok`);
2115
+ }
2116
+ const factCol = target.columns["fact"];
2117
+ const factDef = factCol !== void 0 && "_def" in factCol ? factCol._def : factCol;
2118
+ if (factDef === void 0 || factDef.type !== "text") {
2119
+ throw new Error(`table ${name2}: memory.into "${m.into}" tablosunda "fact" (text) kolonu zorunlu`);
2120
+ }
2121
+ for (const c of m.from) {
2122
+ if (!(c in def.columns)) {
2123
+ throw new Error(`table ${name2}: memory.from kolonu "${c}" kaynak tabloda yok`);
2124
+ }
2125
+ }
2126
+ }
2127
+ const extensions = [...new Set(input.extensions ?? [])];
2128
+ return {
2129
+ name,
2130
+ tables,
2131
+ extensions,
2132
+ exposed: input.exposed ?? name === "public"
2133
+ };
2024
2134
  }
2025
2135
 
2026
2136
  // src/channels.ts
@@ -2222,59 +2332,46 @@ var ColumnBuilder = class _ColumnBuilder {
2222
2332
  ignored: true
2223
2333
  });
2224
2334
  }
2225
- references(table, column) {
2226
- refuseOnVector(this._def, "references");
2227
- this._def.references = { table, column };
2228
- return new _ColumnBuilder(this._def.type, this._def);
2229
- }
2230
2335
  /**
2231
- * Add a real DB-level foreign key to the built-in auth users
2232
- * (`REFERENCES auth.users(id)`), so a column like `user_id` gets true
2233
- * database cascade/integrity instead of app-layer-only. Sugar for
2234
- * `.references("auth.users", "id")`.
2336
+ * Foreign key onto another table's column.
2235
2337
  *
2236
- * `auth.users` lives in the SAME tenant database (palauth-owned), so this is
2237
- * a genuine cross-schema integrity constraint scoped to THIS tenant's users.
2238
- * The referenced `auth.users.id` is `text` (palauth ids are `usr_<uuid>`), so
2239
- * the referencing column must be `text()` too.
2338
+ * The target is a THUNK, not a direct reference. In a cycle (`x y`, `y → x`)
2339
+ * the second table does not exist yet when the first is built; a direct
2340
+ * reference makes TypeScript chase its own tail (TS7022 measured, and making
2341
+ * the return type independent of the target does NOT help). The thunk is
2342
+ * invoked in `defineSchema`, where every binding exists and every table
2343
+ * already knows its name.
2240
2344
  *
2241
- * ON DELETE is REQUIRED here and may only be `cascade` or `set null`: an
2242
- * account-erasure request must never be blocked by a lingering FK, so
2243
- * `restrict` / `no action` are not accepted (they don't type-check). Example:
2244
- * `text().notNull().referencesAuthUser("cascade")`, or
2245
- * `text().nullable().referencesAuthUser("set null")`. The server
2246
- * (validateAuthUserFK) enforces this — and the remaining rules the type can't
2247
- * express (referencing column is text, `set null` needs a nullable column) —
2248
- * as the real boundary; this signature is the compile-time DX mirror.
2345
+ * In a cycle, ONE side needs an explicit return type:
2346
+ * `references((): AnyColumn => y.id)`. One side is enough measured.
2347
+ * For a self-reference use `selfReferences(column)`: no thunk, no annotation.
2249
2348
  */
2250
- referencesAuthUser(onDelete) {
2251
- refuseOnVector(this._def, "referencesAuthUser");
2252
- this._def.references = { table: "auth.users", column: "id" };
2253
- this._def.onDeleteAction = onDelete;
2349
+ references(target, opts) {
2350
+ refuseOnVector(this._def, "references");
2351
+ if (typeof target !== "function") {
2352
+ throw new Error(
2353
+ `references(...) takes a callback: write references(() => otherTable.column). The two-string form references("table", "column") is gone \u2014 a string cannot be type-checked and cannot point at a table that does not exist yet.`
2354
+ );
2355
+ }
2356
+ this._def.referencesThunk = target;
2357
+ if (opts?.as !== void 0) this._def.refAs = opts.as;
2358
+ if (opts?.onDelete !== void 0) this._def.onDeleteAction = opts.onDelete;
2254
2359
  return new _ColumnBuilder(this._def.type, this._def);
2255
2360
  }
2256
2361
  /**
2257
- * Add a real DB-level foreign key to the canonical, server-minted installation
2258
- * anchor (`REFERENCES auth.installations(id)`) — the app-scoped verified-device
2259
- * root (`ins_...`). Sugar for `.references("auth.installations", "id")`.
2260
- *
2261
- * An installation is an APP INSTALL, not a user: this FK is NOT user ownership.
2262
- * A user-owned row STILL needs its own `.referencesAuthUser(...)` FK so account
2263
- * erasure removes it — an installation reference alone does not tie a row to a
2264
- * user's deletion. Use this only for install-scoped state (device prefs, push
2265
- * routing, …), alongside a separate auth-user FK where the row is user-owned.
2362
+ * Foreign key onto THIS table (`parent_id id`) — category trees, comment
2363
+ * replies, org charts.
2266
2364
  *
2267
- * `auth.installations` lives in the SAME tenant DB (palauth-owned); its `id` is
2268
- * `text` (`ins_<uuid>`), so the referencing column must be `text()` too. ON
2269
- * DELETE is REQUIRED and may only be `cascade` or `set null` (same allowed set
2270
- * as an auth-user FK): an installation revoke / orphan cleanup must never be
2271
- * blocked by a lingering FK. The server (validateAuthAnchorFK) is the real
2272
- * boundary; this signature is the compile-time DX mirror.
2365
+ * No thunk and no type annotation: the target table is the one being declared,
2366
+ * so there is nothing to defer and nothing for TypeScript to chase in a circle.
2367
+ * Drizzle forces an explicit `(): AnyPgColumn =>` here because its reference
2368
+ * always goes through a callback; measured, we do not need one.
2273
2369
  */
2274
- referencesInstallation(onDelete) {
2275
- refuseOnVector(this._def, "referencesInstallation");
2276
- this._def.references = { table: "auth.installations", column: "id" };
2277
- this._def.onDeleteAction = onDelete;
2370
+ selfReferences(column, opts) {
2371
+ refuseOnVector(this._def, "selfReferences");
2372
+ this._def.selfRefColumn = column;
2373
+ if (opts?.as !== void 0) this._def.refAs = opts.as;
2374
+ if (opts?.onDelete !== void 0) this._def.onDeleteAction = opts.onDelete;
2278
2375
  return new _ColumnBuilder(this._def.type, this._def);
2279
2376
  }
2280
2377
  /** Set the ON DELETE action for a foreign key reference. */
@@ -2349,6 +2446,28 @@ function vector(dimensions) {
2349
2446
  b._def.dimensions = dimensions;
2350
2447
  return b;
2351
2448
  }
2449
+ function ownedByUser() {
2450
+ const b = new ColumnBuilder("text");
2451
+ b._def.nullable = false;
2452
+ b._def.references = { table: "auth.users", column: "id" };
2453
+ b._def.onDeleteAction = "cascade";
2454
+ b._def.owns = true;
2455
+ return b;
2456
+ }
2457
+ function userRef(opts) {
2458
+ const b = new ColumnBuilder("text");
2459
+ b._def.references = { table: "auth.users", column: "id" };
2460
+ b._def.onDeleteAction = opts.onDelete;
2461
+ if (opts.as !== void 0) b._def.refAs = opts.as;
2462
+ return b;
2463
+ }
2464
+ function installationRef(opts) {
2465
+ const b = new ColumnBuilder("text");
2466
+ b._def.references = { table: "auth.installations", column: "id" };
2467
+ b._def.onDeleteAction = opts.onDelete;
2468
+ if (opts.as !== void 0) b._def.refAs = opts.as;
2469
+ return b;
2470
+ }
2352
2471
 
2353
2472
  // src/db/raw.ts
2354
2473
  function raw(name, up, opts) {
@@ -2459,50 +2578,73 @@ function rowType(def) {
2459
2578
  function optionalOnInsert(def) {
2460
2579
  return def.nullable === true || def.defaultRandom === true || def.defaultNow === true || def.defaultValue !== void 0;
2461
2580
  }
2462
- var AUTH_USERS_TABLE = "auth.users";
2463
- function ownerColumnOf(table) {
2464
- for (const [col, builder] of Object.entries(table.columns)) {
2465
- if (builder._def.references?.table === AUTH_USERS_TABLE) return col;
2466
- }
2467
- return null;
2581
+ function forwardName(column) {
2582
+ return column.endsWith("_id") ? column.slice(0, -3) : column;
2468
2583
  }
2469
- function buildChildEdges(schema) {
2470
- const edges = /* @__PURE__ */ new Map();
2471
- for (const name of Object.keys(schema.tables)) edges.set(name, []);
2472
- for (const childName of Object.keys(schema.tables)) {
2473
- const table = schema.tables[childName];
2474
- if (table === void 0) continue;
2475
- for (const [col, builder] of Object.entries(table.columns)) {
2476
- const ref = builder._def.references;
2477
- if (ref === void 0) continue;
2478
- if (ref.table === childName) continue;
2479
- const bucket = edges.get(ref.table);
2480
- if (bucket === void 0) continue;
2481
- if (bucket.some((e) => e.child === childName)) continue;
2482
- bucket.push({ child: childName, column: col });
2483
- }
2484
- }
2485
- return edges;
2584
+ function qualify(schemaName, tableName) {
2585
+ return schemaName === "public" ? tableName : `${schemaName}.${tableName}`;
2486
2586
  }
2487
- function pruneCycles(raw2) {
2488
- const pruned = /* @__PURE__ */ new Map();
2489
- for (const table of raw2.keys()) pruned.set(table, []);
2490
- const state = /* @__PURE__ */ new Map();
2491
- function visit(table) {
2492
- state.set(table, "visiting");
2493
- for (const edge of raw2.get(table) ?? []) {
2494
- if (state.get(edge.child) === "visiting") continue;
2495
- pruned.get(table)?.push(edge);
2496
- if (state.get(edge.child) === void 0) visit(edge.child);
2587
+ function buildRelations(schemas) {
2588
+ const out = /* @__PURE__ */ new Map();
2589
+ const keyOf = /* @__PURE__ */ new Map();
2590
+ for (const schema of schemas) {
2591
+ for (const t of Object.values(schema.tables)) {
2592
+ const key = qualify(schema.name, t.name);
2593
+ out.set(key, []);
2594
+ keyOf.set(t.name, key);
2497
2595
  }
2498
- state.set(table, "done");
2499
2596
  }
2500
- for (const table of raw2.keys()) {
2501
- if (state.get(table) === void 0) visit(table);
2597
+ for (const schema of schemas) {
2598
+ for (const table of Object.values(schema.tables)) {
2599
+ const childKey = qualify(schema.name, table.name);
2600
+ const bucket = out.get(childKey);
2601
+ if (bucket === void 0) continue;
2602
+ const byTarget = /* @__PURE__ */ new Map();
2603
+ for (const [col, builder] of Object.entries(table.columns)) {
2604
+ const ref = builder._def.references;
2605
+ if (ref === void 0) continue;
2606
+ const list = byTarget.get(ref.table) ?? [];
2607
+ list.push(col);
2608
+ byTarget.set(ref.table, list);
2609
+ }
2610
+ for (const [target, cols] of byTarget) {
2611
+ if (cols.length < 2) continue;
2612
+ const unnamed = cols.filter((c) => {
2613
+ const d = table.columns[c]?._def;
2614
+ return d?.refAs === void 0 && d?.owns !== true;
2615
+ });
2616
+ if (unnamed.length > 0) {
2617
+ throw new Error(
2618
+ `table "${table.name}" declares ${cols.length} foreign keys onto "${target}" (${cols.join(", ")}) \u2014 name each one: references(() => \u2026, { as: "\u2026" })`
2619
+ );
2620
+ }
2621
+ }
2622
+ for (const [col, builder] of Object.entries(table.columns)) {
2623
+ const def = builder._def;
2624
+ if (def.references === void 0) continue;
2625
+ const name = def.owns === true ? "owner" : def.refAs ?? forwardName(col);
2626
+ if (bucket.some((r) => r.name === name)) {
2627
+ throw new Error(
2628
+ name === "owner" ? `table "${table.name}": two columns claim ownership of the row \u2014 a table has at most ONE ownedByUser(). If the other column only POINTS at a user, declare it userRef({ onDelete: \u2026 }) instead.` : `table "${table.name}": two relations resolve to the name "${name}" \u2014 give one an explicit { as: "\u2026" }`
2629
+ );
2630
+ }
2631
+ const targetKey = keyOf.get(def.references.table) ?? def.references.table;
2632
+ bucket.push({ name, to: targetKey, kind: "one", via: col });
2633
+ const parent = out.get(targetKey);
2634
+ if (parent === void 0 || targetKey === childKey) continue;
2635
+ const reverse = def.refAs ?? table.name;
2636
+ if (parent.some((r) => r.name === reverse)) {
2637
+ throw new Error(
2638
+ `table "${def.references.table}": two reverse relations resolve to the name "${reverse}" \u2014 give one an explicit { as: "\u2026" }`
2639
+ );
2640
+ }
2641
+ parent.push({ name: reverse, to: childKey, kind: "many", via: col });
2642
+ }
2643
+ }
2502
2644
  }
2503
- return pruned;
2645
+ return out;
2504
2646
  }
2505
- function tableBlock(table, children, indent) {
2647
+ function tableBlock(table, relations, indent) {
2506
2648
  const cols = Object.entries(table.columns);
2507
2649
  const rowLines = cols.map(([col, builder]) => {
2508
2650
  return `${indent} ${col}: ${rowType(builder._def)};`;
@@ -2512,8 +2654,9 @@ function tableBlock(table, children, indent) {
2512
2654
  const opt = optionalOnInsert(def) ? "?" : "";
2513
2655
  return `${indent} ${col}${opt}: ${rowType(def)};`;
2514
2656
  });
2515
- const owner = ownerColumnOf(table);
2516
- const childEntries = children.map((e) => `${JSON.stringify(e.child)}: ${JSON.stringify(e.column)}`).join("; ");
2657
+ const relEntries = relations.map(
2658
+ (r) => `${r.name}: { to: ${JSON.stringify(r.to)}; kind: ${JSON.stringify(r.kind)}; via: ${JSON.stringify(r.via)} }`
2659
+ ).join("; ");
2517
2660
  return [
2518
2661
  `${indent}${table.name}: {`,
2519
2662
  `${indent} row: {`,
@@ -2522,8 +2665,7 @@ function tableBlock(table, children, indent) {
2522
2665
  `${indent} insert: {`,
2523
2666
  ...insertLines,
2524
2667
  `${indent} };`,
2525
- `${indent} owner: ${owner === null ? "null" : JSON.stringify(owner)};`,
2526
- `${indent} children: {${childEntries === "" ? "" : ` ${childEntries} `}};`,
2668
+ `${indent} relations: {${relEntries === "" ? "" : ` ${relEntries} `}};`,
2527
2669
  // searchable: vector kolonu YA DA search beyanı → EnvTypedTable'da search()
2528
2670
  // üyesini açan yapısal bayrak (FR-013). Yokken satır hiç üretilmez ki
2529
2671
  // mevcut şemaların d.ts'i bayt-aynı kalsın.
@@ -2531,22 +2673,37 @@ function tableBlock(table, children, indent) {
2531
2673
  `${indent}};`
2532
2674
  ].join("\n");
2533
2675
  }
2534
- function makeEnvDts(schema) {
2535
- const tableNames = Object.keys(schema.tables);
2536
- const children = pruneCycles(buildChildEdges(schema));
2537
- const blocks = tableNames.map(
2538
- (name) => tableBlock(schema.tables[name], children.get(name) ?? [], " ")
2676
+ function makeEnvDts(schemas) {
2677
+ const relations = buildRelations(schemas);
2678
+ const qualify2 = (schemaName, table) => schemaName === "public" ? table : `${schemaName}.${table}`;
2679
+ const publicSchema = schemas.find((s) => s.name === "public");
2680
+ const others = schemas.filter((s) => s.name !== "public");
2681
+ const publicBlocks = Object.keys(publicSchema?.tables ?? {}).map(
2682
+ (name) => tableBlock(publicSchema.tables[name], relations.get(name) ?? [], " ")
2539
2683
  );
2540
- const body = blocks.length > 0 ? `
2541
- ${blocks.join("\n")}
2684
+ const body = publicBlocks.length > 0 ? `
2685
+ ${publicBlocks.join("\n")}
2686
+ ` : "";
2687
+ const schemaBlocks = others.map((schema) => {
2688
+ const inner = Object.keys(schema.tables).map(
2689
+ (name) => tableBlock(schema.tables[name], relations.get(qualify2(schema.name, name)) ?? [], " ")
2690
+ );
2691
+ return ` ${schema.name}: {
2692
+ ${inner.join("\n")}
2693
+ };`;
2694
+ });
2695
+ const schemasBody = schemaBlocks.length > 0 ? `
2696
+ ${schemaBlocks.join("\n")}
2542
2697
  ` : "";
2543
2698
  return `// AUTO-GENERATED by @palbase/backend \u2014 DO NOT EDIT.
2544
- // Regenerated from db/schema.ts by \`palbase build\` and by every deploy.
2699
+ // Regenerated from db/*.ts by \`palbase build\` and by every deploy.
2545
2700
  // Augments the @palbase/backend/env \`Tables\` interface so \`Database.tables.*\`
2546
- // is typed with no import and no generic.
2701
+ // is typed with no import and no generic. Schemas other than \`public\` land
2702
+ // under \`Schemas\`, reached with \`Database.schema("<name>").tables.*\`.
2547
2703
 
2548
2704
  declare module "@palbase/backend/env" {
2549
2705
  interface Tables {${body}}
2706
+ interface Schemas {${schemasBody}}
2550
2707
  }
2551
2708
 
2552
2709
  export {};
@@ -2602,6 +2759,8 @@ export {};
2602
2759
  // src/decorators/registry.ts
2603
2760
  var ROUTES = /* @__PURE__ */ Symbol.for("palbase.backend.routes");
2604
2761
  var PARAM_BUFFER = /* @__PURE__ */ Symbol.for("palbase.backend.paramBuffer");
2762
+ var ROOM = /* @__PURE__ */ Symbol.for("palbase.backend.room");
2763
+ var ROOM_HOOKS = /* @__PURE__ */ Symbol.for("palbase.backend.roomHooks");
2605
2764
  var RETURN_BUFFER = /* @__PURE__ */ Symbol.for("palbase.backend.returnBuffer");
2606
2765
  var THROWS_BUFFER = /* @__PURE__ */ Symbol.for("palbase.backend.throwsBuffer");
2607
2766
  function carrierOf(target) {
@@ -2690,6 +2849,38 @@ function getRoutes(ctor) {
2690
2849
  ...r.throws !== void 0 ? { throws: r.throws.slice() } : {}
2691
2850
  }));
2692
2851
  }
2852
+ function ownRoomBuffer(carrier) {
2853
+ if (!Object.prototype.hasOwnProperty.call(carrier, ROOM_HOOKS)) {
2854
+ carrier[ROOM_HOOKS] = { hooks: {}, messages: {} };
2855
+ }
2856
+ return carrier[ROOM_HOOKS];
2857
+ }
2858
+ function recordRoomHook(target, hook, fnName) {
2859
+ const buffer = ownRoomBuffer(carrierOf(target));
2860
+ const existing = buffer.hooks[hook];
2861
+ if (existing !== void 0 && existing !== fnName) {
2862
+ const label = `On${hook.charAt(0).toUpperCase()}${hook.slice(1)}`;
2863
+ throw new Error(
2864
+ `@${label} is declared twice in one room (${existing} and ${fnName}). A room has one of each hook.`
2865
+ );
2866
+ }
2867
+ buffer.hooks[hook] = fnName;
2868
+ }
2869
+ function recordRoomMessage(target, name, fnName, schema) {
2870
+ const buffer = ownRoomBuffer(carrierOf(target));
2871
+ const existing = buffer.messages[name];
2872
+ if (existing !== void 0 && existing.fnName !== fnName) {
2873
+ throw new Error(
2874
+ `@OnMessage("${name}") is declared twice in one room (${existing.fnName} and ${fnName}).`
2875
+ );
2876
+ }
2877
+ buffer.messages[name] = { fnName, schema };
2878
+ }
2879
+ function recordRoom(ctor, meta) {
2880
+ const carrier = carrierOf(ctor);
2881
+ const buffer = ownRoomBuffer(carrier);
2882
+ carrier[ROOM] = { ...meta, hooks: buffer.hooks, messages: buffer.messages };
2883
+ }
2693
2884
 
2694
2885
  // src/decorators/controller.ts
2695
2886
  var CONTROLLER_META = /* @__PURE__ */ Symbol.for("palbase.backend.controllerMeta");
@@ -3002,6 +3193,56 @@ function Signal() {
3002
3193
  };
3003
3194
  }
3004
3195
 
3196
+ // src/decorators/room.ts
3197
+ var DEFAULT_GRACE_MS = 6e4;
3198
+ function assertPattern(pattern) {
3199
+ if (pattern.includes("/")) {
3200
+ throw new Error(
3201
+ `@Room("${pattern}"): channel patterns are colon-separated, not slash-separated. Write "${pattern.replace(/\//g, ":")}" \u2014 a slash matches no live topic.`
3202
+ );
3203
+ }
3204
+ const segments = pattern.split(":");
3205
+ if (segments.some((s) => s === "")) {
3206
+ throw new Error(`@Room("${pattern}"): empty segment \u2014 every segment must be non-empty.`);
3207
+ }
3208
+ const seen = /* @__PURE__ */ new Set();
3209
+ for (const s of segments) {
3210
+ if (!s.startsWith("{") || !s.endsWith("}")) continue;
3211
+ const name = s.slice(1, -1);
3212
+ if (name === "") throw new Error(`@Room("${pattern}"): "{}" names no parameter.`);
3213
+ if (seen.has(name)) {
3214
+ throw new Error(`@Room("${pattern}"): parameter "{${name}}" appears twice.`);
3215
+ }
3216
+ seen.add(name);
3217
+ }
3218
+ }
3219
+ function Room(pattern, options) {
3220
+ return function(ctor) {
3221
+ assertPattern(pattern);
3222
+ recordRoom(ctor, {
3223
+ pattern,
3224
+ events: options.events,
3225
+ graceMs: options.graceMs ?? DEFAULT_GRACE_MS
3226
+ });
3227
+ return ctor;
3228
+ };
3229
+ }
3230
+ function hookDecorator(hook) {
3231
+ return () => function(target, propertyKey) {
3232
+ recordRoomHook(target, hook, String(propertyKey));
3233
+ };
3234
+ }
3235
+ var OnAuthorize = hookDecorator("authorize");
3236
+ var OnFirst = hookDecorator("first");
3237
+ var OnJoin = hookDecorator("join");
3238
+ var OnLeave = hookDecorator("leave");
3239
+ var OnEmpty = hookDecorator("empty");
3240
+ function OnMessage(name, schema) {
3241
+ return function(target, propertyKey) {
3242
+ recordRoomMessage(target, name, String(propertyKey), schema);
3243
+ };
3244
+ }
3245
+
3005
3246
  // src/decorators/params.ts
3006
3247
  function makeParamDecorator(kind, extra) {
3007
3248
  return function(target, propertyKey, parameterIndex) {
@@ -3422,6 +3663,12 @@ var import_zod2 = require("zod");
3422
3663
  NotFound,
3423
3664
  Notifications,
3424
3665
  On,
3666
+ OnAuthorize,
3667
+ OnEmpty,
3668
+ OnFirst,
3669
+ OnJoin,
3670
+ OnLeave,
3671
+ OnMessage,
3425
3672
  OptionalUser,
3426
3673
  PALBASE_EXTENSIONS,
3427
3674
  PalError,
@@ -3436,11 +3683,13 @@ var import_zod2 = require("zod");
3436
3683
  Realtime,
3437
3684
  Req,
3438
3685
  RequestId,
3686
+ Room,
3439
3687
  Secrets,
3440
3688
  Signal,
3441
3689
  Sse,
3442
3690
  SseOut,
3443
3691
  Storage,
3692
+ TABLE_META,
3444
3693
  TooManyRequests,
3445
3694
  TraceId,
3446
3695
  TxPlanError,
@@ -3469,6 +3718,7 @@ var import_zod2 = require("zod");
3469
3718
  defineError,
3470
3719
  defineMiddleware,
3471
3720
  defineSchema,
3721
+ defineTable,
3472
3722
  enumType,
3473
3723
  getErrorRegistry,
3474
3724
  getHookConfig,
@@ -3477,6 +3727,7 @@ var import_zod2 = require("zod");
3477
3727
  getRoutes,
3478
3728
  getWebhookConfig,
3479
3729
  inc,
3730
+ installationRef,
3480
3731
  integer,
3481
3732
  isPalbaseExtension,
3482
3733
  jsonb,
@@ -3489,6 +3740,7 @@ var import_zod2 = require("zod");
3489
3740
  onShutdown,
3490
3741
  onStart,
3491
3742
  openai,
3743
+ ownedByUser,
3492
3744
  ownerOnly,
3493
3745
  policy,
3494
3746
  publicChannel,
@@ -3498,6 +3750,7 @@ var import_zod2 = require("zod");
3498
3750
  text,
3499
3751
  timestamp,
3500
3752
  toSchemaJSON,
3753
+ userRef,
3501
3754
  uuid,
3502
3755
  vector,
3503
3756
  z