better-supabase 0.0.0 → 0.1.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 (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -1
  3. package/bin/better-supabase.js +3 -0
  4. package/dist/casing-da0uRTqt.js +14 -0
  5. package/dist/cli/bin.d.ts +1 -0
  6. package/dist/cli/bin.js +15 -0
  7. package/dist/cli/index.d.ts +226 -0
  8. package/dist/cli/index.js +3 -0
  9. package/dist/cli-CoByWYja.js +6472 -0
  10. package/dist/client/index.d.ts +2 -0
  11. package/dist/client/index.js +112 -0
  12. package/dist/compiler-DL18ukPe.d.ts +14 -0
  13. package/dist/config/index.d.ts +3 -0
  14. package/dist/config/index.js +3 -0
  15. package/dist/config-D-aR2ZoN.js +276 -0
  16. package/dist/define-CTfWw-SS.d.ts +662 -0
  17. package/dist/define-QGkq04oe.js +1068 -0
  18. package/dist/edge/index.d.ts +43 -0
  19. package/dist/edge/index.js +103 -0
  20. package/dist/entitlements-BBg61DQZ.d.ts +20 -0
  21. package/dist/entitlements-BC_khwRy.js +17 -0
  22. package/dist/env/index.d.ts +2 -0
  23. package/dist/env/index.js +2 -0
  24. package/dist/env-BEToab0I.js +204 -0
  25. package/dist/errors-Bl3rkOzo.js +196 -0
  26. package/dist/events/index.d.ts +2 -0
  27. package/dist/events/index.js +2 -0
  28. package/dist/events-2OJsVG_d.d.ts +84 -0
  29. package/dist/events-BuzTgn2D.js +135 -0
  30. package/dist/executor-Bazhgsgq.d.ts +195 -0
  31. package/dist/executor-Bje4suQF.d.ts +25 -0
  32. package/dist/executor-Ceq60hPM.js +89 -0
  33. package/dist/hono/index.d.ts +45 -0
  34. package/dist/hono/index.js +70 -0
  35. package/dist/index-BD6wKthp.d.ts +597 -0
  36. package/dist/index-DQVvHzjW.d.ts +74 -0
  37. package/dist/index-DRid0qIv.d.ts +76 -0
  38. package/dist/index-Dup-nSe9.d.ts +117 -0
  39. package/dist/index-Dy6kuF0P.d.ts +71 -0
  40. package/dist/index-Hf8PP9sP.d.ts +116 -0
  41. package/dist/index-JXHBuS53.d.ts +144 -0
  42. package/dist/index-Nxo3TJcm.d.ts +208 -0
  43. package/dist/index-Sw43J8HG.d.ts +62 -0
  44. package/dist/index-cEeweTeM.d.ts +38 -0
  45. package/dist/index.d.ts +110 -0
  46. package/dist/index.js +42 -0
  47. package/dist/invalidate-DbxuL2jL.js +18 -0
  48. package/dist/jobs/index.d.ts +173 -0
  49. package/dist/jobs/index.js +477 -0
  50. package/dist/json-schema-BbwO2mZg.js +181 -0
  51. package/dist/lint/index.d.ts +75 -0
  52. package/dist/lint/index.js +167 -0
  53. package/dist/list/index.d.ts +2 -0
  54. package/dist/list/index.js +410 -0
  55. package/dist/live-6b9i72Ux.d.ts +180 -0
  56. package/dist/live-D8rV1Ldw.js +168 -0
  57. package/dist/mcp/index.d.ts +105 -0
  58. package/dist/mcp/index.js +330 -0
  59. package/dist/mfa-CpogP66z.js +521 -0
  60. package/dist/mfa-TVia554c.d.ts +44 -0
  61. package/dist/next/image/index.d.ts +34 -0
  62. package/dist/next/image/index.js +40 -0
  63. package/dist/next/index.d.ts +210 -0
  64. package/dist/next/index.js +426 -0
  65. package/dist/openapi/index.d.ts +2 -0
  66. package/dist/openapi/index.js +332 -0
  67. package/dist/orpc/index.d.ts +46 -0
  68. package/dist/orpc/index.js +50 -0
  69. package/dist/otel/index.d.ts +40 -0
  70. package/dist/otel/index.js +168 -0
  71. package/dist/path-Cv__-OTj.d.ts +17 -0
  72. package/dist/plugin-BqR6wKMB.js +23 -0
  73. package/dist/plugin-HvGqCurB.d.ts +118 -0
  74. package/dist/plugins/actor/index.d.ts +13 -0
  75. package/dist/plugins/actor/index.js +43 -0
  76. package/dist/plugins/rules/index.d.ts +88 -0
  77. package/dist/plugins/rules/index.js +174 -0
  78. package/dist/plugins/soft-delete/index.d.ts +38 -0
  79. package/dist/plugins/soft-delete/index.js +69 -0
  80. package/dist/plugins/tenant/index.d.ts +43 -0
  81. package/dist/plugins/tenant/index.js +73 -0
  82. package/dist/plugins/timestamps/index.d.ts +9 -0
  83. package/dist/plugins/timestamps/index.js +37 -0
  84. package/dist/plugins/validation/index.d.ts +24 -0
  85. package/dist/plugins/validation/index.js +59 -0
  86. package/dist/pool-BFeqzwQS.d.ts +37 -0
  87. package/dist/postgres/index.d.ts +28 -0
  88. package/dist/postgres/index.js +79 -0
  89. package/dist/postgrest-Dm-vhY6R.d.ts +43 -0
  90. package/dist/problem-DPu6Fh-t.js +97 -0
  91. package/dist/problem-DlgB0lYl.d.ts +47 -0
  92. package/dist/query/index.d.ts +2 -0
  93. package/dist/query/index.js +3 -0
  94. package/dist/query-ChvDbsDE.js +186 -0
  95. package/dist/react/index.d.ts +7 -0
  96. package/dist/react/index.js +5 -0
  97. package/dist/react/server.d.ts +13 -0
  98. package/dist/react/server.js +25 -0
  99. package/dist/react/session.d.ts +2 -0
  100. package/dist/react/session.js +3 -0
  101. package/dist/react-CrOvrnIP.js +241 -0
  102. package/dist/read-set-DhX2d04c.js +216 -0
  103. package/dist/realtime/index.d.ts +3 -0
  104. package/dist/realtime/index.js +253 -0
  105. package/dist/resolve-DW8h9NPm.d.ts +155 -0
  106. package/dist/resource-CYiu6Ygi.js +175 -0
  107. package/dist/resource-SkPTRJDD.d.ts +65 -0
  108. package/dist/respond-4-dhoV7x.js +380 -0
  109. package/dist/respond-PoTpy5DT.d.ts +169 -0
  110. package/dist/result-CTkCZ6JA.d.ts +198 -0
  111. package/dist/result-DWXatkd6.js +103 -0
  112. package/dist/scope-CDHcS7dE.js +90 -0
  113. package/dist/seed-CzecODcJ.js +101 -0
  114. package/dist/server/index.d.ts +42 -0
  115. package/dist/server/index.js +68 -0
  116. package/dist/session-C3osL21k.d.ts +21 -0
  117. package/dist/session-CD-3orO_.js +19 -0
  118. package/dist/shared-_6-uPL7E.js +57 -0
  119. package/dist/simplify-oHiDKRVQ.js +665 -0
  120. package/dist/spec-pins-CJPVghHy.d.ts +20 -0
  121. package/dist/spec-pins-t6sPaHvl.js +20 -0
  122. package/dist/sql-M8l3fnS0.js +260 -0
  123. package/dist/ssr/index.d.ts +5 -0
  124. package/dist/ssr/index.js +4 -0
  125. package/dist/standard-CyhRzctb.d.ts +7 -0
  126. package/dist/standard-FUi3Z3-7.js +21 -0
  127. package/dist/stats-3iy8u3_P.js +566 -0
  128. package/dist/storage/index.d.ts +4 -0
  129. package/dist/storage/index.js +2 -0
  130. package/dist/storage-BWXVUXGk.js +461 -0
  131. package/dist/tables-rZxRxNPW.js +59 -0
  132. package/dist/template-BeOGnynM.js +93 -0
  133. package/dist/template-mbvlysOo.d.ts +6 -0
  134. package/dist/testing/index.d.ts +294 -0
  135. package/dist/testing/index.js +751 -0
  136. package/dist/types-6cYK9pbJ.js +29 -0
  137. package/dist/types-C2fslP1z.d.ts +195 -0
  138. package/dist/version-Cqq1m3UY.js +4 -0
  139. package/dist/view-B0ltUmTi.d.ts +43 -0
  140. package/dist/view-B6F9QbG-.js +35 -0
  141. package/dist/webhooks/index.d.ts +152 -0
  142. package/dist/webhooks/index.js +2 -0
  143. package/dist/webhooks-DbMTJJX2.js +139 -0
  144. package/package.json +246 -17
  145. package/schemas/config-v1.json +328 -0
  146. package/schemas/doctor-report-v1.json +105 -0
  147. package/schemas/snapshot-v2.json +1002 -0
  148. package/skills/better-supabase/SKILL.md +84 -0
  149. package/skills/better-supabase/references/plugins.md +56 -0
  150. package/skills/better-supabase/references/troubleshooting.md +39 -0
  151. package/skills/better-supabase-api/SKILL.md +67 -0
  152. package/skills/better-supabase-api/references/adapters.md +108 -0
  153. package/skills/better-supabase-testing/SKILL.md +65 -0
  154. package/index.js +0 -1
@@ -0,0 +1,410 @@
1
+ //#region src/list/index.ts
2
+ /** Facet value meaning "not set": filters with `is null`. */
3
+ const UNSET = "__unset__";
4
+ const RESERVED = /* @__PURE__ */ new Set([
5
+ "q",
6
+ "sort",
7
+ "page",
8
+ "size"
9
+ ]);
10
+ function first(value) {
11
+ return typeof value === "string" ? value : value?.[0];
12
+ }
13
+ function all(value) {
14
+ return (typeof value === "string" ? [value] : [...value ?? []]).flatMap((entry) => entry.split(",")).map((entry) => entry.trim()).filter(Boolean);
15
+ }
16
+ function isSearchParams(value) {
17
+ return value instanceof URLSearchParams || !("facets" in value && typeof value.facets === "object");
18
+ }
19
+ function readRecord(input) {
20
+ if (!(input instanceof URLSearchParams)) return input;
21
+ const out = {};
22
+ for (const [key, value] of input) (out[key] ??= []).push(value);
23
+ return out;
24
+ }
25
+ function toInt(raw) {
26
+ if (typeof raw === "number") return raw;
27
+ if (typeof raw !== "string" || !/^\d+$/.test(raw)) return NaN;
28
+ return Number(raw);
29
+ }
30
+ /**
31
+ * A reusable list endpoint for one table: search, facets, sorts and
32
+ * pagination, validated from URL params or typed input, with URL parsers,
33
+ * OpenAPI parameters and a JSON Schema derived from the same definition.
34
+ *
35
+ * ```ts
36
+ * export const customerList = defineListQuery(sb, 'customers', {
37
+ * search: ['name', 'kvk'],
38
+ * facets: { status: 'status' },
39
+ * sorts: { name: { name: 'asc' }, newest: [{ createdAt: 'desc' }, { id: 'asc' }] },
40
+ * defaultSort: 'newest',
41
+ * });
42
+ * ```
43
+ */
44
+ function defineListQuery(sb, table, config) {
45
+ const meta = sb.meta.tables[table];
46
+ if (!meta) throw new TypeError(`defineListQuery: unknown table "${table}"`);
47
+ const pageSize = config.pageSize ?? 50;
48
+ const maxPageSize = config.maxPageSize ?? 200;
49
+ const maxSearch = config.maxSearchLength ?? 200;
50
+ const sortKeys = Object.keys(config.sorts);
51
+ const facetEntries = Object.entries(config.facets ?? {});
52
+ for (const [key, column] of facetEntries) {
53
+ if (RESERVED.has(key)) throw new TypeError(`defineListQuery: facet key "${key}" is reserved`);
54
+ if (!meta.columns[column]) throw new TypeError(`defineListQuery: unknown column "${table}.${column}"`);
55
+ }
56
+ if (!sortKeys.includes(config.defaultSort)) throw new TypeError(`defineListQuery: defaultSort "${config.defaultSort}" is not in sorts`);
57
+ const facets = facetEntries.map(([key, column]) => {
58
+ const columnMeta = meta.columns[column];
59
+ return {
60
+ key,
61
+ column,
62
+ nullable: columnMeta.nullable,
63
+ ...columnMeta.enum ? { values: columnMeta.enum } : {}
64
+ };
65
+ });
66
+ const defaults = {
67
+ sort: config.defaultSort,
68
+ page: 1,
69
+ size: pageSize,
70
+ facets: {}
71
+ };
72
+ function normalize(raw) {
73
+ const issues = [];
74
+ let q;
75
+ if (raw.q !== void 0 && raw.q !== null) {
76
+ if (typeof raw.q !== "string") issues.push({
77
+ message: "Must be text",
78
+ path: ["q"]
79
+ });
80
+ else if (raw.q.trim().length > maxSearch) issues.push({
81
+ message: `At most ${String(maxSearch)} characters`,
82
+ path: ["q"]
83
+ });
84
+ else if (raw.q.trim()) q = raw.q.trim();
85
+ }
86
+ let sort = config.defaultSort;
87
+ if (raw.sort !== void 0 && raw.sort !== "") {
88
+ if (typeof raw.sort === "string" && sortKeys.includes(raw.sort)) sort = raw.sort;
89
+ else issues.push({
90
+ message: `Must be one of: ${sortKeys.join(", ")}`,
91
+ path: ["sort"]
92
+ });
93
+ }
94
+ const page = raw.page === void 0 || raw.page === "" ? 1 : toInt(raw.page);
95
+ if (page === void 0 || !Number.isInteger(page) || page < 1) issues.push({
96
+ message: "Must be a whole number of at least 1",
97
+ path: ["page"]
98
+ });
99
+ const size = raw.size === void 0 || raw.size === "" ? pageSize : toInt(raw.size);
100
+ if (size === void 0 || !Number.isInteger(size) || size < 1 || size > maxPageSize) issues.push({
101
+ message: `Must be a whole number from 1 to ${String(maxPageSize)}`,
102
+ path: ["size"]
103
+ });
104
+ const facetValues = {};
105
+ for (const facet of facets) {
106
+ const values = raw.facets[facet.key];
107
+ if (!values || values.length === 0) continue;
108
+ for (const value of values) if (!(value === "__unset__" ? facet.nullable : !facet.values || facet.values.includes(value))) issues.push({
109
+ message: value === "__unset__" ? "This filter cannot be empty" : `Must be one of: ${(facet.values ?? []).join(", ")}`,
110
+ path: ["facets", facet.key]
111
+ });
112
+ facetValues[facet.key] = [...new Set(values)];
113
+ }
114
+ if (issues.length > 0) return {
115
+ ok: false,
116
+ issues
117
+ };
118
+ return {
119
+ ok: true,
120
+ value: {
121
+ ...q ? { q } : {},
122
+ sort,
123
+ page: page ?? 1,
124
+ size: size ?? pageSize,
125
+ facets: facetValues
126
+ }
127
+ };
128
+ }
129
+ function parse(input) {
130
+ if (input === void 0 || input === null) return {
131
+ ok: true,
132
+ value: defaults
133
+ };
134
+ if (typeof input !== "object") return {
135
+ ok: false,
136
+ issues: [{ message: "Expected an object or URL parameters" }]
137
+ };
138
+ if (isSearchParams(input)) {
139
+ const record = readRecord(input);
140
+ const facetValues = {};
141
+ for (const facet of facets) facetValues[facet.key] = all(record[facet.key]);
142
+ return normalize({
143
+ q: first(record["q"]),
144
+ sort: first(record["sort"]),
145
+ page: first(record["page"]),
146
+ size: first(record["size"]),
147
+ facets: facetValues
148
+ });
149
+ }
150
+ const typed = input;
151
+ const rawFacets = typed.facets ?? {};
152
+ for (const [key, value] of Object.entries(rawFacets)) if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) return {
153
+ ok: false,
154
+ issues: [{
155
+ message: "Must be a list of values",
156
+ path: ["facets", key]
157
+ }]
158
+ };
159
+ return normalize({
160
+ q: typed.q,
161
+ sort: typed.sort,
162
+ page: typed.page,
163
+ size: typed.size,
164
+ facets: rawFacets
165
+ });
166
+ }
167
+ function searchWhere(query) {
168
+ if (!query.q || !config.search) return void 0;
169
+ if (Array.isArray(config.search)) {
170
+ const columns = config.search;
171
+ if (columns.length === 0) return void 0;
172
+ return { OR: columns.map((column) => ({ [column]: { contains: query.q } })) };
173
+ }
174
+ const fts = config.search;
175
+ return { [fts.fts]: { search: fts.config ? {
176
+ query: query.q,
177
+ config: fts.config
178
+ } : query.q } };
179
+ }
180
+ function facetWhere(facet, values) {
181
+ if (!values || values.length === 0) return void 0;
182
+ const unset = values.includes(UNSET);
183
+ const set = values.filter((value) => value !== UNSET);
184
+ if (unset && set.length === 0) return { [facet.column]: null };
185
+ if (!unset) return { [facet.column]: { in: set } };
186
+ return { OR: [{ [facet.column]: { in: set } }, { [facet.column]: null }] };
187
+ }
188
+ function and(parts) {
189
+ const present = parts.filter((part) => part !== void 0);
190
+ if (present.length === 0) return void 0;
191
+ return present.length === 1 ? present[0] : { AND: present };
192
+ }
193
+ function where(query) {
194
+ return and([searchWhere(query), ...facets.map((facet) => facetWhere(facet, query.facets[facet.key]))]);
195
+ }
196
+ function args(query) {
197
+ const filter = where(query);
198
+ return {
199
+ ...filter ? { where: filter } : {},
200
+ orderBy: config.sorts[query.sort],
201
+ page: query.page,
202
+ size: query.size,
203
+ count: config.count ?? "exact"
204
+ };
205
+ }
206
+ /** Whether a group's value passes the facet's selected values. */
207
+ function selects(value, selected) {
208
+ if (!selected || selected.length === 0) return true;
209
+ return value === null || value === void 0 ? selected.includes(UNSET) : selected.includes(String(value));
210
+ }
211
+ /**
212
+ * Per facet, rows per value under every other facet's selection: the
213
+ * usual faceted-search counts, where picking a value doesn't zero the
214
+ * other values of the same facet.
215
+ */
216
+ function marginals(groups, query) {
217
+ const out = {};
218
+ for (const facet of facets) {
219
+ const counts = {};
220
+ for (const value of facet.values ?? []) counts[value] = 0;
221
+ for (const group of groups) {
222
+ if (!facets.every((other) => other === facet || selects(group[other.column], query.facets[other.key]))) continue;
223
+ const raw = group[facet.column];
224
+ const key = raw === null || raw === void 0 ? UNSET : String(raw);
225
+ counts[key] = (counts[key] ?? 0) + Number(group["_count"] ?? 0);
226
+ }
227
+ out[facet.key] = counts;
228
+ }
229
+ return out;
230
+ }
231
+ const listParser = {
232
+ parse: (value) => {
233
+ const values = all(value);
234
+ return values.length > 0 ? values : null;
235
+ },
236
+ serialize: (values) => values.join(",")
237
+ };
238
+ const intParser = {
239
+ parse: (value) => /^\d+$/.test(value) ? Number(value) : null,
240
+ serialize: String
241
+ };
242
+ const parsers = {
243
+ q: {
244
+ parse: (value) => value.trim() || null,
245
+ serialize: (value) => value
246
+ },
247
+ sort: {
248
+ parse: (value) => sortKeys.includes(value) ? value : null,
249
+ serialize: (value) => value
250
+ },
251
+ page: intParser,
252
+ size: intParser,
253
+ ...Object.fromEntries(facets.map((facet) => [facet.key, listParser]))
254
+ };
255
+ const facetSchema = (facet) => ({
256
+ type: "array",
257
+ items: {
258
+ type: "string",
259
+ ...facet.values ? { enum: [...facet.values, ...facet.nullable ? [UNSET] : []] } : {}
260
+ },
261
+ uniqueItems: true
262
+ });
263
+ const searchDescription = config.search ? Array.isArray(config.search) ? `Case-insensitive search in ${config.search.join(", ")}.` : "Full-text search (web search syntax)." : void 0;
264
+ return {
265
+ table,
266
+ defaults,
267
+ facets,
268
+ parse,
269
+ schema: { "~standard": {
270
+ version: 1,
271
+ vendor: "better-supabase",
272
+ validate(value) {
273
+ const result = parse(value);
274
+ return result.ok ? { value: result.value } : { issues: result.issues };
275
+ }
276
+ } },
277
+ toSearchParams(query) {
278
+ const params = new URLSearchParams();
279
+ if (query.q) params.set("q", query.q);
280
+ if (query.sort && query.sort !== config.defaultSort) params.set("sort", query.sort);
281
+ if (query.page && query.page !== 1) params.set("page", String(query.page));
282
+ if (query.size && query.size !== pageSize) params.set("size", String(query.size));
283
+ for (const facet of facets) {
284
+ const values = query.facets?.[facet.key];
285
+ if (values && values.length > 0) params.set(facet.key, values.join(","));
286
+ }
287
+ return params;
288
+ },
289
+ parsers,
290
+ nuqs: (createParser) => Object.fromEntries(Object.entries(parsers).map(([key, spec]) => [key, createParser(spec)])),
291
+ args,
292
+ run(db, query, extra) {
293
+ const base = args(query);
294
+ const { where: extraWhere, count, select: _select, include: _include, ...options } = extra ?? {};
295
+ const combined = and([extraWhere, base["where"]]);
296
+ const repository = db[table];
297
+ const page = repository.paginate({
298
+ ...extra,
299
+ ...base,
300
+ count: count ?? base["count"],
301
+ ...combined ? { where: combined } : {}
302
+ });
303
+ if (!config.facetCounts) return page;
304
+ if (facets.length === 0) return page.map((data) => ({
305
+ ...data,
306
+ facetCounts: {}
307
+ }));
308
+ const facetFilter = and([extraWhere, searchWhere(query)]);
309
+ const groups = repository.aggregate({
310
+ ...options,
311
+ ...facetFilter ? { where: facetFilter } : {},
312
+ groupBy: [...new Set(facets.map((facet) => facet.column))],
313
+ _count: true
314
+ });
315
+ return page.andThen((data) => groups.map((rows) => ({
316
+ ...data,
317
+ facetCounts: marginals(rows, query)
318
+ })));
319
+ },
320
+ openapi: [
321
+ ...searchDescription ? [{
322
+ name: "q",
323
+ in: "query",
324
+ required: false,
325
+ description: searchDescription,
326
+ schema: {
327
+ type: "string",
328
+ maxLength: maxSearch
329
+ }
330
+ }] : [],
331
+ {
332
+ name: "sort",
333
+ in: "query",
334
+ required: false,
335
+ description: `Ordering. Default \`${config.defaultSort}\`.`,
336
+ schema: {
337
+ type: "string",
338
+ enum: sortKeys,
339
+ default: config.defaultSort
340
+ }
341
+ },
342
+ {
343
+ name: "page",
344
+ in: "query",
345
+ required: false,
346
+ description: "1-based page number.",
347
+ schema: {
348
+ type: "integer",
349
+ minimum: 1,
350
+ default: 1
351
+ }
352
+ },
353
+ {
354
+ name: "size",
355
+ in: "query",
356
+ required: false,
357
+ description: "Page size.",
358
+ schema: {
359
+ type: "integer",
360
+ minimum: 1,
361
+ maximum: maxPageSize,
362
+ default: pageSize
363
+ }
364
+ },
365
+ ...facets.map((facet) => ({
366
+ name: facet.key,
367
+ in: "query",
368
+ required: false,
369
+ description: `Filter on ${facet.column}. Comma-separated; any value matches.${facet.nullable ? ` \`${UNSET}\` matches empty.` : ""}`,
370
+ schema: facetSchema(facet),
371
+ style: "form",
372
+ explode: false
373
+ }))
374
+ ],
375
+ jsonSchema: {
376
+ type: "object",
377
+ additionalProperties: false,
378
+ properties: {
379
+ ...searchDescription ? { q: {
380
+ type: "string",
381
+ maxLength: maxSearch,
382
+ description: searchDescription
383
+ } } : {},
384
+ sort: {
385
+ type: "string",
386
+ enum: sortKeys,
387
+ default: config.defaultSort
388
+ },
389
+ page: {
390
+ type: "integer",
391
+ minimum: 1,
392
+ default: 1
393
+ },
394
+ size: {
395
+ type: "integer",
396
+ minimum: 1,
397
+ maximum: maxPageSize,
398
+ default: pageSize
399
+ },
400
+ ...facets.length > 0 ? { facets: {
401
+ type: "object",
402
+ additionalProperties: false,
403
+ properties: Object.fromEntries(facets.map((facet) => [facet.key, facetSchema(facet)]))
404
+ } } : {}
405
+ }
406
+ }
407
+ };
408
+ }
409
+ //#endregion
410
+ export { UNSET, defineListQuery };
@@ -0,0 +1,180 @@
1
+ import { E as SchemaMeta, k as TableKey, n as AnyModels, t as AnyFunctions, w as Row } from "./types-C2fslP1z.js";
2
+ import { b as ValidationIssue, o as Result, p as DbError, t as AsyncResult } from "./result-CTkCZ6JA.js";
3
+ import { Z as QuerySpec, t as BetterSupabase } from "./define-CTfWw-SS.js";
4
+ import { n as TemplateValues, t as TemplateParams } from "./template-mbvlysOo.js";
5
+ import { RealtimeChannel, SupabaseClient } from "@supabase/supabase-js";
6
+ import { StandardSchemaV1 } from "@standard-schema/spec";
7
+ //#region src/realtime/index.d.ts
8
+ type EventSchemas = Readonly<Record<string, StandardSchemaV1>>;
9
+ interface TopicOptions<E extends EventSchemas> {
10
+ /** Name used for generated policies and functions. Defaults to the template's literal parts. */
11
+ readonly name?: string;
12
+ /** Payload schemas per event. Incoming and outgoing payloads are validated. */
13
+ readonly events?: E;
14
+ /** Private channels are authorized by `realtime.messages` policies. Defaults to `true`. */
15
+ readonly private?: boolean;
16
+ /**
17
+ * Tenant check for the generated policy. Defaults to `{orgId}` against
18
+ * `org_id` / `app_metadata.org_id` when the template has `{orgId}`.
19
+ */
20
+ readonly tenant?: false | {
21
+ readonly param?: string;
22
+ readonly claim?: string | readonly string[];
23
+ readonly sql?: string;
24
+ };
25
+ /** Owner check. Defaults to `{userId}` against `auth.uid()` when the template has `{userId}`. */
26
+ readonly owner?: false | {
27
+ readonly param?: string;
28
+ };
29
+ /** Let clients broadcast on the topic, not only receive. Defaults to `false`. */
30
+ readonly send?: boolean;
31
+ /** Also authorize presence. Defaults to `false`. */
32
+ readonly presence?: boolean;
33
+ }
34
+ interface TopicMessage {
35
+ readonly event: string;
36
+ readonly payload: unknown;
37
+ readonly topic: string;
38
+ }
39
+ type Output<S> = S extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<S> : unknown;
40
+ type Input<S> = S extends StandardSchemaV1 ? StandardSchemaV1.InferInput<S> : unknown;
41
+ type TopicHandlers<E extends EventSchemas> = [keyof E] extends [never] ? {
42
+ readonly [event: string]: ((payload: unknown, message: TopicMessage) => void) | undefined;
43
+ } : { readonly [K in keyof E & string]?: (payload: Output<E[K]>, message: TopicMessage) => void; } & {
44
+ readonly "*"?: (payload: unknown, message: TopicMessage) => void;
45
+ };
46
+ type TopicEvent<E extends EventSchemas> = [keyof E] extends [never] ? string : keyof E & string;
47
+ type TopicPayload<E extends EventSchemas, K extends string> = [keyof E] extends [never] ? unknown : Input<E[K]>;
48
+ type SubscriptionStatus = "joining" | "subscribed" | "closed" | "error";
49
+ interface SubscribeOptions {
50
+ /** Receive your own broadcasts. */
51
+ readonly self?: boolean;
52
+ readonly onStatus?: (status: SubscriptionStatus, error?: Error) => void;
53
+ /** Called when a payload fails its event schema. The handler is skipped. */
54
+ readonly onInvalid?: (message: TopicMessage, issues: readonly ValidationIssue[]) => void;
55
+ }
56
+ interface Subscription extends Disposable, AsyncDisposable {
57
+ readonly topic: string;
58
+ readonly channel: RealtimeChannel;
59
+ /** Resolves once joined; rejects if the join is refused or times out. */
60
+ readonly ready: Promise<void>;
61
+ unsubscribe(): Promise<void>;
62
+ }
63
+ type RealtimeClient = Pick<SupabaseClient, "channel" | "removeChannel" | "realtime">;
64
+ interface TriggerOptions<M extends AnyModels, T extends keyof M, P extends string> {
65
+ /** Column that fills each placeholder. */
66
+ readonly values: { readonly [K in TemplateParams<P>]: Extract<keyof Row<M, T>, string>; };
67
+ /** Defaults to all three. */
68
+ readonly events?: readonly ("insert" | "update" | "delete")[];
69
+ /** Schema for the generated function. Defaults to `public`. */
70
+ readonly functionSchema?: string;
71
+ }
72
+ interface RowChange<R> {
73
+ readonly operation: "INSERT" | "UPDATE" | "DELETE";
74
+ readonly table: string;
75
+ readonly record: R | null;
76
+ readonly oldRecord: Partial<R> | null;
77
+ }
78
+ interface Topic<P extends string, E extends EventSchemas> {
79
+ readonly template: P;
80
+ readonly name: string;
81
+ readonly params: readonly TemplateParams<P>[];
82
+ readonly private: boolean;
83
+ topic(values: TemplateValues<P>): string;
84
+ match(topic: string): TemplateValues<P> | null;
85
+ /** `realtime.messages` policies for this topic. Idempotent. */
86
+ sql(): string;
87
+ /** A trigger that broadcasts row changes (`realtime.broadcast_changes`) to this topic. */
88
+ triggerSql<M extends AnyModels, D, F extends AnyFunctions, X, T extends TableKey<M>>(sb: BetterSupabase<M, D, F, X>, table: T, options: TriggerOptions<M, T, P>): string;
89
+ subscribe(client: RealtimeClient, values: TemplateValues<P>, handlers: TopicHandlers<E>, options?: SubscribeOptions): Subscription;
90
+ /** Sends over HTTP, without joining the channel. */
91
+ send<K extends TopicEvent<E>>(client: RealtimeClient, values: TemplateValues<P>, event: K, payload: TopicPayload<E, K>): AsyncResult<void>;
92
+ }
93
+ /**
94
+ * A typed Realtime broadcast topic: names from a template, RLS policies for
95
+ * private channels, row-change triggers, and disposable subscriptions.
96
+ *
97
+ * ```ts
98
+ * export const notifications = defineTopic('org:{orgId}:notifications:{userId}', {
99
+ * events: { created: v.object({ id: v.string(), title: v.string() }) },
100
+ * });
101
+ * using sub = notifications.subscribe(supabase, { orgId, userId }, { created: (n) => toast(n.title) });
102
+ * ```
103
+ */
104
+ declare function defineTopic<const P extends string, const E extends EventSchemas = Record<never, never>>(template: P, options?: TopicOptions<E>): Topic<P, E>;
105
+ /**
106
+ * Reads a `realtime.broadcast_changes` message as an app-cased row change.
107
+ * Returns `null` for messages about other tables.
108
+ */
109
+ declare function rowChange<M extends AnyModels, D, F extends AnyFunctions, X, T extends TableKey<M>>(sb: BetterSupabase<M, D, F, X>, table: T, message: TopicMessage): RowChange<Row<M, T>> | null;
110
+ //#endregion
111
+ //#region src/realtime/live.d.ts
112
+ /** What `liveQuery` needs from `sb`: metadata and the tables a spec reads. */
113
+ interface LiveSource {
114
+ readonly meta: SchemaMeta;
115
+ tablesOf(spec: QuerySpec): string[];
116
+ }
117
+ interface LiveQueryOptions {
118
+ /** Called once per burst of changes, with the app keys of the tables that changed. */
119
+ readonly onChange: (tables: readonly string[]) => void;
120
+ /** Tenant value for tables scoped by a tenant column (`realtime-tables` topics). */
121
+ readonly tenant?: string;
122
+ /** Waits this long after the last change before calling `onChange`. Defaults to 100 ms. */
123
+ readonly debounceMs?: number;
124
+ readonly onStatus?: (status: SubscriptionStatus, error?: Error) => void;
125
+ }
126
+ interface LiveSubscription extends Disposable, AsyncDisposable {
127
+ /** App keys of the watched tables. */
128
+ readonly tables: readonly string[];
129
+ /** Tables the query reads that don't broadcast changes (not in `realtime.tables`). */
130
+ readonly unwatched: readonly string[];
131
+ readonly ready: Promise<void>;
132
+ unsubscribe(): Promise<void>;
133
+ }
134
+ /**
135
+ * A count from the server plus the spec to keep it live on the client:
136
+ * `next.liveCount(spec)` makes one, `useLiveCount(seed)` reads it. `count`
137
+ * is `null` when the server read failed; the client then fetches it.
138
+ */
139
+ interface LiveCountSeed<T extends string = string> {
140
+ readonly spec: QuerySpec<T, "count", number>;
141
+ readonly count: number | null;
142
+ }
143
+ /** Topic a `realtime-tables` trigger broadcasts on: `bs:t:<schema>.<table>[:<tenant>]`. */
144
+ declare function liveTopic(meta: SchemaMeta, table: string, tenant?: string): string;
145
+ /**
146
+ * Calls `onChange` when a table the spec reads changes, so you can refetch.
147
+ * Needs the `realtime-tables` SQL module and the tables in
148
+ * `realtime.tables`. Live queries on the same client share one channel per
149
+ * table, so any number of them can run at once.
150
+ *
151
+ * ```ts
152
+ * using live = liveQuery(sb, supabase, spec, { onChange: () => refetch() });
153
+ * ```
154
+ */
155
+ declare function liveQuery(sb: LiveSource, client: RealtimeClient, spec: QuerySpec | readonly string[], options: LiveQueryOptions): LiveSubscription;
156
+ /** What `liveCount` runs the spec with: a `db` from `sb.connect()` or `createBrowser()`. */
157
+ interface CountRunner {
158
+ $run(spec: QuerySpec): PromiseLike<Result<unknown>>;
159
+ }
160
+ interface LiveCountOptions extends Omit<LiveQueryOptions, "onChange"> {
161
+ readonly onCount: (count: number) => void;
162
+ /** A failed refetch; the previous count stays valid. */
163
+ readonly onError?: (error: DbError) => void;
164
+ /** Count right away. Defaults to `true`; pass `false` when you have a seed. */
165
+ readonly immediate?: boolean;
166
+ }
167
+ /**
168
+ * Keeps a count current: runs the `count` spec (a HEAD request) now, after
169
+ * each debounced change to a table it reads, and after the channel rejoins.
170
+ * Responses that arrive out of order are dropped.
171
+ *
172
+ * ```ts
173
+ * using live = liveCount(sb, supabase, db, sb.spec.notes.count(), {
174
+ * onCount: (count) => render(count),
175
+ * });
176
+ * ```
177
+ */
178
+ declare function liveCount(sb: LiveSource, client: RealtimeClient, db: CountRunner, spec: QuerySpec<string, "count", number>, options: LiveCountOptions): LiveSubscription;
179
+ //#endregion
180
+ export { defineTopic as C, TriggerOptions as S, TopicEvent as _, LiveSource as a, TopicOptions as b, liveQuery as c, RealtimeClient as d, RowChange as f, Topic as g, SubscriptionStatus as h, LiveQueryOptions as i, liveTopic as l, Subscription as m, LiveCountOptions as n, LiveSubscription as o, SubscribeOptions as p, LiveCountSeed as r, liveCount as s, CountRunner as t, EventSchemas as u, TopicHandlers as v, rowChange as w, TopicPayload as x, TopicMessage as y };