better-supabase 0.3.0 → 0.5.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 (211) hide show
  1. package/README.md +9 -6
  2. package/bin/better-supabase.js +4 -0
  3. package/dist/{base64-CQeB1eSf.js → base64-D2gsioCY.js} +2 -2
  4. package/dist/channel-FQW-CnGR.d.ts +70 -0
  5. package/dist/claims-BAf6c7is.js +48 -0
  6. package/dist/cli/_format-Cax5EsM9.js +101 -0
  7. package/dist/cli/bin.d.ts +1 -0
  8. package/dist/cli/bin.js +48 -0
  9. package/dist/cli/codemod-DqV7612m.js +388 -0
  10. package/dist/cli/command-CK14fP91.js +502 -0
  11. package/dist/cli/compare-BmIWVEui.js +9 -0
  12. package/dist/cli/config-CPVpXP5I.js +1965 -0
  13. package/dist/cli/config-dtoekM0v.js +2 -0
  14. package/dist/cli/db-DOBPt660.js +52 -0
  15. package/dist/cli/diff-SjkO5rNr.js +752 -0
  16. package/dist/cli/dist-DBPdlPYK.js +1112 -0
  17. package/dist/cli/dist-DreZ24Sz.js +129 -0
  18. package/dist/cli/dist-xn4wlboc.js +732 -0
  19. package/dist/cli/doctor-L7Z8mYTx.js +2863 -0
  20. package/dist/cli/env-DmKv_G4A.js +108 -0
  21. package/dist/cli/env-jIFUADLY.js +511 -0
  22. package/dist/cli/errors-4v4DTtda.js +56 -0
  23. package/dist/cli/fastest-levenshtein-CFN-lNDv.js +120 -0
  24. package/dist/cli/gen-BJc9vdyu.js +2 -0
  25. package/dist/cli/gen-C2osRcr5.js +2204 -0
  26. package/dist/cli/index.d.ts +493 -0
  27. package/dist/cli/index.js +184 -0
  28. package/dist/cli/init-Saf5-R9T.js +550 -0
  29. package/dist/cli/introspect--H7oGmcd.js +81 -0
  30. package/dist/cli/io-k41_z-jT.js +24 -0
  31. package/dist/cli/json5-ChRycquj.js +787 -0
  32. package/dist/cli/jsonc-BUniUpOk.js +2 -0
  33. package/dist/cli/jsonc-D5Fu67jw.js +45 -0
  34. package/dist/cli/keys-ByKHnvP9.js +83 -0
  35. package/dist/cli/ohash-D7bG_EOb.js +277 -0
  36. package/dist/cli/openapi-Cp5Fyta-.js +66 -0
  37. package/dist/cli/project-B_GeXyHh.js +70 -0
  38. package/dist/cli/prompts-BNcNfAve.js +1142 -0
  39. package/dist/cli/prompts-BTYQGunc.js +2 -0
  40. package/dist/cli/read-sets-BfMQcKy8.js +289 -0
  41. package/dist/cli/rolldown-runtime-BPOCksWG.js +24 -0
  42. package/dist/cli/run-BKR2W_79.js +351 -0
  43. package/dist/cli/seed-CUlqWh-8.js +98 -0
  44. package/dist/cli/skills-LJJIhLFR.js +137 -0
  45. package/dist/cli/sql-DzPbfngx.js +348 -0
  46. package/dist/cli/style-C0NFdJRL.js +29 -0
  47. package/dist/cli/toml-DB-MNnFs.js +424 -0
  48. package/dist/cli/version-BDJuO0UR.js +4 -0
  49. package/dist/cli/yaml-CYCbJ6et.js +2252 -0
  50. package/dist/client/index.d.ts +2 -2
  51. package/dist/client/index.js +15 -22
  52. package/dist/clone-BfgmLU90.js +21 -0
  53. package/dist/{compiler-2zqGzxVZ.d.ts → compiler-BXH_aZ7N.d.ts} +2 -2
  54. package/dist/config/index.d.ts +4 -4
  55. package/dist/config/index.js +5 -5
  56. package/dist/{config-DOsGYSiT.js → config-DreERlKo.js} +1 -1
  57. package/dist/{config-C1CYcm6o.d.ts → config-T_ejwyd3.d.ts} +176 -11
  58. package/dist/{define-DUbydmJa.d.ts → define-Bp_BZa5h.d.ts} +37 -29
  59. package/dist/{define-DF341GDj.js → define-cNKzqwL_.js} +356 -89
  60. package/dist/edge/index.d.ts +20 -13
  61. package/dist/edge/index.js +55 -26
  62. package/dist/{entitlements-DvJXoI02.d.ts → entitlements-DUxU4oHe.d.ts} +2 -2
  63. package/dist/env/index.d.ts +2 -2
  64. package/dist/env/index.js +34 -1
  65. package/dist/{errors-Bl3rkOzo.js → errors-DtwXa6b8.js} +2 -1
  66. package/dist/events/index.d.ts +3 -2
  67. package/dist/events/index.js +69 -16
  68. package/dist/events-aIO7vsIk.d.ts +230 -0
  69. package/dist/executor-BunPOO-k.d.ts +402 -0
  70. package/dist/{executor-B26lgbHD.d.ts → executor-Cghdstwu.d.ts} +1 -2
  71. package/dist/{executor-C57jx2pn.js → executor-XycESQPH.js} +3 -3
  72. package/dist/hono/index.d.ts +27 -23
  73. package/dist/hono/index.js +46 -11
  74. package/dist/impersonation-VfaGc4Q_.js +41 -0
  75. package/dist/{index-BNsABq7P.d.ts → index-BGdgBPdf.d.ts} +66 -25
  76. package/dist/{index-qriRM9cZ.d.ts → index-C7f8cOBM.d.ts} +4 -4
  77. package/dist/{index-Dy6kuF0P.d.ts → index-CLg87OBW.d.ts} +6 -1
  78. package/dist/{index-GV77NpRP.d.ts → index-DOESKG27.d.ts} +34 -9
  79. package/dist/{index-CriFFJ_p.d.ts → index-DgILJRXj.d.ts} +43 -17
  80. package/dist/{index-UJ-LUxtI.d.ts → index-DmXSxbLi.d.ts} +14 -14
  81. package/dist/{index-Cf-D2jk1.d.ts → index-Dxn2a5cw.d.ts} +2 -2
  82. package/dist/{index-CTTMS6_1.d.ts → index-SaTsEfGP.d.ts} +86 -9
  83. package/dist/{index-CADlNHK-.d.ts → index-fag8VJZ2.d.ts} +5 -6
  84. package/dist/index.d.ts +34 -17
  85. package/dist/index.js +15 -14
  86. package/dist/jobs/index.d.ts +147 -95
  87. package/dist/jobs/index.js +773 -250
  88. package/dist/{json-schema--aOXAGfU.js → json-schema-m1fiwKgN.js} +23 -2
  89. package/dist/kit-events-CTFDfSwb.js +60 -0
  90. package/dist/kit-transport-B82k1djG.d.ts +33 -0
  91. package/dist/kit-transport-DJlHQUBU.js +55 -0
  92. package/dist/{config-Be4GwsqI.js → kits-CGLLrNPd.js} +38 -10
  93. package/dist/list/index.d.ts +1 -1
  94. package/dist/list/index.js +3 -3
  95. package/dist/{live-D8rV1Ldw.js → live-CdJBOc4f.js} +13 -9
  96. package/dist/{live-QE3UPbGm.d.ts → live-D9W2M5aO.d.ts} +16 -15
  97. package/dist/lookup-BaP5bI27.js +18 -0
  98. package/dist/mcp/index.d.ts +39 -22
  99. package/dist/mcp/index.js +39 -25
  100. package/dist/{mfa-x3V3vGh-.d.ts → mfa-CusDgPaD.d.ts} +5 -1
  101. package/dist/next/image/index.js +6 -3
  102. package/dist/next/index.d.ts +60 -36
  103. package/dist/next/index.js +167 -50
  104. package/dist/notifications/index.d.ts +162 -0
  105. package/dist/notifications/index.js +264 -0
  106. package/dist/openapi/index.d.ts +1 -1
  107. package/dist/openapi/index.js +6 -6
  108. package/dist/orgs/index.d.ts +132 -0
  109. package/dist/orgs/index.js +245 -0
  110. package/dist/orpc/index.d.ts +37 -20
  111. package/dist/orpc/index.js +24 -11
  112. package/dist/otel/index.d.ts +24 -5
  113. package/dist/otel/index.js +124 -64
  114. package/dist/permdock-sql-C2Inc6Xc.d.ts +29 -0
  115. package/dist/permdock-sql-DgOfeixi.js +51 -0
  116. package/dist/plugin-DwARMuQp.js +59 -0
  117. package/dist/{plugin-DVRB8qNE.d.ts → plugin-o144f322.d.ts} +17 -5
  118. package/dist/plugins/actor/index.d.ts +5 -2
  119. package/dist/plugins/actor/index.js +22 -8
  120. package/dist/plugins/rules/index.d.ts +2 -2
  121. package/dist/plugins/rules/index.js +52 -17
  122. package/dist/plugins/soft-delete/index.d.ts +3 -3
  123. package/dist/plugins/soft-delete/index.js +14 -5
  124. package/dist/plugins/tenant/index.d.ts +3 -3
  125. package/dist/plugins/tenant/index.js +45 -21
  126. package/dist/plugins/timestamps/index.d.ts +1 -1
  127. package/dist/plugins/timestamps/index.js +4 -3
  128. package/dist/plugins/validation/index.d.ts +2 -2
  129. package/dist/plugins/validation/index.js +26 -6
  130. package/dist/{pool-B9qsLyFR.d.ts → pool-C9voM4Kw.d.ts} +24 -6
  131. package/dist/postgres/index.d.ts +5 -5
  132. package/dist/postgres/index.js +20 -10
  133. package/dist/{postgrest-DmBlevh-.d.ts → postgrest-CruYKAvV.d.ts} +1 -1
  134. package/dist/{problem-CGt1ADix.js → problem-BW1KhzSG.js} +2 -1
  135. package/dist/{problem-Z2RTQjZD.d.ts → problem-Blxe8S31.d.ts} +1 -1
  136. package/dist/query/index.d.ts +2 -2
  137. package/dist/query/index.js +3 -3
  138. package/dist/{query-BdKLTPlj.js → query-CCbo44lG.js} +18 -18
  139. package/dist/queue-BYUCjr59.d.ts +252 -0
  140. package/dist/react/index.d.ts +8 -7
  141. package/dist/react/index.js +4 -3
  142. package/dist/react/server.d.ts +16 -5
  143. package/dist/react/server.js +12 -4
  144. package/dist/react/session.d.ts +10 -4
  145. package/dist/react/session.js +11 -3
  146. package/dist/{react-DNBYNazu.js → react-C_s7kiNz.js} +138 -46
  147. package/dist/{read-set-D0qBcboM.js → read-set-DQFt1N_Y.js} +71 -15
  148. package/dist/realtime/index.d.ts +3 -3
  149. package/dist/realtime/index.js +14 -12
  150. package/dist/{index-kpgopLNv.d.ts → refresh-BTrQ_ZTp.d.ts} +6 -3
  151. package/dist/{resolve-BRciX2jK.d.ts → resolve-C15TJq77.d.ts} +22 -11
  152. package/dist/{resource-D30rp0GN.js → resource-CRxxwkQq.js} +24 -9
  153. package/dist/{resource-CwVgiI5q.d.ts → resource-D8iqWMr-.d.ts} +6 -6
  154. package/dist/respond-D2OQl62-.d.ts +371 -0
  155. package/dist/{result-DWXatkd6.js → result-BKiliPY7.js} +2 -2
  156. package/dist/server/index.d.ts +17 -15
  157. package/dist/server/index.js +16 -22
  158. package/dist/server-Csx2sRjg.js +655 -0
  159. package/dist/{shared-_6-uPL7E.js → shared-D1dRUrAg.js} +19 -4
  160. package/dist/signers-Cc6r98SW.d.ts +162 -0
  161. package/dist/{simplify-LakqK1vH.js → simplify-_0oUKqCd.js} +27 -95
  162. package/dist/sql/index.d.ts +247 -10
  163. package/dist/sql/index.js +8591 -780
  164. package/dist/{sql-BIgk12ew.js → sql-Coh6n4cC.js} +11 -3
  165. package/dist/ssr/index.d.ts +4 -4
  166. package/dist/ssr/index.js +1 -2
  167. package/dist/{standard-FUi3Z3-7.js → standard-D7r6dMUu.js} +6 -3
  168. package/dist/{standard-eudBiRk1.d.ts → standard-J4YgMOLm.d.ts} +1 -1
  169. package/dist/{stats-D78UQ8Yu.js → stats-Bqob3WqT.js} +34 -9
  170. package/dist/storage/index.d.ts +4 -4
  171. package/dist/storage/index.js +174 -27
  172. package/dist/support-DT-OhSxD.d.ts +88 -0
  173. package/dist/support-Dk6vdmEw.js +291 -0
  174. package/dist/support-view-Da5peKFg.d.ts +17 -0
  175. package/dist/support-view-g4YLBeqX.js +18 -0
  176. package/dist/{tables-Dhha3BXM.js → tables-Cb-3_VWc.js} +14 -0
  177. package/dist/{permdock-sql-Bta_G0v-.js → template-TxrGI5jR.js} +2 -31
  178. package/dist/{temporal-BwshQk0W.js → temporal-CVlIV2DZ.js} +17 -4
  179. package/dist/{temporal-required-CMxeSVPo.js → temporal-required-CAt4Dc9Z.js} +2 -2
  180. package/dist/testing/index.d.ts +113 -24
  181. package/dist/testing/index.js +375 -47
  182. package/dist/{types-DONv9SHN.d.ts → types-Dnf_wRDz.d.ts} +26 -3
  183. package/dist/user-change-CHFEUro1.js +49 -0
  184. package/dist/{webhooks-CAIFQSTM.js → verify-Ce49ETPz.js} +38 -19
  185. package/dist/{version-BfpG2gks.js → version-DM_Roffb.js} +1 -1
  186. package/dist/{view-3Po_PVlr.d.ts → view-BEf0diC5.d.ts} +10 -5
  187. package/dist/{mfa-BxoR1RkN.js → view-Bq64ki8-.js} +210 -64
  188. package/dist/webhooks/index.d.ts +136 -5
  189. package/dist/webhooks/index.js +588 -2
  190. package/dist/wire-D2dT86vi.js +35 -0
  191. package/package.json +50 -10
  192. package/schemas/config-v1.json +131 -1
  193. package/schemas/doctor-report-v1.json +10 -0
  194. package/skills/better-supabase/SKILL.md +60 -10
  195. package/skills/better-supabase/references/plugins.md +13 -14
  196. package/skills/better-supabase/references/troubleshooting.md +25 -4
  197. package/skills/better-supabase-api/SKILL.md +74 -19
  198. package/skills/better-supabase-api/references/adapters.md +48 -27
  199. package/skills/better-supabase-auth/SKILL.md +149 -0
  200. package/skills/better-supabase-auth/references/permdock.md +100 -0
  201. package/skills/better-supabase-testing/SKILL.md +58 -4
  202. package/dist/claims-CoTobNC-.js +0 -25
  203. package/dist/events-Do8jXh4D.d.ts +0 -84
  204. package/dist/executor-DynmIWhq.d.ts +0 -195
  205. package/dist/invalidate-DbxuL2jL.js +0 -18
  206. package/dist/permdock-sql-DImJULY-.d.ts +0 -17
  207. package/dist/plugin-BqR6wKMB.js +0 -23
  208. package/dist/respond-C44EZAJo.js +0 -399
  209. package/dist/respond-DAkPIdTH.d.ts +0 -175
  210. package/dist/result-c2OYgCV2.d.ts +0 -203
  211. package/dist/view-wZ-9qIUz.js +0 -45
@@ -0,0 +1,2204 @@
1
+ import { r as defineCliCommand } from "./command-CK14fP91.js";
2
+ import { n as CliError } from "./errors-4v4DTtda.js";
3
+ import { n as importPath, r as writeIfChanged, t as display } from "./io-k41_z-jT.js";
4
+ import { r as painter } from "./style-C0NFdJRL.js";
5
+ import { t as VERSION } from "./version-BDJuO0UR.js";
6
+ import { d as pgFunctionHooks, f as readSupabaseToml, m as tomlGet, s as stdinDatabaseUrl, t as databaseUrl } from "./config-CPVpXP5I.js";
7
+ import { t as fileDiff } from "./diff-SjkO5rNr.js";
8
+ import { a as parseHookMarker, i as parseGrantsMarker, n as entitlementRequirements, r as entitlementsMode, s as readPermdock, t as compiledReadSets } from "./read-sets-BfMQcKy8.js";
9
+ import { t as byCodePoint } from "./compare-BmIWVEui.js";
10
+ import { n as withSpinner } from "./prompts-BNcNfAve.js";
11
+ import { t as connect } from "./db-DOBPt660.js";
12
+ import { basename, dirname, resolve } from "node:path";
13
+ import { existsSync } from "node:fs";
14
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
15
+ import { setTimeout } from "node:timers/promises";
16
+ import { PERMDOCK_SCHEMA, kitLayout, permdockKeyStatus, permdockKeys, renderKit, sameKitFile } from "better-supabase/sql";
17
+ import { DEFAULT_CLAIMS, tenantClaimPaths } from "better-supabase/config";
18
+ import { toCamel, toSnake } from "better-supabase";
19
+ import { generateTypescript, pgTypeToTsType, sortGeneratorMetadata } from "@supabase/postgrest-typegen/generation";
20
+ import { createHash } from "node:crypto";
21
+ //#region src/cli/introspect/catalog.ts
22
+ const byName = (a, b) => a.schema === b.schema ? a.name < b.name ? -1 : a.name > b.name ? 1 : 0 : a.schema < b.schema ? -1 : 1;
23
+ function toColumn(column) {
24
+ const isArray = column.data_type === "ARRAY";
25
+ const udt = isArray ? column.format.replace(/^_/, "") : column.format;
26
+ const defaultValue = column.default_value === null || column.default_value === void 0 ? null : String(column.default_value);
27
+ return {
28
+ name: column.name,
29
+ udt,
30
+ format: column.format,
31
+ typeSchema: column.type_schema,
32
+ isArray,
33
+ isEnum: column.enums.length > 0,
34
+ nullable: column.is_nullable,
35
+ hasDefault: defaultValue !== null || column.identity_generation !== null || column.is_generated,
36
+ default: defaultValue,
37
+ identity: column.identity_generation === "ALWAYS" ? "always" : column.identity_generation === "BY DEFAULT" ? "by default" : null,
38
+ generated: column.is_generated,
39
+ updatable: column.is_updatable,
40
+ comment: column.comment
41
+ };
42
+ }
43
+ function toFunction(fn, typeName, relationName) {
44
+ const args = [];
45
+ const table = [];
46
+ fn.args.forEach((arg, index) => {
47
+ const raw = typeName(arg.type_id) ?? "unknown";
48
+ const name = arg.name || `arg${index + 1}`;
49
+ if (arg.mode === "in" || arg.mode === "inout" || arg.mode === "variadic") {
50
+ const isArray = raw.startsWith("_");
51
+ args.push({
52
+ name,
53
+ udt: isArray ? raw.slice(1) : raw,
54
+ isArray,
55
+ hasDefault: arg.has_default === true
56
+ });
57
+ }
58
+ if (arg.mode === "table" || arg.mode === "out" || arg.mode === "inout") table.push({
59
+ name,
60
+ udt: raw
61
+ });
62
+ });
63
+ const searchPath = fn.config_params?.["search_path"];
64
+ return {
65
+ schema: fn.schema,
66
+ name: fn.name,
67
+ signature: fn.identity_argument_types,
68
+ args,
69
+ returnsTable: fn.args.some((arg) => arg.mode === "table") ? table : null,
70
+ returns: typeName(fn.return_type_id) ?? fn.return_type,
71
+ returnsRelation: relationName(fn.return_type_relation_id),
72
+ returnsSet: fn.is_set_returning_function,
73
+ volatility: fn.behavior.toLowerCase(),
74
+ securityDefiner: fn.security_definer,
75
+ language: fn.language,
76
+ searchPath: searchPath === void 0 ? null : searchPath.replace(/^""$/, "")
77
+ };
78
+ }
79
+ /** Joins the typegen metadata and the extras into one record per object. */
80
+ function toCatalog(snapshot) {
81
+ const meta = snapshot.generator;
82
+ const extras = new Map(snapshot.extras.tables.map((table) => [table.id, table]));
83
+ const columns = /* @__PURE__ */ new Map();
84
+ for (const column of meta.columns) {
85
+ const list = columns.get(column.table_id);
86
+ if (list) list.push(column);
87
+ else columns.set(column.table_id, [column]);
88
+ }
89
+ const primaryKeys = /* @__PURE__ */ new Map();
90
+ for (const key of meta.primaryKeys) {
91
+ const list = primaryKeys.get(key.table_id);
92
+ if (list) list.push(key.name);
93
+ else primaryKeys.set(key.table_id, [key.name]);
94
+ }
95
+ const relationNames = /* @__PURE__ */ new Map();
96
+ for (const list of [
97
+ meta.tables,
98
+ meta.foreignTables,
99
+ meta.views,
100
+ meta.materializedViews
101
+ ]) for (const entry of list) relationNames.set(entry.id, `${entry.schema}.${entry.name}`);
102
+ const foreignKeysOf = (schema, name, extra) => {
103
+ const actions = new Map((extra?.foreignKeys ?? []).map((fk) => [fk.name, fk]));
104
+ return meta.relationships.filter((rel) => rel.schema === schema && rel.relation === name && actions.has(rel.foreign_key_name)).map((rel) => {
105
+ const action = actions.get(rel.foreign_key_name);
106
+ return {
107
+ name: rel.foreign_key_name,
108
+ columns: rel.columns,
109
+ refSchema: rel.referenced_schema,
110
+ refTable: rel.referenced_relation,
111
+ refColumns: rel.referenced_columns,
112
+ oneToOne: rel.is_one_to_one,
113
+ onDelete: action?.onDelete ?? "no action",
114
+ onUpdate: action?.onUpdate ?? "no action"
115
+ };
116
+ }).sort((a, b) => byCodePoint(a.name, b.name));
117
+ };
118
+ const build = (entry, flags) => {
119
+ const extra = extras.get(entry.id);
120
+ const own = [...columns.get(entry.id) ?? []].sort((a, b) => a.ordinal_position - b.ordinal_position);
121
+ return {
122
+ id: entry.id,
123
+ schema: entry.schema,
124
+ name: entry.name,
125
+ comment: entry.comment,
126
+ ...flags,
127
+ columns: own.map(toColumn),
128
+ primaryKey: extra && extra.primaryKey.length > 0 ? extra.primaryKey : primaryKeys.get(entry.id) ?? [],
129
+ uniques: extra?.uniques ?? [],
130
+ foreignKeys: foreignKeysOf(entry.schema, entry.name, extra),
131
+ checks: extra?.checks ?? [],
132
+ indexes: extra?.indexes ?? [],
133
+ policies: extra?.policies ?? [],
134
+ triggers: extra?.triggers ?? [],
135
+ grants: extra?.grants ?? [],
136
+ ...extra?.columnGrants ? { columnGrants: extra.columnGrants } : {}
137
+ };
138
+ };
139
+ const tables = [
140
+ ...meta.tables.map((table) => build(table, {
141
+ kind: "table",
142
+ rls: table.rls_enabled,
143
+ forceRls: table.rls_forced,
144
+ replicaIdentity: table.replica_identity,
145
+ insertable: true,
146
+ updatable: true
147
+ })),
148
+ ...meta.foreignTables.map((table) => build(table, {
149
+ kind: "table",
150
+ rls: false,
151
+ forceRls: false,
152
+ replicaIdentity: null,
153
+ insertable: true,
154
+ updatable: true
155
+ })),
156
+ ...meta.views.map((view) => build(view, {
157
+ kind: "view",
158
+ rls: false,
159
+ forceRls: false,
160
+ replicaIdentity: null,
161
+ insertable: view.is_insert_enabled ?? view.is_updatable,
162
+ updatable: view.is_update_enabled ?? view.is_updatable
163
+ })),
164
+ ...meta.materializedViews.map((view) => build(view, {
165
+ kind: "view",
166
+ rls: false,
167
+ forceRls: false,
168
+ replicaIdentity: null,
169
+ insertable: false,
170
+ updatable: false
171
+ }))
172
+ ].sort(byName);
173
+ const included = new Set(snapshot.schemas);
174
+ const enums = meta.types.filter((type) => type.enums.length > 0 && included.has(type.schema)).map((type) => ({
175
+ schema: type.schema,
176
+ name: type.name,
177
+ values: type.enums
178
+ })).sort(byName);
179
+ const typeNames = new Map(meta.types.map((type) => [type.id, type.name]));
180
+ const functions = meta.functions.map((fn) => toFunction(fn, (id) => typeNames.get(id), (id) => id === null ? null : relationNames.get(id) ?? null)).sort((a, b) => byName(a, b) || byCodePoint(a.signature, b.signature));
181
+ return {
182
+ schemas: snapshot.schemas,
183
+ tables,
184
+ enums,
185
+ functions,
186
+ buckets: snapshot.extras.buckets,
187
+ realtime: snapshot.extras.realtime
188
+ };
189
+ }
190
+ //#endregion
191
+ //#region src/cli/doctor/shared.ts
192
+ const catalogs = /* @__PURE__ */ new WeakMap();
193
+ /** The snapshot joined per table and function, computed once per snapshot. */
194
+ function catalogOf(context) {
195
+ let catalog = catalogs.get(context.snapshot);
196
+ if (!catalog) {
197
+ catalog = toCatalog(context.snapshot);
198
+ catalogs.set(context.snapshot, catalog);
199
+ }
200
+ return catalog;
201
+ }
202
+ /** `[api] schemas` from `config.toml`, the schemas PostgREST serves, or `config.schemas` without one. */
203
+ function exposedSchemas(context) {
204
+ const listed = context.configToml ? tomlGet(context.configToml.document, ["api", "schemas"]) : void 0;
205
+ return Array.isArray(listed) && listed.every((entry) => typeof entry === "string") ? listed : context.config.schemas;
206
+ }
207
+ const exposed = (context) => {
208
+ const schemas = exposedSchemas(context);
209
+ return catalogOf(context).tables.filter((table) => schemas.includes(table.schema));
210
+ };
211
+ const qualified = (table) => `${table.schema}.${table.name}`;
212
+ const tableObject = (table) => ({
213
+ kind: "table",
214
+ schema: table.schema,
215
+ name: table.name
216
+ });
217
+ const policyObject = (table, policy) => ({
218
+ kind: "policy",
219
+ schema: table.schema,
220
+ name: policy.name
221
+ });
222
+ const splitTexts = /* @__PURE__ */ new Map();
223
+ const SPLIT_CACHE_SIZE = 64;
224
+ /** `text` split into lines; doctor locates many objects in the same SQL files. */
225
+ function linesOf(text) {
226
+ let lines = splitTexts.get(text);
227
+ if (!lines) {
228
+ if (splitTexts.size >= SPLIT_CACHE_SIZE) splitTexts.clear();
229
+ lines = text.split("\n");
230
+ splitTexts.set(text, lines);
231
+ }
232
+ return lines;
233
+ }
234
+ /** Line number (1-based) of the first line matching `pattern`. */
235
+ function lineOf(text, pattern) {
236
+ const index = linesOf(text).findIndex((line) => pattern.test(line));
237
+ return index === -1 ? void 0 : index + 1;
238
+ }
239
+ //#endregion
240
+ //#region src/cli/doctor/permdock.ts
241
+ /** Tables whose policies PermDock's helpers can't row-check (PermDock's PD037 scans the same ones). */
242
+ const HELPER_TABLES = /* @__PURE__ */ new Set(["storage.objects", "realtime.messages"]);
243
+ const POLICY = /create\s+policy\s+("(?:[^"]|"")+"|[\w$]+)\s+on\s+((?:"[^"]+"|\w+)\s*\.\s*(?:"[^"]+"|\w+))([\s\S]*?);/gi;
244
+ /** A helper call: optional schema, `permitted_<scope>_ids` or `permdock_has`, and the key, positional or named. */
245
+ const HELPER_CALL = /(?:(?:"((?:[^"]|"")+)"|([a-z_][\w$]*))\s*\.\s*)?"?(?:permitted_(\w+?)_ids|(permdock_has))"?\s*\(\s*(?:\w+\s*=>\s*)?'((?:[^']|'')*)'/gi;
246
+ const unquote = (name) => name.replaceAll(/^"|"$/g, "").replaceAll("\"\"", "\"");
247
+ /**
248
+ * Policies better-supabase generates (`bs_` names) on `storage.objects` and
249
+ * `realtime.messages` that call PermDock's helpers, with the keys they pass.
250
+ * The first file that declares a policy wins, matching the `sqlFiles` order
251
+ * (declarative schemas, then the newest migration).
252
+ */
253
+ function helperPolicies(files) {
254
+ const seen = /* @__PURE__ */ new Set();
255
+ const found = [];
256
+ for (const file of files) for (const match of file.text.matchAll(POLICY)) {
257
+ const name = unquote(match[1]);
258
+ const table = match[2].split(".").map((part) => unquote(part.trim())).join(".");
259
+ if (!HELPER_TABLES.has(table) || !name.startsWith("bs_")) continue;
260
+ const id = `${table}.${name}`;
261
+ if (seen.has(id)) continue;
262
+ seen.add(id);
263
+ const calls = [...match[3].matchAll(HELPER_CALL)].map((call) => {
264
+ const schema = call[1]?.replaceAll("\"\"", "\"") ?? call[2];
265
+ return {
266
+ key: call[5].replaceAll("''", "'"),
267
+ scope: call[4] ? "global" : call[3],
268
+ ...schema ? { schema } : {}
269
+ };
270
+ });
271
+ if (calls.length === 0) continue;
272
+ found.push({
273
+ name,
274
+ table,
275
+ keys: [...new Set(calls.map((call) => call.key))],
276
+ calls,
277
+ file: file.path,
278
+ line: file.text.slice(0, match.index).split("\n").length
279
+ });
280
+ }
281
+ return found;
282
+ }
283
+ /** `buckets` entries with a PermDock policy, the keys they name, their scope and helper schema. */
284
+ function configuredPermdockKeys(context) {
285
+ return Object.entries(context.config.buckets).flatMap(([bucket, config]) => config.policy !== void 0 && typeof config.policy !== "string" && "permdock" in config.policy ? [{
286
+ bucket,
287
+ keys: permdockKeys(config.policy),
288
+ scope: config.policy.scope,
289
+ schema: config.policy.schema ?? PERMDOCK_SCHEMA
290
+ }] : []);
291
+ }
292
+ /** A scope name or PermDock's alias for it (`tenant` is the first scope, `team` the second), resolved against the manifest. */
293
+ function resolveManifestScope(manifest, name) {
294
+ const scopes = manifest?.rls?.scopes;
295
+ if (!scopes) return void 0;
296
+ if (scopes.some((scope) => scope.name === name)) return name;
297
+ if (name === "tenant") return scopes[0]?.name;
298
+ if (name === "team") return scopes[1]?.name;
299
+ }
300
+ /**
301
+ * Why a policy's helper schema or scope can't work with PermDock's
302
+ * manifest and catalog: a schema other than `rls.schema`, a scope the
303
+ * manifest doesn't declare, or a key the catalog declares at another scope.
304
+ */
305
+ function placementProblems(project, catalog, call, where) {
306
+ const rls = project.manifest?.rls;
307
+ const problems = [];
308
+ if (rls && call.schema !== void 0 && call.schema !== rls.schema) problems.push(`${where} calls PermDock's helpers in schema ${call.schema}, but ${project.manifestPath} puts them in ${rls.schema}. Set schema: '${rls.schema}' on the policy.`);
309
+ if (call.scope === "global") return problems;
310
+ if (rls && !rls.scopes.some((scope) => scope.name === call.scope)) {
311
+ const resolved = resolveManifestScope(project.manifest, call.scope);
312
+ problems.push(`${where} uses scope "${call.scope}", which ${project.manifestPath} doesn't declare, so permitted_${call.scope}_ids doesn't exist. ${resolved ? `Use "${resolved}", the scope "${call.scope}" stands for.` : `Use one of ${rls.scopes.map((scope) => scope.name).join(", ") || "its scopes"}.`}`);
313
+ return problems;
314
+ }
315
+ const declared = catalog.permissions.find((permission) => permission.key === call.key)?.scope;
316
+ if (declared === void 0) return problems;
317
+ const resolved = resolveManifestScope(project.manifest, declared) ?? declared;
318
+ if (resolved !== call.scope) problems.push(`${where} checks "${call.key}" at scope "${call.scope}", but ${project.catalogPath} declares it at "${resolved}", so PermDock's helpers never grant it there. Use scope "${resolved}".`);
319
+ return problems;
320
+ }
321
+ const ROW_CONDITIONS_FIX = "Use the policies `permdock rls generate` writes for it, or a permission whose catalog entry has rowConditions: false.";
322
+ const REGENERATE_FIX = "Regenerate it with a current `permdock catalog`, which writes rowConditions for every permission.";
323
+ /**
324
+ * Why the SQL helpers can't be trusted with `key`, or `undefined` when the
325
+ * catalog marks it `rowConditions: false`. An entry without the flag and a
326
+ * key the catalog doesn't list are unknown, so they count as unsafe.
327
+ */
328
+ function unsafeKey(catalog, key, catalogPath) {
329
+ const status = permdockKeyStatus(catalog, key);
330
+ switch (status) {
331
+ case "scope-only": return;
332
+ case "row-conditions": return {
333
+ reason: `has row conditions in ${catalogPath}`,
334
+ fix: ROW_CONDITIONS_FIX
335
+ };
336
+ case "no-flag": return {
337
+ reason: `has no rowConditions flag in ${catalogPath}, so whether it has row conditions is unknown`,
338
+ fix: REGENERATE_FIX
339
+ };
340
+ case "missing": return {
341
+ reason: `is not in ${catalogPath}`,
342
+ fix: REGENERATE_FIX
343
+ };
344
+ default: return status;
345
+ }
346
+ }
347
+ /** PermDock's helpers for the `entitlements` kit module, when the manifest allows PermDock mode. */
348
+ function entitlementsKit(context) {
349
+ const mode = entitlementsMode(context.config, context.permdock);
350
+ return mode.kind === "permdock" ? mode.permdock : void 0;
351
+ }
352
+ const PERMDOCK_RULES = [
353
+ {
354
+ code: "BS214",
355
+ severity: "error",
356
+ title: "PermDock permission with row conditions in a Storage or Realtime policy",
357
+ description: "PermDock's SQL helpers (`permitted_<scope>_ids`, `permdock_has`) check role and scope, not row conditions. A bucket or topic policy that names a permission whose catalog entry has `rowConditions: true` grants every object or topic in the scope. Use PermDock's generated policies for those permissions. A key whose entry has no boolean `rowConditions` (an older `permdock catalog`) or that the catalog doesn't list is unknown and reported the same way, and so are all of them when the catalog is missing or unreadable. With the manifest, a helper schema other than `rls.schema`, a scope the manifest doesn't declare and a key checked at another scope than its catalog entry's are reported too.",
358
+ check: (context) => {
359
+ const project = context.permdock;
360
+ if (!project) return [];
361
+ const fromConfig = configuredPermdockKeys(context);
362
+ const fromSql = helperPolicies(context.sqlFiles ?? []);
363
+ const uses = fromConfig.length > 0 || fromSql.length > 0;
364
+ const findings = project.problems.filter((problem) => problem.startsWith(project.catalogPath)).map((problem) => ({
365
+ ...uses ? {} : { severity: "info" },
366
+ message: `Could not read PermDock's catalog: ${problem}`,
367
+ target: project.catalogPath
368
+ }));
369
+ const catalog = project.catalog;
370
+ if (!catalog) {
371
+ if (findings.length === 0 && uses) findings.push({
372
+ message: `Storage or Realtime policies call PermDock's helpers, but there is no ${project.catalogPath}, so whether their permissions have row conditions is unknown. Run \`permdock catalog\`, or set permdock.catalog in the config.`,
373
+ target: project.catalogPath
374
+ });
375
+ return findings;
376
+ }
377
+ for (const { bucket, keys, scope, schema } of fromConfig) for (const key of keys) {
378
+ for (const message of placementProblems(project, catalog, {
379
+ key,
380
+ scope,
381
+ schema
382
+ }, `buckets.${bucket} ("${key}")`)) findings.push({
383
+ message,
384
+ target: `buckets.${bucket}:${key}`
385
+ });
386
+ const problem = unsafeKey(catalog, key, project.catalogPath);
387
+ if (!problem) continue;
388
+ findings.push({
389
+ message: `buckets.${bucket} uses PermDock permission "${key}", which ${problem.reason}. The bucket policy could grant every object in the scope. ${problem.fix}`,
390
+ target: `buckets.${bucket}:${key}`
391
+ });
392
+ }
393
+ for (const policy of fromSql) {
394
+ const seen = /* @__PURE__ */ new Set();
395
+ for (const call of policy.calls) for (const message of placementProblems(project, catalog, call, `Policy "${policy.name}" on ${policy.table}`)) {
396
+ if (seen.has(message)) continue;
397
+ seen.add(message);
398
+ findings.push({
399
+ message,
400
+ target: `${policy.table}.${policy.name}:${call.key}`,
401
+ location: {
402
+ file: policy.file,
403
+ line: policy.line
404
+ }
405
+ });
406
+ }
407
+ for (const key of policy.keys) {
408
+ const problem = unsafeKey(catalog, key, project.catalogPath);
409
+ if (!problem) continue;
410
+ findings.push({
411
+ message: `Policy "${policy.name}" on ${policy.table} passes "${key}" to PermDock's helpers, but it ${problem.reason}. ${problem.fix}`,
412
+ target: `${policy.table}.${policy.name}:${key}`,
413
+ location: {
414
+ file: policy.file,
415
+ line: policy.line
416
+ }
417
+ });
418
+ }
419
+ }
420
+ return findings;
421
+ }
422
+ },
423
+ {
424
+ code: "BS408",
425
+ severity: "warning",
426
+ title: "PermDock helpers the entitlements module calls are missing",
427
+ description: "With a PermDock manifest, the `entitlements` kit module reads memberships from PermDock's `member_<scope>_ids()` (in `has_entitlement`, as `authenticated`) and `member_<scope>_ids_for(uuid)` (in `feature_claims`, which PermDock's hook calls as `supabase_auth_admin`). Doctor warns when a PermDock project has no readable manifest or no `rls` block, when the scope is not one of the manifest's scopes or can't be chosen, when the scope's id type is missing or not `uuid`, `text`, `bigint` or `integer`, when the manifest has no `claims.features` claim filled by `better_supabase.feature_claims`, when no membership source covers the scope, when the manifest's `rls.helpers` lacks one of those helpers or doesn't grant it to that role (naming the `permdock.config.ts` setting that adds it), or when the snapshot lacks it (apply the migration `permdock rls generate` wrote).",
428
+ check: (context) => {
429
+ if (!context.config.sql.kit.includes("entitlements")) return [];
430
+ const mode = entitlementsMode(context.config, context.permdock);
431
+ if (mode.kind === "tenant") return [];
432
+ const manifest = context.config.permdock.manifest;
433
+ if (mode.kind === "invalid") return [{
434
+ message: mode.problem,
435
+ target: "entitlements.permdock"
436
+ }];
437
+ const listed = new Map((context.permdock?.manifest?.rls?.helpers ?? []).map((helper) => [`${mode.permdock.schema}.${helper.name}`, helper]));
438
+ const read = context.snapshot.schemas.includes(mode.permdock.schema);
439
+ const present = new Set(catalogOf(context).functions.map((fn) => `${fn.schema}.${fn.name}`));
440
+ const regenerate = `then run \`permdock rls generate\`, \`permdock supabase hook generate\` and \`permdock supabase inspect --out\`.`;
441
+ const findings = [];
442
+ const project = context.permdock;
443
+ const features = context.config.claims.features;
444
+ const claims = project?.manifest?.claims ?? [];
445
+ const featureClaim = claims.find((claim) => claim.name === features);
446
+ if (featureClaim?.source !== "better_supabase.feature_claims") {
447
+ const registered = claims.find((claim) => claim.source === "better_supabase.feature_claims");
448
+ findings.push({
449
+ message: registered ? `${manifest} registers better_supabase.feature_claims as the "${registered.name}" claim, but claims.features is "${features}", so hasEntitlement() never finds the plan features. Set claims.features to "${registered.name}", or rename the claim in permdock.config.ts (supabase.hook.claims).` : featureClaim ? `${manifest} fills the "${features}" claim from ${featureClaim.source}, not better_supabase.feature_claims, so the claim doesn't carry the entitlements module's features. Point supabase.hook.claims.${features} in permdock.config.ts at 'better_supabase.feature_claims', ${regenerate}` : `${manifest} has no "${features}" claim, so PermDock's hook never writes the plan features hasEntitlement() reads. Add \`supabase.hook.claims: { ${features}: 'better_supabase.feature_claims' }\` to permdock.config.ts, ${regenerate}`,
450
+ target: `claims.${features}`
451
+ });
452
+ }
453
+ const scope = mode.permdock.scope;
454
+ if (!mode.permdock.memberships.some((source) => !("value" in source.scope) || source.scope.value === scope)) findings.push({
455
+ message: `No membership source in ${manifest} covers scope "${scope}", so entitlement_members() finds no users and plan changes never invalidate sessions. Add a source for "${scope}" (\`supabase.hook.memberships\` in permdock.config.ts), ${regenerate}`,
456
+ target: `entitlements.permdock.${scope}`
457
+ });
458
+ return findings.concat(entitlementRequirements(mode.permdock).flatMap((requirement) => {
459
+ const { helper, caller, role } = requirement;
460
+ const entry = listed.get(helper);
461
+ if (!entry) return [{
462
+ message: requirement.kind === "member" ? `${manifest} lists no ${helper}, but ${caller} calls it. Run \`permdock rls generate\` with a current PermDock, then \`permdock supabase inspect --out\`.` : `${manifest} lists no ${helper}, but ${caller} calls it. PermDock writes it only for a scope with a membership source: add one for "${mode.permdock.scope}" (\`supabase.hook.memberships\`, \`rls.membershipSources\` or a mapped \`rls.memberships\` table in permdock.config.ts), ${regenerate}`,
463
+ target: helper
464
+ }];
465
+ if (!entry.execute.includes(role)) {
466
+ const message = requirement.kind === "member" ? `${manifest} says ${role} may not execute ${helper}, but ${caller} calls it as ${role}. Run \`permdock rls generate\` with a current PermDock, then \`permdock supabase inspect --out\`.` : `${manifest} says ${role} may not execute ${helper}. feature_claims runs as its owner, so this only matters when something else calls it as ${role}; PermDock grants it when the hook has claims (\`supabase.hook.claims\` in permdock.config.ts), ${regenerate}`;
467
+ return [{
468
+ ...requirement.kind === "member-for" ? { severity: "info" } : {},
469
+ message,
470
+ target: helper
471
+ }];
472
+ }
473
+ if (read && !present.has(helper)) return [{
474
+ message: `${helper} is in ${manifest} but not in the database. Apply the migration \`permdock rls generate\` wrote before the entitlements module.`,
475
+ target: helper
476
+ }];
477
+ return [];
478
+ }));
479
+ }
480
+ },
481
+ {
482
+ code: "BS409",
483
+ severity: "warning",
484
+ title: "PermDock's manifest and the better-supabase config disagree",
485
+ description: "PermDock's hook and helpers read the active tenant from the manifest's `rls.tenantClaim`, and its generated files start with a `-- permdock:hook v1` or `-- permdock:grants v1` marker. Doctor warns when `claims.tenant` names another claim (the tenant plugin, guards and RLS would read a claim PermDock never writes), reports an error for a marker version this release can't read, and notes when `claims.scope` resolves to another scope than the manifest's root scope.",
486
+ check: (context) => {
487
+ const project = context.permdock;
488
+ if (!project) return [];
489
+ const findings = [];
490
+ const manifest = project.manifest;
491
+ const tenantClaim = manifest?.rls?.tenantClaim ?? manifest?.tenantClaim;
492
+ if (tenantClaim !== void 0 && tenantClaim !== context.config.claims.tenant) findings.push({
493
+ message: `${project.manifestPath} says PermDock writes the active tenant to "${tenantClaim}", but claims.tenant is "${context.config.claims.tenant}". Set claims.tenant to "${tenantClaim}", or change rls.tenantClaim in permdock.config.ts.`,
494
+ target: "claims.tenant"
495
+ });
496
+ for (const file of context.sqlFiles ?? []) {
497
+ const markers = [["hook", parseHookMarker(file.text)?.version], ["grants", parseGrantsMarker(file.text)?.version]];
498
+ for (const [kind, version] of markers) {
499
+ if (version === void 0 || version === 1) continue;
500
+ findings.push({
501
+ severity: "error",
502
+ message: `${file.path} has a \`-- permdock:${kind} v${String(version)}\` marker, but this release reads only v1, so doctor can't trust what it says about PermDock's ${kind === "hook" ? "hook" : "grants"}. Upgrade better-supabase, or regenerate the file with the PermDock version that matches it.`,
503
+ target: file.path,
504
+ location: {
505
+ file: file.path,
506
+ line: 1
507
+ }
508
+ });
509
+ }
510
+ }
511
+ const root = manifest?.rls?.scopes.find((scope) => scope.within === void 0)?.name;
512
+ const scope = context.config.claims.scope;
513
+ const resolved = resolveManifestScope(manifest, scope);
514
+ if (root !== void 0 && resolved !== root) findings.push({
515
+ severity: "info",
516
+ message: `claims.scope is "${scope}", which ${resolved ? `stands for "${resolved}"` : "is not a scope"} in ${project.manifestPath}, whose root scope is "${root}". The tenant module's membership claims then use another scope than PermDock's. Set claims.scope to "${root}" (or "tenant").`,
517
+ target: "claims.scope"
518
+ });
519
+ return findings;
520
+ }
521
+ }
522
+ ];
523
+ //#endregion
524
+ //#region src/cli/gen/shared.ts
525
+ const q = (value) => JSON.stringify(value);
526
+ /** A property key: bare when it is a valid identifier, quoted otherwise. */
527
+ function prop(name) {
528
+ return /^[A-Za-z_$][\w$]*$/.test(name) ? name : q(name);
529
+ }
530
+ function nullable(type, isNullable) {
531
+ return isNullable ? `${type} | null` : type;
532
+ }
533
+ /** `T[]`, parenthesized when `T` is a union or other compound type. */
534
+ function arrayOf(type) {
535
+ return /^[\w.$]+$/.test(type) ? `${type}[]` : `(${type})[]`;
536
+ }
537
+ function indent(lines, depth) {
538
+ const pad = " ".repeat(depth);
539
+ return lines.flatMap((line) => line.split("\n")).map((line) => line === "" ? line : pad + line);
540
+ }
541
+ /** `name: {` + indented children + `};`, or an inline empty type. */
542
+ function block(name, children, empty = "{ [_ in never]: never }") {
543
+ if (children.length === 0) return [`${name}: ${empty};`];
544
+ return [
545
+ `${name}: {`,
546
+ ...indent(children, 1),
547
+ "};"
548
+ ];
549
+ }
550
+ function sameColumns(left, right) {
551
+ return left.length === right.length && left.every((column) => right.includes(column));
552
+ }
553
+ function singular(name) {
554
+ if (name.endsWith("ies")) return `${name.slice(0, -3)}y`;
555
+ if (name.endsWith("sses")) return name.slice(0, -2);
556
+ if (name.endsWith("s") && !name.endsWith("ss")) return name.slice(0, -1);
557
+ return name;
558
+ }
559
+ function pascal(name) {
560
+ return name.replaceAll(/(^|[_-]+)([a-z0-9])/g, (_, __, char) => char.toUpperCase()).replace(/^[a-z]/, (char) => char.toUpperCase());
561
+ }
562
+ function isJsonUdt(udt) {
563
+ return udt === "json" || udt === "jsonb";
564
+ }
565
+ const CHECK_ANY = /^CHECK \(\(\(?"?(\w+)"?\)?(?:::\w+)? = ANY \(\(?ARRAY\[(.+)\]\)?(?:::\w+\[\])?\)\)\)$/;
566
+ const CHECK_OR = /^CHECK \(\((.+)\)\)$/;
567
+ const OR_TERM = /^\(?"?(\w+)"?\)?(?:::\w+)? = '((?:[^']|'')*)'(?:::\w+)?\)?$/;
568
+ const LITERAL = /'((?:[^']|'')*)'(?:::[\w ]+)?/g;
569
+ /**
570
+ * Extracts `column in ('a', 'b')` CHECK constraints, which Postgres stores as
571
+ * `CHECK ((col = ANY (ARRAY['a'::text, 'b'::text])))` or an OR chain.
572
+ */
573
+ function parseCheckUnion(definition) {
574
+ const any = CHECK_ANY.exec(definition);
575
+ if (any?.[1] && any[2]) {
576
+ const values = [...any[2].matchAll(LITERAL)].map((match) => (match[1] ?? "").replaceAll("''", "'"));
577
+ return values.length > 0 ? {
578
+ column: any[1],
579
+ values
580
+ } : void 0;
581
+ }
582
+ const chain = CHECK_OR.exec(definition);
583
+ if (chain?.[1]) {
584
+ const terms = chain[1].split(/\) OR \(|\s+OR\s+/);
585
+ let column;
586
+ const values = [];
587
+ for (const term of terms) {
588
+ const match = OR_TERM.exec(term.trim());
589
+ if (!match?.[1]) return void 0;
590
+ if (column && column !== match[1]) return void 0;
591
+ column = match[1];
592
+ values.push((match[2] ?? "").replaceAll("''", "'"));
593
+ }
594
+ return column && values.length > 1 ? {
595
+ column,
596
+ values
597
+ } : void 0;
598
+ }
599
+ }
600
+ //#endregion
601
+ //#region src/cli/gen/emit.ts
602
+ const GENERATED_HEADER = "// @generated by better-supabase gen. Do not edit; run `better-supabase gen` instead.";
603
+ function rowEntries(columns, name) {
604
+ return columns.map((column) => `${prop(column[name])}: ${nullable(column.tsType, column.nullable)};`);
605
+ }
606
+ function insertEntries(columns, name) {
607
+ return columns.map((column) => {
608
+ if (!column.insertable) return `${prop(column[name])}?: never;`;
609
+ const optional = column.optional ? "?" : "";
610
+ return `${prop(column[name])}${optional}: ${nullable(column.tsType, column.nullable)};`;
611
+ });
612
+ }
613
+ function updateEntries(columns, name) {
614
+ return columns.map((column) => {
615
+ if (!column.updatable) return `${prop(column[name])}?: never;`;
616
+ return `${prop(column[name])}?: ${nullable(column.tsType, column.nullable)};`;
617
+ });
618
+ }
619
+ function argEntries(fn) {
620
+ return fn.args.map((arg) => `${prop(arg.name)}${arg.optional ? "?" : ""}: ${arg.tsType};`);
621
+ }
622
+ function enrichedDatabase(model) {
623
+ const schemas = model.config.schemas.flatMap((schema) => {
624
+ const tables = model.tables.filter((t) => t.snapshot.schema === schema).flatMap((table) => {
625
+ const overrides = table.columns.filter((column) => column.values !== void 0 && !column.snapshot.isEnum || column.json && column.tsType !== "Json" || column.storage !== void 0).map((column) => `${prop(column.db)}: ${column.tsType};`);
626
+ return overrides.length > 0 ? block(prop(table.snapshot.name), overrides) : [];
627
+ });
628
+ return tables.length > 0 ? block(prop(schema), tables) : [];
629
+ });
630
+ if (schemas.length === 0) return ["export type Database = SupabaseDatabase;"];
631
+ return [
632
+ "export type Database = EnrichDatabase<",
633
+ " SupabaseDatabase,",
634
+ " {",
635
+ ...indent(schemas, 2),
636
+ " }",
637
+ ">;"
638
+ ];
639
+ }
640
+ function modelsType(model) {
641
+ return [
642
+ "export type Models = {",
643
+ ...indent(model.tables.flatMap((table) => {
644
+ const relations = table.relations.map((relation) => `${prop(relation.name)}: { table: ${q(relation.meta.table)}; kind: ${q(relation.meta.kind)}; nullable: ${String(relation.meta.nullable)} };`);
645
+ const pk = table.meta.primaryKey;
646
+ const uniques = Object.entries(table.meta.uniqueKeys).map(([name, columns]) => `${prop(name)}: readonly [${columns.map(q).join(", ")}];`);
647
+ const flags = [];
648
+ const tableFlags = table.meta.flags;
649
+ if (tableFlags.softDelete) flags.push(`softDelete: ${q(tableFlags.softDelete)};`);
650
+ if (tableFlags.timestamps) flags.push("timestamps: true;");
651
+ if (tableFlags.tenant) flags.push(`tenant: ${q(tableFlags.tenant)};`);
652
+ if (tableFlags.actor) flags.push("actor: true;");
653
+ const none = "Record<never, never>";
654
+ const union = (names) => names.length === 0 ? "never" : names.map(q).join(" | ");
655
+ return block(prop(table.key), [
656
+ ...block("Row", rowEntries(table.columns, "app"), none),
657
+ ...table.snapshot.insertable ? block("Insert", insertEntries(table.columns, "app"), none) : ["Insert: never;"],
658
+ ...table.snapshot.updatable ? block("Update", updateEntries(table.columns, "app"), none) : ["Update: never;"],
659
+ ...block("Relations", relations, none),
660
+ `PrimaryKey: ${union(pk)};`,
661
+ ...block("UniqueKeys", uniques, none),
662
+ `Checks: ${union(table.snapshot.checks.map((check) => check.name))};`,
663
+ `ForeignKeys: ${union(table.snapshot.foreignKeys.map((fk) => fk.name))};`,
664
+ ...block("Flags", flags, none)
665
+ ]);
666
+ }), 1),
667
+ "};"
668
+ ];
669
+ }
670
+ function functionsType(model) {
671
+ if (model.functions.length === 0) return ["export type Functions = Record<never, never>;"];
672
+ return [
673
+ "export type Functions = {",
674
+ ...indent(model.functions.flatMap((fn) => block(prop(fn.key), [...block("Args", argEntries(fn), "Record<never, never>"), `Returns: ${fn.returns};`])), 1),
675
+ "};"
676
+ ];
677
+ }
678
+ /** The metadata module next to `generated.ts`: `generated.meta.js` and its `.d.ts`. */
679
+ function metaPaths(output) {
680
+ const base = output.replace(/\.[cm]?[jt]sx?$/, "");
681
+ return {
682
+ js: `${base}.meta.js`,
683
+ dts: `${base}.meta.d.ts`
684
+ };
685
+ }
686
+ /**
687
+ * The schema metadata as plain JavaScript plus a one-line declaration, so
688
+ * TypeScript reads `SchemaMeta` instead of checking thousands of lines of
689
+ * object literal in every program that imports the generated module.
690
+ */
691
+ function emitMeta(model, options = {}) {
692
+ const runtime = options.runtimeImport ?? "better-supabase";
693
+ return {
694
+ js: [
695
+ GENERATED_HEADER,
696
+ `// @ts-self-types="${options.types ?? "./generated.meta.d.ts"}"`,
697
+ "/* oxlint-disable */",
698
+ "/* eslint-disable */",
699
+ "",
700
+ `export default ${JSON.stringify(model.meta, null, 2)};`,
701
+ ""
702
+ ].join("\n"),
703
+ dts: [
704
+ GENERATED_HEADER,
705
+ "/* oxlint-disable */",
706
+ "/* eslint-disable */",
707
+ "",
708
+ `import type { SchemaMeta } from ${q(runtime)};`,
709
+ "",
710
+ "declare const meta: SchemaMeta;",
711
+ "export default meta;",
712
+ ""
713
+ ].join("\n")
714
+ };
715
+ }
716
+ /** Renders the generated module. */
717
+ function emitModule(model, options) {
718
+ const runtime = options.runtimeImport ?? "better-supabase";
719
+ const lines = [
720
+ GENERATED_HEADER,
721
+ "/* oxlint-disable */",
722
+ "/* eslint-disable */",
723
+ "",
724
+ `import { defineSchema, type Schema } from ${q(runtime)};`
725
+ ];
726
+ const storagePaths = model.tables.some((table) => table.columns.some((column) => column.storage !== void 0));
727
+ lines.push(`import type { EnrichDatabase${storagePaths ? ", StoragePath" : ""} } from ${q(runtime)};`, `import type { Database as SupabaseDatabase } from ${q(options.databaseTypesImport ?? "./database.types.ts")};`);
728
+ const byFile = /* @__PURE__ */ new Map();
729
+ for (const entry of model.jsonImports) {
730
+ const list = byFile.get(entry.from) ?? [];
731
+ list.push(entry.name);
732
+ byFile.set(entry.from, list);
733
+ }
734
+ for (const [from, names] of [...byFile.entries()].sort(([a], [b]) => byCodePoint(a, b))) lines.push(`import type { ${names.sort().join(", ")} } from ${q(options.importPathFor(from))};`);
735
+ lines.push(`import meta from ${q(options.metaImport ?? "./generated.meta.js")};`, "", "export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[];", "", ...enrichedDatabase(model), "");
736
+ for (const entry of model.enums) lines.push(`export const ${entry.constant} = [${entry.values.map(q).join(", ")}] as const;`, `export type ${pascal(entry.name)} = (typeof ${entry.constant})[number];`);
737
+ for (const table of model.tables) for (const column of table.columns) if (column.values && !column.snapshot.isEnum) {
738
+ const constant = `${table.key}${pascal(column.app)}Values`.replace(/^[A-Z]/, (c) => c.toLowerCase());
739
+ lines.push(`export const ${constant} = [${column.values.map(q).join(", ")}] as const;`, `export type ${pascal(table.key)}${pascal(column.app)} = (typeof ${constant})[number];`);
740
+ }
741
+ if (model.enums.length > 0) lines.push("");
742
+ lines.push(...modelsType(model), "", ...functionsType(model), "", "export type TableName = keyof Models;", "/** Constraint names for `isConflict`, `isCheck` and `isForeignKey`. */", "export type UniqueConstraint = { [T in TableName]: Extract<keyof Models[T]['UniqueKeys'], string> }[TableName];", "export type CheckConstraint = Models[TableName]['Checks'];", "export type ForeignKeyConstraint = Models[TableName]['ForeignKeys'];", "export type RowOf<T extends TableName> = Models[T]['Row'];", "export type InsertOf<T extends TableName> = Models[T]['Insert'];", "export type UpdateOf<T extends TableName> = Models[T]['Update'];", "");
743
+ if (model.meta.buckets) lines.push("/** Bucket configs for `defineBucket` from `better-supabase/storage`. */", `export const buckets = ${JSON.stringify(model.meta.buckets, null, 2)} as const;`, "");
744
+ if (model.meta.topics) lines.push("/** Topic templates for `defineTopic` from `better-supabase/realtime`. */", `export const topics = ${JSON.stringify(model.meta.topics, null, 2)} as const;`, "");
745
+ lines.push("export const schema: Schema<Models, Database, Functions> = defineSchema(meta);", "");
746
+ return lines.join("\n");
747
+ }
748
+ //#endregion
749
+ //#region src/cli/introspect/typegen.ts
750
+ /**
751
+ * The package root and `/introspection` build their arktype schemas on
752
+ * import (about 90 ms), so they load when a command reads or validates
753
+ * metadata, not for `--help`.
754
+ */
755
+ const metadataModule = () => import("@supabase/postgrest-typegen");
756
+ const introspectionModule = () => import("@supabase/postgrest-typegen/introspection");
757
+ /** Introspects `schemas`, keeping only the types they reference. */
758
+ async function readGeneratorMetadata(db, schemas) {
759
+ const { introspect } = await introspectionModule();
760
+ return pruneTypes(await introspect(db, { includedSchemas: [...schemas] }), schemas);
761
+ }
762
+ /** Postgres assigns oids below this to built-in objects; those are stable. */
763
+ const FIRST_NORMAL_OID = 16384;
764
+ /**
765
+ * Replaces the oids of user objects with ids derived from their names and
766
+ * zeroes size estimates, so a snapshot of the same schema is byte-identical
767
+ * after `supabase db reset` or on another machine. Returns the id mapping
768
+ * for data keyed by the old oids.
769
+ */
770
+ function stabilizeMetadata(metadata) {
771
+ const ids = /* @__PURE__ */ new Map();
772
+ const assign = (base, entries) => {
773
+ [...entries].sort((a, b) => byCodePoint(a.key, b.key)).forEach((entry, index) => {
774
+ if (entry.id >= FIRST_NORMAL_OID) ids.set(entry.id, base + index);
775
+ });
776
+ };
777
+ assign(1e6, [
778
+ ...metadata.tables,
779
+ ...metadata.foreignTables,
780
+ ...metadata.views,
781
+ ...metadata.materializedViews
782
+ ].map((entry) => ({
783
+ id: entry.id,
784
+ key: `${entry.schema}.${entry.name}`
785
+ })));
786
+ assign(2e6, metadata.types.map((entry) => ({
787
+ id: entry.id,
788
+ key: `${entry.schema}.${entry.name}`
789
+ })));
790
+ assign(3e6, metadata.functions.map((entry) => ({
791
+ id: entry.id,
792
+ key: `${entry.schema}.${entry.name}(${entry.identity_argument_types})`
793
+ })));
794
+ assign(4e6, metadata.schemas.map((entry) => ({
795
+ id: entry.id,
796
+ key: entry.name
797
+ })));
798
+ const map = (id) => ids.get(id) ?? id;
799
+ const mapNullable = (id) => id === null ? null : map(id);
800
+ const stable = {
801
+ ...metadata,
802
+ schemas: metadata.schemas.map((entry) => ({
803
+ ...entry,
804
+ id: map(entry.id)
805
+ })),
806
+ tables: metadata.tables.map((entry) => ({
807
+ ...entry,
808
+ id: map(entry.id),
809
+ bytes: 0,
810
+ size: "0 bytes",
811
+ live_rows_estimate: 0,
812
+ dead_rows_estimate: 0
813
+ })),
814
+ foreignTables: metadata.foreignTables.map((entry) => ({
815
+ ...entry,
816
+ id: map(entry.id)
817
+ })),
818
+ views: metadata.views.map((entry) => ({
819
+ ...entry,
820
+ id: map(entry.id)
821
+ })),
822
+ materializedViews: metadata.materializedViews.map((entry) => ({
823
+ ...entry,
824
+ id: map(entry.id)
825
+ })),
826
+ columns: metadata.columns.map((entry) => ({
827
+ ...entry,
828
+ table_id: map(entry.table_id),
829
+ id: `${map(entry.table_id)}.${entry.ordinal_position}`
830
+ })),
831
+ primaryKeys: metadata.primaryKeys.map((entry) => ({
832
+ ...entry,
833
+ table_id: map(entry.table_id)
834
+ })),
835
+ functions: metadata.functions.map((entry) => ({
836
+ ...entry,
837
+ id: map(entry.id),
838
+ return_type_id: map(entry.return_type_id),
839
+ return_type_relation_id: mapNullable(entry.return_type_relation_id),
840
+ args: entry.args.map((arg) => ({
841
+ ...arg,
842
+ type_id: map(arg.type_id)
843
+ }))
844
+ })),
845
+ types: metadata.types.map((entry) => ({
846
+ ...entry,
847
+ id: map(entry.id),
848
+ type_relation_id: mapNullable(entry.type_relation_id),
849
+ attributes: entry.attributes.map((attribute) => ({
850
+ ...attribute,
851
+ type_id: map(attribute.type_id)
852
+ }))
853
+ }))
854
+ };
855
+ return {
856
+ metadata: sortGeneratorMetadata(stable),
857
+ ids
858
+ };
859
+ }
860
+ /** Validates metadata loaded from a snapshot file. */
861
+ async function validateGeneratorMetadata(data) {
862
+ const { parseGeneratorMetadata } = await metadataModule();
863
+ return parseGeneratorMetadata(data);
864
+ }
865
+ /** The JSON document out-of-process generators read. */
866
+ async function serializeGenerator(metadata) {
867
+ const { serializeGeneratorMetadata } = await metadataModule();
868
+ return serializeGeneratorMetadata(sortGeneratorMetadata(metadata));
869
+ }
870
+ /**
871
+ * Keeps only what belongs to `schemas`, so the Database type matches
872
+ * `supabase gen types --schema ...` for exactly those schemas.
873
+ */
874
+ function restrictSchemas(metadata, schemas) {
875
+ const keep = new Set(schemas);
876
+ return {
877
+ ...metadata,
878
+ schemas: metadata.schemas.filter((schema) => keep.has(schema.name)),
879
+ tables: metadata.tables.filter((table) => keep.has(table.schema)),
880
+ foreignTables: metadata.foreignTables.filter((table) => keep.has(table.schema)),
881
+ views: metadata.views.filter((view) => keep.has(view.schema)),
882
+ materializedViews: metadata.materializedViews.filter((view) => keep.has(view.schema)),
883
+ columns: metadata.columns.filter((column) => keep.has(column.schema)),
884
+ primaryKeys: metadata.primaryKeys.filter((key) => keep.has(key.schema)),
885
+ relationships: metadata.relationships.filter((relationship) => keep.has(relationship.schema)),
886
+ functions: metadata.functions.filter((fn) => keep.has(fn.schema))
887
+ };
888
+ }
889
+ let oxfmt;
890
+ /** oxfmt is an optional peer, loaded on demand; `undefined` when it is not installed. */
891
+ function loadOxfmt() {
892
+ oxfmt ??= import("oxfmt").catch(() => void 0);
893
+ return oxfmt;
894
+ }
895
+ /** Whether `database.types.ts` comes out formatted. */
896
+ async function oxfmtInstalled() {
897
+ return await loadOxfmt() !== void 0;
898
+ }
899
+ /** Formats like `supabase gen types`, or returns `code` as it is without oxfmt. */
900
+ async function formatTypes(code) {
901
+ const formatter = await loadOxfmt();
902
+ if (!formatter) return code;
903
+ const { code: formatted, errors } = await formatter.format("output.ts", code, {
904
+ semi: false,
905
+ printWidth: 80
906
+ });
907
+ if (errors.length > 0) throw new Error(`oxfmt failed to format database.types.ts: ${errors.map((error) => error.message).join("; ")}`);
908
+ return formatted;
909
+ }
910
+ /**
911
+ * Renders `database.types.ts`, byte for byte what `supabase gen types` prints
912
+ * when oxfmt is installed.
913
+ */
914
+ function generateDatabaseTypes(metadata, options) {
915
+ return generateTypescript(sortGeneratorMetadata(restrictSchemas(metadata, options.schemas)), {
916
+ postgrestVersion: options.postgrestVersion,
917
+ detectOneToOneRelationships: true,
918
+ format: formatTypes
919
+ });
920
+ }
921
+ /**
922
+ * TypeScript type for a column or argument, resolved exactly like the
923
+ * `Database` type does (domains, composites, enums in other schemas).
924
+ */
925
+ function tsTypeOf(metadata, schema, format, typeSchema) {
926
+ const owner = metadata.schemas.find((entry) => entry.name === schema) ?? {
927
+ id: 0,
928
+ name: schema,
929
+ owner: ""
930
+ };
931
+ return pgTypeToTsType(owner, format, {
932
+ types: metadata.types,
933
+ schemas: metadata.schemas,
934
+ tables: metadata.tables,
935
+ views: metadata.views,
936
+ foreignTables: metadata.foreignTables,
937
+ materializedViews: metadata.materializedViews
938
+ }, typeSchema);
939
+ }
940
+ /**
941
+ * `introspect()` returns every type in the database (hundreds of built-ins).
942
+ * Keep the ones in the read schemas and those referenced by columns,
943
+ * functions and composite attributes; built-in scalars resolve by name.
944
+ */
945
+ function pruneTypes(metadata, schemas) {
946
+ const byId = new Map(metadata.types.map((type) => [type.id, type]));
947
+ const byName = new Map(metadata.types.map((type) => [`${type.schema}.${type.name}`, type]));
948
+ const keep = /* @__PURE__ */ new Set();
949
+ const visit = (id) => {
950
+ if (id === null || id === void 0 || keep.has(id)) return;
951
+ const type = byId.get(id);
952
+ if (!type) return;
953
+ keep.add(id);
954
+ for (const attribute of type.attributes) visit(attribute.type_id);
955
+ };
956
+ for (const type of metadata.types) if (schemas.includes(type.schema)) visit(type.id);
957
+ for (const column of metadata.columns) {
958
+ visit(byName.get(`${column.type_schema}.${column.format}`)?.id);
959
+ visit(byName.get(`${column.type_schema}.${column.format.replace(/^_/, "")}`)?.id);
960
+ }
961
+ for (const fn of metadata.functions) {
962
+ visit(fn.return_type_id);
963
+ for (const arg of fn.args) visit(arg.type_id);
964
+ }
965
+ return {
966
+ ...metadata,
967
+ types: metadata.types.filter((type) => keep.has(type.id))
968
+ };
969
+ }
970
+ //#endregion
971
+ //#region src/cli/gen/model.ts
972
+ const SOFT_DELETE_UDTS = /* @__PURE__ */ new Set(["timestamptz", "timestamp"]);
973
+ const applyCasing = (name, casing) => casing === "camel" ? toCamel(name) : name;
974
+ function tableKey(table, casing, schemas) {
975
+ const base = applyCasing(table.name, casing);
976
+ if (schemas.length === 1 || table.schema === "public") return base;
977
+ return applyCasing(`${table.schema}_${table.name}`, casing);
978
+ }
979
+ const CODEC_TYPE = {
980
+ instant: "Temporal.Instant",
981
+ plainDateTime: "Temporal.PlainDateTime",
982
+ bigint: "bigint",
983
+ string: "string"
984
+ };
985
+ function codecFor(udt, config) {
986
+ const { codecs } = config;
987
+ if (codecs.timestamptz === "instant") {
988
+ if (udt === "timestamptz") return "instant";
989
+ if (udt === "timestamp") return "plainDateTime";
990
+ }
991
+ if (udt === "int8" && codecs.int8 !== "number") return codecs.int8;
992
+ if (udt === "numeric" && codecs.numeric === "string") return "string";
993
+ }
994
+ const TEXT_UDTS = /* @__PURE__ */ new Set([
995
+ "text",
996
+ "varchar",
997
+ "bpchar"
998
+ ]);
999
+ /** `config.storagePaths` resolved to bucket ids, with a check that every key is used. */
1000
+ function storagePathsOf(config) {
1001
+ const bucketId = (name) => {
1002
+ const bucket = config.buckets[name];
1003
+ if (!bucket) return name;
1004
+ return bucket.id ?? name.replaceAll(/[A-Z]/g, (char) => `-${char.toLowerCase()}`);
1005
+ };
1006
+ const unused = new Set(Object.keys(config.storagePaths));
1007
+ return {
1008
+ lookup(schema, table, column, udt) {
1009
+ const key = [`${schema}.${table}.${column}`, `${table}.${column}`].find((candidate) => candidate in config.storagePaths);
1010
+ if (key === void 0) return;
1011
+ unused.delete(key);
1012
+ if (!TEXT_UDTS.has(udt)) throw new TypeError(`storagePaths["${key}"]: ${schema}.${table}.${column} is ${udt}, not a text column`);
1013
+ return bucketId(config.storagePaths[key]);
1014
+ },
1015
+ assertUsed() {
1016
+ const [first] = unused;
1017
+ if (first !== void 0) throw new TypeError(`storagePaths["${first}"]: no such column. Use \`table.column\` or \`schema.table.column\` (database names) in \`schemas\`.`);
1018
+ }
1019
+ };
1020
+ }
1021
+ function enumType(values) {
1022
+ return values.map((value) => JSON.stringify(value)).join(" | ");
1023
+ }
1024
+ /** Builds the codegen model from a snapshot and config. */
1025
+ function buildModel(snapshot, config) {
1026
+ const catalog = toCatalog(snapshot);
1027
+ const introspection = restrictSchemas(snapshot.generator, config.schemas);
1028
+ const tsType = (schema, format, typeSchema) => tsTypeOf(introspection, schema, format, typeSchema).replaceAll(/\(([\w.$]+)\)\[\]/g, "$1[]");
1029
+ const enumsByName = /* @__PURE__ */ new Map();
1030
+ for (const entry of catalog.enums) enumsByName.set(`${entry.schema}.${entry.name}`, entry.values);
1031
+ const jsonImports = /* @__PURE__ */ new Map();
1032
+ const jsonTypeFor = (table, column) => {
1033
+ const override = config.json[`${table}.${column}`];
1034
+ if (!override) return void 0;
1035
+ if ("type" in override) return override.type;
1036
+ const [from, name] = override.import.split("#");
1037
+ if (!from || !name) throw new TypeError(`json["${table}.${column}"].import must look like "./path.ts#TypeName"`);
1038
+ jsonImports.set(name, {
1039
+ name,
1040
+ from
1041
+ });
1042
+ return name;
1043
+ };
1044
+ const storagePaths = storagePathsOf(config);
1045
+ const included = catalog.tables.filter((table) => config.schemas.includes(table.schema) && !config.tables[table.name]?.exclude);
1046
+ const keyOf = /* @__PURE__ */ new Map();
1047
+ for (const table of included) keyOf.set(`${table.schema}.${table.name}`, tableKey(table, config.casing, config.schemas));
1048
+ const tables = included.map((table) => {
1049
+ const casing = config.tables[table.name]?.casing ?? config.casing;
1050
+ const checks = /* @__PURE__ */ new Map();
1051
+ for (const check of table.checks) {
1052
+ const union = parseCheckUnion(check.definition);
1053
+ if (union) checks.set(union.column, union.values);
1054
+ }
1055
+ return {
1056
+ table,
1057
+ casing,
1058
+ columns: table.columns.map((column) => {
1059
+ const values = (column.isEnum ? enumsByName.get(`${column.typeSchema}.${column.udt}`) : void 0) ?? checks.get(column.name);
1060
+ const json = isJsonUdt(column.udt);
1061
+ const override = json ? jsonTypeFor(table.name, column.name) : void 0;
1062
+ const storage = storagePaths.lookup(table.schema, table.name, column.name, column.udt);
1063
+ const codec = override || values || storage ? void 0 : codecFor(column.udt, config);
1064
+ const base = override ?? (values ? enumType(values) : storage ? `StoragePath<${JSON.stringify(storage)}>` : codec ? CODEC_TYPE[codec] : void 0);
1065
+ const columnType = base === void 0 ? tsType(table.schema, column.format, column.typeSchema) : column.isArray ? arrayOf(base) : base;
1066
+ const readonly = column.generated || column.identity === "always";
1067
+ const insertable = !readonly && table.insertable;
1068
+ const updatable = !readonly && table.updatable && column.updatable;
1069
+ return {
1070
+ app: applyCasing(column.name, casing),
1071
+ db: column.name,
1072
+ snapshot: column,
1073
+ tsType: columnType,
1074
+ nullable: column.nullable,
1075
+ optional: column.nullable || column.hasDefault || readonly,
1076
+ readonly,
1077
+ values,
1078
+ json,
1079
+ codec,
1080
+ storage,
1081
+ insertable,
1082
+ updatable
1083
+ };
1084
+ })
1085
+ };
1086
+ });
1087
+ storagePaths.assertUsed();
1088
+ const byKey = /* @__PURE__ */ new Map();
1089
+ for (const entry of tables) byKey.set(`${entry.table.schema}.${entry.table.name}`, entry);
1090
+ const appName = (schema, table, column) => {
1091
+ return byKey.get(`${schema}.${table}`)?.columns.find((col) => col.db === column)?.app ?? column;
1092
+ };
1093
+ const relations = /* @__PURE__ */ new Map();
1094
+ const push = (tableId, relation) => {
1095
+ const list = relations.get(tableId);
1096
+ if (list) list.push(relation);
1097
+ else relations.set(tableId, [relation]);
1098
+ };
1099
+ for (const { table, casing, columns } of tables) {
1100
+ const sourceId = `${table.schema}.${table.name}`;
1101
+ for (const fk of table.foreignKeys) {
1102
+ const targetId = `${fk.refSchema}.${fk.refTable}`;
1103
+ const target = byKey.get(targetId);
1104
+ if (!target) continue;
1105
+ const targetKey = keyOf.get(targetId) ?? fk.refTable;
1106
+ const sourceColumns = fk.columns.map((column) => appName(table.schema, table.name, column));
1107
+ const targetColumns = fk.refColumns.map((column) => appName(fk.refSchema, fk.refTable, column));
1108
+ const nullable = fk.columns.some((column) => columns.find((col) => col.db === column)?.nullable ?? true);
1109
+ const onDelete = fk.onDelete === "cascade" || fk.onDelete === "set null" || fk.onDelete === "set default" ? { onDelete: fk.onDelete } : {};
1110
+ const naming = namingColumns(fk.columns, fk.refColumns);
1111
+ const [only] = naming;
1112
+ const forwardBase = naming.length === 1 && only?.endsWith("_id") ? only.slice(0, -3) : singular(fk.refTable);
1113
+ push(sourceId, {
1114
+ name: applyCasing(forwardBase, casing),
1115
+ meta: {
1116
+ table: targetKey,
1117
+ kind: "one",
1118
+ nullable,
1119
+ foreignKey: fk.name,
1120
+ columns: sourceColumns,
1121
+ references: targetColumns,
1122
+ direction: "forward",
1123
+ ...onDelete
1124
+ }
1125
+ });
1126
+ const unique = fk.oneToOne || sameColumns(table.primaryKey, fk.columns) || table.uniques.some((key) => sameColumns(key.columns, fk.columns));
1127
+ push(targetId, {
1128
+ name: applyCasing(unique ? singular(table.name) : table.name, target.casing),
1129
+ meta: {
1130
+ table: keyOf.get(sourceId) ?? table.name,
1131
+ kind: unique ? "one" : "many",
1132
+ nullable: true,
1133
+ foreignKey: fk.name,
1134
+ columns: targetColumns,
1135
+ references: sourceColumns,
1136
+ direction: "reverse",
1137
+ ...onDelete
1138
+ }
1139
+ });
1140
+ }
1141
+ }
1142
+ const flagsConfig = config.plugins;
1143
+ const tableModels = tables.map(({ table, casing, columns }) => {
1144
+ const id = `${table.schema}.${table.name}`;
1145
+ const renames = config.tables[table.name]?.relations ?? {};
1146
+ const columnNames = new Set(columns.map((column) => column.app));
1147
+ const list = dedupeRelations(relations.get(id) ?? [], columnNames, casing).map((relation) => ({
1148
+ ...relation,
1149
+ name: renames[relation.name] ?? relation.name
1150
+ }));
1151
+ const has = (db) => columns.find((col) => col.db === db)?.app;
1152
+ const flags = {};
1153
+ if (flagsConfig.softDelete) {
1154
+ const db = flagsConfig.softDelete.column;
1155
+ const column = columns.find((col) => col.db === db);
1156
+ if (column) {
1157
+ const udt = column.snapshot.udt;
1158
+ if (column.snapshot.isArray || !SOFT_DELETE_UDTS.has(udt)) throw new TypeError(`plugins.softDelete.column: ${table.schema}.${table.name}.${db} is ${udt}, but softDelete() writes a timestamp. Use timestamptz or timestamp.`);
1159
+ flags.softDelete = column.app;
1160
+ }
1161
+ }
1162
+ if (flagsConfig.timestamps) {
1163
+ const createdAt = has(flagsConfig.timestamps.createdAt);
1164
+ const updatedAt = has(flagsConfig.timestamps.updatedAt);
1165
+ if (createdAt || updatedAt) flags.timestamps = {
1166
+ ...createdAt ? { createdAt } : {},
1167
+ ...updatedAt ? { updatedAt } : {}
1168
+ };
1169
+ }
1170
+ if (flagsConfig.tenant) {
1171
+ const column = has(flagsConfig.tenant.column);
1172
+ if (column) flags.tenant = column;
1173
+ }
1174
+ if (flagsConfig.actor) {
1175
+ const createdBy = has(flagsConfig.actor.createdBy);
1176
+ const updatedBy = has(flagsConfig.actor.updatedBy);
1177
+ const impersonatedBy = has(flagsConfig.actor.impersonatedBy);
1178
+ if (createdBy || updatedBy || impersonatedBy) flags.actor = {
1179
+ ...createdBy ? { createdBy } : {},
1180
+ ...updatedBy ? { updatedBy } : {},
1181
+ ...impersonatedBy ? { impersonatedBy } : {}
1182
+ };
1183
+ }
1184
+ const key = keyOf.get(id) ?? table.name;
1185
+ const columnMeta = {};
1186
+ for (const column of columns) {
1187
+ const meta = {
1188
+ db: column.db,
1189
+ type: column.snapshot.udt,
1190
+ nullable: column.nullable,
1191
+ hasDefault: column.snapshot.hasDefault
1192
+ };
1193
+ if (column.readonly) meta.generated = true;
1194
+ if (column.snapshot.identity) meta.identity = column.snapshot.identity;
1195
+ if (!column.readonly && !column.insertable) meta.insertable = false;
1196
+ if (!column.readonly && !column.updatable) meta.updatable = false;
1197
+ if (column.snapshot.isArray) meta.array = true;
1198
+ if (column.json) meta.json = true;
1199
+ if (column.values) meta.enum = column.values;
1200
+ if (column.codec) meta.codec = column.codec;
1201
+ if (column.storage) meta.storage = column.storage;
1202
+ if (config.sensitive.includes(`${table.name}.${column.db}`) || config.sensitive.includes(`${table.schema}.${table.name}.${column.db}`)) meta.sensitive = true;
1203
+ columnMeta[column.app] = meta;
1204
+ }
1205
+ const app = (db) => columns.find((col) => col.db === db)?.app ?? db;
1206
+ const uniqueKeys = {};
1207
+ for (const unique of table.uniques) uniqueKeys[unique.name] = unique.columns.map(app);
1208
+ const relationMeta = {};
1209
+ for (const relation of list) relationMeta[relation.name] = relation.meta;
1210
+ return {
1211
+ key,
1212
+ snapshot: table,
1213
+ casing,
1214
+ columns,
1215
+ relations: list,
1216
+ meta: {
1217
+ key,
1218
+ name: table.name,
1219
+ schema: table.schema,
1220
+ kind: table.kind,
1221
+ columns: columnMeta,
1222
+ primaryKey: table.primaryKey.map(app),
1223
+ uniqueKeys,
1224
+ relations: relationMeta,
1225
+ flags
1226
+ }
1227
+ };
1228
+ });
1229
+ const enums = catalog.enums.filter((entry) => config.schemas.includes(entry.schema)).map((entry) => ({
1230
+ schema: entry.schema,
1231
+ name: entry.name,
1232
+ constant: `${toCamel(entry.name)}Values`,
1233
+ values: entry.values
1234
+ }));
1235
+ const seen = /* @__PURE__ */ new Set();
1236
+ const functions = catalog.functions.filter((fn) => config.schemas.includes(fn.schema)).filter((fn) => fn.returns !== "trigger" && fn.returns !== "event_trigger").filter((fn) => !fn.args.some((arg) => arg.udt === "internal")).filter((fn) => {
1237
+ if (seen.has(fn.name)) return false;
1238
+ seen.add(fn.name);
1239
+ return true;
1240
+ }).map((fn) => {
1241
+ const typeOf = (format) => {
1242
+ const isArray = format.startsWith("_");
1243
+ const values = enumsByName.get(`${fn.schema}.${isArray ? format.slice(1) : format}`);
1244
+ if (values) return isArray ? arrayOf(enumType(values)) : enumType(values);
1245
+ return tsType(fn.schema, format);
1246
+ };
1247
+ let returns;
1248
+ if (fn.returnsTable) returns = `{ ${fn.returnsTable.map((column) => `${JSON.stringify(column.name)}: ${typeOf(column.udt)}`).join("; ")} }[]`;
1249
+ else {
1250
+ returns = typeOf(fn.returns);
1251
+ if (fn.returnsSet) returns = arrayOf(returns);
1252
+ }
1253
+ return {
1254
+ key: fn.name,
1255
+ meta: {
1256
+ name: fn.name,
1257
+ schema: fn.schema,
1258
+ args: fn.args.map((arg) => ({
1259
+ name: arg.name,
1260
+ type: arg.udt
1261
+ })),
1262
+ returns: fn.returns,
1263
+ returnsSet: fn.returnsSet,
1264
+ volatility: fn.volatility
1265
+ },
1266
+ args: fn.args.map((arg) => ({
1267
+ name: arg.name,
1268
+ tsType: typeOf(arg.isArray ? `_${arg.udt}` : arg.udt),
1269
+ optional: arg.hasDefault
1270
+ })),
1271
+ returns
1272
+ };
1273
+ });
1274
+ const tablesMeta = {};
1275
+ for (const table of tableModels) tablesMeta[table.key] = table.meta;
1276
+ const enumsMeta = {};
1277
+ for (const entry of enums) enumsMeta[entry.name] = entry.values;
1278
+ const functionsMeta = {};
1279
+ for (const fn of functions) functionsMeta[fn.key] = fn.meta;
1280
+ const claimOverrides = {};
1281
+ for (const key of Object.keys(DEFAULT_CLAIMS)) if (config.claims[key] !== DEFAULT_CLAIMS[key]) claimOverrides[key] = config.claims[key];
1282
+ const tenantClaim = config.claims.tenant === DEFAULT_CLAIMS.tenant ? void 0 : tenantClaimPaths(config.claims.tenant);
1283
+ const meta = {
1284
+ version: 1,
1285
+ casing: config.casing,
1286
+ tables: tablesMeta,
1287
+ enums: enumsMeta,
1288
+ functions: functionsMeta,
1289
+ ...Object.keys(config.buckets).length > 0 ? { buckets: Object.fromEntries(Object.entries(config.buckets).map(([name, bucket]) => [name, {
1290
+ id: bucket.id ?? name.replaceAll(/[A-Z]/g, (char) => `-${char.toLowerCase()}`),
1291
+ public: bucket.public ?? false,
1292
+ path: bucket.path,
1293
+ ...bucket.policy ? { policy: bucket.policy } : {},
1294
+ ...bucket.policy === "tenant" && tenantClaim ? { tenant: { claim: tenantClaim } } : {},
1295
+ ...bucket.fileSizeLimit ? { fileSizeLimit: bucket.fileSizeLimit } : {},
1296
+ ...bucket.allowedMimeTypes ? { allowedMimeTypes: bucket.allowedMimeTypes } : {}
1297
+ }])) } : {},
1298
+ ...Object.keys(config.topics).length > 0 ? { topics: config.topics } : {},
1299
+ ...config.realtime.tables.length > 0 ? { realtime: realtimeMeta(config.realtime.tables, tableModels) } : {},
1300
+ ...Object.keys(claimOverrides).length > 0 ? { claims: claimOverrides } : {}
1301
+ };
1302
+ return {
1303
+ tables: tableModels,
1304
+ enums,
1305
+ functions,
1306
+ jsonImports: [...jsonImports.values()],
1307
+ meta,
1308
+ config,
1309
+ catalog,
1310
+ introspection
1311
+ };
1312
+ }
1313
+ /**
1314
+ * The columns that name a relation. A composite key that repeats a column on
1315
+ * both sides, such as `(customer_id, organization_id)` referencing
1316
+ * `(id, organization_id)` to keep rows in one tenant, is named by the rest.
1317
+ */
1318
+ function namingColumns(columns, references) {
1319
+ const own = columns.filter((column, index) => column !== references[index]);
1320
+ return own.length > 0 ? own : columns;
1321
+ }
1322
+ /**
1323
+ * Makes relation names unique per table and distinct from column names:
1324
+ * two FKs to the same table become `customersByPrimaryContact`-style names.
1325
+ */
1326
+ function dedupeRelations(list, columns, casing) {
1327
+ const counts = /* @__PURE__ */ new Map();
1328
+ for (const relation of list) counts.set(relation.name, (counts.get(relation.name) ?? 0) + 1);
1329
+ const used = /* @__PURE__ */ new Set();
1330
+ return list.map((relation) => {
1331
+ let name = relation.name;
1332
+ if ((counts.get(name) ?? 0) > 1 || columns.has(name)) {
1333
+ const { columns: from, references: to } = relation.meta;
1334
+ const suffix = (relation.meta.direction === "forward" ? namingColumns(from, to) : namingColumns(to, from)).map((column) => column.replace(/_?[iI]d$/, "")).join("_");
1335
+ name = applyCasing(`${casing === "camel" ? toSnake(name) : name}_by_${casing === "camel" ? toSnake(suffix) : suffix}`, casing);
1336
+ }
1337
+ while (used.has(name) || columns.has(name)) name = `${name}_`;
1338
+ used.add(name);
1339
+ return {
1340
+ ...relation,
1341
+ name
1342
+ };
1343
+ });
1344
+ }
1345
+ function realtimeMeta(names, tables) {
1346
+ const out = {};
1347
+ for (const name of names) {
1348
+ const table = tables.find((candidate) => candidate.meta.name === name || `${candidate.meta.schema}.${candidate.meta.name}` === name);
1349
+ if (!table) throw new Error(`realtime.tables: unknown table "${name}". Check the name and \`schemas\`.`);
1350
+ const tenant = table.meta.flags.tenant;
1351
+ out[table.key] = tenant ? { tenant } : {};
1352
+ }
1353
+ return out;
1354
+ }
1355
+ //#endregion
1356
+ //#region src/cli/introspect/extras.ts
1357
+ /**
1358
+ * Catalog details `@supabase/postgrest-typegen` leaves out: named unique and
1359
+ * CHECK constraints, foreign key actions, ordered primary keys, indexes,
1360
+ * policies, triggers, grants, buckets and the realtime publication.
1361
+ *
1362
+ * The SQL takes no bind parameters because the Management API endpoint only
1363
+ * accepts a query string; schema names are inlined as escaped literals.
1364
+ */
1365
+ const literalArray = (values) => `array[${values.map((value) => `'${value.replaceAll("'", "''")}'`).join(", ")}]::text[]`;
1366
+ /** Row estimate from which a table counts as large in the snapshot. */
1367
+ const LARGE_TABLE_ROWS = 1e4;
1368
+ const RELATIONS = (schemas) => `
1369
+ select c.oid::int8 as id, n.nspname as schema, c.relname as name,
1370
+ c.reltuples >= ${LARGE_TABLE_ROWS} as large
1371
+ from pg_class c
1372
+ join pg_namespace n on n.oid = c.relnamespace
1373
+ where n.nspname = any(${schemas}) and c.relkind in ('r', 'p', 'v', 'm', 'f')
1374
+ and not c.relispartition
1375
+ order by 2, 3`;
1376
+ const CONSTRAINTS = (schemas) => `
1377
+ select con.conrelid::int8 as table_id, con.conname as name, con.contype as type,
1378
+ array(select a.attname from unnest(con.conkey) with ordinality k(attnum, ord)
1379
+ join pg_attribute a on a.attrelid = con.conrelid and a.attnum = k.attnum order by k.ord)::text[] as columns,
1380
+ con.confdeltype as on_delete, con.confupdtype as on_update,
1381
+ pg_get_constraintdef(con.oid) as definition
1382
+ from pg_constraint con
1383
+ join pg_class c on c.oid = con.conrelid
1384
+ join pg_namespace n on n.oid = c.relnamespace
1385
+ where n.nspname = any(${schemas}) and con.contype in ('p', 'u', 'f', 'c')
1386
+ order by 1, 2`;
1387
+ const INDEXES = (schemas) => `
1388
+ select ix.indrelid::int8 as table_id, i.relname as name,
1389
+ ix.indisunique as unique, ix.indisprimary as primary,
1390
+ (ix.indpred is not null or 0 = any(ix.indkey::int2[])) as partial,
1391
+ am.amname as method, pg_get_expr(ix.indpred, ix.indrelid) as predicate,
1392
+ array(select a.attname from unnest(ix.indkey::int2[]) with ordinality k(attnum, ord)
1393
+ join pg_attribute a on a.attrelid = ix.indrelid and a.attnum = k.attnum order by k.ord)::text[] as columns
1394
+ from pg_index ix
1395
+ join pg_class i on i.oid = ix.indexrelid
1396
+ join pg_am am on am.oid = i.relam
1397
+ join pg_class c on c.oid = ix.indrelid
1398
+ join pg_namespace n on n.oid = c.relnamespace
1399
+ where n.nspname = any(${schemas})
1400
+ order by 1, 2`;
1401
+ const POLICIES = (schemas) => `
1402
+ select c.oid::int8 as table_id, p.policyname as name,
1403
+ (p.permissive = 'PERMISSIVE') as permissive, p.roles::text[] as roles, lower(p.cmd) as command,
1404
+ p.qual as using_expr, p.with_check as check_expr,
1405
+ array(
1406
+ select distinct fn.nspname || '.' || pr.proname
1407
+ from pg_policy pol
1408
+ join pg_depend d on d.classid = 'pg_policy'::regclass and d.objid = pol.oid
1409
+ and d.refclassid = 'pg_proc'::regclass
1410
+ join pg_proc pr on pr.oid = d.refobjid
1411
+ join pg_namespace fn on fn.oid = pr.pronamespace
1412
+ where pol.polrelid = c.oid and pol.polname = p.policyname
1413
+ order by 1
1414
+ )::text[] as functions
1415
+ from pg_policies p
1416
+ join pg_namespace n on n.nspname = p.schemaname
1417
+ join pg_class c on c.relnamespace = n.oid and c.relname = p.tablename
1418
+ where p.schemaname = any(${schemas})
1419
+ order by 1, 2`;
1420
+ const FUNCTIONS = (schemas) => `
1421
+ select n.nspname as schema, p.proname as name,
1422
+ pg_get_function_identity_arguments(p.oid) as signature,
1423
+ l.lanname as language, p.provolatile as volatility,
1424
+ p.prosecdef as security_definer, coalesce(p.proconfig, '{}')::text[] as config,
1425
+ array(
1426
+ select r from unnest(array['anon', 'authenticated']) r
1427
+ where has_function_privilege(r, p.oid, 'EXECUTE')
1428
+ ) as execute
1429
+ from pg_proc p
1430
+ join pg_namespace n on n.oid = p.pronamespace
1431
+ join pg_language l on l.oid = p.prolang
1432
+ where p.oid in (
1433
+ select d.refobjid
1434
+ from pg_depend d
1435
+ join pg_policy pol on d.classid = 'pg_policy'::regclass and d.objid = pol.oid
1436
+ join pg_class c on c.oid = pol.polrelid
1437
+ join pg_namespace cn on cn.oid = c.relnamespace
1438
+ where d.refclassid = 'pg_proc'::regclass and cn.nspname = any(${schemas})
1439
+ )
1440
+ or (n.nspname = any(${schemas}) and ((p.prosecdef
1441
+ and p.prorettype not in ('trigger'::regtype, 'event_trigger'::regtype)) or exists (
1442
+ select 1 from unnest(p.proconfig) setting where setting not like 'search_path=%'
1443
+ )))
1444
+ order by 1, 2, 3`;
1445
+ /** Roles the hook checks care about: the one Auth calls it as, and the API roles. */
1446
+ const HOOK_ROLES = literalArray([
1447
+ "supabase_auth_admin",
1448
+ "authenticated",
1449
+ "anon"
1450
+ ]);
1451
+ const HOOKS = (names) => `
1452
+ select n.nspname as schema, p.proname as name,
1453
+ pg_get_function_identity_arguments(p.oid) as signature,
1454
+ l.lanname as language, p.provolatile as volatility,
1455
+ p.prosecdef as security_definer, coalesce(p.proconfig, '{}')::text[] as config,
1456
+ array(select r.rolname::text from pg_roles r
1457
+ where r.rolname = any(${HOOK_ROLES}) and has_function_privilege(r.oid, p.oid, 'execute')
1458
+ order by 1) as execute,
1459
+ exists(select 1 from aclexplode(coalesce(p.proacl, acldefault('f', p.proowner))) a
1460
+ where a.grantee = 0 and a.privilege_type = 'EXECUTE') as public_execute,
1461
+ array(select r.rolname::text from pg_roles r
1462
+ where r.rolname = any(${HOOK_ROLES}) and has_schema_privilege(r.oid, n.oid, 'usage')
1463
+ order by 1) as schema_usage,
1464
+ p.prosrc as source
1465
+ from pg_proc p
1466
+ join pg_namespace n on n.oid = p.pronamespace
1467
+ join pg_language l on l.oid = p.prolang
1468
+ where n.nspname || '.' || p.proname = any(${names})
1469
+ order by 1, 2, 3`;
1470
+ const TRIGGERS = (schemas) => `
1471
+ select t.tgrelid::int8 as table_id, t.tgname as name,
1472
+ pn.nspname || '.' || p.proname as function,
1473
+ case when t.tgtype & 2 = 2 then 'before' when t.tgtype & 64 = 64 then 'instead of' else 'after' end as timing,
1474
+ array_remove(array[
1475
+ case when t.tgtype & 4 = 4 then 'insert' end,
1476
+ case when t.tgtype & 16 = 16 then 'update' end,
1477
+ case when t.tgtype & 8 = 8 then 'delete' end,
1478
+ case when t.tgtype & 32 = 32 then 'truncate' end
1479
+ ], null)::text[] as events,
1480
+ case when t.tgtype & 1 = 1 then 'row' else 'statement' end as level
1481
+ from pg_trigger t
1482
+ join pg_class c on c.oid = t.tgrelid
1483
+ join pg_namespace n on n.oid = c.relnamespace
1484
+ join pg_proc p on p.oid = t.tgfoid
1485
+ join pg_namespace pn on pn.oid = p.pronamespace
1486
+ where not t.tgisinternal and n.nspname = any(${schemas})
1487
+ order by 1, 2`;
1488
+ const GRANTS = (schemas) => `
1489
+ select c.oid::int8 as table_id, g.grantee as role,
1490
+ array_agg(g.privilege_type::text order by g.privilege_type)::text[] as privileges
1491
+ from information_schema.role_table_grants g
1492
+ join pg_namespace n on n.nspname = g.table_schema
1493
+ join pg_class c on c.relnamespace = n.oid and c.relname = g.table_name
1494
+ where g.table_schema = any(${schemas}) and g.grantee in ('anon', 'authenticated', 'service_role')
1495
+ group by 1, 2
1496
+ order by 1, 2`;
1497
+ const COLUMN_GRANTS = (schemas) => `
1498
+ select c.oid::int8 as table_id, a.attname as column, r.rolname as role,
1499
+ array_agg(distinct acl.privilege_type order by acl.privilege_type)::text[] as privileges
1500
+ from pg_class c
1501
+ join pg_namespace n on n.oid = c.relnamespace
1502
+ join pg_attribute a on a.attrelid = c.oid and a.attnum > 0 and not a.attisdropped and a.attacl is not null
1503
+ cross join lateral aclexplode(a.attacl) acl
1504
+ join pg_roles r on r.oid = acl.grantee
1505
+ where n.nspname = any(${schemas}) and r.rolname in ('anon', 'authenticated')
1506
+ and acl.privilege_type in ('INSERT', 'UPDATE')
1507
+ group by 1, 2, 3
1508
+ order by 1, 2, 3`;
1509
+ const BUCKETS = `
1510
+ select id, public, file_size_limit, allowed_mime_types
1511
+ from storage.buckets
1512
+ order by id`;
1513
+ const REALTIME = `
1514
+ select schemaname || '.' || tablename as name
1515
+ from pg_publication_tables
1516
+ where pubname = 'supabase_realtime'
1517
+ order by 1`;
1518
+ const ROLE_SETTINGS = `
1519
+ select r.rolname as role, s.setconfig as config
1520
+ from pg_catalog.pg_db_role_setting s
1521
+ join pg_catalog.pg_roles r on r.oid = s.setrole
1522
+ where r.rolname in ('authenticator', 'anon', 'authenticated')
1523
+ and s.setdatabase in (0, (select oid from pg_catalog.pg_database where datname = current_database()))
1524
+ order by s.setdatabase`;
1525
+ const HAS_TABLE = (qualified) => `select to_regclass('${qualified}') is not null as present`;
1526
+ const ACTIONS = {
1527
+ a: "no action",
1528
+ r: "restrict",
1529
+ c: "cascade",
1530
+ n: "set null",
1531
+ d: "set default"
1532
+ };
1533
+ const VOLATILITY = {
1534
+ i: "immutable",
1535
+ s: "stable",
1536
+ v: "volatile"
1537
+ };
1538
+ /** `['statement_timeout=5s']` as `{ statement_timeout: '5s' }`. */
1539
+ function settingsOf(config) {
1540
+ const settings = {};
1541
+ for (const entry of config ?? []) {
1542
+ const at = entry.indexOf("=");
1543
+ if (at > 0) settings[entry.slice(0, at)] = entry.slice(at + 1);
1544
+ }
1545
+ return settings;
1546
+ }
1547
+ const functionOf = (row) => ({
1548
+ schema: row.schema,
1549
+ name: row.name,
1550
+ signature: row.signature,
1551
+ language: row.language,
1552
+ volatility: VOLATILITY[row.volatility],
1553
+ securityDefiner: row.security_definer,
1554
+ settings: settingsOf(row.config)
1555
+ });
1556
+ /** The functions behind Auth hooks, in any schema, with their ACLs. */
1557
+ async function readHooks(db, targets) {
1558
+ if (targets.length === 0) return [];
1559
+ const found = await rows(db, HOOKS(literalArray(targets.map((t) => `${t.schema}.${t.name}`))));
1560
+ return targets.map((target) => ({
1561
+ hook: target.hook,
1562
+ schema: target.schema,
1563
+ name: target.name,
1564
+ functions: found.filter((row) => row.schema === target.schema && row.name === target.name).map((row) => ({
1565
+ ...functionOf(row),
1566
+ execute: row.execute,
1567
+ publicExecute: row.public_execute,
1568
+ schemaUsage: row.schema_usage,
1569
+ ...row.source === null ? {} : { source: row.source }
1570
+ }))
1571
+ }));
1572
+ }
1573
+ async function rows(db, sql) {
1574
+ return (await db.query(sql)).rows;
1575
+ }
1576
+ /** Rows from a table owned by another Supabase service, if it exists. */
1577
+ async function serviceRows(db, qualified, sql) {
1578
+ const [probe] = await rows(db, HAS_TABLE(qualified));
1579
+ return probe?.present ? rows(db, sql) : [];
1580
+ }
1581
+ function groupById(list) {
1582
+ const map = /* @__PURE__ */ new Map();
1583
+ for (const row of list) {
1584
+ const id = Number(row.table_id);
1585
+ const entry = map.get(id);
1586
+ if (entry) entry.push(row);
1587
+ else map.set(id, [row]);
1588
+ }
1589
+ return map;
1590
+ }
1591
+ /** Reads the extras for `schemas`, with the catalog queries in flight together. */
1592
+ async function readExtras(db, schemas, hooks = []) {
1593
+ const list = literalArray(schemas);
1594
+ const [relations, constraintRows, indexRows, policyRows, triggerRows, grantRows, columnGrantRows, buckets, realtime, functions, roleRows, hookRows] = await Promise.all([
1595
+ rows(db, RELATIONS(list)),
1596
+ rows(db, CONSTRAINTS(list)),
1597
+ rows(db, INDEXES(list)),
1598
+ rows(db, POLICIES(list)),
1599
+ rows(db, TRIGGERS(list)),
1600
+ rows(db, GRANTS(list)),
1601
+ rows(db, COLUMN_GRANTS(list)),
1602
+ serviceRows(db, "storage.buckets", BUCKETS),
1603
+ rows(db, REALTIME),
1604
+ rows(db, FUNCTIONS(list)),
1605
+ rows(db, ROLE_SETTINGS),
1606
+ hooks.length > 0 ? readHooks(db, hooks) : void 0
1607
+ ]);
1608
+ const constraints = groupById(constraintRows);
1609
+ const indexes = groupById(indexRows);
1610
+ const policies = groupById(policyRows);
1611
+ const triggers = groupById(triggerRows);
1612
+ const grants = groupById(grantRows);
1613
+ const columnGrants = groupById(columnGrantRows);
1614
+ const roleSettings = {};
1615
+ for (const row of roleRows) Object.assign(roleSettings[row.role] ??= {}, settingsOf(row.config));
1616
+ return {
1617
+ tables: relations.map((relation) => {
1618
+ const id = Number(relation.id);
1619
+ const own = constraints.get(id) ?? [];
1620
+ const ownIndexes = indexes.get(id) ?? [];
1621
+ const uniques = own.filter((con) => con.type === "u").map((con) => ({
1622
+ name: con.name,
1623
+ columns: con.columns
1624
+ }));
1625
+ const constraintNames = new Set(own.map((con) => con.name));
1626
+ for (const index of ownIndexes) if (index.unique && !index.primary && !index.partial && !constraintNames.has(index.name)) uniques.push({
1627
+ name: index.name,
1628
+ columns: index.columns
1629
+ });
1630
+ return {
1631
+ id,
1632
+ schema: relation.schema,
1633
+ name: relation.name,
1634
+ primaryKey: own.find((con) => con.type === "p")?.columns ?? [],
1635
+ uniques,
1636
+ foreignKeys: own.filter((con) => con.type === "f").map((con) => ({
1637
+ name: con.name,
1638
+ onDelete: ACTIONS[con.on_delete] ?? "no action",
1639
+ onUpdate: ACTIONS[con.on_update] ?? "no action"
1640
+ })),
1641
+ checks: own.filter((con) => con.type === "c").map((con) => ({
1642
+ name: con.name,
1643
+ definition: con.definition
1644
+ })),
1645
+ indexes: ownIndexes.map((index) => ({
1646
+ name: index.name,
1647
+ columns: index.columns,
1648
+ unique: index.unique,
1649
+ primary: index.primary,
1650
+ partial: index.partial,
1651
+ method: index.method,
1652
+ predicate: index.predicate
1653
+ })),
1654
+ policies: (policies.get(id) ?? []).map((policy) => ({
1655
+ name: policy.name,
1656
+ command: policy.command,
1657
+ roles: policy.roles,
1658
+ permissive: policy.permissive,
1659
+ using: policy.using_expr,
1660
+ check: policy.check_expr,
1661
+ functions: policy.functions ?? []
1662
+ })),
1663
+ triggers: (triggers.get(id) ?? []).map((trigger) => ({
1664
+ name: trigger.name,
1665
+ timing: trigger.timing,
1666
+ events: trigger.events,
1667
+ level: trigger.level,
1668
+ function: trigger.function
1669
+ })),
1670
+ grants: (grants.get(id) ?? []).map((grant) => ({
1671
+ role: grant.role,
1672
+ privileges: grant.privileges
1673
+ })),
1674
+ columnGrants: (columnGrants.get(id) ?? []).map((grant) => ({
1675
+ column: grant.column,
1676
+ role: grant.role,
1677
+ privileges: grant.privileges
1678
+ })),
1679
+ ...relation.large ? { large: true } : {}
1680
+ };
1681
+ }),
1682
+ buckets: buckets.map((row) => ({
1683
+ id: row.id,
1684
+ public: row.public,
1685
+ fileSizeLimit: row.file_size_limit === null ? null : Number(row.file_size_limit),
1686
+ allowedMimeTypes: row.allowed_mime_types
1687
+ })),
1688
+ realtime: realtime.map((row) => row.name),
1689
+ roleSettings,
1690
+ functions: functions.map((row) => ({
1691
+ ...functionOf(row),
1692
+ execute: row.execute
1693
+ })),
1694
+ ...hookRows ? { hooks: hookRows } : {}
1695
+ };
1696
+ }
1697
+ //#endregion
1698
+ //#region src/cli/introspect/fingerprint.ts
1699
+ /** Catalogs whose rows only change with DDL, grants, comments or settings. */
1700
+ const CATALOGS = [
1701
+ "pg_namespace",
1702
+ "pg_attribute",
1703
+ "pg_attrdef",
1704
+ "pg_constraint",
1705
+ "pg_index",
1706
+ "pg_inherits",
1707
+ "pg_rewrite",
1708
+ "pg_policy",
1709
+ "pg_trigger",
1710
+ "pg_proc",
1711
+ "pg_type",
1712
+ "pg_enum",
1713
+ "pg_description",
1714
+ "pg_db_role_setting",
1715
+ "pg_publication",
1716
+ "pg_publication_rel",
1717
+ "pg_extension"
1718
+ ];
1719
+ const digest = (from) => `(select md5(coalesce(string_agg(x::text, ',' order by x::text), '')) from ${from} x)`;
1720
+ /**
1721
+ * One cheap query whose result changes whenever introspection could. pg_class
1722
+ * leaves out the planner statistics that VACUUM and ANALYZE rewrite, except
1723
+ * the row-count threshold the snapshot records. Bucket rows are data, so they
1724
+ * are hashed when `storage.buckets` exists.
1725
+ */
1726
+ const FINGERPRINT_SQL = `select md5(concat_ws('|',
1727
+ ${digest(`(select oid, relname, relnamespace, relkind, relacl, reloptions, relrowsecurity, relforcerowsecurity, relreplident, relispartition, reltuples >= ${LARGE_TABLE_ROWS} as large from pg_catalog.pg_class)`)},
1728
+ ${CATALOGS.map((name) => digest(`pg_catalog.${name}`)).join(",\n ")},
1729
+ (select md5(coalesce(string_agg(rolname, ',' order by rolname), '')) from pg_catalog.pg_roles),
1730
+ case when to_regclass('storage.buckets') is not null
1731
+ then md5(query_to_xml('select id, public, file_size_limit, allowed_mime_types from storage.buckets order by id', false, false, '')::text)
1732
+ end
1733
+ )) as fingerprint`;
1734
+ /** A hash of the catalog; equal values mean introspection would return the same snapshot. */
1735
+ async function catalogFingerprint(db) {
1736
+ const [row] = (await db.query(FINGERPRINT_SQL)).rows;
1737
+ const value = row && typeof row === "object" && "fingerprint" in row ? row.fingerprint : void 0;
1738
+ if (typeof value !== "string") throw new TypeError("The catalog fingerprint query returned no row.");
1739
+ return value;
1740
+ }
1741
+ //#endregion
1742
+ //#region src/cli/introspect/cache.ts
1743
+ /** Shared with the splinter download cache. */
1744
+ const CACHE_DIR = "node_modules/.cache/better-supabase";
1745
+ const sha256 = (text) => createHash("sha256").update(text).digest("hex");
1746
+ function isEntry(value) {
1747
+ return typeof value === "object" && value !== null && "key" in value && typeof value.key === "string" && "snapshot" in value && typeof value.snapshot === "object";
1748
+ }
1749
+ /**
1750
+ * Calls `read`, or returns the snapshot cached under `root` when the catalog
1751
+ * fingerprint, the `inputs` (schemas and hooks) and the CLI version all
1752
+ * match. One file per source keeps the cache from growing. A source that
1753
+ * cannot report a fingerprint is read every time.
1754
+ */
1755
+ async function cachedIntrospect(root, source, inputs, read) {
1756
+ const fingerprint = await catalogFingerprint(source.queryable).catch(() => void 0);
1757
+ if (fingerprint === void 0) return read();
1758
+ const key = sha256(JSON.stringify([
1759
+ VERSION,
1760
+ source.describe,
1761
+ inputs,
1762
+ fingerprint
1763
+ ]));
1764
+ const file = resolve(root, CACHE_DIR, `snapshot-${sha256(source.describe).slice(0, 16)}.json`);
1765
+ const cached = await readFile(file, "utf8").then((text) => JSON.parse(text)).catch(() => void 0);
1766
+ if (isEntry(cached) && cached.key === key) return cached.snapshot;
1767
+ const snapshot = await read();
1768
+ const entry = {
1769
+ key,
1770
+ snapshot
1771
+ };
1772
+ await mkdir(dirname(file), { recursive: true }).then(() => writeFile(file, JSON.stringify(entry))).catch(() => void 0);
1773
+ return snapshot;
1774
+ }
1775
+ //#endregion
1776
+ //#region src/cli/introspect/source.ts
1777
+ const POOL_SIZE = 4;
1778
+ const STATEMENT_TIMEOUT_MS = 12e4;
1779
+ /**
1780
+ * A small `pg` pool, since introspection issues its queries concurrently. A
1781
+ * `connect` that returns a single client gets its queries queued, because one
1782
+ * `pg.Client` runs them one at a time.
1783
+ */
1784
+ async function pgSource(url, open = connect, signal) {
1785
+ const { client, pooled, close, describe } = await open(url, void 0, {
1786
+ pool: POOL_SIZE,
1787
+ statementTimeout: STATEMENT_TIMEOUT_MS,
1788
+ ...signal ? { signal } : {}
1789
+ });
1790
+ if (pooled) return {
1791
+ queryable: client,
1792
+ close,
1793
+ describe
1794
+ };
1795
+ let queue = Promise.resolve();
1796
+ return {
1797
+ queryable: { query(sql) {
1798
+ const next = queue.then(() => client.query(sql));
1799
+ queue = next.catch(() => void 0);
1800
+ return next;
1801
+ } },
1802
+ close,
1803
+ describe
1804
+ };
1805
+ }
1806
+ /**
1807
+ * Runs introspection SQL through the Supabase Management API as the
1808
+ * read-only database user, so hosted projects need no database password.
1809
+ */
1810
+ function managementSource(options) {
1811
+ const base = (options.apiUrl ?? "https://api.supabase.com").replace(/\/$/, "");
1812
+ const doFetch = options.fetch ?? fetch;
1813
+ const endpoint = `${base}/v1/projects/${encodeURIComponent(options.projectRef)}/database/query/read-only`;
1814
+ return {
1815
+ queryable: { async query(sql) {
1816
+ const response = await doFetch(endpoint, {
1817
+ method: "POST",
1818
+ headers: {
1819
+ authorization: `Bearer ${options.accessToken}`,
1820
+ "content-type": "application/json"
1821
+ },
1822
+ body: JSON.stringify({ query: sql }),
1823
+ ...options.signal ? { signal: options.signal } : {}
1824
+ });
1825
+ const text = await response.text();
1826
+ if (!response.ok) throw new Error(`Management API query failed (${response.status}) for project ${options.projectRef}: ${text.slice(0, 500)}`);
1827
+ const rows = JSON.parse(text);
1828
+ if (!Array.isArray(rows)) throw new TypeError(`Management API returned an unexpected body for project ${options.projectRef}.`);
1829
+ return { rows };
1830
+ } },
1831
+ describe: `project ${options.projectRef} (Management API)`,
1832
+ close: () => Promise.resolve()
1833
+ };
1834
+ }
1835
+ //#endregion
1836
+ //#region src/cli/introspect/index.ts
1837
+ /** Reads a v2 snapshot: typegen metadata plus the extras. */
1838
+ async function introspect(db, schemas, options = {}) {
1839
+ const unique = [...new Set(schemas)].sort();
1840
+ const [metadata, extras] = await Promise.all([readGeneratorMetadata(db, unique), readExtras(db, unique, options.hooks)]);
1841
+ const { metadata: generator, ids } = stabilizeMetadata(metadata);
1842
+ return {
1843
+ version: 2,
1844
+ schemas: unique,
1845
+ generator,
1846
+ extras: {
1847
+ ...extras,
1848
+ tables: extras.tables.map((table) => ({
1849
+ ...table,
1850
+ id: ids.get(table.id) ?? table.id
1851
+ }))
1852
+ }
1853
+ };
1854
+ }
1855
+ //#endregion
1856
+ //#region src/cli/commands/snapshot.ts
1857
+ const SNAPSHOT_SCHEMA_URL = "https://unpkg.com/better-supabase/schemas/snapshot-v2.json";
1858
+ /** Schemas that are always read, for doctor checks on helper functions. */
1859
+ const EXTRA_SCHEMAS = ["better_supabase"];
1860
+ /** The hosted project to read through the Management API, if any. */
1861
+ function managementTarget(config, env, source) {
1862
+ if (source.dbUrl) return void 0;
1863
+ const projectRef = source.projectRef ?? (config.source.dbUrl ? void 0 : config.source.projectRef);
1864
+ if (!projectRef) return void 0;
1865
+ const accessToken = config.source.accessToken ?? env.SUPABASE_ACCESS_TOKEN;
1866
+ if (!accessToken) throw new CliError("missing_value", `Reading project ${projectRef} needs a Supabase access token. Set SUPABASE_ACCESS_TOKEN to a token scoped to this project (https://supabase.com/dashboard/account/tokens), or read the database with $DATABASE_URL or --db-url-stdin.`, { flag: "SUPABASE_ACCESS_TOKEN" });
1867
+ return {
1868
+ projectRef,
1869
+ accessToken,
1870
+ ...env.SUPABASE_API_URL ? { apiUrl: env.SUPABASE_API_URL } : {}
1871
+ };
1872
+ }
1873
+ /**
1874
+ * Where to read the schema from: `--db-url-stdin`, `--project-ref`, then the
1875
+ * config's `source.dbUrl` / `source.projectRef`, `$DATABASE_URL`, and finally
1876
+ * the local stack.
1877
+ */
1878
+ async function openSource(config, env, source, open = connect) {
1879
+ if (source.dbUrl) return pgSource(source.dbUrl, open, source.signal);
1880
+ const target = managementTarget(config, env, source);
1881
+ if (target) return managementSource({
1882
+ ...target,
1883
+ ...source.signal ? { signal: source.signal } : {}
1884
+ });
1885
+ return pgSource(await databaseUrl(config, env), open, source.signal);
1886
+ }
1887
+ /** Whether `loadSnapshot` reads a saved file rather than a database. */
1888
+ function snapshotFile(config, source) {
1889
+ return source.snapshotPath ?? (source.live || source.dbUrl || source.projectRef ? void 0 : config.source.snapshot);
1890
+ }
1891
+ /**
1892
+ * Loads a saved snapshot or introspects the database. With `shared`, it
1893
+ * introspects over that source and leaves closing it to the caller, so later
1894
+ * queries reuse the connection.
1895
+ */
1896
+ async function loadSnapshot(config, env, source, open = connect, shared) {
1897
+ const path = snapshotFile(config, source);
1898
+ if (path) return readSnapshotFile(resolve(config.root, path), path);
1899
+ const [toml, db] = await Promise.all([readSupabaseToml(config.root), shared ? shared() : openSource(config, env, source, open)]);
1900
+ const hooks = toml ? pgFunctionHooks(toml.document) : [];
1901
+ const schemas = [...config.schemas, ...EXTRA_SCHEMAS];
1902
+ try {
1903
+ const read = () => introspect(db.queryable, schemas, { hooks });
1904
+ return await (source.cache ? cachedIntrospect(config.root, db, [schemas, hooks], read) : read());
1905
+ } finally {
1906
+ if (!shared) await db.close();
1907
+ }
1908
+ }
1909
+ async function readSnapshotFile(absolute, label = absolute) {
1910
+ if (!existsSync(absolute)) throw new Error(`Snapshot not found: ${label}`);
1911
+ return parseSnapshot(JSON.parse(await readFile(absolute, "utf8")), label);
1912
+ }
1913
+ /** Validates a parsed snapshot document. */
1914
+ async function parseSnapshot(data, label = "snapshot") {
1915
+ const doc = data;
1916
+ if (!doc || typeof doc !== "object" || doc.version !== 2) throw new Error(`${label} is not a version 2 snapshot. Run \`better-supabase introspect\` to refresh it.`);
1917
+ if (!Array.isArray(doc.schemas) || !doc.extras || !Array.isArray(doc.extras.tables)) throw new Error(`${label} is missing "schemas" or "extras".`);
1918
+ let generator;
1919
+ try {
1920
+ generator = await validateGeneratorMetadata(doc.generator);
1921
+ } catch (cause) {
1922
+ throw new Error(`${label} has invalid "generator" metadata: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
1923
+ }
1924
+ return {
1925
+ version: 2,
1926
+ schemas: doc.schemas,
1927
+ generator,
1928
+ extras: {
1929
+ tables: doc.extras.tables,
1930
+ buckets: doc.extras.buckets ?? [],
1931
+ realtime: doc.extras.realtime ?? [],
1932
+ ...doc.extras.roleSettings ? { roleSettings: doc.extras.roleSettings } : {},
1933
+ ...doc.extras.functions ? { functions: doc.extras.functions } : {},
1934
+ ...doc.extras.hooks ? { hooks: doc.extras.hooks } : {}
1935
+ }
1936
+ };
1937
+ }
1938
+ function serializeSnapshot(snapshot) {
1939
+ const { $schema: _ignored, ...rest } = snapshot;
1940
+ return `${JSON.stringify({
1941
+ $schema: SNAPSHOT_SCHEMA_URL,
1942
+ ...rest
1943
+ }, null, 2)}\n`;
1944
+ }
1945
+ //#endregion
1946
+ //#region src/cli/commands/gen.ts
1947
+ /**
1948
+ * Renders every generated file for a snapshot, without touching disk:
1949
+ * `database.types.ts` (what `supabase gen types` prints), the main module,
1950
+ * then each configured generator's files.
1951
+ */
1952
+ async function renderFiles(config, snapshot) {
1953
+ const model = buildModel(snapshot, config);
1954
+ const output = resolve(config.root, config.output);
1955
+ const databaseTypes = await generateDatabaseTypes(snapshot.generator, {
1956
+ schemas: config.schemas,
1957
+ postgrestVersion: config.postgrestVersion
1958
+ });
1959
+ const metaFiles = metaPaths(config.output);
1960
+ const main = emitModule(model, {
1961
+ databaseTypesImport: importPath(output, resolve(config.root, config.databaseTypesOutput)),
1962
+ importPathFor: (from) => importPath(output, resolve(config.root, from)),
1963
+ metaImport: `./${basename(metaFiles.js)}`
1964
+ });
1965
+ const meta = emitMeta(model, { types: `./${basename(metaFiles.dts)}` });
1966
+ const files = [
1967
+ {
1968
+ path: config.databaseTypesOutput,
1969
+ contents: databaseTypes
1970
+ },
1971
+ {
1972
+ path: config.output,
1973
+ contents: main
1974
+ },
1975
+ {
1976
+ path: metaFiles.js,
1977
+ contents: meta.js
1978
+ },
1979
+ {
1980
+ path: metaFiles.dts,
1981
+ contents: meta.dts
1982
+ }
1983
+ ];
1984
+ for (const generator of config.generators) {
1985
+ const extra = await generator.generate({
1986
+ meta: model.meta,
1987
+ introspection: model.introspection,
1988
+ extras: snapshot.extras,
1989
+ config,
1990
+ output,
1991
+ importPath: (from, to) => importPath(resolve(config.root, from), resolve(config.root, to))
1992
+ });
1993
+ files.push(...extra);
1994
+ }
1995
+ return files;
1996
+ }
1997
+ /** The `read-sets` SQL kit file for `config.readSets`, if any are configured. */
1998
+ async function readSetFile(config) {
1999
+ if (config.readSets.length === 0) return void 0;
2000
+ const readSets = await compiledReadSets(config);
2001
+ return renderKit(["read-sets"], kitLayout(config, config.sql.testsDir, readSets))[0];
2002
+ }
2003
+ /**
2004
+ * `buckets` keys the catalog doesn't mark `rowConditions: false`, as error
2005
+ * lines. Without a readable catalog every PermDock bucket is refused, since
2006
+ * whether its keys have row conditions is unknown.
2007
+ */
2008
+ async function rowConditionedBuckets(config) {
2009
+ const configured = configuredPermdockKeys({ config });
2010
+ if (configured.length === 0) return [];
2011
+ const project = await readPermdock(config.root, config.permdock);
2012
+ const catalog = project?.catalog;
2013
+ if (!catalog) {
2014
+ const path = project?.catalogPath ?? config.permdock.catalog;
2015
+ const problem = project?.problems.find((entry) => entry.startsWith(path));
2016
+ return [` ${configured.map(({ bucket }) => `buckets.${bucket}`).join(", ")}: ${problem ? `could not read PermDock's catalog (${problem})` : `there is no ${path}`}, so whether the keys have row conditions is unknown. Run \`permdock catalog\`, or set permdock.catalog in the config.`];
2017
+ }
2018
+ return configured.flatMap(({ bucket, keys }) => keys.flatMap((key) => {
2019
+ const problem = unsafeKey(catalog, key, project.catalogPath);
2020
+ return problem ? [` buckets.${bucket}: "${key}" ${problem.reason}. ${problem.fix}`] : [];
2021
+ }));
2022
+ }
2023
+ const UNFORMATTED = "oxfmt is not installed, so database.types.ts is not formatted like `supabase gen types` output. Install the version @supabase/postgrest-typegen pins: pnpm add -D oxfmt@0.66.0. To use a newer oxfmt, allow it in pnpm-workspace.yaml under peerDependencyRules.allowedVersions, keyed \"@supabase/postgrest-typegen>oxfmt\".";
2024
+ async function runGen(options) {
2025
+ const { config } = options;
2026
+ const refused = await rowConditionedBuckets(config);
2027
+ if (refused.length > 0) return {
2028
+ code: 1,
2029
+ error: `PermDock's SQL helpers don't check row conditions, so these bucket policies could grant every object in the scope:\n${refused.join("\n")}\nSee doctor BS214.`
2030
+ };
2031
+ const snapshot = options.snapshot ?? await loadSnapshot(config, options.env, options);
2032
+ const files = await renderFiles(config, snapshot);
2033
+ const notice = await oxfmtInstalled() ? "" : `\n${UNFORMATTED}`;
2034
+ if (options.check) {
2035
+ const stale = [];
2036
+ const diffs = [];
2037
+ const compare = (file, current) => {
2038
+ const shown = display(config.root, file.path);
2039
+ stale.push(shown);
2040
+ diffs.push(fileDiff(shown, current, file.contents, options.paint));
2041
+ };
2042
+ for (const file of files) {
2043
+ const path = resolve(config.root, file.path);
2044
+ const current = existsSync(path) ? await readFile(path, "utf8") : void 0;
2045
+ if (current !== file.contents) compare(file, current);
2046
+ }
2047
+ const readSets = await readSetFile(config);
2048
+ if (readSets) {
2049
+ const path = resolve(config.root, readSets.path);
2050
+ const current = existsSync(path) ? await readFile(path, "utf8") : void 0;
2051
+ if (!sameKitFile(current, readSets.contents)) compare(readSets, current);
2052
+ }
2053
+ if (stale.length > 0) return {
2054
+ code: 1,
2055
+ output: diffs.join("\n\n"),
2056
+ error: `Generated files are out of date:\n${stale.map((path) => ` ${path}`).join("\n")}\nRun \`better-supabase gen\`.`,
2057
+ data: {
2058
+ upToDate: false,
2059
+ stale
2060
+ }
2061
+ };
2062
+ return {
2063
+ code: 0,
2064
+ output: `Generated files are up to date (${files.length + (readSets ? 1 : 0)}).${notice}`,
2065
+ data: {
2066
+ upToDate: true,
2067
+ stale: []
2068
+ }
2069
+ };
2070
+ }
2071
+ const written = [];
2072
+ for (const file of files) if (await writeIfChanged(resolve(config.root, file.path), file.contents)) written.push(display(config.root, file.path));
2073
+ const readSets = await readSetFile(config);
2074
+ if (readSets && await writeIfChanged(resolve(config.root, readSets.path), readSets.contents)) written.push(display(config.root, readSets.path));
2075
+ const tables = snapshot.extras.tables.filter((table) => config.schemas.includes(table.schema)).length;
2076
+ return {
2077
+ code: 0,
2078
+ output: written.length === 0 ? `No changes (${tables} tables).` : `Generated ${tables} tables:\n${written.map((path) => ` ${path}`).join("\n")}${notice}`,
2079
+ data: {
2080
+ tables,
2081
+ written
2082
+ }
2083
+ };
2084
+ }
2085
+ /** Where to read the schema from, shared by gen, introspect and doctor. */
2086
+ const SOURCE_ARGS = {
2087
+ snapshot: {
2088
+ type: "string",
2089
+ description: "Read a saved snapshot instead of the database",
2090
+ valueHint: "file"
2091
+ },
2092
+ "db-url-stdin": {
2093
+ type: "boolean",
2094
+ description: "Read the connection string from stdin instead of $DATABASE_URL or the config"
2095
+ },
2096
+ "project-ref": {
2097
+ type: "string",
2098
+ description: "Read a hosted project through the Management API",
2099
+ valueHint: "ref"
2100
+ }
2101
+ };
2102
+ async function sourceArgs(args, io) {
2103
+ const snapshotPath = args.snapshot;
2104
+ const dbUrl = await stdinDatabaseUrl(args["db-url-stdin"], io);
2105
+ const projectRef = args["project-ref"];
2106
+ return {
2107
+ ...snapshotPath ? { snapshotPath } : {},
2108
+ ...dbUrl ? { dbUrl } : {},
2109
+ ...projectRef ? { projectRef } : {}
2110
+ };
2111
+ }
2112
+ const ARGS = {
2113
+ check: {
2114
+ type: "boolean",
2115
+ description: "Fail when a generated file is out of date"
2116
+ },
2117
+ watch: {
2118
+ type: "boolean",
2119
+ description: "Regenerate whenever the schema changes"
2120
+ },
2121
+ interval: {
2122
+ type: "string",
2123
+ description: "Milliseconds between checks in --watch. Defaults to 2000",
2124
+ valueHint: "ms"
2125
+ },
2126
+ ...SOURCE_ARGS
2127
+ };
2128
+ const sleep = (ms, signal) => setTimeout(ms, void 0, signal ? { signal } : {}).catch(() => void 0);
2129
+ /**
2130
+ * Regenerates whenever the schema changes. A database source keeps one
2131
+ * connection open and polls the catalog fingerprint, so an unchanged schema
2132
+ * costs one small query per interval; a source without a fingerprint is
2133
+ * introspected each time and compared.
2134
+ */
2135
+ /**
2136
+ * Reads the schema when it may have changed. Returns the new change marker (the
2137
+ * catalog fingerprint, or the snapshot itself when the source has none) and
2138
+ * the snapshot, or no snapshot when the fingerprint still matches `last`.
2139
+ */
2140
+ async function poll(options, db, last) {
2141
+ const fingerprint = db ? await catalogFingerprint(db.queryable).catch(() => void 0) : void 0;
2142
+ if (fingerprint !== void 0 && fingerprint === last) return { marker: last };
2143
+ const snapshot = await loadSnapshot(options.config, options.env, options, void 0, db ? () => Promise.resolve(db) : void 0);
2144
+ const marker = fingerprint ?? JSON.stringify(snapshot);
2145
+ return marker === last ? { marker } : {
2146
+ marker,
2147
+ snapshot
2148
+ };
2149
+ }
2150
+ /**
2151
+ * Regenerates whenever the schema changes. A database source keeps one
2152
+ * connection open and polls the catalog fingerprint, so an unchanged schema
2153
+ * costs one small query per interval.
2154
+ */
2155
+ async function watch(options, interval, { io, signal }) {
2156
+ let last = "";
2157
+ let db;
2158
+ const aborted = () => signal?.aborted ?? false;
2159
+ const fromFile = snapshotFile(options.config, options) !== void 0;
2160
+ try {
2161
+ while (!aborted()) {
2162
+ try {
2163
+ db ??= fromFile ? void 0 : await openSource(options.config, options.env, options);
2164
+ const { marker, snapshot } = await poll(options, db, last);
2165
+ last = marker;
2166
+ const result = snapshot ? await runGen({
2167
+ ...options,
2168
+ snapshot
2169
+ }) : void 0;
2170
+ if (result?.output) io.stdout(`${result.output}\n`);
2171
+ if (result?.error) io.stderr(`${result.error}\n`);
2172
+ } catch (cause) {
2173
+ io.stderr(`${cause instanceof Error ? cause.message : String(cause)}\n`);
2174
+ await db?.close().catch(() => void 0);
2175
+ db = void 0;
2176
+ }
2177
+ await sleep(interval, signal);
2178
+ }
2179
+ } finally {
2180
+ await db?.close().catch(() => void 0);
2181
+ }
2182
+ return { code: 0 };
2183
+ }
2184
+ const genCommand = defineCliCommand({
2185
+ meta: {
2186
+ name: "gen",
2187
+ description: "Writes database.types.ts, generated.ts and its metadata module from the database"
2188
+ },
2189
+ args: ARGS,
2190
+ run: async (args, context) => {
2191
+ const options = {
2192
+ config: context.config,
2193
+ env: context.env,
2194
+ check: args.check === true,
2195
+ paint: painter(context.io.color),
2196
+ ...await sourceArgs(args, context.io),
2197
+ ...context.signal ? { signal: context.signal } : {},
2198
+ cache: true
2199
+ };
2200
+ return args.watch === true ? watch(options, Number(args.interval ?? 2e3), context) : withSpinner(context.io.prompts, "Generating from the schema", () => runGen(options));
2201
+ }
2202
+ });
2203
+ //#endregion
2204
+ export { toCatalog as A, catalogOf as C, policyObject as D, lineOf as E, qualified as O, entitlementsKit as S, exposedSchemas as T, restrictSchemas as _, sourceArgs as a, emitModule as b, openSource as c, snapshotFile as d, introspect as f, buildModel as g, CACHE_DIR as h, runGen as i, tableObject as k, parseSnapshot as l, pgSource as m, genCommand as n, loadSnapshot as o, managementSource as p, renderFiles as r, managementTarget as s, SOURCE_ARGS as t, serializeSnapshot as u, serializeGenerator as v, exposed as w, PERMDOCK_RULES as x, GENERATED_HEADER as y };