@zeno-lib/db 0.3.1 → 0.3.2

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.
@@ -0,0 +1,42 @@
1
+ import { AnyRelations, EmptyRelations } from "drizzle-orm";
2
+ import { DrizzlePgConfig } from "drizzle-orm/pg-core";
3
+ import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
4
+ import { JwtPayload, SupabaseClient } from "@supabase/supabase-js";
5
+ //#region src/clients.d.ts
6
+ type CloseFn = (options?: {
7
+ timeout?: number;
8
+ }) => Promise<void>;
9
+ type CreateClientConfig<TRelations extends AnyRelations = EmptyRelations> = DrizzlePgConfig<TRelations> & {
10
+ /** Overrides `process.env.SUPABASE_DATABASE_URL`. */
11
+ connectionString?: string;
12
+ };
13
+ type DrizzleClient<TRelations extends AnyRelations = EmptyRelations> = PostgresJsDatabase<TRelations> & {
14
+ close: CloseFn;
15
+ };
16
+ type SupabaseToken = Pick<JwtPayload, "role" | "sub">;
17
+ /**
18
+ * RLS-bypassing client (the docs' `admin`). Connects with
19
+ * `SUPABASE_DATABASE_URL` and queries drizzle directly. Use for webhooks,
20
+ * background jobs, and seeding, never for user-scoped reads/writes.
21
+ */
22
+ declare function createAdminClient<TRelations extends AnyRelations = EmptyRelations>(config?: CreateClientConfig<TRelations>): DrizzleClient<TRelations>;
23
+ /**
24
+ * RLS client scoped to an already-verified, decoded token (e.g. from
25
+ * `supabase.auth.getClaims()`). The role is clamped to the allowlist.
26
+ */
27
+ declare function createSupabaseClient<TRelations extends AnyRelations = EmptyRelations>(accessToken: SupabaseToken, config?: CreateClientConfig<TRelations>): DrizzleClient<TRelations>;
28
+ /**
29
+ * RLS client bound to a Supabase client. Verified claims are resolved via
30
+ * `supabase.auth.getClaims()` on every query, so it always reflects the live
31
+ * session.
32
+ */
33
+ declare function createAuthClient<TRelations extends AnyRelations = EmptyRelations>(supabase: SupabaseClient, config?: CreateClientConfig<TRelations>): DrizzleClient<TRelations>;
34
+ /** RLS client that runs every query as the `anon` role. */
35
+ declare function createAnonClient<TRelations extends AnyRelations = EmptyRelations>(config?: CreateClientConfig<TRelations>): DrizzleClient<TRelations>;
36
+ /**
37
+ * Client that runs every query as `service_role`, which bypasses RLS via
38
+ * Supabase's BYPASSRLS grant. Use for trusted server-side work.
39
+ */
40
+ declare function createServiceClient<TRelations extends AnyRelations = EmptyRelations>(config?: CreateClientConfig<TRelations>): DrizzleClient<TRelations>;
41
+ //#endregion
42
+ export { type CreateClientConfig, type DrizzleClient, type SupabaseToken, createAdminClient, createAnonClient, createAuthClient, createServiceClient, createSupabaseClient };
package/dist/index.mjs ADDED
@@ -0,0 +1,167 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { drizzle } from "drizzle-orm/postgres-js";
3
+ import postgres from "postgres";
4
+ //#region src/rls-query-client.ts
5
+ function replayAsUserChain(tx, path) {
6
+ let receiver = tx;
7
+ let current = tx;
8
+ for (const step of path) if (step.kind === "get") {
9
+ receiver = current;
10
+ current = current[step.prop];
11
+ } else current = current.apply(receiver, step.args);
12
+ return current;
13
+ }
14
+ const PROMISE_METHODS = /* @__PURE__ */ new Set([
15
+ "then",
16
+ "catch",
17
+ "finally"
18
+ ]);
19
+ function replayPromiseMethod(prop, path, runTransaction) {
20
+ return (...promiseArgs) => {
21
+ const promise = runTransaction((tx) => replayAsUserChain(tx, path));
22
+ return promise[prop].apply(promise, promiseArgs);
23
+ };
24
+ }
25
+ function createRlsQueryClient(runTransaction, close) {
26
+ const build = (path, isRoot) => {
27
+ const target = () => void 0;
28
+ return new Proxy(target, {
29
+ apply(_target, _thisArg, args) {
30
+ if (isRoot) throw new Error("The createSupabaseDrizzle() client is queried directly (e.g. db.select().from(table)). Use db.transaction(cb) to run multiple statements in one RLS transaction.");
31
+ return build([...path, {
32
+ args,
33
+ kind: "apply"
34
+ }], false);
35
+ },
36
+ get(_target, prop) {
37
+ if (isRoot && prop === "transaction") return runTransaction;
38
+ if (isRoot && prop === "close") return close;
39
+ if (PROMISE_METHODS.has(prop)) return isRoot ? void 0 : replayPromiseMethod(prop, path, runTransaction);
40
+ if (typeof prop === "symbol") return;
41
+ return build([...path, {
42
+ kind: "get",
43
+ prop
44
+ }], false);
45
+ }
46
+ });
47
+ };
48
+ return build([], true);
49
+ }
50
+ //#endregion
51
+ //#region src/clients.ts
52
+ const ALLOWED_RLS_ROLES = /* @__PURE__ */ new Set(["anon", "authenticated"]);
53
+ function clampClaims(token) {
54
+ const role = token.role && ALLOWED_RLS_ROLES.has(token.role) ? token.role : "anon";
55
+ return {
56
+ claims: JSON.stringify({
57
+ ...token,
58
+ role
59
+ }),
60
+ role,
61
+ sub: token.sub ?? ""
62
+ };
63
+ }
64
+ function fixedContext(role) {
65
+ return {
66
+ claims: JSON.stringify({ role }),
67
+ role,
68
+ sub: ""
69
+ };
70
+ }
71
+ const poolCache = /* @__PURE__ */ new Map();
72
+ function acquirePool(connectionString, kind) {
73
+ const key = `${kind} ${connectionString}`;
74
+ let entry = poolCache.get(key);
75
+ if (!entry) {
76
+ entry = {
77
+ client: postgres(connectionString, { prepare: false }),
78
+ ended: false,
79
+ key,
80
+ refCount: 0
81
+ };
82
+ poolCache.set(key, entry);
83
+ }
84
+ entry.refCount += 1;
85
+ return entry;
86
+ }
87
+ function makeClose(entry) {
88
+ let released = false;
89
+ return async (options) => {
90
+ if (released) return;
91
+ released = true;
92
+ entry.refCount -= 1;
93
+ if (entry.refCount <= 0 && !entry.ended) {
94
+ entry.ended = true;
95
+ if (poolCache.get(entry.key) === entry) poolCache.delete(entry.key);
96
+ await entry.client.end(options);
97
+ }
98
+ };
99
+ }
100
+ function buildDrizzle(kind, config) {
101
+ const { connectionString, ...drizzleConfig } = config ?? {};
102
+ const url = connectionString ?? process.env.SUPABASE_DATABASE_URL ?? "";
103
+ if (!url) throw new Error("Missing SUPABASE_DATABASE_URL environment variable");
104
+ const entry = acquirePool(url, kind);
105
+ const db = drizzle({
106
+ client: entry.client,
107
+ ...drizzleConfig
108
+ });
109
+ return {
110
+ close: makeClose(entry),
111
+ db
112
+ };
113
+ }
114
+ function buildRlsClient(resolveContext, config) {
115
+ const { close, db } = buildDrizzle("rls", config);
116
+ const runTransaction = async (transaction) => {
117
+ const { claims, role, sub } = await resolveContext();
118
+ return db.transaction(async (tx) => {
119
+ await tx.execute(sql`select set_config('request.jwt.claims', ${claims}, true), set_config('request.jwt.claim.sub', ${sub}, true)`);
120
+ await tx.execute(sql`set local role ${sql.raw(role)}`);
121
+ return transaction(tx);
122
+ });
123
+ };
124
+ return createRlsQueryClient(runTransaction, close);
125
+ }
126
+ /**
127
+ * RLS-bypassing client (the docs' `admin`). Connects with
128
+ * `SUPABASE_DATABASE_URL` and queries drizzle directly. Use for webhooks,
129
+ * background jobs, and seeding, never for user-scoped reads/writes.
130
+ */
131
+ function createAdminClient(config) {
132
+ const { close, db } = buildDrizzle("admin", config);
133
+ return Object.assign(db, { close });
134
+ }
135
+ /**
136
+ * RLS client scoped to an already-verified, decoded token (e.g. from
137
+ * `supabase.auth.getClaims()`). The role is clamped to the allowlist.
138
+ */
139
+ function createSupabaseClient(accessToken, config) {
140
+ const context = clampClaims(accessToken);
141
+ return buildRlsClient(() => Promise.resolve(context), config);
142
+ }
143
+ /**
144
+ * RLS client bound to a Supabase client. Verified claims are resolved via
145
+ * `supabase.auth.getClaims()` on every query, so it always reflects the live
146
+ * session.
147
+ */
148
+ function createAuthClient(supabase, config) {
149
+ return buildRlsClient(async () => {
150
+ const { data, error } = await supabase.auth.getClaims();
151
+ if (error) throw error;
152
+ return clampClaims(data?.claims ?? {});
153
+ }, config);
154
+ }
155
+ /** RLS client that runs every query as the `anon` role. */
156
+ function createAnonClient(config) {
157
+ return buildRlsClient(() => Promise.resolve(fixedContext("anon")), config);
158
+ }
159
+ /**
160
+ * Client that runs every query as `service_role`, which bypasses RLS via
161
+ * Supabase's BYPASSRLS grant. Use for trusted server-side work.
162
+ */
163
+ function createServiceClient(config) {
164
+ return buildRlsClient(() => Promise.resolve(fixedContext("service_role")), config);
165
+ }
166
+ //#endregion
167
+ export { createAdminClient, createAnonClient, createAuthClient, createServiceClient, createSupabaseClient };
@@ -0,0 +1,149 @@
1
+ import { AnyPgColumn, ExtraConfigColumn, HasIdentity, PgBigInt53Builder, PgBigInt64Builder, PgIntegerBuilder, PgPolicyConfig, PgUUIDBuilder, Precision, ReferenceConfig, SetHasDefault, SetIsPrimaryKey, SetNotNull, isPgEnum as isEnum, isPgMaterializedView as isMaterializedView, isPgSchema as isSchema, isPgSequence as isSequence, isPgView as isView, pgEnum as enum$1, pgMaterializedView as materializedView, pgPolicy as policy, pgRole as role, pgSequence as sequence, pgTableCreator as tableCreator, pgView as view } from "drizzle-orm/pg-core";
2
+ import { anonRole, authUid, authenticatedRole, postgresRole, realtimeMessages, realtimeTopic, serviceRole, supabaseAuthAdminRole } from "drizzle-orm/supabase";
3
+ //#region src/schema.d.ts
4
+ type TimestampsOptions = {
5
+ withTimezone?: boolean;
6
+ /** Fractional-second digits. Postgres allows 0 to 6. */
7
+ precision?: Precision;
8
+ };
9
+ declare const timestamps: ({ precision, withTimezone }?: TimestampsOptions) => {
10
+ createdAt: SetHasDefault<SetNotNull<import("drizzle-orm/pg-core").PgTimestampBuilder>>;
11
+ updatedAt: SetHasDefault<SetNotNull<import("drizzle-orm/pg-core").PgTimestampBuilder>>;
12
+ };
13
+ type ReferenceActions = ReferenceConfig["config"];
14
+ type AuthorColumn<TNotNull extends boolean> = [TNotNull] extends [true] ? SetNotNull<PgUUIDBuilder> : PgUUIDBuilder;
15
+ type AuthorReference = {
16
+ reference?: () => AnyPgColumn;
17
+ actions?: ReferenceActions;
18
+ } | {
19
+ reference: null;
20
+ actions?: never;
21
+ };
22
+ type AuthorshipOptions<TNotNull extends boolean = false> = AuthorReference & {
23
+ notNull?: TNotNull;
24
+ };
25
+ declare function authUserId<TNotNull extends boolean = false>(options?: AuthorshipOptions<TNotNull> & {
26
+ name?: string;
27
+ }): AuthorColumn<TNotNull>;
28
+ declare function userId<TNotNull extends boolean = false>(reference: () => AnyPgColumn, options?: {
29
+ name?: string;
30
+ actions?: ReferenceActions;
31
+ notNull?: TNotNull;
32
+ }): AuthorColumn<TNotNull>;
33
+ declare function createdBy<TNotNull extends boolean = false>(options?: AuthorshipOptions<TNotNull>): SetHasDefault<AuthorColumn<TNotNull>>;
34
+ declare function updatedBy<TNotNull extends boolean = false>(options?: AuthorshipOptions<TNotNull>): SetHasDefault<AuthorColumn<TNotNull>>;
35
+ declare const authorship: <TNotNull extends boolean = false>(options?: AuthorshipOptions<TNotNull>) => {
36
+ createdBy: SetHasDefault<AuthorColumn<TNotNull>>;
37
+ updatedBy: SetHasDefault<AuthorColumn<TNotNull>>;
38
+ };
39
+ declare const auditColumns: <TNotNull extends boolean = false>(options?: AuthorshipOptions<TNotNull> & TimestampsOptions) => {
40
+ createdAt: SetHasDefault<SetNotNull<import("drizzle-orm/pg-core").PgTimestampBuilder>>;
41
+ updatedAt: SetHasDefault<SetNotNull<import("drizzle-orm/pg-core").PgTimestampBuilder>>;
42
+ createdBy: SetHasDefault<AuthorColumn<TNotNull>>;
43
+ updatedBy: SetHasDefault<AuthorColumn<TNotNull>>;
44
+ };
45
+ type UuidPrimaryIdColumn<TDefaultRandom extends boolean> = [TDefaultRandom] extends [false] ? SetIsPrimaryKey<PgUUIDBuilder> : SetHasDefault<SetIsPrimaryKey<PgUUIDBuilder>>;
46
+ declare function uuidPrimaryId<TDefaultRandom extends boolean = true>(options?: {
47
+ name?: string;
48
+ defaultRandom?: TDefaultRandom;
49
+ }): UuidPrimaryIdColumn<TDefaultRandom>;
50
+ type IdentityType = "integer" | "bigint";
51
+ type IdentityMode = "number" | "bigint";
52
+ type IdentityGeneration = "byDefault" | "always";
53
+ type IdentityBuilder<TType extends IdentityType, TMode extends IdentityMode> = TType extends "integer" ? PgIntegerBuilder : TMode extends "bigint" ? PgBigInt64Builder : PgBigInt53Builder;
54
+ type SequentialPrimaryIdColumn<TType extends IdentityType, TMode extends IdentityMode, TGeneration extends IdentityGeneration> = HasIdentity<SetIsPrimaryKey<IdentityBuilder<TType, TMode>>, TGeneration>;
55
+ declare function sequentialPrimaryId<TType extends IdentityType = "bigint", TMode extends IdentityMode = "number", TGeneration extends IdentityGeneration = "byDefault">(options?: {
56
+ name?: string;
57
+ type?: TType;
58
+ mode?: TType extends "integer" ? never : TMode;
59
+ generated?: TGeneration;
60
+ }): SequentialPrimaryIdColumn<TType, TMode, TGeneration>;
61
+ declare const assignedPrimaryId: ({ name, length }?: {
62
+ name?: string;
63
+ length?: number;
64
+ }) => SetIsPrimaryKey<import("drizzle-orm/pg-core").PgVarcharBuilder<[string, ...string[]]>>;
65
+ type PrimaryIdKind = "uuid" | "sequential" | "assigned";
66
+ type PrimaryIdColumn<TKind extends PrimaryIdKind> = TKind extends "uuid" ? UuidPrimaryIdColumn<true> : TKind extends "sequential" ? SequentialPrimaryIdColumn<"bigint", "number", "byDefault"> : ReturnType<typeof assignedPrimaryId>;
67
+ declare function primaryId<TKind extends PrimaryIdKind = "sequential">(kind?: TKind): PrimaryIdColumn<TKind>;
68
+ type PolicyOptions = Omit<PgPolicyConfig, "for">;
69
+ type PolicyOperation = NonNullable<PgPolicyConfig["for"]>;
70
+ type AuthenticatedPolicyOptions = Omit<PgPolicyConfig, "for" | "to">;
71
+ declare const selectPolicy: (name: string, config?: PolicyOptions) => import("drizzle-orm/pg-core").PgPolicy;
72
+ declare const insertPolicy: (name: string, config?: PolicyOptions) => import("drizzle-orm/pg-core").PgPolicy;
73
+ declare const updatePolicy: (name: string, config?: PolicyOptions) => import("drizzle-orm/pg-core").PgPolicy;
74
+ declare const deletePolicy: (name: string, config?: PolicyOptions) => import("drizzle-orm/pg-core").PgPolicy;
75
+ declare const allPolicy: (name: string, config?: PolicyOptions) => import("drizzle-orm/pg-core").PgPolicy;
76
+ declare const authenticatedSelectPolicy: (name: string, config?: AuthenticatedPolicyOptions) => import("drizzle-orm/pg-core").PgPolicy;
77
+ declare const authenticatedInsertPolicy: (name: string, config?: AuthenticatedPolicyOptions) => import("drizzle-orm/pg-core").PgPolicy;
78
+ declare const authenticatedUpdatePolicy: (name: string, config?: AuthenticatedPolicyOptions) => import("drizzle-orm/pg-core").PgPolicy;
79
+ declare const authenticatedDeletePolicy: (name: string, config?: AuthenticatedPolicyOptions) => import("drizzle-orm/pg-core").PgPolicy;
80
+ declare const authenticatedAllPolicy: (name: string, config?: AuthenticatedPolicyOptions) => import("drizzle-orm/pg-core").PgPolicy;
81
+ declare const FUNCTION_POLICY_OPERATIONS: readonly ["select", "insert", "update", "delete"];
82
+ type FunctionPolicyOperation = (typeof FUNCTION_POLICY_OPERATIONS)[number];
83
+ /** Columns one function is called with. `null` calls it with none. */
84
+ type FunctionArgument = AnyPgColumn | readonly AnyPgColumn[] | null;
85
+ type FunctionPoliciesOptions = {
86
+ /**
87
+ * Passed to each function, e.g. the row's id: one column, an array for a
88
+ * function taking several, or a record to vary them per operation. The
89
+ * common shape is that the row-scoped operations take the row and `insert`
90
+ * takes nothing, there being no row yet to authorise, only the caller. An
91
+ * operation missing from the record, or set to `null`, is called with no
92
+ * arguments; so is every operation when `argument` is omitted.
93
+ */
94
+ argument?: FunctionArgument | Partial<Record<FunctionPolicyOperation, FunctionArgument>>;
95
+ /**
96
+ * Schema the functions live in, e.g. `"billing"` for
97
+ * `billing.can_select_invoices`. Omit to emit the name unqualified and let
98
+ * the `search_path` in effect resolve it.
99
+ */
100
+ schema?: string;
101
+ /** Prefix for the default function and policy names. Default `"can"`. */
102
+ prefix?: string;
103
+ /** Overrides the generated policy name. */
104
+ name?: (operation: PolicyOperation, table: string) => string;
105
+ };
106
+ type ExtraConfigColumns = Record<string, ExtraConfigColumn>;
107
+ /**
108
+ * One policy per operation, each delegating to a `security definer` function.
109
+ *
110
+ * Access rarely depends only on the row's own owner column, and the usual
111
+ * answer is a function, which is also the standard advice for keeping RLS
112
+ * predicates out of the planner's way:
113
+ *
114
+ * ```ts
115
+ * table("posts", { id: primaryId("uuid") }, (t) =>
116
+ * functionPolicies(t, { argument: t.id })
117
+ * )
118
+ * ```
119
+ *
120
+ * ```sql
121
+ * CREATE POLICY "can_select_posts" ON "posts" FOR SELECT TO "authenticated"
122
+ * USING ((select "can_select_posts"("posts"."id")));
123
+ * ```
124
+ *
125
+ * `argument` also takes a record, for the usual shape where `insert` has no
126
+ * row to authorise yet:
127
+ *
128
+ * ```ts
129
+ * functionPolicies(t, {
130
+ * argument: { delete: t.id, select: t.id, update: t.id },
131
+ * })
132
+ * ```
133
+ */
134
+ declare const functionPolicies: (columns: ExtraConfigColumns, { argument, name, prefix, schema: functionSchema }?: FunctionPoliciesOptions) => import("drizzle-orm/pg-core").PgPolicy[];
135
+ declare const authUserOwns: (ownerColumn: AnyPgColumn) => import("drizzle-orm").SQL<unknown>;
136
+ type OwnerPolicyOptions = Omit<PgPolicyConfig, "for" | "to">;
137
+ declare const authenticatedOwnerSelectPolicy: (name: string, ownerColumn: AnyPgColumn, config?: OwnerPolicyOptions) => import("drizzle-orm/pg-core").PgPolicy;
138
+ declare const authenticatedOwnerInsertPolicy: (name: string, ownerColumn: AnyPgColumn, config?: OwnerPolicyOptions) => import("drizzle-orm/pg-core").PgPolicy;
139
+ declare const authenticatedOwnerUpdatePolicy: (name: string, ownerColumn: AnyPgColumn, config?: OwnerPolicyOptions) => import("drizzle-orm/pg-core").PgPolicy;
140
+ declare const authenticatedOwnerDeletePolicy: (name: string, ownerColumn: AnyPgColumn, config?: OwnerPolicyOptions) => import("drizzle-orm/pg-core").PgPolicy;
141
+ declare const authenticatedOwnerAllPolicy: (name: string, ownerColumn: AnyPgColumn, config?: OwnerPolicyOptions) => import("drizzle-orm/pg-core").PgPolicy;
142
+ declare const table: import("drizzle-orm/pg-core").PgTableFnInternal<undefined>;
143
+ declare const unsecureTable: import("drizzle-orm/pg-core").PgTableFn<undefined>;
144
+ declare const schema: <TName extends string>(name: TName) => import("drizzle-orm/pg-core").PgSchema<TName> & {
145
+ table: import("drizzle-orm/pg-core").PgTableFnInternal<TName>;
146
+ unsecureTable: import("drizzle-orm/pg-core").PgTableFn<TName>;
147
+ };
148
+ //#endregion
149
+ export { AuthorshipOptions, allPolicy, anonRole, assignedPrimaryId, auditColumns, authUid, authUserId, authUserOwns, authenticatedAllPolicy, authenticatedDeletePolicy, authenticatedInsertPolicy, authenticatedOwnerAllPolicy, authenticatedOwnerDeletePolicy, authenticatedOwnerInsertPolicy, authenticatedOwnerSelectPolicy, authenticatedOwnerUpdatePolicy, authenticatedRole, authenticatedSelectPolicy, authenticatedUpdatePolicy, authorship, createdBy, deletePolicy, enum$1 as enum, functionPolicies, insertPolicy, isEnum, isMaterializedView, isSchema, isSequence, isView, materializedView, policy, postgresRole, primaryId, realtimeMessages, realtimeTopic, role, schema, selectPolicy, sequence, sequentialPrimaryId, serviceRole, supabaseAuthAdminRole, table, tableCreator, timestamps, unsecureTable, updatePolicy, updatedBy, userId, uuidPrimaryId, view };
@@ -0,0 +1,220 @@
1
+ import { authUsers } from "./auth-schema.mjs";
2
+ import { Column, getColumnTable, getTableName, is, sql } from "drizzle-orm";
3
+ import { bigint, integer, isPgEnum as isEnum, isPgMaterializedView as isMaterializedView, isPgSchema as isSchema, isPgSequence as isSequence, isPgView as isView, pgEnum as enum$1, pgMaterializedView as materializedView, pgPolicy, pgPolicy as policy, pgRole as role, pgSequence as sequence, pgTableCreator as tableCreator, pgView as view, timestamp, uuid, varchar } from "drizzle-orm/pg-core";
4
+ import { snakeCase } from "drizzle-orm/pg-core/casing";
5
+ import { anonRole, authUid, authUid as authUid$1, authenticatedRole, authenticatedRole as authenticatedRole$1, postgresRole, realtimeMessages, realtimeTopic, serviceRole, supabaseAuthAdminRole } from "drizzle-orm/supabase";
6
+ //#region src/schema.ts
7
+ const timestamps = ({ precision, withTimezone = true } = {}) => {
8
+ const config = {
9
+ precision,
10
+ withTimezone
11
+ };
12
+ return {
13
+ createdAt: timestamp("created_at", config).notNull().defaultNow(),
14
+ updatedAt: timestamp("updated_at", config).notNull().defaultNow()
15
+ };
16
+ };
17
+ const NULLABLE_AUTHOR_ACTIONS = {
18
+ onDelete: "set null",
19
+ onUpdate: "cascade"
20
+ };
21
+ const REQUIRED_AUTHOR_ACTIONS = {
22
+ onDelete: "restrict",
23
+ onUpdate: "cascade"
24
+ };
25
+ const authorColumn = ({ name, reference = () => authUsers.id, actions, notNull = false }) => {
26
+ const column = reference === null ? uuid(name) : uuid(name).references(reference, actions ?? (notNull ? REQUIRED_AUTHOR_ACTIONS : NULLABLE_AUTHOR_ACTIONS));
27
+ return notNull ? column.notNull() : column;
28
+ };
29
+ function authUserId(options = {}) {
30
+ return authorColumn(options);
31
+ }
32
+ function userId(reference, options = {}) {
33
+ return authorColumn({
34
+ ...options,
35
+ reference
36
+ });
37
+ }
38
+ const AUTH_UID_DEFAULT = sql`auth.uid()`;
39
+ function createdBy(options = {}) {
40
+ return authorColumn({
41
+ ...options,
42
+ name: "created_by"
43
+ }).default(AUTH_UID_DEFAULT);
44
+ }
45
+ function updatedBy(options = {}) {
46
+ return authorColumn({
47
+ ...options,
48
+ name: "updated_by"
49
+ }).default(AUTH_UID_DEFAULT);
50
+ }
51
+ const authorship = (options) => ({
52
+ createdBy: createdBy(options),
53
+ updatedBy: updatedBy(options)
54
+ });
55
+ const auditColumns = (options = {}) => ({
56
+ ...timestamps(options),
57
+ ...authorship(options)
58
+ });
59
+ const DEFAULT_ID_NAME = "id";
60
+ function uuidPrimaryId(options = {}) {
61
+ const { name = DEFAULT_ID_NAME, defaultRandom = true } = options;
62
+ const column = uuid(name).primaryKey();
63
+ return defaultRandom ? column.defaultRandom() : column;
64
+ }
65
+ function sequentialPrimaryId(options = {}) {
66
+ const { name = DEFAULT_ID_NAME, type = "bigint", mode = "number", generated = "byDefault" } = options;
67
+ const column = type === "integer" ? integer(name).primaryKey() : bigint(name, { mode }).primaryKey();
68
+ return generated === "always" ? column.generatedAlwaysAsIdentity() : column.generatedByDefaultAsIdentity();
69
+ }
70
+ const assignedPrimaryId = ({ name = DEFAULT_ID_NAME, length } = {}) => varchar(name, { length }).primaryKey();
71
+ function primaryId(kind = "sequential") {
72
+ if (kind === "uuid") return uuidPrimaryId();
73
+ if (kind === "assigned") return assignedPrimaryId();
74
+ return sequentialPrimaryId();
75
+ }
76
+ function operationPolicy(name, operation, config = {}) {
77
+ return pgPolicy(name, {
78
+ ...config,
79
+ for: operation
80
+ });
81
+ }
82
+ const selectPolicy = (name, config = {}) => operationPolicy(name, "select", config);
83
+ const insertPolicy = (name, config = {}) => operationPolicy(name, "insert", config);
84
+ const updatePolicy = (name, config = {}) => operationPolicy(name, "update", config);
85
+ const deletePolicy = (name, config = {}) => operationPolicy(name, "delete", config);
86
+ const allPolicy = (name, config = {}) => operationPolicy(name, "all", config);
87
+ const authenticatedSelectPolicy = (name, config = {}) => selectPolicy(name, {
88
+ ...config,
89
+ to: authenticatedRole$1
90
+ });
91
+ const authenticatedInsertPolicy = (name, config = {}) => insertPolicy(name, {
92
+ ...config,
93
+ to: authenticatedRole$1
94
+ });
95
+ const authenticatedUpdatePolicy = (name, config = {}) => updatePolicy(name, {
96
+ ...config,
97
+ to: authenticatedRole$1
98
+ });
99
+ const authenticatedDeletePolicy = (name, config = {}) => deletePolicy(name, {
100
+ ...config,
101
+ to: authenticatedRole$1
102
+ });
103
+ const authenticatedAllPolicy = (name, config = {}) => allPolicy(name, {
104
+ ...config,
105
+ to: authenticatedRole$1
106
+ });
107
+ const POLICY_CLAUSE = {
108
+ all: "using",
109
+ delete: "using",
110
+ insert: "withCheck",
111
+ select: "using",
112
+ update: "using"
113
+ };
114
+ const POLICY_BUILDERS = {
115
+ all: allPolicy,
116
+ delete: deletePolicy,
117
+ insert: insertPolicy,
118
+ select: selectPolicy,
119
+ update: updatePolicy
120
+ };
121
+ const FUNCTION_POLICY_OPERATIONS = [
122
+ "select",
123
+ "insert",
124
+ "update",
125
+ "delete"
126
+ ];
127
+ const isWholeSetArgument = (argument) => is(argument, Column) || Array.isArray(argument);
128
+ const argumentsFor = (argument, operation) => {
129
+ if (!argument) return [];
130
+ const forOperation = isWholeSetArgument(argument) ? argument : argument[operation];
131
+ if (!forOperation) return [];
132
+ return is(forOperation, Column) ? [forOperation] : [...forOperation];
133
+ };
134
+ /**
135
+ * One policy per operation, each delegating to a `security definer` function.
136
+ *
137
+ * Access rarely depends only on the row's own owner column, and the usual
138
+ * answer is a function, which is also the standard advice for keeping RLS
139
+ * predicates out of the planner's way:
140
+ *
141
+ * ```ts
142
+ * table("posts", { id: primaryId("uuid") }, (t) =>
143
+ * functionPolicies(t, { argument: t.id })
144
+ * )
145
+ * ```
146
+ *
147
+ * ```sql
148
+ * CREATE POLICY "can_select_posts" ON "posts" FOR SELECT TO "authenticated"
149
+ * USING ((select "can_select_posts"("posts"."id")));
150
+ * ```
151
+ *
152
+ * `argument` also takes a record, for the usual shape where `insert` has no
153
+ * row to authorise yet:
154
+ *
155
+ * ```ts
156
+ * functionPolicies(t, {
157
+ * argument: { delete: t.id, select: t.id, update: t.id },
158
+ * })
159
+ * ```
160
+ */
161
+ const functionPolicies = (columns, { argument, name, prefix = "can", schema: functionSchema } = {}) => {
162
+ const [firstColumn] = Object.values(columns);
163
+ if (!firstColumn) throw new Error("functionPolicies needs a table with at least one column");
164
+ const tableName = getTableName(getColumnTable(firstColumn));
165
+ return FUNCTION_POLICY_OPERATIONS.map((operation) => {
166
+ const functionName = `${prefix}_${operation}_${tableName}`;
167
+ const callee = functionSchema ? sql`${sql.identifier(functionSchema)}.${sql.identifier(functionName)}` : sql`${sql.identifier(functionName)}`;
168
+ const condition = sql`(select ${callee}(${sql.join(argumentsFor(argument, operation), sql`, `)}))`;
169
+ const clause = { [POLICY_CLAUSE[operation]]: condition };
170
+ return POLICY_BUILDERS[operation](name?.(operation, tableName) ?? functionName, {
171
+ to: authenticatedRole$1,
172
+ ...clause
173
+ });
174
+ });
175
+ };
176
+ const authUserOwns = (ownerColumn) => sql`${ownerColumn} = ${authUid$1}`;
177
+ const authenticatedOwnerSelectPolicy = (name, ownerColumn, config = {}) => selectPolicy(name, {
178
+ ...config,
179
+ to: authenticatedRole$1,
180
+ using: config.using ?? authUserOwns(ownerColumn)
181
+ });
182
+ const authenticatedOwnerInsertPolicy = (name, ownerColumn, config = {}) => insertPolicy(name, {
183
+ ...config,
184
+ to: authenticatedRole$1,
185
+ withCheck: config.withCheck ?? authUserOwns(ownerColumn)
186
+ });
187
+ const authenticatedOwnerUpdatePolicy = (name, ownerColumn, config = {}) => {
188
+ const ownerCondition = authUserOwns(ownerColumn);
189
+ return updatePolicy(name, {
190
+ ...config,
191
+ to: authenticatedRole$1,
192
+ using: config.using ?? ownerCondition,
193
+ withCheck: config.withCheck ?? ownerCondition
194
+ });
195
+ };
196
+ const authenticatedOwnerDeletePolicy = (name, ownerColumn, config = {}) => deletePolicy(name, {
197
+ ...config,
198
+ to: authenticatedRole$1,
199
+ using: config.using ?? authUserOwns(ownerColumn)
200
+ });
201
+ const authenticatedOwnerAllPolicy = (name, ownerColumn, config = {}) => {
202
+ const ownerCondition = authUserOwns(ownerColumn);
203
+ return allPolicy(name, {
204
+ ...config,
205
+ to: authenticatedRole$1,
206
+ using: config.using ?? ownerCondition,
207
+ withCheck: config.withCheck ?? ownerCondition
208
+ });
209
+ };
210
+ const table = snakeCase.table.withRLS;
211
+ const unsecureTable = snakeCase.table;
212
+ const schema = (name) => {
213
+ const built = snakeCase.schema(name);
214
+ return Object.assign(built, {
215
+ table: built.table.withRLS,
216
+ unsecureTable: built.table
217
+ });
218
+ };
219
+ //#endregion
220
+ export { allPolicy, anonRole, assignedPrimaryId, auditColumns, authUid, authUserId, authUserOwns, authenticatedAllPolicy, authenticatedDeletePolicy, authenticatedInsertPolicy, authenticatedOwnerAllPolicy, authenticatedOwnerDeletePolicy, authenticatedOwnerInsertPolicy, authenticatedOwnerSelectPolicy, authenticatedOwnerUpdatePolicy, authenticatedRole, authenticatedSelectPolicy, authenticatedUpdatePolicy, authorship, createdBy, deletePolicy, enum$1 as enum, functionPolicies, insertPolicy, isEnum, isMaterializedView, isSchema, isSequence, isView, materializedView, policy, postgresRole, primaryId, realtimeMessages, realtimeTopic, role, schema, selectPolicy, sequence, sequentialPrimaryId, serviceRole, supabaseAuthAdminRole, table, tableCreator, timestamps, unsecureTable, updatePolicy, updatedBy, userId, uuidPrimaryId, view };
@@ -0,0 +1,65 @@
1
+ import { PgTable } from "drizzle-orm/pg-core";
2
+ //#region src/triggers.d.ts
3
+ type UpdatedAtTriggerOptions = {
4
+ /** Column the trigger maintains. Default `"updated_at"`. */
5
+ column?: string;
6
+ /** Schema holding the table. Default `"public"`, or the table's own. */
7
+ schema?: string;
8
+ /** Trigger name. Default `handle_<column>`. */
9
+ name?: string;
10
+ /** Schema `moddatetime` is installed into. Default `"extensions"`. */
11
+ extensionSchema?: string;
12
+ /** Emit `create extension if not exists`. Default `true`. */
13
+ createExtension?: boolean;
14
+ };
15
+ /**
16
+ * `CREATE TRIGGER` SQL that keeps `updated_at` current for **every** writer,
17
+ * including PostgREST, the dashboard and psql.
18
+ *
19
+ * `timestamps()` gives the column a `DEFAULT now()`, which covers the insert.
20
+ * SQL has no "on update" default, so without this trigger the column is only
21
+ * ever the insert time. Required, not optional.
22
+ *
23
+ * ```ts
24
+ * // pnpm db:generate --custom --name=posts_audit_triggers
25
+ * // then paste the output into the generated migration:
26
+ * console.log(updatedAtTrigger(posts))
27
+ * ```
28
+ */
29
+ declare function updatedAtTrigger(table: PgTable | string, { column, createExtension, extensionSchema, name, schema }?: UpdatedAtTriggerOptions): string;
30
+ /** `create extension if not exists moddatetime`, safe to repeat per migration. */
31
+ declare function moddatetimeExtension(schema?: string): string;
32
+ type UpdatedByTriggerOptions = {
33
+ /** Column the trigger maintains. Default `"updated_by"`. */
34
+ column?: string;
35
+ /** Schema holding the table. Default `"public"`, or the table's own. */
36
+ schema?: string;
37
+ /** Trigger name. Default `handle_<column>`. */
38
+ name?: string;
39
+ /** Schema the helper function is created in. Default `"public"`. */
40
+ functionSchema?: string;
41
+ /** Emit `create or replace function`. Default `true`. */
42
+ createFunction?: boolean;
43
+ };
44
+ /**
45
+ * `CREATE TRIGGER` SQL that stamps an author column with `auth.uid()` on every
46
+ * update, the `updated_at` story for `updated_by`.
47
+ *
48
+ * `updatedBy()` gives the column a `DEFAULT (select auth.uid())`, which covers
49
+ * the insert. This covers the update, for every writer rather than only the
50
+ * ones going through Drizzle.
51
+ *
52
+ * A write with no session (the admin or `service_role` clients) records `NULL`,
53
+ * because that is what `auth.uid()` returns there.
54
+ */
55
+ declare function updatedByTrigger(table: PgTable | string, { column, createFunction, functionSchema, name, schema }?: UpdatedByTriggerOptions): string;
56
+ /**
57
+ * Both update triggers `auditColumns()` needs, in one string.
58
+ *
59
+ * `auditColumns()` and `timestamps()` leave `updated_at` and `updated_by` at
60
+ * their insert values without these, so a table using either helper wants this
61
+ * in a `drizzle-kit generate --custom` migration.
62
+ */
63
+ declare function auditTriggers(table: PgTable | string, options?: UpdatedAtTriggerOptions & UpdatedByTriggerOptions): string;
64
+ //#endregion
65
+ export { auditTriggers, moddatetimeExtension, updatedAtTrigger, updatedByTrigger };