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,84 @@
|
|
|
1
|
+
import { g as Operation } from "./executor-Bazhgsgq.js";
|
|
2
|
+
import { p as DbError } from "./result-CTkCZ6JA.js";
|
|
3
|
+
import { h as RequestContext, l as MutationKind } from "./plugin-HvGqCurB.js";
|
|
4
|
+
//#region src/core/logger.d.ts
|
|
5
|
+
type LogFields = Readonly<Record<string, unknown>>;
|
|
6
|
+
/**
|
|
7
|
+
* Where better-supabase reports problems it swallows on purpose: event
|
|
8
|
+
* handlers, `afterMutation` hooks, sinks and cache adapters that throw.
|
|
9
|
+
* Structured loggers (pino, consola, OpenTelemetry logs) fit directly.
|
|
10
|
+
*/
|
|
11
|
+
interface Logger {
|
|
12
|
+
debug(message: string, fields?: LogFields): void;
|
|
13
|
+
info(message: string, fields?: LogFields): void;
|
|
14
|
+
warn(message: string, fields?: LogFields): void;
|
|
15
|
+
error(message: string, fields?: LogFields): void;
|
|
16
|
+
}
|
|
17
|
+
/** The default: `console`, with a `better-supabase:` prefix. */
|
|
18
|
+
declare const consoleLogger: Logger;
|
|
19
|
+
declare const silentLogger: Logger;
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/core/events.d.ts
|
|
22
|
+
interface QueryEvent {
|
|
23
|
+
readonly table: string;
|
|
24
|
+
readonly operation: Operation["kind"];
|
|
25
|
+
readonly ok: boolean;
|
|
26
|
+
readonly durationMs: number;
|
|
27
|
+
readonly rows: number;
|
|
28
|
+
/**
|
|
29
|
+
* An unbounded read returned `maxRows` rows: PostgREST's `db-max-rows`
|
|
30
|
+
* probably cut it short. Add `limit` or use `paginate`.
|
|
31
|
+
*/
|
|
32
|
+
readonly truncated: boolean;
|
|
33
|
+
}
|
|
34
|
+
interface MutationNotice {
|
|
35
|
+
readonly table: string;
|
|
36
|
+
readonly kind: MutationKind;
|
|
37
|
+
readonly rows: readonly Readonly<Record<string, unknown>>[];
|
|
38
|
+
readonly context: RequestContext;
|
|
39
|
+
}
|
|
40
|
+
/** A successful RPC registered with `sb.defineRpc(name, { invalidates })`. */
|
|
41
|
+
interface RpcNotice {
|
|
42
|
+
readonly name: string;
|
|
43
|
+
/** App keys of the tables the function changes. */
|
|
44
|
+
readonly invalidates: readonly string[];
|
|
45
|
+
readonly context: RequestContext;
|
|
46
|
+
}
|
|
47
|
+
interface ErrorEvent {
|
|
48
|
+
readonly table?: string;
|
|
49
|
+
readonly error: DbError;
|
|
50
|
+
}
|
|
51
|
+
interface AuthEvent {
|
|
52
|
+
readonly source: "bearer" | "cookie" | "none";
|
|
53
|
+
readonly ok: boolean;
|
|
54
|
+
readonly userId?: string;
|
|
55
|
+
}
|
|
56
|
+
interface RefreshEvent {
|
|
57
|
+
readonly ok: boolean;
|
|
58
|
+
readonly shared: boolean;
|
|
59
|
+
readonly durationMs: number;
|
|
60
|
+
}
|
|
61
|
+
interface BetterSupabaseEvents {
|
|
62
|
+
query: QueryEvent;
|
|
63
|
+
mutation: MutationNotice;
|
|
64
|
+
rpc: RpcNotice;
|
|
65
|
+
error: ErrorEvent;
|
|
66
|
+
auth: AuthEvent;
|
|
67
|
+
refresh: RefreshEvent;
|
|
68
|
+
}
|
|
69
|
+
type EventName = keyof BetterSupabaseEvents;
|
|
70
|
+
type EventHandler<K extends EventName> = (event: BetterSupabaseEvents[K]) => void;
|
|
71
|
+
/**
|
|
72
|
+
* Observers only: a handler that throws is reported to the logger and never
|
|
73
|
+
* changes a result.
|
|
74
|
+
*/
|
|
75
|
+
declare class EventHub {
|
|
76
|
+
#private;
|
|
77
|
+
readonly logger: Logger;
|
|
78
|
+
constructor(logger?: Logger);
|
|
79
|
+
on<K extends EventName>(name: K, handler: EventHandler<K>): () => void;
|
|
80
|
+
has(name: EventName): boolean;
|
|
81
|
+
emit<K extends EventName>(name: K, event: BetterSupabaseEvents[K]): void;
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
export { EventHub as a, QueryEvent as c, LogFields as d, Logger as f, EventHandler as i, RefreshEvent as l, silentLogger as m, BetterSupabaseEvents as n, EventName as o, consoleLogger as p, ErrorEvent as r, MutationNotice as s, AuthEvent as t, RpcNotice as u };
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
//#region src/events/index.ts
|
|
2
|
+
const ROW_EVENT_TYPES = {
|
|
3
|
+
insert: "dev.better-supabase.row.created",
|
|
4
|
+
upsert: "dev.better-supabase.row.upserted",
|
|
5
|
+
update: "dev.better-supabase.row.updated",
|
|
6
|
+
delete: "dev.better-supabase.row.deleted"
|
|
7
|
+
};
|
|
8
|
+
function subjectOf(meta, table, row) {
|
|
9
|
+
const key = meta?.tables[table]?.primaryKey;
|
|
10
|
+
if (!key || key.length === 0) return void 0;
|
|
11
|
+
const values = key.map((column) => row[column]);
|
|
12
|
+
if (values.some((value) => value === void 0 || value === null)) return void 0;
|
|
13
|
+
return `${table}/${values.map((value) => encodeURIComponent(String(value))).join(",")}`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* One CloudEvent per mutated row: `dev.better-supabase.row.created` with the
|
|
17
|
+
* app-cased row as `data`, the primary key as `subject`, the tenant as
|
|
18
|
+
* `partitionkey` and the actor as `actorid`.
|
|
19
|
+
*/
|
|
20
|
+
function toCloudEvents(notice, options) {
|
|
21
|
+
const type = options.typePrefix ? `${options.typePrefix}.row.${ROW_EVENT_TYPES[notice.kind].split(".").at(-1)}` : ROW_EVENT_TYPES[notice.kind];
|
|
22
|
+
const time = (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
23
|
+
return notice.rows.map((row) => {
|
|
24
|
+
const subject = subjectOf(options.meta, notice.table, row);
|
|
25
|
+
return {
|
|
26
|
+
specversion: "1.0",
|
|
27
|
+
id: options.id?.() ?? crypto.randomUUID(),
|
|
28
|
+
source: options.source,
|
|
29
|
+
type,
|
|
30
|
+
...subject ? { subject } : {},
|
|
31
|
+
time,
|
|
32
|
+
datacontenttype: "application/json",
|
|
33
|
+
data: {
|
|
34
|
+
table: notice.table,
|
|
35
|
+
row
|
|
36
|
+
},
|
|
37
|
+
...notice.context.tenant ? { partitionkey: notice.context.tenant } : {},
|
|
38
|
+
...notice.context.actor?.id ? { actorid: notice.context.actor.id } : {}
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Sends a CloudEvent for every mutation to `sink`. Returns a function that
|
|
44
|
+
* stops forwarding. Use an outbox (SQL kit) when events must not be lost.
|
|
45
|
+
*/
|
|
46
|
+
function forwardMutations(sb, sink, options) {
|
|
47
|
+
return sb.events.on("mutation", (notice) => {
|
|
48
|
+
if (options.filter && !options.filter(notice)) return;
|
|
49
|
+
const events = toCloudEvents(notice, {
|
|
50
|
+
...options,
|
|
51
|
+
meta: sb.meta
|
|
52
|
+
});
|
|
53
|
+
if (events.length === 0) return;
|
|
54
|
+
const report = (error) => (options.onError ?? ((cause) => sb.events.logger.error("event sink failed", { cause })))(error, events);
|
|
55
|
+
try {
|
|
56
|
+
Promise.resolve(sink.send(events)).catch(report);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
report(error);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const STRUCTURED = "application/cloudevents+json";
|
|
63
|
+
const BATCH = "application/cloudevents-batch+json";
|
|
64
|
+
/** The HTTP protocol binding: headers and body for one event (or a batch). */
|
|
65
|
+
function toHttp(events, mode = "structured") {
|
|
66
|
+
const list = Array.isArray(events) ? events : [events];
|
|
67
|
+
switch (mode) {
|
|
68
|
+
case "batch": return [{
|
|
69
|
+
headers: { "content-type": BATCH },
|
|
70
|
+
body: JSON.stringify(list)
|
|
71
|
+
}];
|
|
72
|
+
case "structured": return list.map((event) => ({
|
|
73
|
+
headers: { "content-type": STRUCTURED },
|
|
74
|
+
body: JSON.stringify(event)
|
|
75
|
+
}));
|
|
76
|
+
case "binary": return list.map((event) => {
|
|
77
|
+
const headers = {};
|
|
78
|
+
for (const [key, value] of Object.entries(event)) {
|
|
79
|
+
if (key === "data" || key === "datacontenttype" || value === void 0) continue;
|
|
80
|
+
headers[`ce-${key}`] = encodeURIComponent(String(value)).replace(/%20/g, " ");
|
|
81
|
+
}
|
|
82
|
+
headers["content-type"] = event.datacontenttype ?? "application/json";
|
|
83
|
+
return {
|
|
84
|
+
headers,
|
|
85
|
+
body: event.data === void 0 ? "" : JSON.stringify(event.data)
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
default: throw new TypeError(`Unknown CloudEvents mode ${String(mode)}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function isCloudEvent(value) {
|
|
92
|
+
if (typeof value !== "object" || value === null) return false;
|
|
93
|
+
const event = value;
|
|
94
|
+
return event["specversion"] === "1.0" && typeof event["id"] === "string" && typeof event["source"] === "string" && typeof event["type"] === "string";
|
|
95
|
+
}
|
|
96
|
+
/** Reads CloudEvents from a request in any of the three HTTP modes. */
|
|
97
|
+
async function fromHttp(request) {
|
|
98
|
+
const contentType = request.headers.get("content-type") ?? "";
|
|
99
|
+
const text = await request.text();
|
|
100
|
+
if (contentType.startsWith(BATCH)) {
|
|
101
|
+
const parsed = JSON.parse(text);
|
|
102
|
+
if (!Array.isArray(parsed) || !parsed.every(isCloudEvent)) throw new TypeError("Invalid CloudEvents batch");
|
|
103
|
+
return parsed;
|
|
104
|
+
}
|
|
105
|
+
if (contentType.startsWith(STRUCTURED)) {
|
|
106
|
+
const parsed = JSON.parse(text);
|
|
107
|
+
if (!isCloudEvent(parsed)) throw new TypeError("Invalid CloudEvent");
|
|
108
|
+
return [parsed];
|
|
109
|
+
}
|
|
110
|
+
const event = {};
|
|
111
|
+
for (const [key, value] of request.headers) if (key.startsWith("ce-")) event[key.slice(3)] = decodeURIComponent(value);
|
|
112
|
+
if (contentType) event["datacontenttype"] = contentType;
|
|
113
|
+
if (text) event["data"] = /json/.test(contentType) ? JSON.parse(text) : text;
|
|
114
|
+
if (!isCloudEvent(event)) throw new TypeError("Request is not a CloudEvent");
|
|
115
|
+
return [event];
|
|
116
|
+
}
|
|
117
|
+
/** POSTs events to `url`. Non-2xx responses reject. */
|
|
118
|
+
function httpSink(url, options = {}) {
|
|
119
|
+
const send = options.fetch ?? fetch;
|
|
120
|
+
return { async send(events) {
|
|
121
|
+
for (const message of toHttp(events, options.mode ?? "batch")) {
|
|
122
|
+
const response = await send(url, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers: {
|
|
125
|
+
...options.headers,
|
|
126
|
+
...message.headers
|
|
127
|
+
},
|
|
128
|
+
body: message.body
|
|
129
|
+
});
|
|
130
|
+
if (!response.ok) throw new Error(`Event sink responded ${String(response.status)}`);
|
|
131
|
+
}
|
|
132
|
+
} };
|
|
133
|
+
}
|
|
134
|
+
//#endregion
|
|
135
|
+
export { isCloudEvent as a, httpSink as i, forwardMutations as n, toCloudEvents as o, fromHttp as r, toHttp as s, ROW_EVENT_TYPES as t };
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { A as TableMeta, o as Codec, x as RelationMeta } from "./types-C2fslP1z.js";
|
|
2
|
+
import { o as Result, v as ErrorMapper } from "./result-CTkCZ6JA.js";
|
|
3
|
+
//#region src/ir/types.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The query IR. Every repository call becomes one of these operations; the
|
|
6
|
+
* executor compiles it to PostgREST or SQL. Column names in the IR are
|
|
7
|
+
* database names; `Selection` carries the app aliases.
|
|
8
|
+
*/
|
|
9
|
+
type ColumnOp = "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "in" | "is" | "like" | "ilike" | "contains" | "containedBy" | "overlaps" | "fts";
|
|
10
|
+
type Quantifier = "some" | "none" | "every";
|
|
11
|
+
type Condition = {
|
|
12
|
+
readonly kind: "and";
|
|
13
|
+
readonly items: readonly Condition[];
|
|
14
|
+
} | {
|
|
15
|
+
readonly kind: "or";
|
|
16
|
+
readonly items: readonly Condition[];
|
|
17
|
+
} | {
|
|
18
|
+
readonly kind: "not";
|
|
19
|
+
readonly item: Condition;
|
|
20
|
+
} | {
|
|
21
|
+
readonly kind: "column";
|
|
22
|
+
readonly column: string;
|
|
23
|
+
readonly op: ColumnOp;
|
|
24
|
+
readonly value: unknown;
|
|
25
|
+
/** Full-text search configuration for `fts`. */
|
|
26
|
+
readonly config?: string;
|
|
27
|
+
} | {
|
|
28
|
+
readonly kind: "relation";
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly relation: RelationMeta;
|
|
31
|
+
readonly target: TableMeta;
|
|
32
|
+
readonly quantifier: Quantifier;
|
|
33
|
+
readonly where: Condition | undefined;
|
|
34
|
+
};
|
|
35
|
+
interface SelectColumn {
|
|
36
|
+
/** Key in the returned row. */
|
|
37
|
+
readonly alias: string;
|
|
38
|
+
/** Database column name. */
|
|
39
|
+
readonly column: string;
|
|
40
|
+
/** Read the column as text, so exact `int8`/`numeric` values survive JSON. */
|
|
41
|
+
readonly cast?: "text";
|
|
42
|
+
/** Decodes the returned value (see `ColumnMeta.codec`). */
|
|
43
|
+
readonly codec?: Codec;
|
|
44
|
+
}
|
|
45
|
+
interface OrderTerm {
|
|
46
|
+
readonly column: string;
|
|
47
|
+
readonly direction: "asc" | "desc";
|
|
48
|
+
readonly nulls?: "first" | "last";
|
|
49
|
+
}
|
|
50
|
+
interface Include {
|
|
51
|
+
readonly alias: string;
|
|
52
|
+
readonly relation: RelationMeta;
|
|
53
|
+
readonly target: TableMeta;
|
|
54
|
+
readonly selection: Selection;
|
|
55
|
+
readonly where: Condition | undefined;
|
|
56
|
+
readonly orderBy: readonly OrderTerm[];
|
|
57
|
+
readonly limit: number | undefined;
|
|
58
|
+
/** Drop parent rows without a match (`!inner`). */
|
|
59
|
+
readonly required: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* `_count` include: returns the number of matching related rows instead of
|
|
62
|
+
* rows, folded into `row._count[count]` when decoded.
|
|
63
|
+
*/
|
|
64
|
+
readonly count?: string;
|
|
65
|
+
/**
|
|
66
|
+
* `_sum`/`_avg`/`_min`/`_max` include: `selection.aggregate` holds the
|
|
67
|
+
* measures, folded into `row[`_${fn}`][name]` when decoded.
|
|
68
|
+
*/
|
|
69
|
+
readonly aggregate?: {
|
|
70
|
+
readonly fn: AggregateFn;
|
|
71
|
+
readonly name: string;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
type AggregateFn = "sum" | "avg" | "min" | "max";
|
|
75
|
+
interface Measure {
|
|
76
|
+
readonly fn: AggregateFn;
|
|
77
|
+
/** Key in the returned row (`_sum_amount`, or the column alias in an include). */
|
|
78
|
+
readonly key: string;
|
|
79
|
+
/** App column name, the key under `_sum` in the decoded row. */
|
|
80
|
+
readonly alias: string;
|
|
81
|
+
/** Database column name. */
|
|
82
|
+
readonly column: string;
|
|
83
|
+
/** Read the result as text, so exact `int8`/`numeric` values survive JSON. */
|
|
84
|
+
readonly cast?: "text";
|
|
85
|
+
readonly codec?: Codec;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Aggregates instead of rows. `Selection.columns` become the grouping
|
|
89
|
+
* columns; without any, the result is exactly one row.
|
|
90
|
+
*/
|
|
91
|
+
interface Aggregation {
|
|
92
|
+
/** Also return `_count`, the number of rows in each group. */
|
|
93
|
+
readonly count: boolean;
|
|
94
|
+
readonly measures: readonly Measure[];
|
|
95
|
+
}
|
|
96
|
+
interface Selection {
|
|
97
|
+
readonly columns: readonly SelectColumn[];
|
|
98
|
+
readonly includes: readonly Include[];
|
|
99
|
+
readonly aggregate?: Aggregation;
|
|
100
|
+
}
|
|
101
|
+
type CountMode = "exact" | "planned" | "estimated";
|
|
102
|
+
interface SelectOp {
|
|
103
|
+
readonly kind: "select";
|
|
104
|
+
readonly table: TableMeta;
|
|
105
|
+
readonly selection: Selection;
|
|
106
|
+
readonly where: Condition | undefined;
|
|
107
|
+
readonly orderBy: readonly OrderTerm[];
|
|
108
|
+
readonly limit: number | undefined;
|
|
109
|
+
readonly offset: number | undefined;
|
|
110
|
+
readonly count: CountMode | undefined;
|
|
111
|
+
/** Only count, return no rows. */
|
|
112
|
+
readonly head: boolean;
|
|
113
|
+
readonly single: "one" | "maybe" | undefined;
|
|
114
|
+
/**
|
|
115
|
+
* Read from this set-returning function instead of the table (`db.$search`).
|
|
116
|
+
* Only executors with `functionSources` honor it.
|
|
117
|
+
*/
|
|
118
|
+
readonly source?: FunctionSource;
|
|
119
|
+
}
|
|
120
|
+
/** A function returning `setof <table>`, called with named arguments. */
|
|
121
|
+
interface FunctionSource {
|
|
122
|
+
readonly schema: string;
|
|
123
|
+
readonly name: string;
|
|
124
|
+
readonly args: Readonly<Record<string, unknown>>;
|
|
125
|
+
}
|
|
126
|
+
interface InsertOp {
|
|
127
|
+
readonly kind: "insert";
|
|
128
|
+
readonly table: TableMeta;
|
|
129
|
+
/** Rows keyed by database column names. */
|
|
130
|
+
readonly rows: readonly Readonly<Record<string, unknown>>[];
|
|
131
|
+
readonly returning: Selection | undefined;
|
|
132
|
+
readonly onConflict: {
|
|
133
|
+
readonly columns: readonly string[];
|
|
134
|
+
readonly action: "update" | "ignore";
|
|
135
|
+
} | undefined;
|
|
136
|
+
/** Missing columns become `null` instead of their default in bulk inserts. */
|
|
137
|
+
readonly defaultToNull: boolean;
|
|
138
|
+
}
|
|
139
|
+
interface UpdateOp {
|
|
140
|
+
readonly kind: "update";
|
|
141
|
+
readonly table: TableMeta;
|
|
142
|
+
/** Keyed by database column names. */
|
|
143
|
+
readonly set: Readonly<Record<string, unknown>>;
|
|
144
|
+
readonly where: Condition | undefined;
|
|
145
|
+
readonly returning: Selection | undefined;
|
|
146
|
+
}
|
|
147
|
+
interface DeleteOp {
|
|
148
|
+
readonly kind: "delete";
|
|
149
|
+
readonly table: TableMeta;
|
|
150
|
+
readonly where: Condition | undefined;
|
|
151
|
+
readonly returning: Selection | undefined;
|
|
152
|
+
}
|
|
153
|
+
type Operation = SelectOp | InsertOp | UpdateOp | DeleteOp;
|
|
154
|
+
type MutationOp = InsertOp | UpdateOp | DeleteOp;
|
|
155
|
+
declare const and: (...items: (Condition | undefined)[]) => Condition | undefined;
|
|
156
|
+
declare const or: (...items: Condition[]) => Condition;
|
|
157
|
+
declare const not: (item: Condition) => Condition;
|
|
158
|
+
declare const column: (name: string, op: ColumnOp, value: unknown) => Condition;
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/core/executor.d.ts
|
|
161
|
+
interface ExecuteContext {
|
|
162
|
+
readonly signal?: AbortSignal;
|
|
163
|
+
readonly errorMappers: readonly ErrorMapper[];
|
|
164
|
+
}
|
|
165
|
+
interface ExecuteResult {
|
|
166
|
+
/** Rows keyed by the aliases in the operation's selection. */
|
|
167
|
+
readonly rows: readonly Record<string, unknown>[];
|
|
168
|
+
readonly count: number | null;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Runs IR operations. PostgREST (`postgrestExecutor`) and SQL
|
|
172
|
+
* (`better-supabase/postgres`) are built in; any other backend can implement
|
|
173
|
+
* this interface and prove it with `testExecutor` from `better-supabase/testing`.
|
|
174
|
+
*/
|
|
175
|
+
interface Executor {
|
|
176
|
+
readonly name: string;
|
|
177
|
+
/** Honors `SelectOp.source`. `db.$search` fails on executors without it. */
|
|
178
|
+
readonly functionSources?: boolean;
|
|
179
|
+
execute(op: Operation, context: ExecuteContext): Promise<Result<ExecuteResult>>;
|
|
180
|
+
/** Calls a database function. Optional; `db.$rpc` fails without it. */
|
|
181
|
+
rpc?(name: string, args: Readonly<Record<string, unknown>>, context: RpcContext): Promise<Result<unknown>>;
|
|
182
|
+
/**
|
|
183
|
+
* Runs several reads together, for `db.$many`: one transaction on a SQL
|
|
184
|
+
* connection. Returns one result per operation, in order. Optional;
|
|
185
|
+
* without it `db.$many` runs the operations in parallel.
|
|
186
|
+
*/
|
|
187
|
+
batch?(ops: readonly Operation[], context: ExecuteContext): Promise<readonly Result<ExecuteResult>[]>;
|
|
188
|
+
}
|
|
189
|
+
interface RpcContext extends ExecuteContext {
|
|
190
|
+
readonly schema: string;
|
|
191
|
+
/** The function is `stable`: send it as a GET, so read replicas can serve it. */
|
|
192
|
+
readonly get?: boolean;
|
|
193
|
+
}
|
|
194
|
+
//#endregion
|
|
195
|
+
export { and as C, or as E, UpdateOp as S, not as T, OrderTerm as _, AggregateFn as a, SelectOp as b, Condition as c, FunctionSource as d, Include as f, Operation as g, MutationOp as h, RpcContext as i, CountMode as l, Measure as m, ExecuteResult as n, Aggregation as o, InsertOp as p, Executor as r, ColumnOp as s, ExecuteContext as t, DeleteOp as u, Quantifier as v, column as w, Selection as x, SelectColumn as y };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { r as Executor } from "./executor-Bazhgsgq.js";
|
|
2
|
+
import { y as RawDbError } from "./result-CTkCZ6JA.js";
|
|
3
|
+
//#region src/postgres/executor.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Anything that runs parameterized SQL: `ctx.postgres` from
|
|
6
|
+
* `@supabase/server/middleware/postgres`, or a client from `createPostgres()`.
|
|
7
|
+
*/
|
|
8
|
+
interface SqlClient {
|
|
9
|
+
queryRaw<T = Record<string, unknown>>(text: string, params?: unknown[]): Promise<T[]>;
|
|
10
|
+
/**
|
|
11
|
+
* Runs `fn` on one connection in one transaction. Optional; without it
|
|
12
|
+
* `db.$many` sends its statements separately.
|
|
13
|
+
*/
|
|
14
|
+
transaction?<T>(fn: (client: SqlClient) => Promise<T>): Promise<T>;
|
|
15
|
+
}
|
|
16
|
+
/** node-postgres error fields to the shape `mapDbError` reads. */
|
|
17
|
+
declare function fromPgError(error: unknown): RawDbError | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Executes repository operations as SQL on a direct Postgres connection.
|
|
20
|
+
* Rows have the same shape as over PostgREST. RLS applies when the client
|
|
21
|
+
* runs as the caller (`ctx.postgres`, `createPostgres().asUser()`).
|
|
22
|
+
*/
|
|
23
|
+
declare function postgresExecutor(client: SqlClient): Executor;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { fromPgError as n, postgresExecutor as r, SqlClient as t };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { c as mapDbError, n as dbError } from "./errors-Bl3rkOzo.js";
|
|
2
|
+
import { i as ok, n as err, o as toDbError } from "./result-DWXatkd6.js";
|
|
3
|
+
import { t as compileSql } from "./sql-M8l3fnS0.js";
|
|
4
|
+
//#region src/postgres/executor.ts
|
|
5
|
+
function isPgError(value) {
|
|
6
|
+
return typeof value === "object" && value !== null && typeof value.code === "string";
|
|
7
|
+
}
|
|
8
|
+
/** node-postgres error fields to the shape `mapDbError` reads. */
|
|
9
|
+
function fromPgError(error) {
|
|
10
|
+
if (!isPgError(error)) return void 0;
|
|
11
|
+
const raw = {};
|
|
12
|
+
if (error.code !== void 0) raw.code = error.code;
|
|
13
|
+
if (error.message !== void 0) raw.message = error.message;
|
|
14
|
+
if (error.detail !== void 0) raw.details = error.detail;
|
|
15
|
+
if (error.hint !== void 0) raw.hint = error.hint;
|
|
16
|
+
if (error.constraint !== void 0) raw.constraint = error.constraint;
|
|
17
|
+
if (error.column !== void 0) raw.column = error.column;
|
|
18
|
+
if (error.table !== void 0) raw.table = error.table;
|
|
19
|
+
return raw;
|
|
20
|
+
}
|
|
21
|
+
async function run(client, plan, op) {
|
|
22
|
+
const rows = plan.rows ? (await client.queryRaw(plan.rows.text, [...plan.rows.params])).map((entry) => entry.row) : [];
|
|
23
|
+
let count = null;
|
|
24
|
+
if (plan.count) {
|
|
25
|
+
const [first] = await client.queryRaw(plan.count.text, [...plan.count.params]);
|
|
26
|
+
count = first?.count ?? 0;
|
|
27
|
+
} else if (op.kind !== "select") count = rows.length;
|
|
28
|
+
return {
|
|
29
|
+
rows,
|
|
30
|
+
count
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Executes repository operations as SQL on a direct Postgres connection.
|
|
35
|
+
* Rows have the same shape as over PostgREST. RLS applies when the client
|
|
36
|
+
* runs as the caller (`ctx.postgres`, `createPostgres().asUser()`).
|
|
37
|
+
*/
|
|
38
|
+
function postgresExecutor(client) {
|
|
39
|
+
return {
|
|
40
|
+
name: "postgres",
|
|
41
|
+
functionSources: true,
|
|
42
|
+
execute: (op, context) => executeOn(client, op, context),
|
|
43
|
+
async batch(ops, context) {
|
|
44
|
+
if (!client.transaction) return Promise.all(ops.map((op) => executeOn(client, op, context)));
|
|
45
|
+
try {
|
|
46
|
+
return await client.transaction(async (scoped) => {
|
|
47
|
+
const results = [];
|
|
48
|
+
for (const op of ops) {
|
|
49
|
+
const result = await executeOn(scoped, op, context);
|
|
50
|
+
if (!result.ok) throw new BatchFailed();
|
|
51
|
+
results.push(result);
|
|
52
|
+
}
|
|
53
|
+
return results;
|
|
54
|
+
});
|
|
55
|
+
} catch (cause) {
|
|
56
|
+
if (!(cause instanceof BatchFailed)) throw cause;
|
|
57
|
+
return Promise.all(ops.map((op) => executeOn(client, op, context)));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
var BatchFailed = class extends Error {};
|
|
63
|
+
async function executeOn(client, op, context) {
|
|
64
|
+
let plan;
|
|
65
|
+
try {
|
|
66
|
+
plan = compileSql(op);
|
|
67
|
+
} catch (cause) {
|
|
68
|
+
return err(toDbError(cause));
|
|
69
|
+
}
|
|
70
|
+
if (plan.never) return ok({
|
|
71
|
+
rows: [],
|
|
72
|
+
count: 0
|
|
73
|
+
});
|
|
74
|
+
if (context.signal?.aborted) return err(dbError("aborted", "The request was aborted"));
|
|
75
|
+
let result;
|
|
76
|
+
try {
|
|
77
|
+
result = await run(client, plan, op);
|
|
78
|
+
} catch (cause) {
|
|
79
|
+
const raw = fromPgError(cause);
|
|
80
|
+
return err(raw ? mapDbError(raw, context.errorMappers) : toDbError(cause));
|
|
81
|
+
}
|
|
82
|
+
if (op.kind === "select" && op.single) {
|
|
83
|
+
if (result.rows.length > 1) return err(dbError("multiple_rows", `Expected one ${op.table.key} row`));
|
|
84
|
+
if (result.rows.length === 0 && op.single === "one") return err(dbError("not_found", `No ${op.table.key} row matched`));
|
|
85
|
+
}
|
|
86
|
+
return ok(result);
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
export { postgresExecutor as n, fromPgError as t };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { k as TableKey, n as AnyModels, t as AnyFunctions } from "../types-C2fslP1z.js";
|
|
2
|
+
import { t as BetterSupabase } from "../define-CTfWw-SS.js";
|
|
3
|
+
import { r as AuthState } from "../resolve-DW8h9NPm.js";
|
|
4
|
+
import { d as ServerContext, f as ServerOptions, l as BetterServer, n as GuardOptions } from "../respond-PoTpy5DT.js";
|
|
5
|
+
import { i as ResourceRouteOptions } from "../resource-SkPTRJDD.js";
|
|
6
|
+
import { Context, ErrorHandler, Hono, MiddlewareHandler } from "hono";
|
|
7
|
+
//#region src/hono/index.d.ts
|
|
8
|
+
/** Hono `Env` with the request's better-supabase context in `c.var`. */
|
|
9
|
+
export interface BetterEnv<M extends AnyModels, F extends AnyFunctions, E> {
|
|
10
|
+
Variables: {
|
|
11
|
+
readonly bs: ServerContext<M, F, E>;
|
|
12
|
+
readonly db: ServerContext<M, F, E>["db"];
|
|
13
|
+
readonly auth: AuthState;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface HonoOptions extends ServerOptions {
|
|
17
|
+
/** Include error details in problem responses. Defaults to `NODE_ENV === 'development'`. */
|
|
18
|
+
readonly exposeErrors?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface MiddlewareOptions extends GuardOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Refresh an expired cookie session and send the new cookies. Only for
|
|
23
|
+
* routes browsers call with cookies; bearer tokens never refresh.
|
|
24
|
+
*/
|
|
25
|
+
readonly refresh?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface BetterHono<M extends AnyModels, F extends AnyFunctions, E> extends BetterServer<M, F, E> {
|
|
28
|
+
/** Resolves the caller, enforces `allow` and sets `c.var.bs`, `c.var.db`, `c.var.auth`. */
|
|
29
|
+
middleware(options?: MiddlewareOptions): MiddlewareHandler<BetterEnv<M, F, E>>;
|
|
30
|
+
/** Wraps a handler: `Result`s become JSON or Problem Details, `undefined` becomes 204. */
|
|
31
|
+
handle(handler: (c: Context<BetterEnv<M, F, E>>, ctx: ServerContext<M, F, E>) => unknown, options?: {
|
|
32
|
+
readonly status?: number;
|
|
33
|
+
}): (c: Context<BetterEnv<M, F, E>>) => Promise<Response>;
|
|
34
|
+
/** `app.onError(bs.onError)`: `DbException`s (and errors caused by a `DbError`) become Problem Details, others a 500. */
|
|
35
|
+
readonly onError: ErrorHandler<BetterEnv<M, F, E>>;
|
|
36
|
+
/**
|
|
37
|
+
* REST routes for a table matching `createOpenApi`. Mount with
|
|
38
|
+
* `app.route('/customers', bs.resource('customers'))` behind `middleware()`.
|
|
39
|
+
*/
|
|
40
|
+
resource<T extends TableKey<M>>(table: T, options?: ResourceRouteOptions<M, T>): Hono<BetterEnv<M, F, E>>;
|
|
41
|
+
}
|
|
42
|
+
/** Hono integration: a server plus middleware, handlers and REST resources. */
|
|
43
|
+
export declare function createHono<M extends AnyModels, D, F extends AnyFunctions, E>(sb: BetterSupabase<M, D, F, E>, options?: HonoOptions): BetterHono<M, F, E>;
|
|
44
|
+
//#endregion
|
|
45
|
+
export type { GuardOptions, ResourceRouteOptions };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { n as dbError, r as dbErrorOf } from "../errors-Bl3rkOzo.js";
|
|
2
|
+
import { a as problemResponse } from "../problem-DPu6Fh-t.js";
|
|
3
|
+
import { i as respond, n as guard, o as createServer, s as extendServer, t as defaultExpose } from "../respond-4-dhoV7x.js";
|
|
4
|
+
import { t as defineResource } from "../resource-CYiu6Ygi.js";
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
//#region src/hono/index.ts
|
|
7
|
+
function contextOf(c) {
|
|
8
|
+
const ctx = c.get("bs");
|
|
9
|
+
if (!ctx) throw new TypeError("better-supabase: no context on this request; add app.use(bs.middleware()) first");
|
|
10
|
+
return ctx;
|
|
11
|
+
}
|
|
12
|
+
/** Hono integration: a server plus middleware, handlers and REST resources. */
|
|
13
|
+
function createHono(sb, options = {}) {
|
|
14
|
+
const server = createServer(sb, options);
|
|
15
|
+
const expose = options.exposeErrors ?? defaultExpose();
|
|
16
|
+
const onError = (cause, c) => {
|
|
17
|
+
const instance = new URL(c.req.url).pathname;
|
|
18
|
+
const thrown = dbErrorOf(cause);
|
|
19
|
+
if (thrown) return problemResponse(thrown, {
|
|
20
|
+
instance,
|
|
21
|
+
expose
|
|
22
|
+
});
|
|
23
|
+
const error = dbError("unexpected", expose ? cause.message : "Internal server error", { status: 500 });
|
|
24
|
+
return problemResponse(error, {
|
|
25
|
+
instance,
|
|
26
|
+
expose
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
return extendServer(server, {
|
|
30
|
+
onError,
|
|
31
|
+
middleware(middlewareOptions = {}) {
|
|
32
|
+
return async (c, next) => {
|
|
33
|
+
const ctx = await server.context(c.req.raw, { refresh: middlewareOptions.refresh ?? false });
|
|
34
|
+
const denied = guard(ctx.auth, middlewareOptions.allow, middlewareOptions.aal);
|
|
35
|
+
if (denied) return ctx.resolution.apply(problemResponse(denied, {
|
|
36
|
+
instance: new URL(c.req.url).pathname,
|
|
37
|
+
expose
|
|
38
|
+
}));
|
|
39
|
+
c.set("bs", ctx);
|
|
40
|
+
c.set("db", ctx.db);
|
|
41
|
+
c.set("auth", ctx.auth);
|
|
42
|
+
await next();
|
|
43
|
+
c.res = ctx.resolution.apply(c.res);
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
handle(handler, handleOptions = {}) {
|
|
47
|
+
return (c) => {
|
|
48
|
+
const ctx = contextOf(c);
|
|
49
|
+
return respond(() => handler(c, ctx), {
|
|
50
|
+
instance: new URL(c.req.url).pathname,
|
|
51
|
+
expose,
|
|
52
|
+
...handleOptions
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
resource(table, resourceOptions) {
|
|
57
|
+
const resource = defineResource(sb, table, resourceOptions);
|
|
58
|
+
const app = new Hono();
|
|
59
|
+
const serve = (item) => (c) => resource.handle(c.req.raw, contextOf(c).db, item ? c.req.param("id") : void 0, {
|
|
60
|
+
instance: new URL(c.req.url).pathname,
|
|
61
|
+
expose
|
|
62
|
+
});
|
|
63
|
+
app.all("/", serve(false));
|
|
64
|
+
if (resource.keyParam) app.all("/:id", serve(true));
|
|
65
|
+
return app;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { createHono };
|