better-supabase 0.2.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 (190) hide show
  1. package/README.md +37 -33
  2. package/dist/base64-D2gsioCY.js +42 -0
  3. package/dist/claims-BAf6c7is.js +48 -0
  4. package/dist/cli/_format-Cax5EsM9.js +101 -0
  5. package/dist/cli/bin.js +39 -6
  6. package/dist/cli/command-CK14fP91.js +502 -0
  7. package/dist/cli/compare-BmIWVEui.js +9 -0
  8. package/dist/cli/config-D9R0o1LH.js +1895 -0
  9. package/dist/cli/config-SSKSTUl2.js +2 -0
  10. package/dist/cli/db-DOBPt660.js +52 -0
  11. package/dist/cli/dist-B1MvLv-M.js +129 -0
  12. package/dist/cli/dist-CFgGknmZ.js +1112 -0
  13. package/dist/cli/dist-Du7VnEUG.js +621 -0
  14. package/dist/cli/doctor-BOym3mbL.js +2382 -0
  15. package/dist/cli/env-BZ0uy8XK.js +108 -0
  16. package/dist/cli/env-jIFUADLY.js +511 -0
  17. package/dist/cli/errors-4v4DTtda.js +56 -0
  18. package/dist/cli/fastest-levenshtein-CFN-lNDv.js +120 -0
  19. package/dist/cli/gen-Ccn_p90L.js +2008 -0
  20. package/dist/cli/gen-D5cLCULx.js +2 -0
  21. package/dist/cli/index.d.ts +295 -28
  22. package/dist/cli/index.js +184 -3
  23. package/dist/cli/init-ClnxRYBF.js +550 -0
  24. package/dist/cli/introspect-DxqfkVIv.js +81 -0
  25. package/dist/cli/io-k41_z-jT.js +24 -0
  26. package/dist/cli/json5-ChRycquj.js +787 -0
  27. package/dist/cli/jsonc-BUniUpOk.js +2 -0
  28. package/dist/cli/jsonc-D5Fu67jw.js +45 -0
  29. package/dist/cli/keys-ByKHnvP9.js +83 -0
  30. package/dist/cli/ohash-D7bG_EOb.js +277 -0
  31. package/dist/cli/openapi-8WHjxiTJ.js +66 -0
  32. package/dist/cli/project-B_GeXyHh.js +70 -0
  33. package/dist/cli/prompts-BNcNfAve.js +1142 -0
  34. package/dist/cli/prompts-BTYQGunc.js +2 -0
  35. package/dist/cli/read-sets-CMa29t9a.js +971 -0
  36. package/dist/cli/rolldown-runtime-BPOCksWG.js +24 -0
  37. package/dist/cli/run-CP20LKk_.js +347 -0
  38. package/dist/cli/seed-5J6eu_Xg.js +98 -0
  39. package/dist/cli/skills-LJJIhLFR.js +137 -0
  40. package/dist/cli/sql-D4VbqmXi.js +199 -0
  41. package/dist/cli/style-C0NFdJRL.js +29 -0
  42. package/dist/cli/toml-DB-MNnFs.js +424 -0
  43. package/dist/{version-Cqq1m3UY.js → cli/version-5mW_8sY1.js} +1 -1
  44. package/dist/cli/yaml-CYCbJ6et.js +2252 -0
  45. package/dist/client/index.d.ts +2 -2
  46. package/dist/client/index.js +15 -25
  47. package/dist/{compiler-CZ9nIuqy.d.ts → compiler-DB3aSFOA.d.ts} +2 -2
  48. package/dist/config/index.d.ts +4 -3
  49. package/dist/config/index.js +5 -3
  50. package/dist/config-BpxDfnbg.js +129 -0
  51. package/dist/{index-DOsjFvMf.d.ts → config-C1CYcm6o.d.ts} +49 -49
  52. package/dist/{config-ClDXmVtY.js → config-CbvFmGD8.js} +14 -137
  53. package/dist/{define-BXMYD9AY.js → define-Bh0AqTik.js} +221 -120
  54. package/dist/{define-e3zI4_4g.d.ts → define-CRS2MRV3.d.ts} +36 -31
  55. package/dist/edge/index.d.ts +9 -13
  56. package/dist/edge/index.js +51 -25
  57. package/dist/{entitlements-Dng-ctuT.d.ts → entitlements-Dm-FHpNH.d.ts} +2 -2
  58. package/dist/env/index.js +203 -1
  59. package/dist/{errors-Bl3rkOzo.js → errors-L0xK41y_.js} +1 -1
  60. package/dist/events/index.d.ts +1 -1
  61. package/dist/events/index.js +139 -1
  62. package/dist/{events-DOt3nSO-.d.ts → events-B21UOhvP.d.ts} +4 -4
  63. package/dist/{executor-CGdNQ27g.d.ts → executor-B5IoUxLE.d.ts} +2 -2
  64. package/dist/{executor-DeppvirK.js → executor-CVt6dD0B.js} +9 -4
  65. package/dist/{executor-BYXQ9AGn.d.ts → executor-CZ7ImRiD.d.ts} +2 -2
  66. package/dist/hono/index.d.ts +17 -24
  67. package/dist/hono/index.js +11 -11
  68. package/dist/{index-DuP14VHo.d.ts → index-528Ugmyp.d.ts} +30 -10
  69. package/dist/{index-Bw1bwtC0.d.ts → index-BAy4tsAP.d.ts} +28 -15
  70. package/dist/{index-CBDufH1M.d.ts → index-BBdvo-5d.d.ts} +24 -26
  71. package/dist/{index-DbxFr7XG.d.ts → index-BhHEC04r.d.ts} +9 -4
  72. package/dist/index-Cf-D2jk1.d.ts +50 -0
  73. package/dist/{index-Ciu5Xlml.d.ts → index-D5IfUIDh.d.ts} +6 -5
  74. package/dist/{index-K-3WM2Hp.d.ts → index-DFmOpBGC.d.ts} +13 -13
  75. package/dist/{index-D4v7sncu.d.ts → index-RlLXd8dL.d.ts} +13 -13
  76. package/dist/index.d.ts +17 -17
  77. package/dist/index.js +23 -14
  78. package/dist/{invalidate-DbxuL2jL.js → invalidate-CgxmRamI.js} +7 -1
  79. package/dist/jobs/index.d.ts +33 -26
  80. package/dist/jobs/index.js +44 -22
  81. package/dist/{json-schema-BpYqkCKf.js → json-schema-DP6V7cbr.js} +8 -3
  82. package/dist/lint/index.js +1 -1
  83. package/dist/list/index.d.ts +2 -2
  84. package/dist/list/index.js +131 -96
  85. package/dist/{live-DJyZMD6w.d.ts → live-BHakSWWo.d.ts} +26 -16
  86. package/dist/{live-D8rV1Ldw.js → live-CdJBOc4f.js} +13 -9
  87. package/dist/lookup-BaP5bI27.js +18 -0
  88. package/dist/mcp/index.d.ts +34 -27
  89. package/dist/mcp/index.js +59 -25
  90. package/dist/{mfa-TVia554c.d.ts → mfa-C7MNQ1Ob.d.ts} +1 -1
  91. package/dist/{mfa-CpLfJSde.js → mfa-byfjruR9.js} +190 -46
  92. package/dist/next/image/index.js +6 -3
  93. package/dist/next/index.d.ts +64 -48
  94. package/dist/next/index.js +63 -50
  95. package/dist/openapi/index.d.ts +1 -1
  96. package/dist/openapi/index.js +61 -38
  97. package/dist/orpc/index.d.ts +18 -21
  98. package/dist/orpc/index.js +12 -12
  99. package/dist/otel/index.d.ts +5 -5
  100. package/dist/otel/index.js +84 -63
  101. package/dist/{permdock-sql-CghlHPuH.js → permdock-sql-C4KFzLc9.js} +12 -7
  102. package/dist/permdock-sql-DImJULY-.d.ts +17 -0
  103. package/dist/{plugin-C4TR9J0Z.d.ts → plugin-bNhvJwRh.d.ts} +5 -4
  104. package/dist/plugins/actor/index.d.ts +1 -1
  105. package/dist/plugins/actor/index.js +1 -1
  106. package/dist/plugins/rules/index.d.ts +2 -2
  107. package/dist/plugins/rules/index.js +46 -16
  108. package/dist/plugins/soft-delete/index.d.ts +3 -3
  109. package/dist/plugins/soft-delete/index.js +4 -4
  110. package/dist/plugins/tenant/index.d.ts +3 -3
  111. package/dist/plugins/tenant/index.js +10 -4
  112. package/dist/plugins/timestamps/index.d.ts +1 -1
  113. package/dist/plugins/timestamps/index.js +2 -2
  114. package/dist/plugins/validation/index.d.ts +2 -2
  115. package/dist/plugins/validation/index.js +3 -3
  116. package/dist/pool-DFwEQwmN.d.ts +78 -0
  117. package/dist/postgres/index.d.ts +10 -5
  118. package/dist/postgres/index.js +37 -16
  119. package/dist/{postgrest-cAEQ_IQN.d.ts → postgrest-Bc-0Xw62.d.ts} +1 -1
  120. package/dist/{problem-BTkTtRV9.js → problem-BihjPR0t.js} +11 -5
  121. package/dist/{problem-DlgB0lYl.d.ts → problem-U8UEKccv.d.ts} +6 -4
  122. package/dist/query/index.d.ts +2 -2
  123. package/dist/query/index.js +2 -2
  124. package/dist/{query-ChvDbsDE.js → query-BMMSWbI7.js} +19 -19
  125. package/dist/react/index.d.ts +7 -7
  126. package/dist/react/index.js +2 -2
  127. package/dist/react/server.d.ts +6 -6
  128. package/dist/react/server.js +3 -3
  129. package/dist/react/session.d.ts +21 -2
  130. package/dist/react/session.js +19 -1
  131. package/dist/{react-C83BdBGt.js → react-CQHMaESR.js} +59 -55
  132. package/dist/{read-set-4lI3W_gZ.js → read-set-FWK8qQ58.js} +51 -16
  133. package/dist/realtime/index.d.ts +4 -3
  134. package/dist/realtime/index.js +19 -17
  135. package/dist/{index-CG69s15v.d.ts → refresh-BwYkyNOQ.d.ts} +7 -3
  136. package/dist/{resolve-CjHILk2w.d.ts → resolve-BAqQcwMW.d.ts} +17 -11
  137. package/dist/{resource-CgZa3YG_.d.ts → resource-BJcaIfxi.d.ts} +17 -8
  138. package/dist/{resource-BHEWmQJK.js → resource-My_y-Syh.js} +43 -21
  139. package/dist/{respond-Dne1ARSa.d.ts → respond-DgIbdrt8.d.ts} +39 -14
  140. package/dist/{result-DWXatkd6.js → result-Bj6yt33D.js} +2 -2
  141. package/dist/{result-CTkCZ6JA.d.ts → result-CqEsbgYs.d.ts} +16 -11
  142. package/dist/server/index.d.ts +38 -13
  143. package/dist/server/index.js +43 -14
  144. package/dist/{respond-DMcbQwfN.js → server-BA75B0mT.js} +151 -96
  145. package/dist/{shared-_6-uPL7E.js → shared-DnoCdMDr.js} +4 -3
  146. package/dist/{simplify-bwJpLnaS.js → simplify-Cqvfr9dj.js} +76 -39
  147. package/dist/sql/index.d.ts +139 -0
  148. package/dist/sql/index.js +1983 -0
  149. package/dist/{sql-DTjA1LPj.js → sql-B34HU2DI.js} +74 -17
  150. package/dist/ssr/index.d.ts +4 -4
  151. package/dist/ssr/index.js +2 -2
  152. package/dist/{standard-FUi3Z3-7.js → standard-BRyBGTtp.js} +2 -2
  153. package/dist/{standard-CyhRzctb.d.ts → standard-BsaCYQJc.d.ts} +1 -1
  154. package/dist/{stats-C5W-rBfR.js → stats-C_YxA2r1.js} +40 -14
  155. package/dist/storage/index.d.ts +4 -3
  156. package/dist/storage/index.js +548 -1
  157. package/dist/{tables-rZxRxNPW.js → tables-Cb-3_VWc.js} +19 -5
  158. package/dist/temporal-BwshQk0W.js +27 -0
  159. package/dist/temporal-required-CBNHT6ni.js +19 -0
  160. package/dist/testing/index.d.ts +25 -24
  161. package/dist/testing/index.js +165 -66
  162. package/dist/{types-C83v5SQz.d.ts → types-DONv9SHN.d.ts} +14 -11
  163. package/dist/version-Bmf7_FzM.js +4 -0
  164. package/dist/{view-D1s0wa0T.d.ts → view-B64o81wp.d.ts} +36 -6
  165. package/dist/{view-aOYizTeE.js → view-BrJI2LKp.js} +11 -2
  166. package/dist/webhooks/index.d.ts +19 -17
  167. package/dist/webhooks/index.js +1 -1
  168. package/dist/{webhooks-DMipjhSP.js → webhooks-Bh_Jf_-l.js} +45 -24
  169. package/package.json +50 -22
  170. package/schemas/config-v1.json +31 -1
  171. package/schemas/doctor-report-v1.json +17 -1
  172. package/schemas/snapshot-v2.json +35 -0
  173. package/skills/better-supabase/SKILL.md +77 -18
  174. package/skills/better-supabase/references/plugins.md +21 -22
  175. package/skills/better-supabase/references/troubleshooting.md +38 -18
  176. package/skills/better-supabase-api/SKILL.md +66 -18
  177. package/skills/better-supabase-api/references/adapters.md +64 -41
  178. package/skills/better-supabase-auth/SKILL.md +146 -0
  179. package/skills/better-supabase-auth/references/permdock.md +55 -0
  180. package/skills/better-supabase-testing/SKILL.md +68 -11
  181. package/dist/casing-da0uRTqt.js +0 -14
  182. package/dist/claims-CoTobNC-.js +0 -25
  183. package/dist/cli-DrSgDOQC.js +0 -6646
  184. package/dist/env-BEToab0I.js +0 -204
  185. package/dist/events-BuzTgn2D.js +0 -135
  186. package/dist/pool-R2m_DkXi.d.ts +0 -37
  187. package/dist/seed-CzecODcJ.js +0 -101
  188. package/dist/session-C3LxnQ0e.d.ts +0 -22
  189. package/dist/session-CqEwzVup.js +0 -20
  190. package/dist/storage-DuGn38fr.js +0 -527
@@ -1,2 +1,549 @@
1
- import { i as parseSize, n as defineBucket, r as fromStorageError, t as TTL } from "../storage-DuGn38fr.js";
1
+ import { n as dbError, o as isDbError, t as DbException } from "../errors-L0xK41y_.js";
2
+ import { r as optionalTemporal } from "../temporal-BwshQk0W.js";
3
+ import { n as temporalMissing } from "../temporal-required-CBNHT6ni.js";
4
+ import { i as ok, n as err, o as toDbError, t as AsyncResult } from "../result-Bj6yt33D.js";
5
+ import { i as tenantClaimPaths } from "../claims-BAf6c7is.js";
6
+ import { a as sqlIdent, i as slug, o as sqlString, r as parseTemplate, t as permdockCheck } from "../permdock-sql-C4KFzLc9.js";
7
+ //#region src/storage/bucket.ts
8
+ /** Storage operations that list objects; every other `select` is a read. */
9
+ const LIST_OPERATIONS = [
10
+ "object.list",
11
+ "object.list_v2",
12
+ "s3.object.list"
13
+ ];
14
+ const TTL = {
15
+ minute: 60,
16
+ hour: 3600,
17
+ day: 86400,
18
+ week: 604800
19
+ };
20
+ const SIZE_UNITS = {
21
+ b: 1,
22
+ kb: 1e3,
23
+ kib: 1024,
24
+ mb: 1e3 ** 2,
25
+ mib: 1024 ** 2,
26
+ gb: 1e3 ** 3,
27
+ gib: 1024 ** 3
28
+ };
29
+ function parseSize(value) {
30
+ if (typeof value === "number") return value;
31
+ const match = /^\s*(\d+(?:\.\d+)?)\s*([a-z]*)\s*$/i.exec(value);
32
+ const unit = SIZE_UNITS[(match?.[2] || "b").toLowerCase()];
33
+ if (!match || unit === void 0) throw new TypeError(`Invalid size "${value}"`);
34
+ return Math.round(Number(match[1]) * unit);
35
+ }
36
+ const SAFE_SEGMENT = /^[\w!\-.*'() &$@=;:+,?]+$/;
37
+ function validateSegment(_name, value) {
38
+ if (value === "." || value === "..") return "is a relative path";
39
+ if (!SAFE_SEGMENT.test(value)) return "contains characters Storage does not allow";
40
+ }
41
+ function claimSql(claim) {
42
+ const expressions = (typeof claim === "string" ? [claim] : claim).map((path) => {
43
+ const keys = path.split(".");
44
+ const last = keys.pop();
45
+ return `(select auth.jwt())${keys.map((key) => ` -> ${sqlString(key)}`).join("")} ->> ${sqlString(last)}`;
46
+ });
47
+ return expressions.length === 1 ? expressions[0] : `coalesce(${expressions.join(", ")})`;
48
+ }
49
+ function mimeAllowed(allowed, type) {
50
+ const base = type.split(";")[0].trim().toLowerCase();
51
+ return allowed.some((entry) => {
52
+ const pattern = entry.toLowerCase();
53
+ return pattern.endsWith("/*") ? base.startsWith(pattern.slice(0, -1)) : base === pattern;
54
+ });
55
+ }
56
+ function sizeLabel(bytes) {
57
+ for (const [unit, size] of [
58
+ ["GiB", 1024 ** 3],
59
+ ["MiB", 1024 ** 2],
60
+ ["KiB", 1024]
61
+ ]) if (bytes >= size && bytes % size === 0) return `${String(bytes / size)}${unit}`;
62
+ return `${String(bytes)}B`;
63
+ }
64
+ /** Maps a Storage error (`StorageApiError`, fetch failures) to a `DbError`. */
65
+ function fromStorageError(raw, table) {
66
+ if (isDbError(raw)) return raw;
67
+ if (raw instanceof DbException) return raw.error;
68
+ if (typeof raw !== "object" || raw === null) return toDbError(raw);
69
+ const failure = raw;
70
+ if (failure.name === "AbortError") return toDbError(raw);
71
+ const message = failure.message ?? "Storage request failed";
72
+ const code = failure.code ?? failure.statusCode;
73
+ const base = {
74
+ ...code ? { code } : {},
75
+ ...table ? { table } : {}
76
+ };
77
+ const statusCode = Number(failure.statusCode);
78
+ const status = Number.isFinite(statusCode) && statusCode >= 400 ? statusCode : failure.status ?? 0;
79
+ if (/row-level security|unauthorized to|AccessDenied/i.test(`${message} ${code ?? ""}`)) return dbError("forbidden", message, base);
80
+ if (/already exists|duplicate/i.test(`${message} ${code ?? ""}`)) return dbError("conflict", message, base);
81
+ if (failure.name === "StorageUnknownError" && status === 0) return dbError("network", message, base);
82
+ switch (status) {
83
+ case 400: return dbError("invalid_request", message, base);
84
+ case 401: return dbError("unauthorized", message, base);
85
+ case 403: return dbError("forbidden", message, base);
86
+ case 404: return dbError("not_found", message, base);
87
+ case 409: return dbError("conflict", message, base);
88
+ case 413:
89
+ case 415: return dbError("invalid_input", message, {
90
+ ...base,
91
+ status
92
+ });
93
+ case 408:
94
+ case 429: return dbError("network", message, {
95
+ ...base,
96
+ status
97
+ });
98
+ default: return status >= 500 ? dbError("network", message, base) : dbError("unexpected", message, base);
99
+ }
100
+ }
101
+ /**
102
+ * A typed Storage bucket: path builders from a template, limits, generated
103
+ * SQL policies and `config.toml`, and helpers for the multi-step work apps
104
+ * repeat (replace, signed uploads, orphan cleanup).
105
+ *
106
+ * ```ts
107
+ * export const logos = defineBucket({
108
+ * id: 'customer-logos',
109
+ * path: '{orgId}/{customerId}/logo/{version}.webp',
110
+ * policy: 'tenant',
111
+ * fileSizeLimit: '5MiB',
112
+ * allowedMimeTypes: ['image/webp'],
113
+ * });
114
+ * ```
115
+ */
116
+ function defineBucket(config) {
117
+ const template = parseTemplate(config.path, "/", validateSegment);
118
+ const policy = config.policy ?? "none";
119
+ const fileSizeLimit = config.fileSizeLimit === void 0 ? void 0 : parseSize(config.fileSizeLimit);
120
+ const allowedMimeTypes = config.allowedMimeTypes;
121
+ function segmentFor(param, kind) {
122
+ const index = template.segmentOf(param);
123
+ if (index === void 0) throw new TypeError(`defineBucket: a ${kind} policy needs {${param}} as a whole path segment in "${config.path}"`);
124
+ return index;
125
+ }
126
+ const mode = typeof policy === "string" ? policy : "permdock";
127
+ const accessCheck = (() => {
128
+ switch (mode) {
129
+ case "tenant": {
130
+ const index = segmentFor(config.tenant?.param ?? "orgId", "tenant");
131
+ const expression = config.tenant?.sql ?? claimSql(config.tenant?.claim ?? tenantClaimPaths());
132
+ return `split_part(name, '/', ${String(index)}) = (${expression})`;
133
+ }
134
+ case "owner": {
135
+ const index = segmentFor(config.owner?.param ?? "userId", "owner");
136
+ return `split_part(name, '/', ${String(index)}) = (select auth.uid())::text`;
137
+ }
138
+ case "public":
139
+ case "none":
140
+ case "permdock": return;
141
+ default: throw new TypeError(`defineBucket: unknown policy "${String(mode)}"`);
142
+ }
143
+ })();
144
+ const permdock = (() => {
145
+ if (typeof policy === "string") return void 0;
146
+ const where = `defineBucket(${config.id})`;
147
+ const id = policy.scope === "global" ? void 0 : `split_part(name, '/', ${String(policy.segment ?? segmentFor(config.tenant?.param ?? "orgId", "PermDock"))})`;
148
+ const check = (key) => permdockCheck(where, policy, key, id, config.catalog);
149
+ const keys = policy.permdock;
150
+ return {
151
+ read: check(keys.read),
152
+ ...keys.list === void 0 ? {} : { list: check(keys.list) },
153
+ write: check(keys.write),
154
+ delete: check(keys.delete ?? keys.write)
155
+ };
156
+ })();
157
+ const resolve = (target) => {
158
+ if (typeof target !== "string") return template.build(target);
159
+ if (!template.match(target)) throw new DbException(dbError("invalid_input", `Path "${target}" does not match "${config.path}"`));
160
+ return target;
161
+ };
162
+ const check = (file) => {
163
+ if (fileSizeLimit !== void 0 && file.size !== void 0 && file.size > fileSizeLimit) return dbError("invalid_input", `File is larger than ${sizeLabel(fileSizeLimit)}`, { status: 413 });
164
+ if (allowedMimeTypes && file.type && !mimeAllowed(allowedMimeTypes, file.type)) return dbError("invalid_input", `File type ${file.type} is not allowed`, { status: 415 });
165
+ };
166
+ const bucket = {
167
+ id: config.id,
168
+ template: config.path,
169
+ params: template.params,
170
+ owner: policy === "owner" ? config.owner?.param ?? "userId" : template.params.includes("userId") ? "userId" : void 0,
171
+ public: config.public ?? false,
172
+ policy,
173
+ fileSizeLimit,
174
+ allowedMimeTypes,
175
+ path: (values) => template.build(values),
176
+ match: (path) => template.match(path),
177
+ prefix: (values = {}) => template.prefix(values, template.segments - 1),
178
+ check,
179
+ sql() {
180
+ const id = sqlString(config.id);
181
+ const name = slug(config.id);
182
+ const lines = [
183
+ `-- better-supabase: bucket ${config.id} (${config.path})`,
184
+ "insert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types)",
185
+ `values (${id}, ${id}, ${String(bucket.public)}, ${fileSizeLimit === void 0 ? "null" : String(fileSizeLimit)}, ${allowedMimeTypes ? `array[${allowedMimeTypes.map(sqlString).join(", ")}]` : "null"})`,
186
+ "on conflict (id) do update set public = excluded.public, file_size_limit = excluded.file_size_limit,",
187
+ " allowed_mime_types = excluded.allowed_mime_types;"
188
+ ];
189
+ const policies = [];
190
+ const inBucket = `bucket_id = ${id}`;
191
+ const pathMatch = `name ~ ${sqlString(template.sqlPattern)}`;
192
+ if (accessCheck) {
193
+ const using = `${inBucket} and ${accessCheck}`;
194
+ const write = `${using} and ${pathMatch}`;
195
+ policies.push([
196
+ "select",
197
+ "select",
198
+ "authenticated",
199
+ using,
200
+ void 0
201
+ ], [
202
+ "insert",
203
+ "insert",
204
+ "authenticated",
205
+ void 0,
206
+ write
207
+ ], [
208
+ "update",
209
+ "update",
210
+ "authenticated",
211
+ using,
212
+ write
213
+ ], [
214
+ "delete",
215
+ "delete",
216
+ "authenticated",
217
+ using,
218
+ void 0
219
+ ]);
220
+ } else if (policy === "public") policies.push([
221
+ "select",
222
+ "select",
223
+ "anon, authenticated",
224
+ inBucket,
225
+ void 0
226
+ ]);
227
+ else if (permdock) {
228
+ const listing = `storage.allow_any_operation(array[${LIST_OPERATIONS.map(sqlString).join(", ")}])`;
229
+ if (permdock.list) policies.push([
230
+ "select",
231
+ "select",
232
+ "authenticated",
233
+ `${inBucket} and not ${listing} and ${permdock.read}`,
234
+ void 0
235
+ ], [
236
+ "list",
237
+ "select",
238
+ "authenticated",
239
+ `${inBucket} and ${listing} and ${permdock.list}`,
240
+ void 0
241
+ ]);
242
+ else policies.push([
243
+ "select",
244
+ "select",
245
+ "authenticated",
246
+ `${inBucket} and ${permdock.read}`,
247
+ void 0
248
+ ]);
249
+ const write = `${inBucket} and ${permdock.write}`;
250
+ policies.push([
251
+ "insert",
252
+ "insert",
253
+ "authenticated",
254
+ void 0,
255
+ `${write} and ${pathMatch}`
256
+ ], [
257
+ "update",
258
+ "update",
259
+ "authenticated",
260
+ write,
261
+ `${write} and ${pathMatch}`
262
+ ], [
263
+ "delete",
264
+ "delete",
265
+ "authenticated",
266
+ `${inBucket} and ${permdock.delete}`,
267
+ void 0
268
+ ]);
269
+ }
270
+ for (const [suffix, command, roles, using, withCheck] of policies) {
271
+ const policyName = sqlIdent(`bs_${name}_${suffix}`);
272
+ lines.push("", `drop policy if exists ${policyName} on storage.objects;`, `create policy ${policyName} on storage.objects for ${command} to ${roles}${using ? `\n using (${using})` : ""}${withCheck ? `\n with check (${withCheck})` : ""};`);
273
+ }
274
+ if (permdock && !permdock.list) lines.push("", `drop policy if exists ${sqlIdent(`bs_${name}_list`)} on storage.objects;`);
275
+ return `${lines.join("\n")}\n`;
276
+ },
277
+ toml() {
278
+ const lines = [`[storage.buckets.${config.id}]`, `public = ${String(bucket.public)}`];
279
+ if (config.fileSizeLimit !== void 0) lines.push(`file_size_limit = "${typeof config.fileSizeLimit === "number" ? sizeLabel(config.fileSizeLimit) : config.fileSizeLimit}"`);
280
+ if (allowedMimeTypes) lines.push(`allowed_mime_types = [${allowedMimeTypes.map((type) => JSON.stringify(type)).join(", ")}]`);
281
+ return `${lines.join("\n")}\n`;
282
+ },
283
+ drift(actual) {
284
+ if (!actual) return [{
285
+ field: "missing",
286
+ expected: config.id,
287
+ actual: void 0,
288
+ message: `Bucket "${config.id}" does not exist`
289
+ }];
290
+ const issues = [];
291
+ if (actual.public !== bucket.public) issues.push({
292
+ field: "public",
293
+ expected: bucket.public,
294
+ actual: actual.public,
295
+ message: `Bucket "${config.id}" public is ${String(actual.public)}`
296
+ });
297
+ if ((actual.fileSizeLimit ?? void 0) !== fileSizeLimit) issues.push({
298
+ field: "fileSizeLimit",
299
+ expected: fileSizeLimit,
300
+ actual: actual.fileSizeLimit,
301
+ message: `Bucket "${config.id}" file size limit differs`
302
+ });
303
+ const sorted = (list) => list ? [...list].sort().join(",") : "";
304
+ if (sorted(actual.allowedMimeTypes) !== sorted(allowedMimeTypes)) issues.push({
305
+ field: "allowedMimeTypes",
306
+ expected: allowedMimeTypes,
307
+ actual: actual.allowedMimeTypes,
308
+ message: `Bucket "${config.id}" allowed MIME types differ`
309
+ });
310
+ return issues;
311
+ },
312
+ connect: (client, options) => connectBucket(bucket, client, resolve, options)
313
+ };
314
+ return bucket;
315
+ }
316
+ function bodyInfo(body, contentType) {
317
+ const info = {};
318
+ if (typeof Blob !== "undefined" && body instanceof Blob) {
319
+ info.size = body.size;
320
+ if (body.type) info.type = body.type;
321
+ } else if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) info.size = body.byteLength;
322
+ if (contentType) info.type = contentType;
323
+ return info;
324
+ }
325
+ function ttlSeconds(ttl) {
326
+ return typeof ttl === "number" ? ttl : TTL[ttl ?? "hour"];
327
+ }
328
+ function isErrorResult(value) {
329
+ return typeof value === "object" && value !== null && value.ok === false && "error" in value;
330
+ }
331
+ function connectBucket(bucket, client, resolve, connectOptions = {}) {
332
+ const api = () => client.storage.from(bucket.id);
333
+ const signed = connectOptions.cacheSignedUrls ? /* @__PURE__ */ new Map() : void 0;
334
+ const run = (fn) => AsyncResult.from(async () => {
335
+ try {
336
+ const { data, error } = await fn();
337
+ return error || data === null ? err(fromStorageError(error, bucket.id)) : ok(data);
338
+ } catch (cause) {
339
+ return err(fromStorageError(cause, bucket.id));
340
+ }
341
+ });
342
+ const fileOptions = (options, contentType) => ({
343
+ ...contentType ? { contentType } : {},
344
+ ...options?.cacheControl ? { cacheControl: options.cacheControl } : {},
345
+ ...options?.metadata ? { metadata: { ...options.metadata } } : {},
346
+ upsert: options?.upsert ?? false
347
+ });
348
+ const download = (value) => value === void 0 ? {} : { download: value };
349
+ const upload = (target, body, options) => AsyncResult.from(async () => {
350
+ options?.signal?.throwIfAborted();
351
+ const path = resolve(target);
352
+ const contentType = options?.contentType;
353
+ const problem = bucket.check(bodyInfo(body, contentType));
354
+ if (problem) return err(problem);
355
+ return run(() => api().upload(path, body, fileOptions(options, contentType))).map(() => ({ path }));
356
+ });
357
+ const remove = (targets) => AsyncResult.from(async () => {
358
+ const paths = targets.map(resolve);
359
+ if (paths.length === 0) return ok([]);
360
+ return run(() => api().remove(paths)).map(() => paths);
361
+ });
362
+ const walk = async (folder, signal, out) => {
363
+ const limit = 1e3;
364
+ for (let offset = 0;; offset += limit) {
365
+ signal?.throwIfAborted();
366
+ const { data, error } = await api().list(folder, {
367
+ limit,
368
+ offset,
369
+ sortBy: {
370
+ column: "name",
371
+ order: "asc"
372
+ }
373
+ }, signal ? { signal } : {});
374
+ if (error) throw new DbException(fromStorageError(error, bucket.id));
375
+ for (const item of data) {
376
+ const path = folder ? `${folder}/${item.name}` : item.name;
377
+ if (item.id === null) await walk(path, signal, out);
378
+ else {
379
+ const metadata = item.metadata ?? {};
380
+ out.push({
381
+ path,
382
+ ...typeof metadata.size === "number" ? { size: metadata.size } : {},
383
+ ...metadata.mimetype ? { contentType: metadata.mimetype } : {},
384
+ createdAt: item.created_at,
385
+ updatedAt: item.updated_at
386
+ });
387
+ }
388
+ }
389
+ if (data.length < limit) return;
390
+ }
391
+ };
392
+ const list = (within, options) => AsyncResult.from(async () => {
393
+ const out = [];
394
+ await walk(bucket.prefix(within ?? {}), options?.signal, out);
395
+ return ok(out);
396
+ }).mapError((error) => ({
397
+ ...error,
398
+ table: bucket.id
399
+ }));
400
+ const signedUrl = (target, options) => AsyncResult.from(async () => {
401
+ const path = resolve(target);
402
+ const ttl = ttlSeconds(options?.ttl);
403
+ const key = signed ? JSON.stringify([
404
+ path,
405
+ ttl,
406
+ options?.transform,
407
+ options?.download
408
+ ]) : "";
409
+ const cached = signed?.get(key);
410
+ if (cached && cached.until > Date.now()) return ok(cached.url);
411
+ const result = await run(() => api().createSignedUrl(path, ttl, {
412
+ ...download(options?.download),
413
+ ...options?.transform ? { transform: { ...options.transform } } : {}
414
+ })).map((data) => data.signedUrl);
415
+ if (signed && result.ok) {
416
+ const margin = Math.min(60, ttl / 10);
417
+ signed.set(key, {
418
+ url: result.data,
419
+ until: Date.now() + (ttl - margin) * 1e3
420
+ });
421
+ }
422
+ return result;
423
+ });
424
+ return {
425
+ bucket,
426
+ upload,
427
+ download: (target, options) => AsyncResult.from(async () => {
428
+ const path = resolve(target);
429
+ return run(() => api().download(path, {}, options?.signal ? { signal: options.signal } : {}));
430
+ }),
431
+ exists: (target) => AsyncResult.from(async () => {
432
+ const path = resolve(target);
433
+ const found = await api().exists(path);
434
+ if (!found.data) return ok(false);
435
+ return found.error ? err(fromStorageError(found.error, bucket.id)) : ok(true);
436
+ }),
437
+ remove,
438
+ list,
439
+ signedUrl,
440
+ signedUrls: (targets, options) => AsyncResult.from(async () => {
441
+ const paths = targets.map(resolve);
442
+ if (paths.length === 0) return ok([]);
443
+ return run(() => api().createSignedUrls(paths, ttlSeconds(options?.ttl), download(options?.download))).andThen((data) => {
444
+ const failed = data.find((entry) => entry.error != null || !entry.signedUrl);
445
+ if (failed) return Promise.resolve(err(dbError("not_found", failed.error ?? `No URL for ${String(failed.path)}`, { table: bucket.id })));
446
+ return Promise.resolve(ok(data.map((entry) => entry.signedUrl)));
447
+ });
448
+ }),
449
+ publicUrl(target, options) {
450
+ const path = resolve(target);
451
+ return api().getPublicUrl(path, {
452
+ ...download(options?.download),
453
+ ...options?.transform ? { transform: { ...options.transform } } : {}
454
+ }).data.publicUrl;
455
+ },
456
+ renderUrl(target, transform, options) {
457
+ if (!bucket.public) return signedUrl(target, {
458
+ ...options,
459
+ transform
460
+ });
461
+ return AsyncResult.from(() => Promise.resolve(ok(api().getPublicUrl(resolve(target), {
462
+ ...download(options?.download),
463
+ transform: { ...transform }
464
+ }).data.publicUrl)));
465
+ },
466
+ replace: (target, body, options = {}) => AsyncResult.from(async () => {
467
+ const path = resolve(target);
468
+ const previous = options.previous ?? null;
469
+ const same = previous === path;
470
+ const uploaded = await upload(path, body, {
471
+ ...options,
472
+ upsert: same || (options.upsert ?? false)
473
+ });
474
+ if (!uploaded.ok) return uploaded;
475
+ const undo = async (error) => {
476
+ if (!same) await remove([path]);
477
+ return err(error);
478
+ };
479
+ try {
480
+ options.signal?.throwIfAborted();
481
+ const outcome = await options.commit?.(path);
482
+ if (isErrorResult(outcome)) return await undo(outcome.error);
483
+ } catch (cause) {
484
+ return undo(fromStorageError(cause, bucket.id));
485
+ }
486
+ if (!previous || same) return ok({
487
+ path,
488
+ removed: null
489
+ });
490
+ const removed = await run(() => api().remove([previous]));
491
+ return ok(removed.ok ? {
492
+ path,
493
+ removed: previous
494
+ } : {
495
+ path,
496
+ removed: null,
497
+ cleanup: removed.error
498
+ });
499
+ }),
500
+ reserve: (target, options) => AsyncResult.from(async () => {
501
+ const path = resolve(target);
502
+ return run(() => api().createSignedUploadUrl(path, options?.upsert ? { upsert: true } : void 0)).map((data) => ({
503
+ path: data.path,
504
+ token: data.token,
505
+ signedUrl: data.signedUrl
506
+ }));
507
+ }),
508
+ uploadReserved: (reservation, body, options) => AsyncResult.from(async () => {
509
+ options?.signal?.throwIfAborted();
510
+ const path = resolve(reservation.path);
511
+ const problem = bucket.check(bodyInfo(body, options?.contentType));
512
+ if (problem) return err(problem);
513
+ return run(() => api().uploadToSignedUrl(path, reservation.token, body, fileOptions(options, options?.contentType))).map(() => ({ path }));
514
+ }),
515
+ sweep: (options) => AsyncResult.from(async () => {
516
+ const namespace = optionalTemporal();
517
+ if (namespace === void 0) return err(temporalMissing());
518
+ const now = options.now?.() ?? namespace.Now.instant();
519
+ const cutoff = options.olderThan instanceof namespace.Instant ? options.olderThan.epochMilliseconds : now.epochMilliseconds - options.olderThan.total("milliseconds");
520
+ const found = await list(options.within, options.signal ? { signal: options.signal } : void 0);
521
+ if (!found.ok) return found;
522
+ const candidates = found.data.filter((object) => bucket.match(object.path) !== null).filter((object) => {
523
+ const created = Date.parse(object.createdAt ?? object.updatedAt ?? "");
524
+ return Number.isFinite(created) && created < cutoff;
525
+ }).map((object) => object.path);
526
+ const size = options.batchSize ?? 100;
527
+ const orphans = [];
528
+ const removed = [];
529
+ for (let index = 0; index < candidates.length; index += size) {
530
+ options.signal?.throwIfAborted();
531
+ const batch = candidates.slice(index, index + size);
532
+ const keep = new Set(await options.referenced(batch));
533
+ const unreferenced = batch.filter((path) => !keep.has(path));
534
+ orphans.push(...unreferenced);
535
+ if (options.dryRun || unreferenced.length === 0) continue;
536
+ const result = await run(() => api().remove(unreferenced));
537
+ if (!result.ok) return result;
538
+ removed.push(...unreferenced);
539
+ }
540
+ return ok({
541
+ scanned: found.data.length,
542
+ orphans,
543
+ removed
544
+ });
545
+ })
546
+ };
547
+ }
548
+ //#endregion
2
549
  export { TTL, defineBucket, fromStorageError, parseSize };
@@ -24,16 +24,16 @@ function visitCondition(condition, tables) {
24
24
  case "and":
25
25
  case "or":
26
26
  for (const item of condition.items) visitCondition(item, tables);
27
- return;
27
+ break;
28
28
  case "not":
29
29
  visitCondition(condition.item, tables);
30
- return;
31
- case "column": return;
30
+ break;
31
+ case "column": break;
32
32
  case "relation":
33
33
  tables.add(condition.target.key);
34
34
  visitCondition(condition.where, tables);
35
- return;
36
- default: return condition;
35
+ break;
36
+ default: throw new TypeError(`Unknown condition: ${String(condition)}`);
37
37
  }
38
38
  }
39
39
  /**
@@ -42,6 +42,20 @@ function visitCondition(condition, tables) {
42
42
  * followed through chains of cascades.
43
43
  */
44
44
  function invalidationTargets(meta, table) {
45
+ let byTable = targetsByMeta.get(meta);
46
+ if (!byTable) {
47
+ byTable = /* @__PURE__ */ new Map();
48
+ targetsByMeta.set(meta, byTable);
49
+ }
50
+ let targets = byTable.get(table);
51
+ if (!targets) {
52
+ targets = Object.freeze(computeTargets(meta, table));
53
+ byTable.set(table, targets);
54
+ }
55
+ return targets;
56
+ }
57
+ const targetsByMeta = /* @__PURE__ */ new WeakMap();
58
+ function computeTargets(meta, table) {
45
59
  const targets = /* @__PURE__ */ new Set([table]);
46
60
  const queue = [table];
47
61
  for (let key = queue.shift(); key !== void 0; key = queue.shift()) {
@@ -0,0 +1,27 @@
1
+ const TEMPORAL_MISSING = `Temporal is not available in this runtime. Import "temporal-polyfill/global" once at startup.`;
2
+ /** The runtime's `Temporal`, or `undefined` before the polyfill loads. */
3
+ function optionalTemporal() {
4
+ return globalThis.Temporal;
5
+ }
6
+ /**
7
+ * The current instant, the default `now` everywhere. Without Temporal it
8
+ * throws a `TypeError` that names the polyfill.
9
+ */
10
+ function nowInstant() {
11
+ const namespace = optionalTemporal();
12
+ if (namespace === void 0) throw new TypeError(TEMPORAL_MISSING);
13
+ return namespace.Now.instant();
14
+ }
15
+ /**
16
+ * The text Postgres reads back for a Temporal value: ISO 8601, with a
17
+ * `ZonedDateTime` sent as its instant because Postgres rejects the
18
+ * `[Region/City]` annotation. `undefined` for anything else.
19
+ */
20
+ function temporalText(value) {
21
+ const namespace = optionalTemporal();
22
+ if (namespace === void 0) return void 0;
23
+ if (value instanceof namespace.ZonedDateTime) return value.toInstant().toString();
24
+ if (value instanceof namespace.Instant || value instanceof namespace.PlainDateTime || value instanceof namespace.PlainDate || value instanceof namespace.PlainTime) return value.toString();
25
+ }
26
+ //#endregion
27
+ export { temporalText as i, nowInstant as n, optionalTemporal as r, TEMPORAL_MISSING as t };
@@ -0,0 +1,19 @@
1
+ import { n as dbError, t as DbException } from "./errors-L0xK41y_.js";
2
+ import { r as optionalTemporal, t as TEMPORAL_MISSING } from "./temporal-BwshQk0W.js";
3
+ //#region src/core/temporal-required.ts
4
+ /** The error for a runtime without Temporal, naming the polyfill to load. */
5
+ function temporalMissing() {
6
+ return dbError("unexpected", TEMPORAL_MISSING);
7
+ }
8
+ /**
9
+ * The runtime's `Temporal`. Node 26, Chromium, Firefox and Deno ship it.
10
+ * Elsewhere it throws a `DbException`, which repositories return as an
11
+ * `unexpected` error that names the polyfill.
12
+ */
13
+ function temporal() {
14
+ const namespace = optionalTemporal();
15
+ if (namespace === void 0) throw new DbException(temporalMissing());
16
+ return namespace;
17
+ }
18
+ //#endregion
19
+ export { temporalMissing as n, temporal as t };