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,253 @@
|
|
|
1
|
+
import { n as dbError } from "../errors-Bl3rkOzo.js";
|
|
2
|
+
import { i as ok, n as err, t as AsyncResult } from "../result-DWXatkd6.js";
|
|
3
|
+
import { n as liveQuery, r as liveTopic, t as liveCount } from "../live-D8rV1Ldw.js";
|
|
4
|
+
import { s as toApp } from "../shared-_6-uPL7E.js";
|
|
5
|
+
import { i as sqlString, n as slug, r as sqlIdent, t as parseTemplate } from "../template-BeOGnynM.js";
|
|
6
|
+
import { REALTIME_SUBSCRIBE_STATES } from "@supabase/supabase-js";
|
|
7
|
+
//#region src/realtime/index.ts
|
|
8
|
+
const VALUE = /^[\w.@+=-]+$/;
|
|
9
|
+
function validateValue(_name, value) {
|
|
10
|
+
return VALUE.test(value) ? void 0 : "contains characters topics do not allow";
|
|
11
|
+
}
|
|
12
|
+
function claimSql(claim) {
|
|
13
|
+
const expressions = (typeof claim === "string" ? [claim] : claim).map((path) => {
|
|
14
|
+
const keys = path.split(".");
|
|
15
|
+
const last = keys.pop();
|
|
16
|
+
return `(select auth.jwt())${keys.map((key) => ` -> ${sqlString(key)}`).join("")} ->> ${sqlString(last)}`;
|
|
17
|
+
});
|
|
18
|
+
return expressions.length === 1 ? expressions[0] : `coalesce(${expressions.join(", ")})`;
|
|
19
|
+
}
|
|
20
|
+
async function validate(schema, value) {
|
|
21
|
+
if (!schema) return {
|
|
22
|
+
ok: true,
|
|
23
|
+
value
|
|
24
|
+
};
|
|
25
|
+
const result = await schema["~standard"].validate(value);
|
|
26
|
+
if (result.issues) return {
|
|
27
|
+
ok: false,
|
|
28
|
+
issues: result.issues.map((issue) => ({
|
|
29
|
+
message: issue.message,
|
|
30
|
+
...issue.path ? { path: issue.path.map((part) => typeof part === "object" ? part.key : part) } : {}
|
|
31
|
+
}))
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
ok: true,
|
|
35
|
+
value: result.value
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function sendError(status, message) {
|
|
39
|
+
if (status === 401) return dbError("unauthorized", message);
|
|
40
|
+
if (status === 403) return dbError("forbidden", message);
|
|
41
|
+
if (status === 429 || status >= 500 || status === 0) return dbError("network", message, status ? { status } : {});
|
|
42
|
+
return dbError("invalid_request", message, { status });
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A typed Realtime broadcast topic: names from a template, RLS policies for
|
|
46
|
+
* private channels, row-change triggers, and disposable subscriptions.
|
|
47
|
+
*
|
|
48
|
+
* ```ts
|
|
49
|
+
* export const notifications = defineTopic('org:{orgId}:notifications:{userId}', {
|
|
50
|
+
* events: { created: v.object({ id: v.string(), title: v.string() }) },
|
|
51
|
+
* });
|
|
52
|
+
* using sub = notifications.subscribe(supabase, { orgId, userId }, { created: (n) => toast(n.title) });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
function defineTopic(template, options = {}) {
|
|
56
|
+
const parsed = parseTemplate(template, ":", validateValue);
|
|
57
|
+
const literal = template.replace(/\{[^}]+\}/g, " ");
|
|
58
|
+
const name = slug(options.name ?? literal) || "topic";
|
|
59
|
+
const isPrivate = options.private ?? true;
|
|
60
|
+
const schemas = options.events ?? {};
|
|
61
|
+
const segment = (param, kind) => {
|
|
62
|
+
const index = parsed.segmentOf(param);
|
|
63
|
+
if (index === void 0) throw new TypeError(`defineTopic: the ${kind} check needs {${param}} as a whole segment in "${template}"`);
|
|
64
|
+
return index;
|
|
65
|
+
};
|
|
66
|
+
const checks = [];
|
|
67
|
+
const tenantParam = options.tenant === false ? void 0 : options.tenant?.param ?? "orgId";
|
|
68
|
+
if (tenantParam && (options.tenant || parsed.params.includes(tenantParam))) {
|
|
69
|
+
const tenant = options.tenant || {};
|
|
70
|
+
const expression = tenant.sql ?? claimSql(tenant.claim ?? ["org_id", "app_metadata.org_id"]);
|
|
71
|
+
checks.push(`split_part((select realtime.topic()), ':', ${String(segment(tenantParam, "tenant"))}) = (${expression})`);
|
|
72
|
+
}
|
|
73
|
+
const ownerParam = options.owner === false ? void 0 : options.owner?.param ?? "userId";
|
|
74
|
+
if (ownerParam && (options.owner || parsed.params.includes(ownerParam))) checks.push(`split_part((select realtime.topic()), ':', ${String(segment(ownerParam, "owner"))}) = (select auth.uid())::text`);
|
|
75
|
+
const topicOf = (values) => parsed.build(values);
|
|
76
|
+
return {
|
|
77
|
+
template,
|
|
78
|
+
name,
|
|
79
|
+
params: parsed.params,
|
|
80
|
+
private: isPrivate,
|
|
81
|
+
topic: topicOf,
|
|
82
|
+
match: (topic) => parsed.match(topic),
|
|
83
|
+
sql() {
|
|
84
|
+
const extensions = options.presence ? "('broadcast', 'presence')" : "('broadcast')";
|
|
85
|
+
const condition = [
|
|
86
|
+
`(select realtime.topic()) ~ ${sqlString(parsed.sqlPattern)}`,
|
|
87
|
+
`realtime.messages.extension in ${extensions}`,
|
|
88
|
+
...checks
|
|
89
|
+
].join("\n and ");
|
|
90
|
+
const lines = [`-- better-supabase: topic ${template}`];
|
|
91
|
+
const policies = [[
|
|
92
|
+
"receive",
|
|
93
|
+
"select",
|
|
94
|
+
"using"
|
|
95
|
+
]];
|
|
96
|
+
if (options.send || options.presence) policies.push([
|
|
97
|
+
"send",
|
|
98
|
+
"insert",
|
|
99
|
+
"with check"
|
|
100
|
+
]);
|
|
101
|
+
for (const [suffix, command, clause] of policies) {
|
|
102
|
+
const policy = sqlIdent(`bs_topic_${name}_${suffix}`);
|
|
103
|
+
lines.push(`drop policy if exists ${policy} on realtime.messages;`, `create policy ${policy} on realtime.messages for ${command} to authenticated\n ${clause} (\n ${condition}\n );`);
|
|
104
|
+
}
|
|
105
|
+
return `${lines.join("\n")}\n`;
|
|
106
|
+
},
|
|
107
|
+
triggerSql(sb, table, trigger) {
|
|
108
|
+
const meta = sb.meta.tables[table];
|
|
109
|
+
if (!meta) throw new TypeError(`defineTopic: unknown table "${table}"`);
|
|
110
|
+
const values = trigger.values;
|
|
111
|
+
const parts = template.split(/(\{[^}]+\})/).filter(Boolean).map((part) => {
|
|
112
|
+
const param = /^\{(.+)\}$/.exec(part)?.[1];
|
|
113
|
+
if (param === void 0) return sqlString(part);
|
|
114
|
+
const column = values[param];
|
|
115
|
+
const db = column === void 0 ? void 0 : meta.columns[column]?.db;
|
|
116
|
+
if (!db) throw new TypeError(`defineTopic: no column for {${param}} on "${table}"`);
|
|
117
|
+
return `rec.${sqlIdent(db)}::text`;
|
|
118
|
+
});
|
|
119
|
+
const schema = trigger.functionSchema ?? "public";
|
|
120
|
+
const fn = `${sqlIdent(schema)}.${sqlIdent(`bs_broadcast_${name}_${slug(meta.name)}`)}`;
|
|
121
|
+
const triggerName = sqlIdent(`bs_broadcast_${name}`);
|
|
122
|
+
const target = `${sqlIdent(meta.schema)}.${sqlIdent(meta.name)}`;
|
|
123
|
+
const events = (trigger.events ?? [
|
|
124
|
+
"insert",
|
|
125
|
+
"update",
|
|
126
|
+
"delete"
|
|
127
|
+
]).join(" or ");
|
|
128
|
+
return [
|
|
129
|
+
`-- better-supabase: broadcast ${meta.name} changes to ${template}`,
|
|
130
|
+
`create or replace function ${fn}()`,
|
|
131
|
+
"returns trigger",
|
|
132
|
+
"language plpgsql",
|
|
133
|
+
"security definer",
|
|
134
|
+
"set search_path = ''",
|
|
135
|
+
"as $$",
|
|
136
|
+
"declare",
|
|
137
|
+
" rec record;",
|
|
138
|
+
"begin",
|
|
139
|
+
" if tg_op = 'DELETE' then rec := old; else rec := new; end if;",
|
|
140
|
+
" perform realtime.broadcast_changes(",
|
|
141
|
+
` ${parts.join(" || ")},`,
|
|
142
|
+
" tg_op, tg_op, tg_table_name, tg_table_schema, new, old",
|
|
143
|
+
" );",
|
|
144
|
+
" return null;",
|
|
145
|
+
"end;",
|
|
146
|
+
"$$;",
|
|
147
|
+
`revoke execute on function ${fn}() from public, anon, authenticated;`,
|
|
148
|
+
"",
|
|
149
|
+
`drop trigger if exists ${triggerName} on ${target};`,
|
|
150
|
+
`create trigger ${triggerName} after ${events} on ${target}`,
|
|
151
|
+
` for each row execute function ${fn}();`,
|
|
152
|
+
""
|
|
153
|
+
].join("\n");
|
|
154
|
+
},
|
|
155
|
+
subscribe(client, values, handlers, subscribeOptions = {}) {
|
|
156
|
+
const topic = topicOf(values);
|
|
157
|
+
const channel = client.channel(topic, { config: {
|
|
158
|
+
private: isPrivate,
|
|
159
|
+
broadcast: { self: subscribeOptions.self ?? false }
|
|
160
|
+
} });
|
|
161
|
+
const table = handlers;
|
|
162
|
+
channel.on("broadcast", { event: "*" }, (raw) => {
|
|
163
|
+
const message = {
|
|
164
|
+
event: raw.event,
|
|
165
|
+
payload: raw["payload"],
|
|
166
|
+
topic
|
|
167
|
+
};
|
|
168
|
+
const handler = table[message.event] ?? table["*"];
|
|
169
|
+
if (!handler) return;
|
|
170
|
+
validate(schemas[message.event], message.payload).then((checked) => {
|
|
171
|
+
if (checked.ok) handler(checked.value, message);
|
|
172
|
+
else subscribeOptions.onInvalid?.(message, checked.issues);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
let closed = false;
|
|
176
|
+
subscribeOptions.onStatus?.("joining");
|
|
177
|
+
const ready = (async () => {
|
|
178
|
+
if (isPrivate) await client.realtime.setAuth();
|
|
179
|
+
if (closed) return;
|
|
180
|
+
await new Promise((resolve, reject) => {
|
|
181
|
+
channel.subscribe((status, error) => {
|
|
182
|
+
switch (status) {
|
|
183
|
+
case REALTIME_SUBSCRIBE_STATES.SUBSCRIBED:
|
|
184
|
+
subscribeOptions.onStatus?.("subscribed");
|
|
185
|
+
resolve();
|
|
186
|
+
return;
|
|
187
|
+
case REALTIME_SUBSCRIBE_STATES.CLOSED:
|
|
188
|
+
subscribeOptions.onStatus?.("closed");
|
|
189
|
+
resolve();
|
|
190
|
+
return;
|
|
191
|
+
case REALTIME_SUBSCRIBE_STATES.CHANNEL_ERROR:
|
|
192
|
+
case REALTIME_SUBSCRIBE_STATES.TIMED_OUT: {
|
|
193
|
+
const failure = error ?? /* @__PURE__ */ new Error(`Realtime ${status.toLowerCase()} on ${topic}`);
|
|
194
|
+
subscribeOptions.onStatus?.("error", failure);
|
|
195
|
+
reject(failure);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
default: reject(/* @__PURE__ */ new Error(`Unknown realtime status ${String(status)}`));
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
})();
|
|
203
|
+
ready.catch(() => void 0);
|
|
204
|
+
const unsubscribe = async () => {
|
|
205
|
+
if (closed) return;
|
|
206
|
+
closed = true;
|
|
207
|
+
await client.removeChannel(channel);
|
|
208
|
+
};
|
|
209
|
+
return {
|
|
210
|
+
topic,
|
|
211
|
+
channel,
|
|
212
|
+
ready,
|
|
213
|
+
unsubscribe,
|
|
214
|
+
[Symbol.dispose]: () => void unsubscribe(),
|
|
215
|
+
[Symbol.asyncDispose]: unsubscribe
|
|
216
|
+
};
|
|
217
|
+
},
|
|
218
|
+
send(client, values, event, payload) {
|
|
219
|
+
return AsyncResult.from(async () => {
|
|
220
|
+
const topic = topicOf(values);
|
|
221
|
+
const checked = await validate(schemas[event], payload);
|
|
222
|
+
if (!checked.ok) return err(dbError("validation", `Invalid payload for ${event}`, { issues: checked.issues }));
|
|
223
|
+
if (isPrivate) await client.realtime.setAuth();
|
|
224
|
+
const channel = client.channel(topic, { config: { private: isPrivate } });
|
|
225
|
+
try {
|
|
226
|
+
const response = await channel.httpSend(event, checked.value);
|
|
227
|
+
return response.success ? ok(void 0) : err(sendError(response.status, response.error));
|
|
228
|
+
} finally {
|
|
229
|
+
await client.removeChannel(channel);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Reads a `realtime.broadcast_changes` message as an app-cased row change.
|
|
237
|
+
* Returns `null` for messages about other tables.
|
|
238
|
+
*/
|
|
239
|
+
function rowChange(sb, table, message) {
|
|
240
|
+
const meta = sb.meta.tables[table];
|
|
241
|
+
const payload = message.payload;
|
|
242
|
+
if (!meta || !payload || payload.table !== meta.name || payload.schema && payload.schema !== meta.schema) return null;
|
|
243
|
+
const operation = payload.operation ?? message.event;
|
|
244
|
+
if (operation !== "INSERT" && operation !== "UPDATE" && operation !== "DELETE") return null;
|
|
245
|
+
return {
|
|
246
|
+
operation,
|
|
247
|
+
table,
|
|
248
|
+
record: payload.record ? toApp(meta, payload.record) : null,
|
|
249
|
+
oldRecord: payload.old_record ? toApp(meta, payload.old_record) : null
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
//#endregion
|
|
253
|
+
export { defineTopic, liveCount, liveQuery, liveTopic, rowChange };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { p as DbError } from "./result-CTkCZ6JA.js";
|
|
2
|
+
import { l as RefreshEvent } from "./events-2OJsVG_d.js";
|
|
3
|
+
import { h as RequestContext } from "./plugin-HvGqCurB.js";
|
|
4
|
+
import { t as StandardSchemaV1 } from "./standard-CyhRzctb.js";
|
|
5
|
+
import { t as BetterSupabaseEnv } from "./index-Dy6kuF0P.js";
|
|
6
|
+
import { CookieOptions } from "@supabase/ssr";
|
|
7
|
+
import { JWTClaims, SupabaseEnv, UserClaims } from "@supabase/server";
|
|
8
|
+
//#region src/auth/session.d.ts
|
|
9
|
+
/** The session object auth-js persists (`sb-<ref>-auth-token`). */
|
|
10
|
+
interface StoredSession {
|
|
11
|
+
readonly access_token: string;
|
|
12
|
+
readonly refresh_token: string;
|
|
13
|
+
readonly expires_at?: number;
|
|
14
|
+
readonly expires_in?: number;
|
|
15
|
+
readonly token_type?: string;
|
|
16
|
+
readonly user?: unknown;
|
|
17
|
+
readonly [key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
interface CookieRecord {
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly value: string;
|
|
22
|
+
}
|
|
23
|
+
interface CookieWrite {
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly value: string;
|
|
26
|
+
readonly options: CookieOptions;
|
|
27
|
+
}
|
|
28
|
+
/** Headers that keep responses carrying session cookies out of shared caches. */
|
|
29
|
+
declare const AUTH_CACHE_HEADERS: Readonly<Record<string, string>>;
|
|
30
|
+
/** `sb-<first label of the hostname>-auth-token`, the supabase-js default. */
|
|
31
|
+
declare function sessionCookieName(url: string): string;
|
|
32
|
+
declare function parseCookies(header: string | null | undefined): CookieRecord[];
|
|
33
|
+
declare function serializeCookie(write: CookieWrite): string;
|
|
34
|
+
/**
|
|
35
|
+
* Reads the session written by `@supabase/ssr` (plain or `base64-`, whole or
|
|
36
|
+
* chunked). Mismatched chunks from a partial write read as no session.
|
|
37
|
+
*/
|
|
38
|
+
declare function readSession(cookies: readonly CookieRecord[], name: string): StoredSession | null;
|
|
39
|
+
/**
|
|
40
|
+
* Cookie writes that store `session` (or clear it with `null`) exactly like
|
|
41
|
+
* `@supabase/ssr`, and expire chunks the new value no longer uses.
|
|
42
|
+
*/
|
|
43
|
+
declare function writeSession(existing: readonly CookieRecord[], name: string, session: StoredSession | null, options?: CookieOptions): CookieWrite[];
|
|
44
|
+
/** Applies cookie writes to a cookie list, as the next request would see it. */
|
|
45
|
+
declare function applyCookieWrites(cookies: readonly CookieRecord[], writes: readonly CookieWrite[]): CookieRecord[];
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/auth/resolve.d.ts
|
|
48
|
+
/**
|
|
49
|
+
* Who is calling. `C` is the claims type from `sb.claims(schema)`; the
|
|
50
|
+
* verified payload keeps every JWT claim and adds the schema's output.
|
|
51
|
+
*/
|
|
52
|
+
type AuthState<C = unknown> = {
|
|
53
|
+
readonly kind: "user";
|
|
54
|
+
readonly token: string;
|
|
55
|
+
readonly claims: JWTClaims & C;
|
|
56
|
+
readonly user: UserClaims;
|
|
57
|
+
readonly source: "bearer" | "cookie" | (string & {});
|
|
58
|
+
/** Seconds since epoch, from the token's `exp`. */
|
|
59
|
+
readonly expiresAt: number | null;
|
|
60
|
+
} | {
|
|
61
|
+
readonly kind: "service";
|
|
62
|
+
readonly keyName: string;
|
|
63
|
+
} | {
|
|
64
|
+
readonly kind: "anon";
|
|
65
|
+
/** `expired`: the cookie session needs a refresh that was not allowed here (run the proxy). */
|
|
66
|
+
readonly reason: "none" | "expired" | "signed_out" | "refresh_failed";
|
|
67
|
+
} | {
|
|
68
|
+
readonly kind: "invalid";
|
|
69
|
+
readonly reason: InvalidReason;
|
|
70
|
+
readonly error: DbError;
|
|
71
|
+
};
|
|
72
|
+
type InvalidReason = "token" | "claims";
|
|
73
|
+
/**
|
|
74
|
+
* What an `AuthResolver` returns. `reason` may be left out of an `invalid`
|
|
75
|
+
* state; it then counts as `token`.
|
|
76
|
+
*/
|
|
77
|
+
type ResolvedState = Exclude<AuthState, {
|
|
78
|
+
kind: "invalid";
|
|
79
|
+
}> | {
|
|
80
|
+
readonly kind: "invalid";
|
|
81
|
+
readonly reason?: InvalidReason;
|
|
82
|
+
readonly error: DbError;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Extension point for other credentials (API keys, third-party auth, custom
|
|
86
|
+
* tokens). Return `undefined` when the request carries nothing this resolver
|
|
87
|
+
* understands; the built-in Bearer and cookie resolution runs next.
|
|
88
|
+
*/
|
|
89
|
+
interface AuthResolver {
|
|
90
|
+
readonly name: string;
|
|
91
|
+
resolve(request: Request): Promise<ResolvedState | undefined> | ResolvedState | undefined;
|
|
92
|
+
}
|
|
93
|
+
interface ResolveAuthOptions {
|
|
94
|
+
readonly env: BetterSupabaseEnv;
|
|
95
|
+
/** Allow refreshing an expiring cookie session. Only where cookies can be written (proxy). */
|
|
96
|
+
readonly refresh?: boolean;
|
|
97
|
+
/** Refresh when the token expires within this many seconds. Defaults to 60. */
|
|
98
|
+
readonly leeway?: number;
|
|
99
|
+
/**
|
|
100
|
+
* Accept `sb_secret_` keys (`apikey` header) as service callers: `true` for
|
|
101
|
+
* any configured key, or the key names allowed (`['cron']`). Defaults to false.
|
|
102
|
+
*/
|
|
103
|
+
readonly secret?: boolean | readonly string[];
|
|
104
|
+
readonly audience?: string | string[];
|
|
105
|
+
readonly issuer?: string | string[];
|
|
106
|
+
readonly cookie?: {
|
|
107
|
+
readonly name?: string;
|
|
108
|
+
readonly options?: CookieOptions;
|
|
109
|
+
};
|
|
110
|
+
/** Inline JWKS instead of fetching `env.jwksUrl` (tests, air-gapped). */
|
|
111
|
+
readonly jwks?: SupabaseEnv["jwks"];
|
|
112
|
+
/**
|
|
113
|
+
* Validates the verified claims (`sb.claims(schema)` sets it). A failure
|
|
114
|
+
* resolves to `{ kind: 'invalid', reason: 'claims' }`.
|
|
115
|
+
*/
|
|
116
|
+
readonly claims?: StandardSchemaV1;
|
|
117
|
+
readonly resolvers?: readonly AuthResolver[];
|
|
118
|
+
/**
|
|
119
|
+
* The caller's IP, sent to Auth as `Sb-Forwarded-For` on refreshes so its
|
|
120
|
+
* per-IP rate limit counts users, not your server. Only with a secret key
|
|
121
|
+
* (`env.secretKey`), which Auth requires for the header. Defaults to
|
|
122
|
+
* `clientIp` (the first `x-forwarded-for` hop); `false` turns it off.
|
|
123
|
+
*/
|
|
124
|
+
readonly clientIp?: ((request: Request) => string | undefined) | false;
|
|
125
|
+
/** Called after every refresh attempt (metrics, logging). */
|
|
126
|
+
readonly onRefresh?: (event: RefreshEvent) => void;
|
|
127
|
+
readonly fetch?: typeof fetch;
|
|
128
|
+
readonly now?: () => number;
|
|
129
|
+
}
|
|
130
|
+
interface AuthResolution<C = unknown> {
|
|
131
|
+
readonly auth: AuthState<C>;
|
|
132
|
+
/** Cookie writes for the response. Empty unless the session was refreshed or cleared. */
|
|
133
|
+
readonly cookies: readonly CookieWrite[];
|
|
134
|
+
/** No-store headers, set whenever `cookies` is not empty. */
|
|
135
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
136
|
+
/** Cookies the rest of this request should see, including a refreshed session. */
|
|
137
|
+
readonly requestCookies: readonly CookieRecord[];
|
|
138
|
+
/** Adds `cookies` and `headers` to a response. */
|
|
139
|
+
apply(response: Response): Response;
|
|
140
|
+
}
|
|
141
|
+
/** The first `x-forwarded-for` hop, the client as your edge saw it. */
|
|
142
|
+
declare function clientIp(request: Request): string | undefined;
|
|
143
|
+
/**
|
|
144
|
+
* Resolves who is calling: custom resolvers, then `Authorization: Bearer`
|
|
145
|
+
* (APIs, MCP, mobile), then the `@supabase/ssr` session cookie (browsers).
|
|
146
|
+
*
|
|
147
|
+
* A valid token costs no network call: it is verified locally against the
|
|
148
|
+
* cached JWKS. An expiring cookie session is refreshed only when `refresh` is
|
|
149
|
+
* set, through a single-flight request, and only then are cookies written.
|
|
150
|
+
*/
|
|
151
|
+
declare function resolveAuth(request: Request, options: ResolveAuthOptions): Promise<AuthResolution>;
|
|
152
|
+
/** The repository context (`actor`, `claims`) for an auth state. */
|
|
153
|
+
declare function authContext(auth: AuthState): RequestContext;
|
|
154
|
+
//#endregion
|
|
155
|
+
export { readSession as _, ResolveAuthOptions as a, writeSession as b, clientIp as c, CookieOptions as d, CookieRecord as f, parseCookies as g, applyCookieWrites as h, InvalidReason as i, resolveAuth as l, StoredSession as m, AuthResolver as n, ResolvedState as o, CookieWrite as p, AuthState as r, authContext as s, AuthResolution as t, AUTH_CACHE_HEADERS as u, serializeCookie as v, sessionCookieName as y };
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { n as dbError } from "./errors-Bl3rkOzo.js";
|
|
2
|
+
import { i as ok, n as err } from "./result-DWXatkd6.js";
|
|
3
|
+
import { t as validate } from "./standard-FUi3Z3-7.js";
|
|
4
|
+
import { i as respond } from "./respond-4-dhoV7x.js";
|
|
5
|
+
//#region src/server/resource.ts
|
|
6
|
+
const INTEGER = /* @__PURE__ */ new Set([
|
|
7
|
+
"int2",
|
|
8
|
+
"int4",
|
|
9
|
+
"int8"
|
|
10
|
+
]);
|
|
11
|
+
function keyValue(table, raw) {
|
|
12
|
+
const invalid = err(dbError("invalid_request", `${JSON.stringify(raw)} is not a valid ${table.key} key`));
|
|
13
|
+
const column = table.columns[table.primaryKey[0]];
|
|
14
|
+
if (column && INTEGER.has(column.type)) {
|
|
15
|
+
const value = typeof raw === "string" ? Number(raw) : raw;
|
|
16
|
+
return typeof value === "number" && Number.isSafeInteger(value) ? ok(value) : invalid;
|
|
17
|
+
}
|
|
18
|
+
return typeof raw === "string" && raw !== "" ? ok(raw) : invalid;
|
|
19
|
+
}
|
|
20
|
+
async function readBody(request) {
|
|
21
|
+
try {
|
|
22
|
+
const body = await request.json();
|
|
23
|
+
if (typeof body === "object" && body !== null && !Array.isArray(body)) return ok(body);
|
|
24
|
+
} catch {}
|
|
25
|
+
return err(dbError("invalid_request", "The request body must be a JSON object"));
|
|
26
|
+
}
|
|
27
|
+
function pageArgs(query, max) {
|
|
28
|
+
const read = (name, fallback) => {
|
|
29
|
+
const raw = query instanceof URLSearchParams ? query.get(name) : query?.[name];
|
|
30
|
+
if (raw === null || raw === void 0 || raw === "") return fallback;
|
|
31
|
+
const value = Number(raw);
|
|
32
|
+
return Number.isSafeInteger(value) && value >= 1 ? value : void 0;
|
|
33
|
+
};
|
|
34
|
+
const page = read("page", 1);
|
|
35
|
+
const size = read("size", 50);
|
|
36
|
+
if (page === void 0 || size === void 0 || size > max) return err(dbError("validation", "Invalid page parameters", { issues: [...page === void 0 ? [{
|
|
37
|
+
message: "Must be a positive integer",
|
|
38
|
+
path: ["page"]
|
|
39
|
+
}] : [], ...size === void 0 || size > max ? [{
|
|
40
|
+
message: `Must be between 1 and ${max}`,
|
|
41
|
+
path: ["size"]
|
|
42
|
+
}] : []] }));
|
|
43
|
+
return ok({
|
|
44
|
+
page,
|
|
45
|
+
size
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function asObject(value) {
|
|
49
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? ok(value) : err(dbError("invalid_request", "Expected an object of column values"));
|
|
50
|
+
}
|
|
51
|
+
async function settled(value) {
|
|
52
|
+
return await value;
|
|
53
|
+
}
|
|
54
|
+
function notAllowed(allowed) {
|
|
55
|
+
return new Response(null, {
|
|
56
|
+
status: 405,
|
|
57
|
+
headers: { allow: allowed.join(", ") }
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Operations over one table, matching the paths and responses of
|
|
62
|
+
* `createOpenApi`: `GET/POST /<table>` and `GET/PATCH/DELETE /<table>/{id}`.
|
|
63
|
+
* Shared by the HTTP adapters and the MCP table tools.
|
|
64
|
+
*/
|
|
65
|
+
function defineResource(sb, table, options = {}) {
|
|
66
|
+
const meta = sb.meta.tables[table];
|
|
67
|
+
if (!meta) throw new TypeError(`Unknown table "${table}"`);
|
|
68
|
+
if (options.list && options.list.table !== table) throw new TypeError(`The list query is for "${options.list.table}", not "${table}"`);
|
|
69
|
+
const operations = options.operations ?? (meta.kind === "view" ? ["list", "get"] : [
|
|
70
|
+
"list",
|
|
71
|
+
"get",
|
|
72
|
+
"create",
|
|
73
|
+
"update",
|
|
74
|
+
"delete"
|
|
75
|
+
]);
|
|
76
|
+
const keyParam = meta.primaryKey.length === 1 ? meta.primaryKey[0] : void 0;
|
|
77
|
+
const has = (operation) => operations.includes(operation);
|
|
78
|
+
const extra = options.select === void 0 ? {} : { select: options.select };
|
|
79
|
+
const maxPageSize = options.maxPageSize ?? 200;
|
|
80
|
+
const list = options.list;
|
|
81
|
+
const execute = async (db, operation, input = {}) => {
|
|
82
|
+
const repository = db[table];
|
|
83
|
+
if (!repository) throw new TypeError(`The db has no "${table}" repository`);
|
|
84
|
+
if (!has(operation)) return err(dbError("forbidden", `${operation} is not enabled for ${table}`));
|
|
85
|
+
const key = () => keyParam ? keyValue(meta, input.id) : err(dbError("invalid_request", `${table} has no single-column key`));
|
|
86
|
+
switch (operation) {
|
|
87
|
+
case "list": {
|
|
88
|
+
if (list) {
|
|
89
|
+
const query = list.parse(input.query ?? {});
|
|
90
|
+
if (!query.ok) return err(dbError("validation", "Invalid list query", { issues: query.issues }));
|
|
91
|
+
return settled(list.run(db, query.value, extra));
|
|
92
|
+
}
|
|
93
|
+
const page = pageArgs(input.query, maxPageSize);
|
|
94
|
+
if (!page.ok) return page;
|
|
95
|
+
return settled(repository.paginate({
|
|
96
|
+
...extra,
|
|
97
|
+
...page.data,
|
|
98
|
+
count: "exact"
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
case "get": {
|
|
102
|
+
const id = key();
|
|
103
|
+
if (!id.ok) return id;
|
|
104
|
+
return settled(repository.findById(id.data, extra));
|
|
105
|
+
}
|
|
106
|
+
case "create": {
|
|
107
|
+
const body = asObject(input.data);
|
|
108
|
+
if (!body.ok) return body;
|
|
109
|
+
const data = options.input?.create ? await validate(options.input.create, body.data, "data") : body;
|
|
110
|
+
if (!data.ok) return data;
|
|
111
|
+
return settled(repository.create(data.data, extra));
|
|
112
|
+
}
|
|
113
|
+
case "update": {
|
|
114
|
+
const id = key();
|
|
115
|
+
if (!id.ok) return id;
|
|
116
|
+
const body = asObject(input.data);
|
|
117
|
+
if (!body.ok) return body;
|
|
118
|
+
const data = options.input?.update ? await validate(options.input.update, body.data, "data") : body;
|
|
119
|
+
if (!data.ok) return data;
|
|
120
|
+
return settled(repository.update(id.data, data.data, extra));
|
|
121
|
+
}
|
|
122
|
+
case "delete": {
|
|
123
|
+
const id = key();
|
|
124
|
+
if (!id.ok) return id;
|
|
125
|
+
return settled(repository.delete(id.data));
|
|
126
|
+
}
|
|
127
|
+
default: throw new TypeError(`Unknown operation ${String(operation)}`);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const collectionMethods = [...has("list") ? ["GET"] : [], ...has("create") ? ["POST"] : []];
|
|
131
|
+
const itemMethods = [
|
|
132
|
+
...has("get") ? ["GET"] : [],
|
|
133
|
+
...has("update") ? ["PATCH"] : [],
|
|
134
|
+
...has("delete") ? ["DELETE"] : []
|
|
135
|
+
];
|
|
136
|
+
const route = async (request, db, id) => {
|
|
137
|
+
if (id === void 0) {
|
|
138
|
+
if (request.method === "GET" && has("list")) return execute(db, "list", { query: new URL(request.url).searchParams });
|
|
139
|
+
if (request.method === "POST" && has("create")) {
|
|
140
|
+
const body = await readBody(request);
|
|
141
|
+
return body.ok ? execute(db, "create", { data: body.data }) : body;
|
|
142
|
+
}
|
|
143
|
+
return notAllowed(collectionMethods);
|
|
144
|
+
}
|
|
145
|
+
if (!keyParam) return new Response(null, { status: 404 });
|
|
146
|
+
if (request.method === "GET" && has("get")) return execute(db, "get", { id });
|
|
147
|
+
if (request.method === "PATCH" && has("update")) {
|
|
148
|
+
const body = await readBody(request);
|
|
149
|
+
return body.ok ? execute(db, "update", {
|
|
150
|
+
id,
|
|
151
|
+
data: body.data
|
|
152
|
+
}) : body;
|
|
153
|
+
}
|
|
154
|
+
if (request.method === "DELETE" && has("delete")) return execute(db, "delete", { id });
|
|
155
|
+
return notAllowed(itemMethods);
|
|
156
|
+
};
|
|
157
|
+
return {
|
|
158
|
+
table,
|
|
159
|
+
meta,
|
|
160
|
+
operations,
|
|
161
|
+
keyParam,
|
|
162
|
+
list,
|
|
163
|
+
maxPageSize,
|
|
164
|
+
execute,
|
|
165
|
+
handle(request, db, id, respondOptions = {}) {
|
|
166
|
+
const status = id === void 0 && request.method === "POST" ? 201 : void 0;
|
|
167
|
+
return respond(() => route(request, db, id), status === void 0 ? respondOptions : {
|
|
168
|
+
...respondOptions,
|
|
169
|
+
status
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
//#endregion
|
|
175
|
+
export { defineResource as t };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { A as TableMeta, P as Update, h as Insert, k as TableKey, n as AnyModels, t as AnyFunctions } from "./types-C2fslP1z.js";
|
|
2
|
+
import { b as ValidationIssue, o as Result } from "./result-CTkCZ6JA.js";
|
|
3
|
+
import { t as BetterSupabase, xt as SelectArg } from "./define-CTfWw-SS.js";
|
|
4
|
+
import { r as RespondOptions } from "./respond-PoTpy5DT.js";
|
|
5
|
+
import { i as ResourceOperation } from "./index-DRid0qIv.js";
|
|
6
|
+
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
7
|
+
//#region src/server/resource.d.ts
|
|
8
|
+
/** What a resource needs from `defineListQuery`. */
|
|
9
|
+
interface ResourceList {
|
|
10
|
+
readonly table: string;
|
|
11
|
+
readonly openapi: readonly unknown[];
|
|
12
|
+
readonly jsonSchema: Readonly<Record<string, unknown>>;
|
|
13
|
+
parse(input: never): {
|
|
14
|
+
readonly ok: true;
|
|
15
|
+
readonly value: unknown;
|
|
16
|
+
} | {
|
|
17
|
+
readonly ok: false;
|
|
18
|
+
readonly issues: readonly ValidationIssue[];
|
|
19
|
+
};
|
|
20
|
+
run(db: never, query: never, extra?: never): PromiseLike<unknown>;
|
|
21
|
+
}
|
|
22
|
+
interface ResourceRouteOptions<M extends AnyModels, T extends TableKey<M>> {
|
|
23
|
+
/** Defaults to every operation for tables, `list` and `get` for views. */
|
|
24
|
+
readonly operations?: readonly ResourceOperation[];
|
|
25
|
+
/** Filters, search and sorts for `list`, from `defineListQuery`. */
|
|
26
|
+
readonly list?: ResourceList;
|
|
27
|
+
/** Columns returned by every operation. */
|
|
28
|
+
readonly select?: SelectArg<M, T>;
|
|
29
|
+
/** Validates request bodies before they reach the repository. */
|
|
30
|
+
readonly input?: {
|
|
31
|
+
readonly create?: StandardSchemaV1<unknown, Insert<M, T>>;
|
|
32
|
+
readonly update?: StandardSchemaV1<unknown, Update<M, T>>;
|
|
33
|
+
};
|
|
34
|
+
/** Largest page for the default `page`/`size` list. Defaults to 200. */
|
|
35
|
+
readonly maxPageSize?: number;
|
|
36
|
+
}
|
|
37
|
+
interface ResourceInput {
|
|
38
|
+
/** Primary key value; strings are converted for integer keys. */
|
|
39
|
+
readonly id?: unknown;
|
|
40
|
+
/** Body of `create` and `update`. */
|
|
41
|
+
readonly data?: unknown;
|
|
42
|
+
/** `list` parameters: URL params or typed values. */
|
|
43
|
+
readonly query?: URLSearchParams | Readonly<Record<string, unknown>>;
|
|
44
|
+
}
|
|
45
|
+
interface ResourceHandler {
|
|
46
|
+
readonly table: string;
|
|
47
|
+
readonly meta: TableMeta;
|
|
48
|
+
readonly operations: readonly ResourceOperation[];
|
|
49
|
+
/** Name of the key parameter, or `undefined` for tables without a single-column key. */
|
|
50
|
+
readonly keyParam: string | undefined;
|
|
51
|
+
readonly list: ResourceList | undefined;
|
|
52
|
+
readonly maxPageSize: number;
|
|
53
|
+
/** Runs one operation as whoever `db` is bound to. Resolves to a `Result`. */
|
|
54
|
+
execute(db: object, operation: ResourceOperation, input?: ResourceInput): Promise<Result<unknown>>;
|
|
55
|
+
/** Serves a collection request (`id` undefined) or an item request. */
|
|
56
|
+
handle(request: Request, db: object, id: string | undefined, options?: RespondOptions): Promise<Response>;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Operations over one table, matching the paths and responses of
|
|
60
|
+
* `createOpenApi`: `GET/POST /<table>` and `GET/PATCH/DELETE /<table>/{id}`.
|
|
61
|
+
* Shared by the HTTP adapters and the MCP table tools.
|
|
62
|
+
*/
|
|
63
|
+
declare function defineResource<M extends AnyModels, D, F extends AnyFunctions, E, T extends TableKey<M>>(sb: BetterSupabase<M, D, F, E>, table: T, options?: ResourceRouteOptions<M, T>): ResourceHandler;
|
|
64
|
+
//#endregion
|
|
65
|
+
export { defineResource as a, ResourceRouteOptions as i, ResourceInput as n, ResourceList as r, ResourceHandler as t };
|