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,118 @@
|
|
|
1
|
+
import { A as TableMeta, E as SchemaMeta, n as AnyModels } from "./types-C2fslP1z.js";
|
|
2
|
+
import { g as Operation, h as MutationOp, r as Executor } from "./executor-Bazhgsgq.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-HvGqCurB.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,88 @@
|
|
|
1
|
+
import { g as Operation } from "../../executor-Bazhgsgq.js";
|
|
2
|
+
import { f as Plugin, m as RepositoryExtension } from "../../plugin-HvGqCurB.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 `org_id`). Service connections are exempt.
|
|
24
|
+
*/
|
|
25
|
+
readonly requireTenantContext?: RuleSetting<string>;
|
|
26
|
+
/** A service-role connection used where `window` and `document` exist. */
|
|
27
|
+
readonly noAdminInBrowser?: RuleSetting;
|
|
28
|
+
/**
|
|
29
|
+
* Reads of columns listed in `config.sensitive` without `sensitive: true`
|
|
30
|
+
* on the call.
|
|
31
|
+
*/
|
|
32
|
+
readonly noSensitiveSelect?: RuleSetting;
|
|
33
|
+
/**
|
|
34
|
+
* Delete operations without `where`. The repository refuses `deleteMany`
|
|
35
|
+
* without `where` on its own, so this catches deletes built by plugins;
|
|
36
|
+
* the lint rule of the same name flags the call in the editor.
|
|
37
|
+
*/
|
|
38
|
+
readonly noDeleteManyWithoutWhere?: RuleSetting;
|
|
39
|
+
/**
|
|
40
|
+
* Storage objects written to `*_url` text columns: a column listed in
|
|
41
|
+
* `storagePaths`, or a value that is a Storage URL or matches a bucket's
|
|
42
|
+
* path template. Signed URLs expire and public URLs pin the project host,
|
|
43
|
+
* so store the path in a `*_path` column and build URLs when rendering.
|
|
44
|
+
*/
|
|
45
|
+
readonly storagePathColumns?: RuleSetting;
|
|
46
|
+
}
|
|
47
|
+
export type RuleName = keyof RuleSet;
|
|
48
|
+
export interface RuleViolation {
|
|
49
|
+
readonly rule: RuleName;
|
|
50
|
+
readonly level: "warn" | "error";
|
|
51
|
+
readonly table: string;
|
|
52
|
+
readonly operation: Operation["kind"];
|
|
53
|
+
readonly message: string;
|
|
54
|
+
}
|
|
55
|
+
export interface RulesOptions {
|
|
56
|
+
readonly rules?: RuleSet;
|
|
57
|
+
/**
|
|
58
|
+
* Called for every violation. Defaults to `console.warn` for warnings.
|
|
59
|
+
* Errors fail the call with `invalid_request` after reporting.
|
|
60
|
+
*/
|
|
61
|
+
readonly report?: (violation: RuleViolation) => void;
|
|
62
|
+
}
|
|
63
|
+
export interface RulesFindArgs {
|
|
64
|
+
/** Allow reading `config.sensitive` columns in this call. */
|
|
65
|
+
readonly sensitive?: boolean;
|
|
66
|
+
}
|
|
67
|
+
export interface RulesExtension extends RepositoryExtension {
|
|
68
|
+
readonly findArgs: RulesFindArgs;
|
|
69
|
+
}
|
|
70
|
+
/** Catches data-exposure and correctness mistakes; everything else off. */
|
|
71
|
+
export declare function safe(): RuleSet;
|
|
72
|
+
/** `safe()` plus performance warnings. */
|
|
73
|
+
export declare function recommended(): RuleSet;
|
|
74
|
+
/** Every rule as an error. */
|
|
75
|
+
export declare function strict(): RuleSet;
|
|
76
|
+
/**
|
|
77
|
+
* Runtime query rules, checked before each request is built:
|
|
78
|
+
*
|
|
79
|
+
* ```ts
|
|
80
|
+
* defineSupabase(schema, { plugins: [rules({ rules: recommended() })] });
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* Runs first (`enforce: 'pre'`) so it sees the query as written, before
|
|
84
|
+
* tenant or soft-delete filters are added. Pair with the static
|
|
85
|
+
* `better-supabase/lint` plugin to catch the same mistakes in the editor.
|
|
86
|
+
*/
|
|
87
|
+
export declare function rules(options?: RulesOptions): Plugin<"rules", RulesExtension>;
|
|
88
|
+
//#endregion
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { n as dbError, t as DbException } from "../../errors-Bl3rkOzo.js";
|
|
2
|
+
import { n as definePlugin } from "../../plugin-BqR6wKMB.js";
|
|
3
|
+
//#region src/plugins/rules/index.ts
|
|
4
|
+
/** Catches data-exposure and correctness mistakes; everything else off. */
|
|
5
|
+
function safe() {
|
|
6
|
+
return {
|
|
7
|
+
noAdminInBrowser: "error",
|
|
8
|
+
noDeleteManyWithoutWhere: "error",
|
|
9
|
+
noSensitiveSelect: "error",
|
|
10
|
+
requireTenantContext: "error"
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/** `safe()` plus performance warnings. */
|
|
14
|
+
function recommended() {
|
|
15
|
+
return {
|
|
16
|
+
...safe(),
|
|
17
|
+
noUnboundedFindMany: "warn",
|
|
18
|
+
maxLimit: ["warn", 1e3],
|
|
19
|
+
requireOrderByForCursor: "warn",
|
|
20
|
+
maxIncludeDepth: ["warn", 3],
|
|
21
|
+
storagePathColumns: "warn"
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Every rule as an error. */
|
|
25
|
+
function strict() {
|
|
26
|
+
return {
|
|
27
|
+
...safe(),
|
|
28
|
+
noUnboundedFindMany: "error",
|
|
29
|
+
maxLimit: ["error", 1e3],
|
|
30
|
+
requireOrderByForCursor: "error",
|
|
31
|
+
maxIncludeDepth: ["error", 3],
|
|
32
|
+
storagePathColumns: "error"
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function levelOf(setting) {
|
|
36
|
+
if (setting === void 0) return "off";
|
|
37
|
+
return typeof setting === "string" ? setting : setting[0];
|
|
38
|
+
}
|
|
39
|
+
function optionOf(setting, fallback) {
|
|
40
|
+
return Array.isArray(setting) ? setting[1] : fallback;
|
|
41
|
+
}
|
|
42
|
+
function includeDepth(selection) {
|
|
43
|
+
let depth = 0;
|
|
44
|
+
for (const include of selection.includes) if (include.count !== void 0 || include.aggregate !== void 0) depth = Math.max(depth, 1);
|
|
45
|
+
else depth = Math.max(depth, 1 + includeDepth(include.selection));
|
|
46
|
+
return depth;
|
|
47
|
+
}
|
|
48
|
+
function sensitiveColumns(selection, table) {
|
|
49
|
+
const found = [];
|
|
50
|
+
const sensitive = new Set(Object.values(table.columns).filter((column) => column.sensitive).map((column) => column.db));
|
|
51
|
+
const columns = [...selection.columns, ...selection.aggregate?.measures ?? []];
|
|
52
|
+
for (const column of columns) if (sensitive.has(column.column)) found.push(`${table.key}.${column.column}`);
|
|
53
|
+
for (const include of selection.includes) found.push(...sensitiveColumns(include.selection, include.target));
|
|
54
|
+
return found;
|
|
55
|
+
}
|
|
56
|
+
function isBrowser() {
|
|
57
|
+
return typeof globalThis.window !== "undefined" && typeof globalThis.document !== "undefined";
|
|
58
|
+
}
|
|
59
|
+
function claimValue(context, claim) {
|
|
60
|
+
let value = context.claims;
|
|
61
|
+
for (const part of claim.split(".")) {
|
|
62
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
63
|
+
value = value[part];
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
const TEXT_TYPES = /* @__PURE__ */ new Set([
|
|
68
|
+
"text",
|
|
69
|
+
"varchar",
|
|
70
|
+
"bpchar"
|
|
71
|
+
]);
|
|
72
|
+
const STORAGE_URL = /\/storage\/v1\/(?:object|render\/image)\/(?:public|sign|authenticated)\//;
|
|
73
|
+
const templatesBySchema = /* @__PURE__ */ new WeakMap();
|
|
74
|
+
/** Bucket templates specific enough to identify a path: some literal text besides `/`. */
|
|
75
|
+
function bucketTemplates(schema) {
|
|
76
|
+
let templates = templatesBySchema.get(schema);
|
|
77
|
+
if (!templates) {
|
|
78
|
+
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("[^/]+")}$`));
|
|
79
|
+
templatesBySchema.set(schema, templates);
|
|
80
|
+
}
|
|
81
|
+
return templates;
|
|
82
|
+
}
|
|
83
|
+
function storedObjectColumn(op, schema) {
|
|
84
|
+
const rows = op.kind === "insert" ? op.rows : op.kind === "update" ? [op.set] : [];
|
|
85
|
+
if (rows.length === 0) return void 0;
|
|
86
|
+
for (const column of Object.values(op.table.columns)) {
|
|
87
|
+
if (!rows.some((row) => column.db in row) || !column.db.endsWith("_url") || column.array || !TEXT_TYPES.has(column.type)) continue;
|
|
88
|
+
const path = `${column.db.slice(0, -4)}_path`;
|
|
89
|
+
if (column.storage !== void 0) return `"${column.db}" holds paths in bucket "${column.storage}"; name it "${path}"`;
|
|
90
|
+
if (rows.some((row) => {
|
|
91
|
+
const value = row[column.db];
|
|
92
|
+
return typeof value === "string" && (STORAGE_URL.test(value) || bucketTemplates(schema).some((template) => template.test(value)));
|
|
93
|
+
})) return `"${column.db}" is given a storage object; store its path in "${path}" and build URLs with signedUrl() or renderUrl()`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function checks(rules) {
|
|
97
|
+
return {
|
|
98
|
+
noUnboundedFindMany: (op) => op.kind === "select" && op.single === void 0 && !op.head && op.limit === void 0 ? "findMany without limit reads every visible row" : void 0,
|
|
99
|
+
maxLimit: (op) => {
|
|
100
|
+
const max = optionOf(rules.maxLimit, 1e3);
|
|
101
|
+
return op.kind === "select" && op.limit !== void 0 && op.limit > max ? `limit ${op.limit} is above the maximum of ${max}` : void 0;
|
|
102
|
+
},
|
|
103
|
+
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,
|
|
104
|
+
maxIncludeDepth: (op) => {
|
|
105
|
+
const max = optionOf(rules.maxIncludeDepth, 3);
|
|
106
|
+
const selection = op.kind === "select" ? op.selection : op.returning;
|
|
107
|
+
const depth = selection ? includeDepth(selection) : 0;
|
|
108
|
+
return depth > max ? `includes nest ${depth} levels deep (maximum ${max})` : void 0;
|
|
109
|
+
},
|
|
110
|
+
requireTenantContext: (op, { context }) => {
|
|
111
|
+
if (!op.table.flags.tenant || context.actor?.kind === "service") return void 0;
|
|
112
|
+
const claim = optionOf(rules.requireTenantContext, "org_id");
|
|
113
|
+
return context.tenant === void 0 && claimValue(context, claim) === void 0 ? `"${op.table.key}" is tenant-scoped but the request has no tenant (context.tenant or claim "${claim}")` : void 0;
|
|
114
|
+
},
|
|
115
|
+
noAdminInBrowser: (_op, { context }) => context.actor?.kind === "service" && isBrowser() ? "a service-role connection is running in a browser" : void 0,
|
|
116
|
+
noSensitiveSelect: (op, { options }) => {
|
|
117
|
+
if (options["sensitive"] === true) return void 0;
|
|
118
|
+
const selection = op.kind === "select" ? op.selection : op.returning;
|
|
119
|
+
const columns = selection ? sensitiveColumns(selection, op.table) : [];
|
|
120
|
+
return columns.length > 0 ? `reads sensitive column(s) ${columns.join(", ")}; pass { sensitive: true } to allow` : void 0;
|
|
121
|
+
},
|
|
122
|
+
noDeleteManyWithoutWhere: (op) => op.kind === "delete" && op.where === void 0 ? "delete without where removes every visible row" : void 0,
|
|
123
|
+
storagePathColumns: (op, { schema }) => storedObjectColumn(op, schema)
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function defaultReport(violation) {
|
|
127
|
+
if (violation.level === "warn") console.warn(`[better-supabase] ${violation.rule} (${violation.table}): ${violation.message}`);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Runtime query rules, checked before each request is built:
|
|
131
|
+
*
|
|
132
|
+
* ```ts
|
|
133
|
+
* defineSupabase(schema, { plugins: [rules({ rules: recommended() })] });
|
|
134
|
+
* ```
|
|
135
|
+
*
|
|
136
|
+
* Runs first (`enforce: 'pre'`) so it sees the query as written, before
|
|
137
|
+
* tenant or soft-delete filters are added. Pair with the static
|
|
138
|
+
* `better-supabase/lint` plugin to catch the same mistakes in the editor.
|
|
139
|
+
*/
|
|
140
|
+
function rules(options = {}) {
|
|
141
|
+
const set = options.rules ?? recommended();
|
|
142
|
+
const report = options.report ?? defaultReport;
|
|
143
|
+
const all = checks(set);
|
|
144
|
+
const active = Object.keys(all).flatMap((rule) => {
|
|
145
|
+
const level = levelOf(set[rule]);
|
|
146
|
+
return level === "off" ? [] : [{
|
|
147
|
+
rule,
|
|
148
|
+
level,
|
|
149
|
+
check: all[rule]
|
|
150
|
+
}];
|
|
151
|
+
});
|
|
152
|
+
return definePlugin({
|
|
153
|
+
name: "rules",
|
|
154
|
+
enforce: "pre",
|
|
155
|
+
transformQuery(op, hook) {
|
|
156
|
+
for (const { rule, level, check } of active) {
|
|
157
|
+
const message = check(op, hook);
|
|
158
|
+
if (message === void 0) continue;
|
|
159
|
+
const violation = {
|
|
160
|
+
rule,
|
|
161
|
+
level,
|
|
162
|
+
table: op.table.key,
|
|
163
|
+
operation: op.kind,
|
|
164
|
+
message
|
|
165
|
+
};
|
|
166
|
+
report(violation);
|
|
167
|
+
if (level === "error") throw new DbException(dbError("invalid_request", `${rule}: ${message}`, { table: op.table.key }));
|
|
168
|
+
}
|
|
169
|
+
return op;
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
//#endregion
|
|
174
|
+
export { recommended, rules, safe, strict };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { y as PrimaryKeyValue } from "../../types-C2fslP1z.js";
|
|
2
|
+
import { t as AsyncResult } from "../../result-CTkCZ6JA.js";
|
|
3
|
+
import { f as Plugin, m as RepositoryExtension, o as HasFlag } from "../../plugin-HvGqCurB.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
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { t as and } from "../../types-6cYK9pbJ.js";
|
|
2
|
+
import { i as ok, t as AsyncResult } from "../../result-DWXatkd6.js";
|
|
3
|
+
import { n as definePlugin } from "../../plugin-BqR6wKMB.js";
|
|
4
|
+
import { n as scopeOperation } from "../../scope-CDHcS7dE.js";
|
|
5
|
+
import { a as isNotNull, o as isNull, t as dbName } from "../../shared-_6-uPL7E.js";
|
|
6
|
+
//#region src/plugins/soft-delete/index.ts
|
|
7
|
+
function deletedColumn(table) {
|
|
8
|
+
return dbName(table, table.flags.softDelete);
|
|
9
|
+
}
|
|
10
|
+
function scopeFor(table) {
|
|
11
|
+
const column = deletedColumn(table);
|
|
12
|
+
return column ? isNull(column) : void 0;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Soft delete for tables generated with `Flags.softDelete`:
|
|
16
|
+
*
|
|
17
|
+
* - reads, updates and deletes skip deleted rows, including inside includes
|
|
18
|
+
* and relation filters (`withDeleted` / `onlyDeleted` change the root table);
|
|
19
|
+
* - `delete` sets the column instead (`{ hard: true }` really deletes);
|
|
20
|
+
* - `restore(key)` clears it.
|
|
21
|
+
*
|
|
22
|
+
* Deletes become updates without `RETURNING`, so a SELECT policy that hides
|
|
23
|
+
* deleted rows does not make them fail.
|
|
24
|
+
*/
|
|
25
|
+
function softDelete() {
|
|
26
|
+
return definePlugin({
|
|
27
|
+
name: "softDelete",
|
|
28
|
+
enforce: "pre",
|
|
29
|
+
transformQuery(op, { options }) {
|
|
30
|
+
const hardDelete = op.kind === "delete" && options["hard"] === true;
|
|
31
|
+
const withDeleted = options["withDeleted"] === true || hardDelete;
|
|
32
|
+
const onlyDeleted = options["onlyDeleted"] === true;
|
|
33
|
+
const scoped = scopeOperation(op, scopeFor, !withDeleted && !onlyDeleted);
|
|
34
|
+
const column = deletedColumn(op.table);
|
|
35
|
+
if (!onlyDeleted || !column || scoped.kind === "insert") return scoped;
|
|
36
|
+
return {
|
|
37
|
+
...scoped,
|
|
38
|
+
where: and(scoped.where, isNotNull(column))
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
beforeMutation(op, { table, options, now }) {
|
|
42
|
+
if (op.kind !== "delete" || options["hard"] === true) return op;
|
|
43
|
+
const column = deletedColumn(table);
|
|
44
|
+
if (!column) return op;
|
|
45
|
+
return {
|
|
46
|
+
kind: "update",
|
|
47
|
+
table,
|
|
48
|
+
set: { [column]: now().toISOString() },
|
|
49
|
+
where: op.where,
|
|
50
|
+
returning: void 0
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
repository({ table, base }) {
|
|
54
|
+
const flag = table.flags.softDelete;
|
|
55
|
+
if (!flag) return void 0;
|
|
56
|
+
const update = base["update"];
|
|
57
|
+
return { restore: (key, args) => AsyncResult.from(async () => {
|
|
58
|
+
const result = await update(key, { [flag]: null }, {
|
|
59
|
+
...args,
|
|
60
|
+
withDeleted: true,
|
|
61
|
+
returning: false
|
|
62
|
+
});
|
|
63
|
+
return result.ok ? ok(void 0) : result;
|
|
64
|
+
}) };
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
export { softDelete };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { f as Plugin, h as RequestContext, m as RepositoryExtension, o as HasFlag } from "../../plugin-HvGqCurB.js";
|
|
2
|
+
//#region src/plugins/tenant/index.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Dotted paths to the string claims of `C` (up to three levels), e.g.
|
|
5
|
+
* `'tenant_id' | 'app_metadata.tenant_id'`. Any string without a claims type.
|
|
6
|
+
*/
|
|
7
|
+
export type ClaimPath<C> = unknown extends C ? string : StringPaths<C, [1, 2, 3]>;
|
|
8
|
+
type StringPaths<T, Depth extends readonly unknown[]> = T extends object ? T extends readonly unknown[] ? never : { [K in keyof T & string]-?: (NonNullable<T[K]> extends string ? K : never) | (Depth extends readonly [unknown, ...infer Rest] ? `${K}.${StringPaths<NonNullable<T[K]>, Rest>}` : never); }[keyof T & string] : never;
|
|
9
|
+
/** `C` is the claims type (`sb.claims(schema)`'s output); it types `claim`. */
|
|
10
|
+
export interface TenantOptions<C = unknown> {
|
|
11
|
+
/**
|
|
12
|
+
* JWT claim path(s) holding the tenant id when `context.tenant` is not set.
|
|
13
|
+
* Dots walk into objects. Default `['org_id', 'app_metadata.org_id']`.
|
|
14
|
+
*/
|
|
15
|
+
readonly claim?: ClaimPath<C> | readonly ClaimPath<C>[];
|
|
16
|
+
/** Custom resolution, e.g. from a header validated against memberships. */
|
|
17
|
+
readonly resolve?: (context: RequestContext) => string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* What happens on a tenant table without a tenant in the context:
|
|
20
|
+
* `error` (default) fails with `forbidden`; `skip` leaves it to RLS.
|
|
21
|
+
*/
|
|
22
|
+
readonly onMissing?: "error" | "skip";
|
|
23
|
+
}
|
|
24
|
+
export interface TenantFindArgs {
|
|
25
|
+
/** Skip the tenant filter for this call (admin tooling, jobs). RLS still applies. */
|
|
26
|
+
readonly allTenants?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface TenantExtension extends RepositoryExtension {
|
|
29
|
+
readonly findArgs: HasFlag<this["M"], this["T"], "tenant"> extends true ? TenantFindArgs : unknown;
|
|
30
|
+
readonly methods: unknown;
|
|
31
|
+
readonly deleteArgs: HasFlag<this["M"], this["T"], "tenant"> extends true ? TenantFindArgs : unknown;
|
|
32
|
+
}
|
|
33
|
+
/** The tenant for a request: `context.tenant`, then the configured claim. */
|
|
34
|
+
export declare function resolveTenant<C = unknown>(context: RequestContext, options?: TenantOptions<C>): string | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Multi-tenancy for tables generated with `Flags.tenant`. Scopes reads,
|
|
37
|
+
* updates and deletes to the request's tenant (nested includes and relation
|
|
38
|
+
* filters too), fills the tenant column on insert and rejects writes that
|
|
39
|
+
* would move rows to another tenant. It complements RLS; it does not replace
|
|
40
|
+
* it.
|
|
41
|
+
*/
|
|
42
|
+
export declare function tenant<C = unknown>(options?: TenantOptions<C>): Plugin<"tenant", TenantExtension>;
|
|
43
|
+
//#endregion
|
|
@@ -0,0 +1,73 @@
|
|
|
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 scopeOperation } from "../../scope-CDHcS7dE.js";
|
|
4
|
+
import { n as equals, r as forbidden, t as dbName } from "../../shared-_6-uPL7E.js";
|
|
5
|
+
//#region src/plugins/tenant/index.ts
|
|
6
|
+
/** The tenant for a request: `context.tenant`, then the configured claim. */
|
|
7
|
+
function resolveTenant(context, options = {}) {
|
|
8
|
+
if (options.resolve) return options.resolve(context);
|
|
9
|
+
if (typeof context.tenant === "string") return context.tenant;
|
|
10
|
+
const claim = options.claim;
|
|
11
|
+
const paths = typeof claim === "string" ? [claim] : claim ?? DEFAULT_CLAIMS;
|
|
12
|
+
for (const path of paths) {
|
|
13
|
+
let value = context.claims;
|
|
14
|
+
for (const segment of path.split(".")) value = typeof value === "object" && value !== null ? value[segment] : void 0;
|
|
15
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const DEFAULT_CLAIMS = ["org_id", "app_metadata.org_id"];
|
|
19
|
+
function tenantColumn(table) {
|
|
20
|
+
return dbName(table, table.flags.tenant);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Multi-tenancy for tables generated with `Flags.tenant`. Scopes reads,
|
|
24
|
+
* updates and deletes to the request's tenant (nested includes and relation
|
|
25
|
+
* filters too), fills the tenant column on insert and rejects writes that
|
|
26
|
+
* would move rows to another tenant. It complements RLS; it does not replace
|
|
27
|
+
* it.
|
|
28
|
+
*/
|
|
29
|
+
function tenant(options = {}) {
|
|
30
|
+
const onMissing = options.onMissing ?? "error";
|
|
31
|
+
const current = (table, context) => {
|
|
32
|
+
const id = resolveTenant(context, options);
|
|
33
|
+
if (id === void 0 && onMissing === "error" && tenantColumn(table)) throw new DbException(dbError("forbidden", `No tenant in the request context for "${table.key}"`));
|
|
34
|
+
return id;
|
|
35
|
+
};
|
|
36
|
+
return definePlugin({
|
|
37
|
+
name: "tenant",
|
|
38
|
+
transformQuery(op, { context, options: call }) {
|
|
39
|
+
if (call["allTenants"] === true) return op;
|
|
40
|
+
const id = current(op.table, context);
|
|
41
|
+
if (id === void 0) return op;
|
|
42
|
+
return scopeOperation(op, (table) => {
|
|
43
|
+
const column = tenantColumn(table);
|
|
44
|
+
return column ? equals(column, id) : void 0;
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
beforeMutation(op, { table, context, options: call }) {
|
|
48
|
+
const column = tenantColumn(table);
|
|
49
|
+
if (!column || call["allTenants"] === true) return op;
|
|
50
|
+
const id = current(table, context);
|
|
51
|
+
if (id === void 0) return op;
|
|
52
|
+
switch (op.kind) {
|
|
53
|
+
case "insert": return {
|
|
54
|
+
...op,
|
|
55
|
+
rows: op.rows.map((row) => {
|
|
56
|
+
if (column in row && row[column] !== id) forbidden(`Cannot write a ${table.key} row for another tenant`);
|
|
57
|
+
return {
|
|
58
|
+
...row,
|
|
59
|
+
[column]: id
|
|
60
|
+
};
|
|
61
|
+
})
|
|
62
|
+
};
|
|
63
|
+
case "update":
|
|
64
|
+
if (column in op.set && op.set[column] !== id) forbidden(`Cannot move a ${table.key} row to another tenant`);
|
|
65
|
+
return op;
|
|
66
|
+
case "delete": return op;
|
|
67
|
+
default: return op;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
export { resolveTenant, tenant };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { f as Plugin } from "../../plugin-HvGqCurB.js";
|
|
2
|
+
//#region src/plugins/timestamps/index.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Stamps the columns generated as `Flags.timestamps`: `createdAt` on insert,
|
|
5
|
+
* `updatedAt` on insert and update. Values the caller passes win. Pair it with
|
|
6
|
+
* the `updated-at` SQL kit trigger to cover writes that bypass the app.
|
|
7
|
+
*/
|
|
8
|
+
export declare function timestamps(): Plugin<"timestamps">;
|
|
9
|
+
//#endregion
|