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.
- package/LICENSE +21 -0
- package/README.md +88 -1
- package/bin/better-supabase.js +3 -0
- package/dist/casing-da0uRTqt.js +14 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +15 -0
- package/dist/cli/index.d.ts +226 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli-CoByWYja.js +6472 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +112 -0
- package/dist/compiler-DL18ukPe.d.ts +14 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.js +3 -0
- package/dist/config-D-aR2ZoN.js +276 -0
- package/dist/define-CTfWw-SS.d.ts +662 -0
- package/dist/define-QGkq04oe.js +1068 -0
- package/dist/edge/index.d.ts +43 -0
- package/dist/edge/index.js +103 -0
- package/dist/entitlements-BBg61DQZ.d.ts +20 -0
- package/dist/entitlements-BC_khwRy.js +17 -0
- package/dist/env/index.d.ts +2 -0
- package/dist/env/index.js +2 -0
- package/dist/env-BEToab0I.js +204 -0
- package/dist/errors-Bl3rkOzo.js +196 -0
- package/dist/events/index.d.ts +2 -0
- package/dist/events/index.js +2 -0
- package/dist/events-2OJsVG_d.d.ts +84 -0
- package/dist/events-BuzTgn2D.js +135 -0
- package/dist/executor-Bazhgsgq.d.ts +195 -0
- package/dist/executor-Bje4suQF.d.ts +25 -0
- package/dist/executor-Ceq60hPM.js +89 -0
- package/dist/hono/index.d.ts +45 -0
- package/dist/hono/index.js +70 -0
- package/dist/index-BD6wKthp.d.ts +597 -0
- package/dist/index-DQVvHzjW.d.ts +74 -0
- package/dist/index-DRid0qIv.d.ts +76 -0
- package/dist/index-Dup-nSe9.d.ts +117 -0
- package/dist/index-Dy6kuF0P.d.ts +71 -0
- package/dist/index-Hf8PP9sP.d.ts +116 -0
- package/dist/index-JXHBuS53.d.ts +144 -0
- package/dist/index-Nxo3TJcm.d.ts +208 -0
- package/dist/index-Sw43J8HG.d.ts +62 -0
- package/dist/index-cEeweTeM.d.ts +38 -0
- package/dist/index.d.ts +110 -0
- package/dist/index.js +42 -0
- package/dist/invalidate-DbxuL2jL.js +18 -0
- package/dist/jobs/index.d.ts +173 -0
- package/dist/jobs/index.js +477 -0
- package/dist/json-schema-BbwO2mZg.js +181 -0
- package/dist/lint/index.d.ts +75 -0
- package/dist/lint/index.js +167 -0
- package/dist/list/index.d.ts +2 -0
- package/dist/list/index.js +410 -0
- package/dist/live-6b9i72Ux.d.ts +180 -0
- package/dist/live-D8rV1Ldw.js +168 -0
- package/dist/mcp/index.d.ts +105 -0
- package/dist/mcp/index.js +330 -0
- package/dist/mfa-CpogP66z.js +521 -0
- package/dist/mfa-TVia554c.d.ts +44 -0
- package/dist/next/image/index.d.ts +34 -0
- package/dist/next/image/index.js +40 -0
- package/dist/next/index.d.ts +210 -0
- package/dist/next/index.js +426 -0
- package/dist/openapi/index.d.ts +2 -0
- package/dist/openapi/index.js +332 -0
- package/dist/orpc/index.d.ts +46 -0
- package/dist/orpc/index.js +50 -0
- package/dist/otel/index.d.ts +40 -0
- package/dist/otel/index.js +168 -0
- package/dist/path-Cv__-OTj.d.ts +17 -0
- package/dist/plugin-BqR6wKMB.js +23 -0
- package/dist/plugin-HvGqCurB.d.ts +118 -0
- package/dist/plugins/actor/index.d.ts +13 -0
- package/dist/plugins/actor/index.js +43 -0
- package/dist/plugins/rules/index.d.ts +88 -0
- package/dist/plugins/rules/index.js +174 -0
- package/dist/plugins/soft-delete/index.d.ts +38 -0
- package/dist/plugins/soft-delete/index.js +69 -0
- package/dist/plugins/tenant/index.d.ts +43 -0
- package/dist/plugins/tenant/index.js +73 -0
- package/dist/plugins/timestamps/index.d.ts +9 -0
- package/dist/plugins/timestamps/index.js +37 -0
- package/dist/plugins/validation/index.d.ts +24 -0
- package/dist/plugins/validation/index.js +59 -0
- package/dist/pool-BFeqzwQS.d.ts +37 -0
- package/dist/postgres/index.d.ts +28 -0
- package/dist/postgres/index.js +79 -0
- package/dist/postgrest-Dm-vhY6R.d.ts +43 -0
- package/dist/problem-DPu6Fh-t.js +97 -0
- package/dist/problem-DlgB0lYl.d.ts +47 -0
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.js +3 -0
- package/dist/query-ChvDbsDE.js +186 -0
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.js +5 -0
- package/dist/react/server.d.ts +13 -0
- package/dist/react/server.js +25 -0
- package/dist/react/session.d.ts +2 -0
- package/dist/react/session.js +3 -0
- package/dist/react-CrOvrnIP.js +241 -0
- package/dist/read-set-DhX2d04c.js +216 -0
- package/dist/realtime/index.d.ts +3 -0
- package/dist/realtime/index.js +253 -0
- package/dist/resolve-DW8h9NPm.d.ts +155 -0
- package/dist/resource-CYiu6Ygi.js +175 -0
- package/dist/resource-SkPTRJDD.d.ts +65 -0
- package/dist/respond-4-dhoV7x.js +380 -0
- package/dist/respond-PoTpy5DT.d.ts +169 -0
- package/dist/result-CTkCZ6JA.d.ts +198 -0
- package/dist/result-DWXatkd6.js +103 -0
- package/dist/scope-CDHcS7dE.js +90 -0
- package/dist/seed-CzecODcJ.js +101 -0
- package/dist/server/index.d.ts +42 -0
- package/dist/server/index.js +68 -0
- package/dist/session-C3osL21k.d.ts +21 -0
- package/dist/session-CD-3orO_.js +19 -0
- package/dist/shared-_6-uPL7E.js +57 -0
- package/dist/simplify-oHiDKRVQ.js +665 -0
- package/dist/spec-pins-CJPVghHy.d.ts +20 -0
- package/dist/spec-pins-t6sPaHvl.js +20 -0
- package/dist/sql-M8l3fnS0.js +260 -0
- package/dist/ssr/index.d.ts +5 -0
- package/dist/ssr/index.js +4 -0
- package/dist/standard-CyhRzctb.d.ts +7 -0
- package/dist/standard-FUi3Z3-7.js +21 -0
- package/dist/stats-3iy8u3_P.js +566 -0
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage-BWXVUXGk.js +461 -0
- package/dist/tables-rZxRxNPW.js +59 -0
- package/dist/template-BeOGnynM.js +93 -0
- package/dist/template-mbvlysOo.d.ts +6 -0
- package/dist/testing/index.d.ts +294 -0
- package/dist/testing/index.js +751 -0
- package/dist/types-6cYK9pbJ.js +29 -0
- package/dist/types-C2fslP1z.d.ts +195 -0
- package/dist/version-Cqq1m3UY.js +4 -0
- package/dist/view-B0ltUmTi.d.ts +43 -0
- package/dist/view-B6F9QbG-.js +35 -0
- package/dist/webhooks/index.d.ts +152 -0
- package/dist/webhooks/index.js +2 -0
- package/dist/webhooks-DbMTJJX2.js +139 -0
- package/package.json +246 -17
- package/schemas/config-v1.json +328 -0
- package/schemas/doctor-report-v1.json +105 -0
- package/schemas/snapshot-v2.json +1002 -0
- package/skills/better-supabase/SKILL.md +84 -0
- package/skills/better-supabase/references/plugins.md +56 -0
- package/skills/better-supabase/references/troubleshooting.md +39 -0
- package/skills/better-supabase-api/SKILL.md +67 -0
- package/skills/better-supabase-api/references/adapters.md +108 -0
- package/skills/better-supabase-testing/SKILL.md +65 -0
- 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 };
|