better-supabase 0.0.0 → 0.2.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 +4 -0
- package/dist/casing-da0uRTqt.js +14 -0
- package/dist/claims-CoTobNC-.js +25 -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-DrSgDOQC.js +6646 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +112 -0
- package/dist/compiler-CZ9nIuqy.d.ts +14 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.js +3 -0
- package/dist/config-ClDXmVtY.js +277 -0
- package/dist/define-BXMYD9AY.js +1071 -0
- package/dist/define-e3zI4_4g.d.ts +679 -0
- package/dist/edge/index.d.ts +43 -0
- package/dist/edge/index.js +103 -0
- package/dist/entitlements-CTEBm7IB.js +20 -0
- package/dist/entitlements-Dng-ctuT.d.ts +42 -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-BuzTgn2D.js +135 -0
- package/dist/events-DOt3nSO-.d.ts +84 -0
- package/dist/executor-BYXQ9AGn.d.ts +25 -0
- package/dist/executor-CGdNQ27g.d.ts +195 -0
- package/dist/executor-DeppvirK.js +89 -0
- package/dist/guards-BnzDvFrz.js +10 -0
- package/dist/hono/index.d.ts +45 -0
- package/dist/hono/index.js +70 -0
- package/dist/index-Bw1bwtC0.d.ts +144 -0
- package/dist/index-CBDufH1M.d.ts +120 -0
- package/dist/index-CG69s15v.d.ts +38 -0
- package/dist/index-Ciu5Xlml.d.ts +74 -0
- package/dist/index-D4v7sncu.d.ts +62 -0
- package/dist/index-DOsjFvMf.d.ts +618 -0
- package/dist/index-DbxFr7XG.d.ts +76 -0
- package/dist/index-DuP14VHo.d.ts +216 -0
- package/dist/index-Dy6kuF0P.d.ts +71 -0
- package/dist/index-K-3WM2Hp.d.ts +117 -0
- package/dist/index.d.ts +110 -0
- package/dist/index.js +43 -0
- package/dist/invalidate-DbxuL2jL.js +18 -0
- package/dist/jobs/index.d.ts +173 -0
- package/dist/jobs/index.js +478 -0
- package/dist/json-schema-BpYqkCKf.js +182 -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-D8rV1Ldw.js +168 -0
- package/dist/live-DJyZMD6w.d.ts +191 -0
- package/dist/logger-glM28NVN.js +21 -0
- package/dist/mcp/index.d.ts +142 -0
- package/dist/mcp/index.js +463 -0
- package/dist/mfa-CpLfJSde.js +541 -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 +427 -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/permdock-sql-CghlHPuH.js +117 -0
- package/dist/plugin-BqR6wKMB.js +23 -0
- package/dist/plugin-C4TR9J0Z.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 +89 -0
- package/dist/plugins/rules/index.js +167 -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 +49 -0
- package/dist/plugins/tenant/index.js +75 -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-R2m_DkXi.d.ts +37 -0
- package/dist/postgres/index.d.ts +28 -0
- package/dist/postgres/index.js +79 -0
- package/dist/postgrest-cAEQ_IQN.d.ts +43 -0
- package/dist/problem-BTkTtRV9.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-C83BdBGt.js +242 -0
- package/dist/read-set-4lI3W_gZ.js +216 -0
- package/dist/realtime/index.d.ts +4 -0
- package/dist/realtime/index.js +268 -0
- package/dist/resolve-CjHILk2w.d.ts +170 -0
- package/dist/resource-BHEWmQJK.js +175 -0
- package/dist/resource-CgZa3YG_.d.ts +65 -0
- package/dist/respond-DMcbQwfN.js +383 -0
- package/dist/respond-Dne1ARSa.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-C3LxnQ0e.d.ts +22 -0
- package/dist/session-CqEwzVup.js +20 -0
- package/dist/shared-_6-uPL7E.js +57 -0
- package/dist/simplify-bwJpLnaS.js +665 -0
- package/dist/spec-pins-C3oc-Tdp.js +20 -0
- package/dist/spec-pins-CqorWDnO.d.ts +20 -0
- package/dist/sql-DTjA1LPj.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-C5W-rBfR.js +566 -0
- package/dist/storage/index.d.ts +5 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage-DuGn38fr.js +527 -0
- package/dist/tables-rZxRxNPW.js +59 -0
- package/dist/template-mbvlysOo.d.ts +6 -0
- package/dist/testing/index.d.ts +325 -0
- package/dist/testing/index.js +819 -0
- package/dist/types-6cYK9pbJ.js +29 -0
- package/dist/types-C83v5SQz.d.ts +248 -0
- package/dist/version-Cqq1m3UY.js +4 -0
- package/dist/view-D1s0wa0T.d.ts +49 -0
- package/dist/view-aOYizTeE.js +36 -0
- package/dist/webhooks/index.d.ts +152 -0
- package/dist/webhooks/index.js +2 -0
- package/dist/webhooks-DMipjhSP.js +139 -0
- package/package.json +250 -17
- package/schemas/config-v1.json +373 -0
- package/schemas/doctor-report-v1.json +106 -0
- package/schemas/snapshot-v2.json +1006 -0
- package/skills/better-supabase/SKILL.md +84 -0
- package/skills/better-supabase/references/plugins.md +65 -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 +66 -0
- package/index.js +0 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { n as dbError, t as DbException } from "./errors-Bl3rkOzo.js";
|
|
2
|
+
//#region src/core/template.ts
|
|
3
|
+
const PLACEHOLDER = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
4
|
+
function parseSegment(segment) {
|
|
5
|
+
const parts = [];
|
|
6
|
+
let last = 0;
|
|
7
|
+
for (const match of segment.matchAll(PLACEHOLDER)) {
|
|
8
|
+
if (match.index > last) parts.push({
|
|
9
|
+
kind: "literal",
|
|
10
|
+
text: segment.slice(last, match.index)
|
|
11
|
+
});
|
|
12
|
+
parts.push({
|
|
13
|
+
kind: "param",
|
|
14
|
+
name: match[1]
|
|
15
|
+
});
|
|
16
|
+
last = match.index + match[0].length;
|
|
17
|
+
}
|
|
18
|
+
if (last < segment.length) parts.push({
|
|
19
|
+
kind: "literal",
|
|
20
|
+
text: segment.slice(last)
|
|
21
|
+
});
|
|
22
|
+
return parts;
|
|
23
|
+
}
|
|
24
|
+
const escapeRegex = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
25
|
+
function parseTemplate(source, separator, validate) {
|
|
26
|
+
const segments = source.split(separator).map(parseSegment);
|
|
27
|
+
const params = [];
|
|
28
|
+
for (const segment of segments) {
|
|
29
|
+
if (segment.length === 0) throw new TypeError(`Template "${source}" has an empty segment`);
|
|
30
|
+
for (let index = 1; index < segment.length; index++) if (segment[index].kind === "param" && segment[index - 1].kind === "param") throw new TypeError(`Template "${source}" has adjacent placeholders`);
|
|
31
|
+
for (const part of segment) {
|
|
32
|
+
if (part.kind !== "param") continue;
|
|
33
|
+
if (params.includes(part.name)) throw new TypeError(`Template "${source}" repeats {${part.name}}`);
|
|
34
|
+
params.push(part.name);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (/[{}]/.test(segments.flat().map((part) => part.kind === "literal" ? part.text : "").join(""))) throw new TypeError(`Template "${source}" has an unbalanced brace`);
|
|
38
|
+
const sep = escapeRegex(separator);
|
|
39
|
+
const valueClass = `[^${separator === "/" ? "/" : sep}]+`;
|
|
40
|
+
const regexFor = (named) => segments.map((segment) => segment.map((part) => part.kind === "literal" ? escapeRegex(part.text) : named ? `(?<${part.name}>${valueClass})` : valueClass).join("")).join(sep);
|
|
41
|
+
const matcher = new RegExp(`^${regexFor(true)}$`);
|
|
42
|
+
const render = (name, raw) => {
|
|
43
|
+
if (raw === void 0 || typeof raw !== "string" && typeof raw !== "number") throw new DbException(dbError("invalid_input", `Missing value for {${name}}`));
|
|
44
|
+
const value = String(raw);
|
|
45
|
+
const problem = value === "" ? "is empty" : value.includes(separator) ? `contains "${separator}"` : validate(name, value);
|
|
46
|
+
if (problem) throw new DbException(dbError("invalid_input", `Value for {${name}} ${problem}`));
|
|
47
|
+
return value;
|
|
48
|
+
};
|
|
49
|
+
const renderSegment = (segment, values) => segment.map((part) => part.kind === "literal" ? part.text : render(part.name, values[part.name])).join("");
|
|
50
|
+
return {
|
|
51
|
+
source,
|
|
52
|
+
separator,
|
|
53
|
+
params,
|
|
54
|
+
segments: segments.length,
|
|
55
|
+
build: (values) => segments.map((segment) => renderSegment(segment, values)).join(separator),
|
|
56
|
+
match(value) {
|
|
57
|
+
const found = matcher.exec(value);
|
|
58
|
+
if (!found?.groups) return found ? {} : null;
|
|
59
|
+
const out = {};
|
|
60
|
+
for (const name of params) {
|
|
61
|
+
const part = found.groups[name];
|
|
62
|
+
if (validate(name, part)) return null;
|
|
63
|
+
out[name] = part;
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
},
|
|
67
|
+
prefix(values, maxSegments = segments.length) {
|
|
68
|
+
const out = [];
|
|
69
|
+
for (const segment of segments.slice(0, maxSegments)) {
|
|
70
|
+
if (segment.some((part) => part.kind === "param" && values[part.name] === void 0)) break;
|
|
71
|
+
out.push(renderSegment(segment, values));
|
|
72
|
+
}
|
|
73
|
+
return out.join(separator);
|
|
74
|
+
},
|
|
75
|
+
segmentOf(name) {
|
|
76
|
+
const index = segments.findIndex((segment) => segment.length === 1 && segment[0].kind === "param" && segment[0].name === name);
|
|
77
|
+
return index === -1 ? void 0 : index + 1;
|
|
78
|
+
},
|
|
79
|
+
sqlPattern: `^${regexFor(false)}$`
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function sqlString(value) {
|
|
83
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
84
|
+
}
|
|
85
|
+
function sqlIdent(value) {
|
|
86
|
+
return `"${value.replaceAll("\"", "\"\"")}"`;
|
|
87
|
+
}
|
|
88
|
+
/** Postgres identifier-safe name part: `customer-logos` → `customer_logos`. */
|
|
89
|
+
function slug(value) {
|
|
90
|
+
return value.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_|_$/g, "").toLowerCase();
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/core/permdock-sql.ts
|
|
94
|
+
const SCOPE = /^[a-z_][a-z0-9_]*$/;
|
|
95
|
+
/** PermDock splits a key with row conditions into `key#1`, `key#2`, ...: those need its own policies. */
|
|
96
|
+
const SPLIT_KEY = /#\d+$/;
|
|
97
|
+
/**
|
|
98
|
+
* The SQL condition for one PermDock permission key. `id` is the text
|
|
99
|
+
* expression holding the scope id; it is compared as text, so a malformed
|
|
100
|
+
* path never raises a cast error.
|
|
101
|
+
*
|
|
102
|
+
* The helpers check role and scope, not a permission's row conditions, so
|
|
103
|
+
* the condition is only correct for permissions whose grants have none
|
|
104
|
+
* beyond the scope. `#n` keys are refused; other row-conditioned keys can't
|
|
105
|
+
* be detected here.
|
|
106
|
+
*/
|
|
107
|
+
function permdockCheck(where, target, key, id) {
|
|
108
|
+
if (key === "") throw new TypeError(`${where}: a PermDock permission key is empty`);
|
|
109
|
+
if (SPLIT_KEY.test(key)) throw new TypeError(`${where}: "${key}" is one part of a permission PermDock splits by row condition. Use PermDock's generated policies for it.`);
|
|
110
|
+
const schema = sqlIdent(target.schema ?? "public");
|
|
111
|
+
if (target.scope === "global") return `(select ${schema}.permdock_has(${sqlString(key)}))`;
|
|
112
|
+
if (!SCOPE.test(target.scope)) throw new TypeError(`${where}: invalid PermDock scope "${target.scope}"`);
|
|
113
|
+
if (id === void 0) throw new TypeError(`${where}: scope "${target.scope}" needs a segment`);
|
|
114
|
+
return `${id} in (select t.id::text from ${schema}.${sqlIdent(`permitted_${target.scope}_ids`)}(${sqlString(key)}) as t(id))`;
|
|
115
|
+
}
|
|
116
|
+
//#endregion
|
|
117
|
+
export { sqlString as a, sqlIdent as i, parseTemplate as n, slug as r, permdockCheck as t };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/core/plugin.ts
|
|
2
|
+
const PLUGIN_API_VERSION = 1;
|
|
3
|
+
const RANK = {
|
|
4
|
+
pre: 0,
|
|
5
|
+
normal: 1,
|
|
6
|
+
post: 2
|
|
7
|
+
};
|
|
8
|
+
/** Plugins in hook order: `pre`, then unmarked in `use()` order, then `post`. */
|
|
9
|
+
function orderPlugins(plugins) {
|
|
10
|
+
return plugins.map((plugin, index) => ({
|
|
11
|
+
plugin,
|
|
12
|
+
index
|
|
13
|
+
})).sort((a, b) => RANK[a.plugin.enforce ?? "normal"] - RANK[b.plugin.enforce ?? "normal"] || a.index - b.index).map(({ plugin }) => plugin);
|
|
14
|
+
}
|
|
15
|
+
/** Identity helper that fixes `apiVersion` and infers the plugin types. */
|
|
16
|
+
function definePlugin(plugin) {
|
|
17
|
+
return {
|
|
18
|
+
...plugin,
|
|
19
|
+
apiVersion: 1
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
export { definePlugin as n, orderPlugins as r, PLUGIN_API_VERSION as t };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { A as SchemaMeta, P as TableMeta, n as AnyModels } from "./types-C83v5SQz.js";
|
|
2
|
+
import { g as Operation, h as MutationOp, r as Executor } from "./executor-CGdNQ27g.js";
|
|
3
|
+
import { v as ErrorMapper } from "./result-CTkCZ6JA.js";
|
|
4
|
+
//#region src/core/plugin.d.ts
|
|
5
|
+
/** Who is making the request. */
|
|
6
|
+
interface Actor {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly kind: "user" | "service" | "anon";
|
|
9
|
+
readonly role?: string;
|
|
10
|
+
readonly email?: string;
|
|
11
|
+
/** The admin acting as this user (the `act` claim's `sub`). */
|
|
12
|
+
readonly impersonator?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Per-connection context. Server adapters fill it from the verified JWT;
|
|
16
|
+
* plugins read it (`tenant()` reads `tenant`, `actor()` reads `actor`).
|
|
17
|
+
*/
|
|
18
|
+
interface RequestContext {
|
|
19
|
+
readonly actor?: Actor;
|
|
20
|
+
readonly tenant?: string;
|
|
21
|
+
readonly claims?: Readonly<Record<string, unknown>>;
|
|
22
|
+
readonly [key: string]: unknown;
|
|
23
|
+
}
|
|
24
|
+
/** Plugin-specific per-call options (`withDeleted`, `hard`, ...). */
|
|
25
|
+
type CallOptions = Readonly<Record<string, unknown>>;
|
|
26
|
+
interface HookArgs {
|
|
27
|
+
readonly table: TableMeta;
|
|
28
|
+
readonly schema: SchemaMeta;
|
|
29
|
+
readonly context: RequestContext;
|
|
30
|
+
readonly options: CallOptions;
|
|
31
|
+
readonly now: () => Date;
|
|
32
|
+
}
|
|
33
|
+
type MutationKind = "insert" | "upsert" | "update" | "delete";
|
|
34
|
+
interface MutationEvent {
|
|
35
|
+
readonly table: TableMeta;
|
|
36
|
+
readonly kind: MutationKind;
|
|
37
|
+
/** App-cased rows returned by the mutation. */
|
|
38
|
+
readonly rows: readonly Readonly<Record<string, unknown>>[];
|
|
39
|
+
readonly context: RequestContext;
|
|
40
|
+
}
|
|
41
|
+
/** Runtime handle passed to `Plugin.repository`. */
|
|
42
|
+
interface RepositoryApi {
|
|
43
|
+
readonly table: TableMeta;
|
|
44
|
+
readonly base: Readonly<Record<string, (...args: never[]) => unknown>>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Type-level repository extension, applied per table. Implement it as an
|
|
48
|
+
* interface that reads `this['M']` and `this['T']`:
|
|
49
|
+
*
|
|
50
|
+
* ```ts
|
|
51
|
+
* interface RestoreExtension extends RepositoryExtension {
|
|
52
|
+
* readonly methods: HasFlag<this['M'], this['T'], 'softDelete'> extends true
|
|
53
|
+
* ? { restore(id: string): AsyncResult<unknown> }
|
|
54
|
+
* : unknown;
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
interface RepositoryExtension {
|
|
59
|
+
readonly M: AnyModels;
|
|
60
|
+
readonly T: string;
|
|
61
|
+
readonly methods: unknown;
|
|
62
|
+
readonly findArgs: unknown;
|
|
63
|
+
readonly deleteArgs: unknown;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Applies the extensions in `E` to table `T`. `E` is a tuple of extensions (as
|
|
67
|
+
* built by `use()`) or a single extension. Each is applied on its own because
|
|
68
|
+
* indexing an intersection of `this`-typed interfaces overflows TS < 7.
|
|
69
|
+
*/
|
|
70
|
+
/** Default extension of plugins that add no repository types. */
|
|
71
|
+
interface NoExtension extends RepositoryExtension {
|
|
72
|
+
readonly "~none": true;
|
|
73
|
+
}
|
|
74
|
+
type ApplyExtension<E, M extends AnyModels, T extends keyof M, K extends "methods" | "findArgs" | "deleteArgs"> = E extends readonly [infer Head, ...infer Rest] ? ApplyOne<Head, M, T, K> & ApplyExtension<Rest, M, T, K> : ApplyOne<E, M, T, K>;
|
|
75
|
+
type ApplyOne<E, M extends AnyModels, T extends keyof M, K extends "methods" | "findArgs" | "deleteArgs"> = E extends RepositoryExtension ? (E & {
|
|
76
|
+
readonly M: M;
|
|
77
|
+
readonly T: T;
|
|
78
|
+
})[K] : unknown;
|
|
79
|
+
/** True when table `T` has the flag set by codegen. */
|
|
80
|
+
type HasFlag<M, T, F extends string> = M extends AnyModels ? T extends keyof M ? M[T]["Flags"] extends { readonly [K in F]: unknown; } ? true : false : false : false;
|
|
81
|
+
declare const PLUGIN_API_VERSION = 1;
|
|
82
|
+
/**
|
|
83
|
+
* Plugin API v1. Every hook is optional. Hooks run in `use()` order.
|
|
84
|
+
*/
|
|
85
|
+
interface Plugin<Name extends string = string, Ext extends RepositoryExtension = NoExtension> {
|
|
86
|
+
readonly apiVersion: 1;
|
|
87
|
+
readonly name: Name;
|
|
88
|
+
/**
|
|
89
|
+
* Hook order: `pre` plugins run first, `post` plugins last, the rest in
|
|
90
|
+
* `use()` order. Validation is `post` so it sees columns other plugins fill.
|
|
91
|
+
*/
|
|
92
|
+
readonly enforce?: "pre" | "post";
|
|
93
|
+
/** `ir`: rewrite an operation before it runs (filters, defaults). */
|
|
94
|
+
readonly transformQuery?: (op: Operation, args: HookArgs) => Operation;
|
|
95
|
+
/** `mutation`: rewrite a mutation or reject it by throwing a `DbException`. */
|
|
96
|
+
readonly beforeMutation?: (op: MutationOp, args: HookArgs) => MutationOp | Promise<MutationOp>;
|
|
97
|
+
/** `mutation`: observe a finished mutation. Cannot change the result. */
|
|
98
|
+
readonly afterMutation?: (event: MutationEvent) => void | Promise<void>;
|
|
99
|
+
/** `execute`: wrap the executor for tracing, caching or retries. */
|
|
100
|
+
readonly wrapExecutor?: (executor: Executor) => Executor;
|
|
101
|
+
/** `errors`: map custom SQLSTATE or hint codes to `DbError`s. */
|
|
102
|
+
readonly mapError?: ErrorMapper;
|
|
103
|
+
/** `repository`: add methods to the repositories of matching tables. */
|
|
104
|
+
readonly repository?: (api: RepositoryApi) => Readonly<Record<string, (...args: never[]) => unknown>> | undefined;
|
|
105
|
+
/** Phantom carrying the repository extension type. */
|
|
106
|
+
readonly "~ext"?: Ext;
|
|
107
|
+
}
|
|
108
|
+
type AnyPlugin = Plugin<string, any>;
|
|
109
|
+
/** The plugin's repository extension; `unknown` for plugins that add none. */
|
|
110
|
+
type ExtensionOf<P> = P extends Plugin<string, infer E> ? E extends NoExtension ? unknown : E : never;
|
|
111
|
+
/** `E` with extension `X` appended, as a tuple. */
|
|
112
|
+
type WithExtension<E, X> = unknown extends X ? E : E extends readonly unknown[] ? readonly [...E, X] : unknown extends E ? readonly [X] : readonly [E, X];
|
|
113
|
+
/** Identity helper that fixes `apiVersion` and infers the plugin types. */
|
|
114
|
+
declare function definePlugin<const Name extends string, Ext extends RepositoryExtension = NoExtension>(plugin: Omit<Plugin<Name, Ext>, "apiVersion"> & {
|
|
115
|
+
readonly apiVersion?: 1;
|
|
116
|
+
}): Plugin<Name, Ext>;
|
|
117
|
+
//#endregion
|
|
118
|
+
export { definePlugin as _, ExtensionOf as a, MutationEvent as c, PLUGIN_API_VERSION as d, Plugin as f, WithExtension as g, RequestContext as h, CallOptions as i, MutationKind as l, RepositoryExtension as m, AnyPlugin as n, HasFlag as o, RepositoryApi as p, ApplyExtension as r, HookArgs as s, Actor as t, NoExtension as u };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { f as Plugin, h as RequestContext } from "../../plugin-C4TR9J0Z.js";
|
|
2
|
+
//#region src/plugins/actor/index.d.ts
|
|
3
|
+
export interface ActorOptions {
|
|
4
|
+
/** Custom actor id resolution. Defaults to `context.actor.id` for users and services. */
|
|
5
|
+
readonly resolve?: (context: RequestContext) => string | undefined;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Stamps the columns generated as `Flags.actor`: `createdBy` on insert and
|
|
9
|
+
* `updatedBy` on insert and update. Soft deletes are updates, so they record
|
|
10
|
+
* who deleted the row. Without an actor (anonymous requests) nothing is set.
|
|
11
|
+
*/
|
|
12
|
+
export declare function actor(options?: ActorOptions): Plugin<"actor">;
|
|
13
|
+
//#endregion
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { n as definePlugin } from "../../plugin-BqR6wKMB.js";
|
|
2
|
+
import { i as insertsOnly, l as withDefault, t as dbName } from "../../shared-_6-uPL7E.js";
|
|
3
|
+
//#region src/plugins/actor/index.ts
|
|
4
|
+
function defaultActor(context) {
|
|
5
|
+
const actor = context.actor;
|
|
6
|
+
return actor && actor.kind !== "anon" ? actor.id : void 0;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Stamps the columns generated as `Flags.actor`: `createdBy` on insert and
|
|
10
|
+
* `updatedBy` on insert and update. Soft deletes are updates, so they record
|
|
11
|
+
* who deleted the row. Without an actor (anonymous requests) nothing is set.
|
|
12
|
+
*/
|
|
13
|
+
function actor(options = {}) {
|
|
14
|
+
const resolve = options.resolve ?? defaultActor;
|
|
15
|
+
return definePlugin({
|
|
16
|
+
name: "actor",
|
|
17
|
+
beforeMutation(op, { table, context }) {
|
|
18
|
+
const flags = table.flags.actor;
|
|
19
|
+
if (!flags) return op;
|
|
20
|
+
const id = resolve(context);
|
|
21
|
+
if (id === void 0) return op;
|
|
22
|
+
const created = dbName(table, flags.createdBy);
|
|
23
|
+
const updated = dbName(table, flags.updatedBy);
|
|
24
|
+
switch (op.kind) {
|
|
25
|
+
case "insert": {
|
|
26
|
+
const stampCreated = insertsOnly(op);
|
|
27
|
+
return {
|
|
28
|
+
...op,
|
|
29
|
+
rows: op.rows.map((row) => withDefault(stampCreated ? withDefault(row, created, id) : row, updated, id))
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
case "update": return {
|
|
33
|
+
...op,
|
|
34
|
+
set: withDefault(op.set, updated, id)
|
|
35
|
+
};
|
|
36
|
+
case "delete": return op;
|
|
37
|
+
default: return op;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { actor };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { g as Operation } from "../../executor-CGdNQ27g.js";
|
|
2
|
+
import { f as Plugin, m as RepositoryExtension } from "../../plugin-C4TR9J0Z.js";
|
|
3
|
+
//#region src/plugins/rules/index.d.ts
|
|
4
|
+
export type RuleLevel = "off" | "warn" | "error";
|
|
5
|
+
/** A level, or a level with the rule's option: `['error', 500]`. */
|
|
6
|
+
export type RuleSetting<Option = never> = RuleLevel | ([Option] extends [never] ? never : readonly [RuleLevel, Option]);
|
|
7
|
+
export interface RuleSet {
|
|
8
|
+
/** `findMany` without `limit` (reads the whole table). */
|
|
9
|
+
readonly noUnboundedFindMany?: RuleSetting;
|
|
10
|
+
/** `limit` above the given maximum (default 1000). */
|
|
11
|
+
readonly maxLimit?: RuleSetting<number>;
|
|
12
|
+
/**
|
|
13
|
+
* Paging (`offset`, or `limit` above 1) without `orderBy`: Postgres returns
|
|
14
|
+
* rows in no particular order, so pages overlap or skip rows. `findMany`
|
|
15
|
+
* orders by the primary key by default, so this only fires on views and
|
|
16
|
+
* tables without one.
|
|
17
|
+
*/
|
|
18
|
+
readonly requireOrderByForCursor?: RuleSetting;
|
|
19
|
+
/** Includes nested deeper than the given depth (default 3). */
|
|
20
|
+
readonly maxIncludeDepth?: RuleSetting<number>;
|
|
21
|
+
/**
|
|
22
|
+
* Tenant tables queried without a tenant: neither `context.tenant` nor the
|
|
23
|
+
* given claim (default `config.claims.tenant`, `tenant_id`). Service
|
|
24
|
+
* connections are exempt.
|
|
25
|
+
*/
|
|
26
|
+
readonly requireTenantContext?: RuleSetting<string>;
|
|
27
|
+
/** A service-role connection used where `window` and `document` exist. */
|
|
28
|
+
readonly noAdminInBrowser?: RuleSetting;
|
|
29
|
+
/**
|
|
30
|
+
* Reads of columns listed in `config.sensitive` without `sensitive: true`
|
|
31
|
+
* on the call.
|
|
32
|
+
*/
|
|
33
|
+
readonly noSensitiveSelect?: RuleSetting;
|
|
34
|
+
/**
|
|
35
|
+
* Delete operations without `where`. The repository refuses `deleteMany`
|
|
36
|
+
* without `where` on its own, so this catches deletes built by plugins;
|
|
37
|
+
* the lint rule of the same name flags the call in the editor.
|
|
38
|
+
*/
|
|
39
|
+
readonly noDeleteManyWithoutWhere?: RuleSetting;
|
|
40
|
+
/**
|
|
41
|
+
* Storage objects written to `*_url` text columns: a column listed in
|
|
42
|
+
* `storagePaths`, or a value that is a Storage URL or matches a bucket's
|
|
43
|
+
* path template. Signed URLs expire and public URLs pin the project host,
|
|
44
|
+
* so store the path in a `*_path` column and build URLs when rendering.
|
|
45
|
+
*/
|
|
46
|
+
readonly storagePathColumns?: RuleSetting;
|
|
47
|
+
}
|
|
48
|
+
export type RuleName = keyof RuleSet;
|
|
49
|
+
export interface RuleViolation {
|
|
50
|
+
readonly rule: RuleName;
|
|
51
|
+
readonly level: "warn" | "error";
|
|
52
|
+
readonly table: string;
|
|
53
|
+
readonly operation: Operation["kind"];
|
|
54
|
+
readonly message: string;
|
|
55
|
+
}
|
|
56
|
+
export interface RulesOptions {
|
|
57
|
+
readonly rules?: RuleSet;
|
|
58
|
+
/**
|
|
59
|
+
* Called for every violation. Defaults to `console.warn` for warnings.
|
|
60
|
+
* Errors fail the call with `invalid_request` after reporting.
|
|
61
|
+
*/
|
|
62
|
+
readonly report?: (violation: RuleViolation) => void;
|
|
63
|
+
}
|
|
64
|
+
export interface RulesFindArgs {
|
|
65
|
+
/** Allow reading `config.sensitive` columns in this call. */
|
|
66
|
+
readonly sensitive?: boolean;
|
|
67
|
+
}
|
|
68
|
+
export interface RulesExtension extends RepositoryExtension {
|
|
69
|
+
readonly findArgs: RulesFindArgs;
|
|
70
|
+
}
|
|
71
|
+
/** Catches data-exposure and correctness mistakes; everything else off. */
|
|
72
|
+
export declare function safe(): RuleSet;
|
|
73
|
+
/** `safe()` plus performance warnings. */
|
|
74
|
+
export declare function recommended(): RuleSet;
|
|
75
|
+
/** Every rule as an error. */
|
|
76
|
+
export declare function strict(): RuleSet;
|
|
77
|
+
/**
|
|
78
|
+
* Runtime query rules, checked before each request is built:
|
|
79
|
+
*
|
|
80
|
+
* ```ts
|
|
81
|
+
* defineSupabase(schema, { plugins: [rules({ rules: recommended() })] });
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* Runs first (`enforce: 'pre'`) so it sees the query as written, before
|
|
85
|
+
* tenant or soft-delete filters are added. Pair with the static
|
|
86
|
+
* `better-supabase/lint` plugin to catch the same mistakes in the editor.
|
|
87
|
+
*/
|
|
88
|
+
export declare function rules(options?: RulesOptions): Plugin<"rules", RulesExtension>;
|
|
89
|
+
//#endregion
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { n as dbError, t as DbException } from "../../errors-Bl3rkOzo.js";
|
|
2
|
+
import { n as definePlugin } from "../../plugin-BqR6wKMB.js";
|
|
3
|
+
import { n as claimAt, r as claimsOf } from "../../claims-CoTobNC-.js";
|
|
4
|
+
//#region src/plugins/rules/index.ts
|
|
5
|
+
/** Catches data-exposure and correctness mistakes; everything else off. */
|
|
6
|
+
function safe() {
|
|
7
|
+
return {
|
|
8
|
+
noAdminInBrowser: "error",
|
|
9
|
+
noDeleteManyWithoutWhere: "error",
|
|
10
|
+
noSensitiveSelect: "error",
|
|
11
|
+
requireTenantContext: "error"
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/** `safe()` plus performance warnings. */
|
|
15
|
+
function recommended() {
|
|
16
|
+
return {
|
|
17
|
+
...safe(),
|
|
18
|
+
noUnboundedFindMany: "warn",
|
|
19
|
+
maxLimit: ["warn", 1e3],
|
|
20
|
+
requireOrderByForCursor: "warn",
|
|
21
|
+
maxIncludeDepth: ["warn", 3],
|
|
22
|
+
storagePathColumns: "warn"
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Every rule as an error. */
|
|
26
|
+
function strict() {
|
|
27
|
+
return {
|
|
28
|
+
...safe(),
|
|
29
|
+
noUnboundedFindMany: "error",
|
|
30
|
+
maxLimit: ["error", 1e3],
|
|
31
|
+
requireOrderByForCursor: "error",
|
|
32
|
+
maxIncludeDepth: ["error", 3],
|
|
33
|
+
storagePathColumns: "error"
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function levelOf(setting) {
|
|
37
|
+
if (setting === void 0) return "off";
|
|
38
|
+
return typeof setting === "string" ? setting : setting[0];
|
|
39
|
+
}
|
|
40
|
+
function optionOf(setting, fallback) {
|
|
41
|
+
return Array.isArray(setting) ? setting[1] : fallback;
|
|
42
|
+
}
|
|
43
|
+
function includeDepth(selection) {
|
|
44
|
+
let depth = 0;
|
|
45
|
+
for (const include of selection.includes) if (include.count !== void 0 || include.aggregate !== void 0) depth = Math.max(depth, 1);
|
|
46
|
+
else depth = Math.max(depth, 1 + includeDepth(include.selection));
|
|
47
|
+
return depth;
|
|
48
|
+
}
|
|
49
|
+
function sensitiveColumns(selection, table) {
|
|
50
|
+
const found = [];
|
|
51
|
+
const sensitive = new Set(Object.values(table.columns).filter((column) => column.sensitive).map((column) => column.db));
|
|
52
|
+
const columns = [...selection.columns, ...selection.aggregate?.measures ?? []];
|
|
53
|
+
for (const column of columns) if (sensitive.has(column.column)) found.push(`${table.key}.${column.column}`);
|
|
54
|
+
for (const include of selection.includes) found.push(...sensitiveColumns(include.selection, include.target));
|
|
55
|
+
return found;
|
|
56
|
+
}
|
|
57
|
+
function isBrowser() {
|
|
58
|
+
return typeof globalThis.window !== "undefined" && typeof globalThis.document !== "undefined";
|
|
59
|
+
}
|
|
60
|
+
const TEXT_TYPES = /* @__PURE__ */ new Set([
|
|
61
|
+
"text",
|
|
62
|
+
"varchar",
|
|
63
|
+
"bpchar"
|
|
64
|
+
]);
|
|
65
|
+
const STORAGE_URL = /\/storage\/v1\/(?:object|render\/image)\/(?:public|sign|authenticated)\//;
|
|
66
|
+
const templatesBySchema = /* @__PURE__ */ new WeakMap();
|
|
67
|
+
/** Bucket templates specific enough to identify a path: some literal text besides `/`. */
|
|
68
|
+
function bucketTemplates(schema) {
|
|
69
|
+
let templates = templatesBySchema.get(schema);
|
|
70
|
+
if (!templates) {
|
|
71
|
+
templates = Object.values(schema.buckets ?? {}).filter((bucket) => bucket.path.replace(/\{[^}]*\}|\//g, "").length > 0).map((bucket) => new RegExp(`^${bucket.path.split(/\{[^}]*\}/).map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[^/]+")}$`));
|
|
72
|
+
templatesBySchema.set(schema, templates);
|
|
73
|
+
}
|
|
74
|
+
return templates;
|
|
75
|
+
}
|
|
76
|
+
function storedObjectColumn(op, schema) {
|
|
77
|
+
const rows = op.kind === "insert" ? op.rows : op.kind === "update" ? [op.set] : [];
|
|
78
|
+
if (rows.length === 0) return void 0;
|
|
79
|
+
for (const column of Object.values(op.table.columns)) {
|
|
80
|
+
if (!rows.some((row) => column.db in row) || !column.db.endsWith("_url") || column.array || !TEXT_TYPES.has(column.type)) continue;
|
|
81
|
+
const path = `${column.db.slice(0, -4)}_path`;
|
|
82
|
+
if (column.storage !== void 0) return `"${column.db}" holds paths in bucket "${column.storage}"; name it "${path}"`;
|
|
83
|
+
if (rows.some((row) => {
|
|
84
|
+
const value = row[column.db];
|
|
85
|
+
return typeof value === "string" && (STORAGE_URL.test(value) || bucketTemplates(schema).some((template) => template.test(value)));
|
|
86
|
+
})) return `"${column.db}" is given a storage object; store its path in "${path}" and build URLs with signedUrl() or renderUrl()`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function checks(rules) {
|
|
90
|
+
return {
|
|
91
|
+
noUnboundedFindMany: (op) => op.kind === "select" && op.single === void 0 && !op.head && op.limit === void 0 ? "findMany without limit reads every visible row" : void 0,
|
|
92
|
+
maxLimit: (op) => {
|
|
93
|
+
const max = optionOf(rules.maxLimit, 1e3);
|
|
94
|
+
return op.kind === "select" && op.limit !== void 0 && op.limit > max ? `limit ${op.limit} is above the maximum of ${max}` : void 0;
|
|
95
|
+
},
|
|
96
|
+
requireOrderByForCursor: (op) => op.kind === "select" && !op.head && op.orderBy.length === 0 && (op.offset !== void 0 || op.limit !== void 0 && op.limit > 1) ? "paging without orderBy returns rows in no stable order" : void 0,
|
|
97
|
+
maxIncludeDepth: (op) => {
|
|
98
|
+
const max = optionOf(rules.maxIncludeDepth, 3);
|
|
99
|
+
const selection = op.kind === "select" ? op.selection : op.returning;
|
|
100
|
+
const depth = selection ? includeDepth(selection) : 0;
|
|
101
|
+
return depth > max ? `includes nest ${depth} levels deep (maximum ${max})` : void 0;
|
|
102
|
+
},
|
|
103
|
+
requireTenantContext: (op, { context, schema }) => {
|
|
104
|
+
if (!op.table.flags.tenant || context.actor?.kind === "service") return void 0;
|
|
105
|
+
const claim = optionOf(rules.requireTenantContext, claimsOf(schema).tenant);
|
|
106
|
+
return context.tenant === void 0 && claimAt(context.claims, claim) === void 0 ? `"${op.table.key}" is tenant-scoped but the request has no tenant (context.tenant or claim "${claim}")` : void 0;
|
|
107
|
+
},
|
|
108
|
+
noAdminInBrowser: (_op, { context }) => context.actor?.kind === "service" && isBrowser() ? "a service-role connection is running in a browser" : void 0,
|
|
109
|
+
noSensitiveSelect: (op, { options }) => {
|
|
110
|
+
if (options["sensitive"] === true) return void 0;
|
|
111
|
+
const selection = op.kind === "select" ? op.selection : op.returning;
|
|
112
|
+
const columns = selection ? sensitiveColumns(selection, op.table) : [];
|
|
113
|
+
return columns.length > 0 ? `reads sensitive column(s) ${columns.join(", ")}; pass { sensitive: true } to allow` : void 0;
|
|
114
|
+
},
|
|
115
|
+
noDeleteManyWithoutWhere: (op) => op.kind === "delete" && op.where === void 0 ? "delete without where removes every visible row" : void 0,
|
|
116
|
+
storagePathColumns: (op, { schema }) => storedObjectColumn(op, schema)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function defaultReport(violation) {
|
|
120
|
+
if (violation.level === "warn") console.warn(`[better-supabase] ${violation.rule} (${violation.table}): ${violation.message}`);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Runtime query rules, checked before each request is built:
|
|
124
|
+
*
|
|
125
|
+
* ```ts
|
|
126
|
+
* defineSupabase(schema, { plugins: [rules({ rules: recommended() })] });
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* Runs first (`enforce: 'pre'`) so it sees the query as written, before
|
|
130
|
+
* tenant or soft-delete filters are added. Pair with the static
|
|
131
|
+
* `better-supabase/lint` plugin to catch the same mistakes in the editor.
|
|
132
|
+
*/
|
|
133
|
+
function rules(options = {}) {
|
|
134
|
+
const set = options.rules ?? recommended();
|
|
135
|
+
const report = options.report ?? defaultReport;
|
|
136
|
+
const all = checks(set);
|
|
137
|
+
const active = Object.keys(all).flatMap((rule) => {
|
|
138
|
+
const level = levelOf(set[rule]);
|
|
139
|
+
return level === "off" ? [] : [{
|
|
140
|
+
rule,
|
|
141
|
+
level,
|
|
142
|
+
check: all[rule]
|
|
143
|
+
}];
|
|
144
|
+
});
|
|
145
|
+
return definePlugin({
|
|
146
|
+
name: "rules",
|
|
147
|
+
enforce: "pre",
|
|
148
|
+
transformQuery(op, hook) {
|
|
149
|
+
for (const { rule, level, check } of active) {
|
|
150
|
+
const message = check(op, hook);
|
|
151
|
+
if (message === void 0) continue;
|
|
152
|
+
const violation = {
|
|
153
|
+
rule,
|
|
154
|
+
level,
|
|
155
|
+
table: op.table.key,
|
|
156
|
+
operation: op.kind,
|
|
157
|
+
message
|
|
158
|
+
};
|
|
159
|
+
report(violation);
|
|
160
|
+
if (level === "error") throw new DbException(dbError("invalid_request", `${rule}: ${message}`, { table: op.table.key }));
|
|
161
|
+
}
|
|
162
|
+
return op;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
//#endregion
|
|
167
|
+
export { recommended, rules, safe, strict };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { C as PrimaryKeyValue } from "../../types-C83v5SQz.js";
|
|
2
|
+
import { t as AsyncResult } from "../../result-CTkCZ6JA.js";
|
|
3
|
+
import { f as Plugin, m as RepositoryExtension, o as HasFlag } from "../../plugin-C4TR9J0Z.js";
|
|
4
|
+
//#region src/plugins/soft-delete/index.d.ts
|
|
5
|
+
export interface SoftDeleteFindArgs {
|
|
6
|
+
/** Include soft-deleted rows of the queried table. */
|
|
7
|
+
readonly withDeleted?: boolean;
|
|
8
|
+
/** Only return soft-deleted rows of the queried table. */
|
|
9
|
+
readonly onlyDeleted?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface SoftDeleteDeleteArgs {
|
|
12
|
+
/** Delete the row for real instead of setting the soft-delete column. */
|
|
13
|
+
readonly hard?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface SoftDeleteMethods<K> {
|
|
16
|
+
/** Clears the soft-delete column. `not_found` when the row does not exist. */
|
|
17
|
+
readonly restore: (key: K, args?: {
|
|
18
|
+
readonly signal?: AbortSignal;
|
|
19
|
+
}) => AsyncResult<void>;
|
|
20
|
+
}
|
|
21
|
+
export interface SoftDeleteExtension extends RepositoryExtension {
|
|
22
|
+
readonly methods: HasFlag<this["M"], this["T"], "softDelete"> extends true ? SoftDeleteMethods<PrimaryKeyValue<this["M"], this["T"]>> : unknown;
|
|
23
|
+
readonly findArgs: HasFlag<this["M"], this["T"], "softDelete"> extends true ? SoftDeleteFindArgs : unknown;
|
|
24
|
+
readonly deleteArgs: HasFlag<this["M"], this["T"], "softDelete"> extends true ? SoftDeleteDeleteArgs : unknown;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Soft delete for tables generated with `Flags.softDelete`:
|
|
28
|
+
*
|
|
29
|
+
* - reads, updates and deletes skip deleted rows, including inside includes
|
|
30
|
+
* and relation filters (`withDeleted` / `onlyDeleted` change the root table);
|
|
31
|
+
* - `delete` sets the column instead (`{ hard: true }` really deletes);
|
|
32
|
+
* - `restore(key)` clears it.
|
|
33
|
+
*
|
|
34
|
+
* Deletes become updates without `RETURNING`, so a SELECT policy that hides
|
|
35
|
+
* deleted rows does not make them fail.
|
|
36
|
+
*/
|
|
37
|
+
export declare function softDelete(): Plugin<"softDelete", SoftDeleteExtension>;
|
|
38
|
+
//#endregion
|