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