@zeno-lib/db 0.3.2 → 0.4.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/dist/clients-BlUEH7QJ.d.mts +58 -0
- package/dist/clients-CdPr3mCZ.mjs +197 -0
- package/dist/errors.d.mts +70 -0
- package/dist/errors.mjs +78 -0
- package/dist/function-names-BDc-AuFm.mjs +10 -0
- package/dist/function-names-hv2mj_x1.d.mts +5 -0
- package/dist/index.d.mts +2 -42
- package/dist/index.mjs +2 -167
- package/dist/next.d.mts +87 -0
- package/dist/next.mjs +69 -0
- package/dist/query.d.mts +103 -0
- package/dist/query.mjs +115 -0
- package/dist/schema.d.mts +20 -11
- package/dist/schema.mjs +11 -12
- package/package.json +29 -5
- package/src/casing.test.ts +43 -20
- package/src/clients.ts +60 -5
- package/src/database-url.test.ts +88 -0
- package/src/define-action.ts +63 -0
- package/src/errors.test.ts +95 -0
- package/src/errors.ts +133 -0
- package/src/function-names.ts +21 -0
- package/src/index.ts +2 -0
- package/src/next.test-d.ts +32 -0
- package/src/next.test.ts +178 -0
- package/src/next.ts +112 -0
- package/src/query.test-d.ts +20 -0
- package/src/query.test.ts +261 -0
- package/src/query.ts +252 -0
- package/src/schema.test-d.ts +24 -0
- package/src/schema.ts +51 -21
package/dist/next.d.mts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { n as DrizzleClient, t as CreateClientConfig } from "./clients-BlUEH7QJ.mjs";
|
|
2
|
+
import { AnyRelations, EmptyRelations } from "drizzle-orm";
|
|
3
|
+
import { JwtPayload, SupabaseClient } from "@supabase/supabase-js";
|
|
4
|
+
//#region src/define-action.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* The slice of a schema `defineAction` needs: a throwing `parse`, plus the
|
|
7
|
+
* Standard Schema `types` marker it reads the caller-facing input type from.
|
|
8
|
+
* Zod 4 schemas satisfy it as they are; so does anything else that implements
|
|
9
|
+
* Standard Schema and exposes `parse`.
|
|
10
|
+
*/
|
|
11
|
+
interface ActionSchema<TInput, TOutput> {
|
|
12
|
+
readonly "~standard": {
|
|
13
|
+
readonly types?: {
|
|
14
|
+
readonly input: TInput;
|
|
15
|
+
readonly output: TOutput;
|
|
16
|
+
} | undefined;
|
|
17
|
+
};
|
|
18
|
+
parse(input: unknown): TOutput;
|
|
19
|
+
}
|
|
20
|
+
/** What a request context must carry for `defineAction` to hand it over. */
|
|
21
|
+
interface ActionContext<TDb> {
|
|
22
|
+
readonly db: TDb;
|
|
23
|
+
}
|
|
24
|
+
/** An action's body: the RLS-bound `db`, the parsed input, and the context. */
|
|
25
|
+
type ActionHandler<TContext extends ActionContext<unknown>, TOutput, TResult> = (db: TContext["db"], input: TOutput, context: TContext) => TResult | Promise<TResult>;
|
|
26
|
+
/**
|
|
27
|
+
* `defineAction(schema, handler)` returns the server action itself: an async
|
|
28
|
+
* function that parses its argument, resolves the request context, then calls
|
|
29
|
+
* `handler(db, input, context)`.
|
|
30
|
+
*/
|
|
31
|
+
type DefineAction<TContext extends ActionContext<unknown>> = <TInput, TOutput, TResult>(schema: ActionSchema<TInput, TOutput>, handler: ActionHandler<TContext, TOutput, TResult>) => (input: TInput) => Promise<TResult>;
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/next.d.ts
|
|
34
|
+
/**
|
|
35
|
+
* Thrown when a request carries no verified session. A named class so a caller
|
|
36
|
+
* with a legitimate answer for that case (a permission check, where "not signed
|
|
37
|
+
* in" means "no") can tell it apart from a real failure with `instanceof`.
|
|
38
|
+
*/
|
|
39
|
+
declare class UnauthenticatedError extends Error {
|
|
40
|
+
constructor(message?: string);
|
|
41
|
+
}
|
|
42
|
+
/** The only part of a Supabase client the request context reads. */
|
|
43
|
+
type ClaimsSource = {
|
|
44
|
+
auth: Pick<SupabaseClient["auth"], "getClaims">;
|
|
45
|
+
};
|
|
46
|
+
/** A verified request: the full `getClaims()` payload and the RLS client. */
|
|
47
|
+
type RequestContext<TRelations extends AnyRelations = EmptyRelations> = {
|
|
48
|
+
readonly claims: JwtPayload;
|
|
49
|
+
readonly db: DrizzleClient<TRelations>;
|
|
50
|
+
};
|
|
51
|
+
type CreateRequestDbOptions<TRelations extends AnyRelations = EmptyRelations> = Omit<CreateClientConfig<TRelations>, "connectionString"> & {
|
|
52
|
+
/**
|
|
53
|
+
* Builds the request's Supabase client, e.g. `createClient` from
|
|
54
|
+
* `@zeno-lib/supabase/next-server` (cookie session via `next/headers`).
|
|
55
|
+
*/
|
|
56
|
+
supabase: () => ClaimsSource | Promise<ClaimsSource>;
|
|
57
|
+
/**
|
|
58
|
+
* Overrides `SUPABASE_DATABASE_URL`. A function is resolved per request, not
|
|
59
|
+
* at import, so a build without server secrets can still import the module.
|
|
60
|
+
*/
|
|
61
|
+
connectionString?: string | (() => string);
|
|
62
|
+
};
|
|
63
|
+
type RequestDb<TRelations extends AnyRelations = EmptyRelations> = {
|
|
64
|
+
/** Memoised per render: `{ claims, db }` for the verified caller. */
|
|
65
|
+
getRequestContext: () => Promise<RequestContext<TRelations>>;
|
|
66
|
+
/** The common case: just the RLS-bound client. */
|
|
67
|
+
getRequestDb: () => Promise<DrizzleClient<TRelations>>;
|
|
68
|
+
/** Wraps a handler into a `"use server"` export; see `DefineAction`. */
|
|
69
|
+
defineAction: DefineAction<RequestContext<TRelations>>;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Request-scoped, RLS-bound Drizzle access for Next.js. Verifies the session
|
|
73
|
+
* with `getClaims()` (which checks the signature) and installs the **whole**
|
|
74
|
+
* claims object, so custom claims reach `auth.jwt()` in policies. There is no
|
|
75
|
+
* fallback to `anon`: a caller without a verified `sub` gets
|
|
76
|
+
* `UnauthenticatedError` rather than the empty result of a policy it never
|
|
77
|
+
* satisfied.
|
|
78
|
+
*
|
|
79
|
+
* `getRequestContext` is wrapped in React's `cache`, so one server render shares
|
|
80
|
+
* one verification and one handle. A server action invoked from the browser
|
|
81
|
+
* runs outside a render and resolves afresh on every call.
|
|
82
|
+
*
|
|
83
|
+
* Do not `close()` the handle: pools are shared and reference-counted.
|
|
84
|
+
*/
|
|
85
|
+
declare function createRequestDb<TRelations extends AnyRelations = EmptyRelations>(options: CreateRequestDbOptions<TRelations>): RequestDb<TRelations>;
|
|
86
|
+
//#endregion
|
|
87
|
+
export { type ActionContext, type ActionHandler, type ActionSchema, ClaimsSource, CreateRequestDbOptions, type DefineAction, RequestContext, RequestDb, UnauthenticatedError, createRequestDb };
|
package/dist/next.mjs
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { a as createSupabaseClient } from "./clients-CdPr3mCZ.mjs";
|
|
2
|
+
import { cache } from "react";
|
|
3
|
+
//#region src/define-action.ts
|
|
4
|
+
/**
|
|
5
|
+
* Binds `defineAction` to a request-context resolver. Deliberately not a
|
|
6
|
+
* `"use server"` module: it only returns a function, and the app file that
|
|
7
|
+
* exports the result is the one that carries the directive.
|
|
8
|
+
*
|
|
9
|
+
* Input is parsed before the context is resolved, so a malformed call fails
|
|
10
|
+
* without a session round trip, and the handler never sees an unparsed value.
|
|
11
|
+
*/
|
|
12
|
+
function createDefineAction(getContext) {
|
|
13
|
+
return (schema, handler) => async (input) => {
|
|
14
|
+
const parsed = schema.parse(input);
|
|
15
|
+
const context = await getContext();
|
|
16
|
+
return await handler(context.db, parsed, context);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/next.ts
|
|
21
|
+
/**
|
|
22
|
+
* Thrown when a request carries no verified session. A named class so a caller
|
|
23
|
+
* with a legitimate answer for that case (a permission check, where "not signed
|
|
24
|
+
* in" means "no") can tell it apart from a real failure with `instanceof`.
|
|
25
|
+
*/
|
|
26
|
+
var UnauthenticatedError = class extends Error {
|
|
27
|
+
constructor(message = "Unauthorized: no verified session on this request") {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = "UnauthenticatedError";
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Request-scoped, RLS-bound Drizzle access for Next.js. Verifies the session
|
|
34
|
+
* with `getClaims()` (which checks the signature) and installs the **whole**
|
|
35
|
+
* claims object, so custom claims reach `auth.jwt()` in policies. There is no
|
|
36
|
+
* fallback to `anon`: a caller without a verified `sub` gets
|
|
37
|
+
* `UnauthenticatedError` rather than the empty result of a policy it never
|
|
38
|
+
* satisfied.
|
|
39
|
+
*
|
|
40
|
+
* `getRequestContext` is wrapped in React's `cache`, so one server render shares
|
|
41
|
+
* one verification and one handle. A server action invoked from the browser
|
|
42
|
+
* runs outside a render and resolves afresh on every call.
|
|
43
|
+
*
|
|
44
|
+
* Do not `close()` the handle: pools are shared and reference-counted.
|
|
45
|
+
*/
|
|
46
|
+
function createRequestDb(options) {
|
|
47
|
+
const { connectionString, supabase, ...config } = options;
|
|
48
|
+
const getRequestContext = cache(async () => {
|
|
49
|
+
const { data, error } = await (await supabase()).auth.getClaims();
|
|
50
|
+
if (error) throw error;
|
|
51
|
+
if (!data?.claims.sub) throw new UnauthenticatedError();
|
|
52
|
+
const url = typeof connectionString === "function" ? connectionString() : connectionString;
|
|
53
|
+
const db = createSupabaseClient(data.claims, url === void 0 ? config : {
|
|
54
|
+
...config,
|
|
55
|
+
connectionString: url
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
claims: data.claims,
|
|
59
|
+
db
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
defineAction: createDefineAction(getRequestContext),
|
|
64
|
+
getRequestContext,
|
|
65
|
+
getRequestDb: async () => (await getRequestContext()).db
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
export { UnauthenticatedError, createRequestDb };
|
package/dist/query.d.mts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { t as FunctionPolicyOperation } from "./function-names-hv2mj_x1.mjs";
|
|
2
|
+
import { SQL } from "drizzle-orm";
|
|
3
|
+
import { PgTable } from "drizzle-orm/pg-core";
|
|
4
|
+
//#region src/query.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* PostgREST's `.single()`: exactly one row, or an error.
|
|
7
|
+
*
|
|
8
|
+
* `findFirst()` returns `undefined` for no rows and silently drops the rest
|
|
9
|
+
* when there are several, and `.returning()` hands back an array.
|
|
10
|
+
*/
|
|
11
|
+
declare const one: <T>(rows: readonly T[]) => T;
|
|
12
|
+
/**
|
|
13
|
+
* PostgREST's `.maybeSingle()`: one row or none, erroring on more than one.
|
|
14
|
+
* Returns `undefined` rather than `null`, which is what `findFirst()` does.
|
|
15
|
+
*/
|
|
16
|
+
declare const maybeOne: <T>(rows: readonly T[]) => T | undefined;
|
|
17
|
+
type ColumnKey<TTable extends PgTable> = Extract<keyof TTable["_"]["columns"], string>;
|
|
18
|
+
type ExcludedSetOptions<TTable extends PgTable> = {
|
|
19
|
+
/**
|
|
20
|
+
* The conflict target's column keys, never written back from `excluded`.
|
|
21
|
+
* The first one that exists is also the empty-set fallback. Default `"id"`.
|
|
22
|
+
*/
|
|
23
|
+
target?: ColumnKey<TTable> | readonly ColumnKey<TTable>[];
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The `set` for `onConflictDoUpdate` that reproduces PostgREST's
|
|
27
|
+
* `.upsert(row, { onConflict })`: on conflict, every column the caller
|
|
28
|
+
* supplied is overwritten from the proposed row (`col = excluded.col`).
|
|
29
|
+
*
|
|
30
|
+
* - A key carrying `undefined` is skipped. Drizzle omits an undefined column
|
|
31
|
+
* from the INSERT, so `excluded.x` would be the column default, and on a
|
|
32
|
+
* conflict `x = excluded.x` would overwrite a stored value with it.
|
|
33
|
+
* - The conflict target (`id` unless `target` says otherwise) is excluded.
|
|
34
|
+
* - An empty result falls back to `<target> = excluded.<target>`. Drizzle
|
|
35
|
+
* throws `No values to set` while *building* the statement, so a payload
|
|
36
|
+
* holding nothing but the key would fail even on the plain insert path.
|
|
37
|
+
* Writing the key back to itself is a no-op that keeps `.returning()`
|
|
38
|
+
* yielding the row, which `onConflictDoNothing` does not.
|
|
39
|
+
*
|
|
40
|
+
* With no target column in the table (a junction keyed only by its foreign
|
|
41
|
+
* keys, with the default target) the fallback cannot apply and the result
|
|
42
|
+
* stays empty: such a table wants `onConflictDoNothing` anyway.
|
|
43
|
+
*
|
|
44
|
+
* Column names come from the schema, never from `values`.
|
|
45
|
+
*/
|
|
46
|
+
declare const excludedSet: <TTable extends PgTable>(table: TTable, values: Record<string, unknown>, { target }?: ExcludedSetOptions<TTable>) => Record<string, SQL>;
|
|
47
|
+
/**
|
|
48
|
+
* The same values with every `undefined` key dropped, so an insert and its
|
|
49
|
+
* `excludedSet` agree on which columns were supplied.
|
|
50
|
+
*/
|
|
51
|
+
declare const definedValues: <T extends Record<string, unknown>>(values: T) => T;
|
|
52
|
+
/** Table name to the schema its `<prefix>_<operation>_<table>` functions live in. */
|
|
53
|
+
type FunctionPermissionTables = ReadonlyMap<string, string>;
|
|
54
|
+
type FunctionPermissionTablesOptions = {
|
|
55
|
+
/** Schemas whose tables are included. Default `["public"]`. */
|
|
56
|
+
schemas?: readonly string[];
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The allowlist `selectFunctionPermissions` resolves a table name against,
|
|
60
|
+
* built from a Drizzle schema object (`import * as schema`) or the result of
|
|
61
|
+
* `defineRelations`. Build it once at module scope. Views and non-table
|
|
62
|
+
* exports are ignored; a table name that appears in two included schemas
|
|
63
|
+
* throws, since a bare name could not tell them apart.
|
|
64
|
+
*/
|
|
65
|
+
declare const functionPermissionTables: (schemaObject: Record<string, unknown>, { schemas }?: FunctionPermissionTablesOptions) => FunctionPermissionTables;
|
|
66
|
+
type FunctionPermissions = Record<FunctionPolicyOperation, boolean>;
|
|
67
|
+
/**
|
|
68
|
+
* Anything with Drizzle's `execute` returning rows: a postgres-js client from
|
|
69
|
+
* `@zeno-lib/db`, a `PgAsyncDatabase`, or a transaction.
|
|
70
|
+
*/
|
|
71
|
+
type QueryExecutor = {
|
|
72
|
+
execute: (query: SQL) => PromiseLike<readonly Record<string, unknown>[]>;
|
|
73
|
+
};
|
|
74
|
+
type SelectFunctionPermissionsOptions = {
|
|
75
|
+
/** From `functionPermissionTables`. */
|
|
76
|
+
tables: FunctionPermissionTables;
|
|
77
|
+
/** The table asked about, checked against `tables`. */
|
|
78
|
+
table: string;
|
|
79
|
+
/**
|
|
80
|
+
* The row, passed to every function but `insert` (there is no row yet) as
|
|
81
|
+
* a named argument. Omit to call all four with none.
|
|
82
|
+
*/
|
|
83
|
+
id?: number | string;
|
|
84
|
+
/** Name of the functions' row parameter. Default `"id"`. */
|
|
85
|
+
argumentName?: string;
|
|
86
|
+
/** Function prefix, as given to `functionPolicies`. Default `"can"`. */
|
|
87
|
+
prefix?: string;
|
|
88
|
+
/** Schema the functions live in. Default: the table's own. */
|
|
89
|
+
functionSchema?: string;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* The four `<prefix>_<operation>_<table>()` answers for one table, in one
|
|
93
|
+
* statement: the read side of `functionPolicies`, for showing or hiding UI.
|
|
94
|
+
*
|
|
95
|
+
* `table` is only ever a map key: every identifier that reaches the statement
|
|
96
|
+
* comes from `tables` and is quoted with `sql.identifier`. A function missing
|
|
97
|
+
* or with another signature raises `42883` (`SqlState.undefinedFunction`).
|
|
98
|
+
* A `null` answer, which a function gives for a row the caller cannot see,
|
|
99
|
+
* counts as `false`, as does anything else that is not `true`.
|
|
100
|
+
*/
|
|
101
|
+
declare const selectFunctionPermissions: (db: QueryExecutor, { argumentName, functionSchema, id, prefix, table, tables }: SelectFunctionPermissionsOptions) => Promise<FunctionPermissions>;
|
|
102
|
+
//#endregion
|
|
103
|
+
export { ExcludedSetOptions, FunctionPermissionTables, FunctionPermissionTablesOptions, FunctionPermissions, QueryExecutor, SelectFunctionPermissionsOptions, definedValues, excludedSet, functionPermissionTables, maybeOne, one, selectFunctionPermissions };
|
package/dist/query.mjs
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { n as functionPolicyName, t as FUNCTION_POLICY_OPERATIONS } from "./function-names-BDc-AuFm.mjs";
|
|
2
|
+
import { getColumns, is, sql } from "drizzle-orm";
|
|
3
|
+
import { PgTable, getTableConfig } from "drizzle-orm/pg-core";
|
|
4
|
+
//#region src/query.ts
|
|
5
|
+
/**
|
|
6
|
+
* PostgREST's `.single()`: exactly one row, or an error.
|
|
7
|
+
*
|
|
8
|
+
* `findFirst()` returns `undefined` for no rows and silently drops the rest
|
|
9
|
+
* when there are several, and `.returning()` hands back an array.
|
|
10
|
+
*/
|
|
11
|
+
const one = (rows) => {
|
|
12
|
+
if (rows.length !== 1) throw new Error(`Expected exactly one row, received ${rows.length}`);
|
|
13
|
+
return rows[0];
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* PostgREST's `.maybeSingle()`: one row or none, erroring on more than one.
|
|
17
|
+
* Returns `undefined` rather than `null`, which is what `findFirst()` does.
|
|
18
|
+
*/
|
|
19
|
+
const maybeOne = (rows) => {
|
|
20
|
+
if (rows.length > 1) throw new Error(`Expected at most one row, received ${rows.length}`);
|
|
21
|
+
return rows[0];
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The `set` for `onConflictDoUpdate` that reproduces PostgREST's
|
|
25
|
+
* `.upsert(row, { onConflict })`: on conflict, every column the caller
|
|
26
|
+
* supplied is overwritten from the proposed row (`col = excluded.col`).
|
|
27
|
+
*
|
|
28
|
+
* - A key carrying `undefined` is skipped. Drizzle omits an undefined column
|
|
29
|
+
* from the INSERT, so `excluded.x` would be the column default, and on a
|
|
30
|
+
* conflict `x = excluded.x` would overwrite a stored value with it.
|
|
31
|
+
* - The conflict target (`id` unless `target` says otherwise) is excluded.
|
|
32
|
+
* - An empty result falls back to `<target> = excluded.<target>`. Drizzle
|
|
33
|
+
* throws `No values to set` while *building* the statement, so a payload
|
|
34
|
+
* holding nothing but the key would fail even on the plain insert path.
|
|
35
|
+
* Writing the key back to itself is a no-op that keeps `.returning()`
|
|
36
|
+
* yielding the row, which `onConflictDoNothing` does not.
|
|
37
|
+
*
|
|
38
|
+
* With no target column in the table (a junction keyed only by its foreign
|
|
39
|
+
* keys, with the default target) the fallback cannot apply and the result
|
|
40
|
+
* stays empty: such a table wants `onConflictDoNothing` anyway.
|
|
41
|
+
*
|
|
42
|
+
* Column names come from the schema, never from `values`.
|
|
43
|
+
*/
|
|
44
|
+
const excludedSet = (table, values, { target = "id" } = {}) => {
|
|
45
|
+
const columns = getColumns(table);
|
|
46
|
+
const targets = typeof target === "string" ? [target] : target;
|
|
47
|
+
const set = {};
|
|
48
|
+
for (const [key, column] of Object.entries(columns)) if (key in values && values[key] !== void 0 && !targets.includes(key)) set[key] = sql`excluded.${sql.identifier(column.name)}`;
|
|
49
|
+
if (Object.keys(set).length > 0) return set;
|
|
50
|
+
const fallback = targets.find((key) => key in columns);
|
|
51
|
+
const fallbackColumn = fallback === void 0 ? void 0 : columns[fallback];
|
|
52
|
+
if (fallback !== void 0 && fallbackColumn) set[fallback] = sql`excluded.${sql.identifier(fallbackColumn.name)}`;
|
|
53
|
+
return set;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* The same values with every `undefined` key dropped, so an insert and its
|
|
57
|
+
* `excludedSet` agree on which columns were supplied.
|
|
58
|
+
*/
|
|
59
|
+
const definedValues = (values) => Object.fromEntries(Object.entries(values).filter(([, value]) => value !== void 0));
|
|
60
|
+
const DEFAULT_SCHEMA = "public";
|
|
61
|
+
const tableOf = (value) => {
|
|
62
|
+
if (is(value, PgTable)) return value;
|
|
63
|
+
if (typeof value === "object" && value !== null && "table" in value) {
|
|
64
|
+
const { table } = value;
|
|
65
|
+
return is(table, PgTable) ? table : void 0;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* The allowlist `selectFunctionPermissions` resolves a table name against,
|
|
70
|
+
* built from a Drizzle schema object (`import * as schema`) or the result of
|
|
71
|
+
* `defineRelations`. Build it once at module scope. Views and non-table
|
|
72
|
+
* exports are ignored; a table name that appears in two included schemas
|
|
73
|
+
* throws, since a bare name could not tell them apart.
|
|
74
|
+
*/
|
|
75
|
+
const functionPermissionTables = (schemaObject, { schemas = [DEFAULT_SCHEMA] } = {}) => {
|
|
76
|
+
const tables = /* @__PURE__ */ new Map();
|
|
77
|
+
for (const value of Object.values(schemaObject)) {
|
|
78
|
+
const table = tableOf(value);
|
|
79
|
+
if (!table) continue;
|
|
80
|
+
const { name, schema = DEFAULT_SCHEMA } = getTableConfig(table);
|
|
81
|
+
if (!schemas.includes(schema)) continue;
|
|
82
|
+
const existing = tables.get(name);
|
|
83
|
+
if (existing !== void 0 && existing !== schema) throw new Error(`Table "${name}" exists in both "${existing}" and "${schema}"; include only one of those schemas`);
|
|
84
|
+
tables.set(name, schema);
|
|
85
|
+
}
|
|
86
|
+
return tables;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* The four `<prefix>_<operation>_<table>()` answers for one table, in one
|
|
90
|
+
* statement: the read side of `functionPolicies`, for showing or hiding UI.
|
|
91
|
+
*
|
|
92
|
+
* `table` is only ever a map key: every identifier that reaches the statement
|
|
93
|
+
* comes from `tables` and is quoted with `sql.identifier`. A function missing
|
|
94
|
+
* or with another signature raises `42883` (`SqlState.undefinedFunction`).
|
|
95
|
+
* A `null` answer, which a function gives for a row the caller cannot see,
|
|
96
|
+
* counts as `false`, as does anything else that is not `true`.
|
|
97
|
+
*/
|
|
98
|
+
const selectFunctionPermissions = async (db, { argumentName = "id", functionSchema, id, prefix = "can", table, tables }) => {
|
|
99
|
+
const tableSchema = tables.get(table);
|
|
100
|
+
if (tableSchema === void 0) throw new Error(`No permission functions for table "${table}"`);
|
|
101
|
+
const schema = functionSchema ?? tableSchema;
|
|
102
|
+
const call = (operation) => {
|
|
103
|
+
const callee = sql`${sql.identifier(schema)}.${sql.identifier(functionPolicyName(prefix, operation, table))}`;
|
|
104
|
+
return operation === "insert" || id === void 0 ? sql`${callee}()` : sql`${callee}(${sql.identifier(argumentName)} => ${id})`;
|
|
105
|
+
};
|
|
106
|
+
const row = one(await db.execute(sql`select ${sql.join(FUNCTION_POLICY_OPERATIONS.map((operation) => sql`${call(operation)} as ${sql.identifier(operation)}`), sql`, `)}`));
|
|
107
|
+
return {
|
|
108
|
+
delete: row.delete === true,
|
|
109
|
+
insert: row.insert === true,
|
|
110
|
+
select: row.select === true,
|
|
111
|
+
update: row.update === true
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
//#endregion
|
|
115
|
+
export { definedValues, excludedSet, functionPermissionTables, maybeOne, one, selectFunctionPermissions };
|
package/dist/schema.d.mts
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as FunctionPolicyOperation } from "./function-names-hv2mj_x1.mjs";
|
|
2
|
+
import { AnyPgColumn, ExtraConfigColumn, HasIdentity, PgBigInt53Builder, PgBigInt64Builder, PgIntegerBuilder, PgPolicyConfig, PgTimestampBuilder, PgTimestampStringBuilder, 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
3
|
import { anonRole, authUid, authenticatedRole, postgresRole, realtimeMessages, realtimeTopic, serviceRole, supabaseAuthAdminRole } from "drizzle-orm/supabase";
|
|
3
4
|
//#region src/schema.d.ts
|
|
4
|
-
|
|
5
|
+
/** How a timestamp column is read back: a `Date` (the default) or Postgres's own text. */
|
|
6
|
+
type TimestampMode = "date" | "string";
|
|
7
|
+
type TimestampsOptions<TMode extends TimestampMode = "date"> = {
|
|
8
|
+
/**
|
|
9
|
+
* `"date"` (the default) reads a `Date`; `"string"` keeps Postgres's text
|
|
10
|
+
* form, which also keeps the microseconds a `Date` truncates. A JavaScript
|
|
11
|
+
* mapping only: it emits no SQL.
|
|
12
|
+
*/
|
|
13
|
+
mode?: TMode;
|
|
5
14
|
withTimezone?: boolean;
|
|
6
15
|
/** Fractional-second digits. Postgres allows 0 to 6. */
|
|
7
16
|
precision?: Precision;
|
|
8
17
|
};
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
18
|
+
type TimestampColumn<TMode extends TimestampMode> = SetHasDefault<SetNotNull<[TMode] extends ["string"] ? PgTimestampStringBuilder : PgTimestampBuilder>>;
|
|
19
|
+
type TimestampColumns<TMode extends TimestampMode = "date"> = {
|
|
20
|
+
createdAt: TimestampColumn<TMode>;
|
|
21
|
+
updatedAt: TimestampColumn<TMode>;
|
|
12
22
|
};
|
|
23
|
+
declare const timestamps: <TMode extends TimestampMode = "date">({ mode, precision, withTimezone }?: TimestampsOptions<TMode>) => TimestampColumns<TMode>;
|
|
13
24
|
type ReferenceActions = ReferenceConfig["config"];
|
|
14
25
|
type AuthorColumn<TNotNull extends boolean> = [TNotNull] extends [true] ? SetNotNull<PgUUIDBuilder> : PgUUIDBuilder;
|
|
15
26
|
type AuthorReference = {
|
|
@@ -36,9 +47,9 @@ declare const authorship: <TNotNull extends boolean = false>(options?: Authorshi
|
|
|
36
47
|
createdBy: SetHasDefault<AuthorColumn<TNotNull>>;
|
|
37
48
|
updatedBy: SetHasDefault<AuthorColumn<TNotNull>>;
|
|
38
49
|
};
|
|
39
|
-
declare const auditColumns: <TNotNull extends boolean = false>(options?: AuthorshipOptions<TNotNull> & TimestampsOptions) => {
|
|
40
|
-
createdAt:
|
|
41
|
-
updatedAt:
|
|
50
|
+
declare const auditColumns: <TNotNull extends boolean = false, TMode extends TimestampMode = "date">(options?: AuthorshipOptions<TNotNull> & TimestampsOptions<TMode>) => {
|
|
51
|
+
createdAt: TimestampColumn<TMode>;
|
|
52
|
+
updatedAt: TimestampColumn<TMode>;
|
|
42
53
|
createdBy: SetHasDefault<AuthorColumn<TNotNull>>;
|
|
43
54
|
updatedBy: SetHasDefault<AuthorColumn<TNotNull>>;
|
|
44
55
|
};
|
|
@@ -78,8 +89,6 @@ declare const authenticatedInsertPolicy: (name: string, config?: AuthenticatedPo
|
|
|
78
89
|
declare const authenticatedUpdatePolicy: (name: string, config?: AuthenticatedPolicyOptions) => import("drizzle-orm/pg-core").PgPolicy;
|
|
79
90
|
declare const authenticatedDeletePolicy: (name: string, config?: AuthenticatedPolicyOptions) => import("drizzle-orm/pg-core").PgPolicy;
|
|
80
91
|
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
92
|
/** Columns one function is called with. `null` calls it with none. */
|
|
84
93
|
type FunctionArgument = AnyPgColumn | readonly AnyPgColumn[] | null;
|
|
85
94
|
type FunctionPoliciesOptions = {
|
|
@@ -146,4 +155,4 @@ declare const schema: <TName extends string>(name: TName) => import("drizzle-orm
|
|
|
146
155
|
unsecureTable: import("drizzle-orm/pg-core").PgTableFn<TName>;
|
|
147
156
|
};
|
|
148
157
|
//#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 };
|
|
158
|
+
export { AuthorshipOptions, TimestampColumns, TimestampMode, TimestampsOptions, 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 };
|
package/dist/schema.mjs
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { authUsers } from "./auth-schema.mjs";
|
|
2
|
+
import { n as functionPolicyName, t as FUNCTION_POLICY_OPERATIONS } from "./function-names-BDc-AuFm.mjs";
|
|
2
3
|
import { Column, getColumnTable, getTableName, is, sql } from "drizzle-orm";
|
|
3
4
|
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
5
|
import { snakeCase } from "drizzle-orm/pg-core/casing";
|
|
5
6
|
import { anonRole, authUid, authUid as authUid$1, authenticatedRole, authenticatedRole as authenticatedRole$1, postgresRole, realtimeMessages, realtimeTopic, serviceRole, supabaseAuthAdminRole } from "drizzle-orm/supabase";
|
|
6
7
|
//#region src/schema.ts
|
|
7
|
-
const timestamps = ({ precision, withTimezone = true } = {}) => {
|
|
8
|
-
const
|
|
8
|
+
const timestamps = ({ mode, precision, withTimezone = true } = {}) => {
|
|
9
|
+
const column = (name) => mode === "string" ? timestamp(name, {
|
|
10
|
+
mode: "string",
|
|
9
11
|
precision,
|
|
10
12
|
withTimezone
|
|
11
|
-
}
|
|
13
|
+
}).notNull().defaultNow() : timestamp(name, {
|
|
14
|
+
precision,
|
|
15
|
+
withTimezone
|
|
16
|
+
}).notNull().defaultNow();
|
|
12
17
|
return {
|
|
13
|
-
createdAt:
|
|
14
|
-
updatedAt:
|
|
18
|
+
createdAt: column("created_at"),
|
|
19
|
+
updatedAt: column("updated_at")
|
|
15
20
|
};
|
|
16
21
|
};
|
|
17
22
|
const NULLABLE_AUTHOR_ACTIONS = {
|
|
@@ -118,12 +123,6 @@ const POLICY_BUILDERS = {
|
|
|
118
123
|
select: selectPolicy,
|
|
119
124
|
update: updatePolicy
|
|
120
125
|
};
|
|
121
|
-
const FUNCTION_POLICY_OPERATIONS = [
|
|
122
|
-
"select",
|
|
123
|
-
"insert",
|
|
124
|
-
"update",
|
|
125
|
-
"delete"
|
|
126
|
-
];
|
|
127
126
|
const isWholeSetArgument = (argument) => is(argument, Column) || Array.isArray(argument);
|
|
128
127
|
const argumentsFor = (argument, operation) => {
|
|
129
128
|
if (!argument) return [];
|
|
@@ -163,7 +162,7 @@ const functionPolicies = (columns, { argument, name, prefix = "can", schema: fun
|
|
|
163
162
|
if (!firstColumn) throw new Error("functionPolicies needs a table with at least one column");
|
|
164
163
|
const tableName = getTableName(getColumnTable(firstColumn));
|
|
165
164
|
return FUNCTION_POLICY_OPERATIONS.map((operation) => {
|
|
166
|
-
const functionName =
|
|
165
|
+
const functionName = functionPolicyName(prefix, operation, tableName);
|
|
167
166
|
const callee = functionSchema ? sql`${sql.identifier(functionSchema)}.${sql.identifier(functionName)}` : sql`${sql.identifier(functionName)}`;
|
|
168
167
|
const condition = sql`(select ${callee}(${sql.join(argumentsFor(argument, operation), sql`, `)}))`;
|
|
169
168
|
const clause = { [POLICY_CLAUSE[operation]]: condition };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeno-lib/db",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -18,6 +18,21 @@
|
|
|
18
18
|
"import": "./dist/config.mjs",
|
|
19
19
|
"default": "./dist/config.mjs"
|
|
20
20
|
},
|
|
21
|
+
"./errors": {
|
|
22
|
+
"types": "./dist/errors.d.mts",
|
|
23
|
+
"import": "./dist/errors.mjs",
|
|
24
|
+
"default": "./dist/errors.mjs"
|
|
25
|
+
},
|
|
26
|
+
"./next": {
|
|
27
|
+
"types": "./dist/next.d.mts",
|
|
28
|
+
"import": "./dist/next.mjs",
|
|
29
|
+
"default": "./dist/next.mjs"
|
|
30
|
+
},
|
|
31
|
+
"./query": {
|
|
32
|
+
"types": "./dist/query.d.mts",
|
|
33
|
+
"import": "./dist/query.mjs",
|
|
34
|
+
"default": "./dist/query.mjs"
|
|
35
|
+
},
|
|
21
36
|
"./schema": {
|
|
22
37
|
"types": "./dist/schema.d.mts",
|
|
23
38
|
"import": "./dist/schema.mjs",
|
|
@@ -44,21 +59,30 @@
|
|
|
44
59
|
"@supabase/supabase-js": ">=2",
|
|
45
60
|
"drizzle-kit": "1.0.0-rc.3",
|
|
46
61
|
"drizzle-orm": "1.0.0-rc.3",
|
|
47
|
-
"postgres": ">=3.4"
|
|
62
|
+
"postgres": ">=3.4",
|
|
63
|
+
"react": ">=19"
|
|
64
|
+
},
|
|
65
|
+
"peerDependenciesMeta": {
|
|
66
|
+
"react": {
|
|
67
|
+
"optional": true
|
|
68
|
+
}
|
|
48
69
|
},
|
|
49
70
|
"devDependencies": {
|
|
50
|
-
"@supabase/supabase-js": "2.
|
|
71
|
+
"@supabase/supabase-js": "2.116.0",
|
|
51
72
|
"@types/node": "24.10.0",
|
|
73
|
+
"@types/react": "19.2.14",
|
|
52
74
|
"dotenv": "17.4.2",
|
|
53
75
|
"drizzle-kit": "1.0.0-rc.3",
|
|
54
76
|
"drizzle-orm": "1.0.0-rc.3",
|
|
55
77
|
"postgres": "3.4.7",
|
|
78
|
+
"react": "^19.2.5",
|
|
56
79
|
"supabase": "2.84.1",
|
|
57
80
|
"tsdown": "^0.22.14",
|
|
58
81
|
"typescript": "7.0.2",
|
|
59
82
|
"vite": "8.0.16",
|
|
60
|
-
"vitest": "4.1.
|
|
61
|
-
"
|
|
83
|
+
"vitest": "4.1.11",
|
|
84
|
+
"zod": "^4.4.3",
|
|
85
|
+
"@zeno-lib/test": "0.1.0",
|
|
62
86
|
"@zeno-lib/typescript": "^1.1.0"
|
|
63
87
|
},
|
|
64
88
|
"scripts": {
|