@svadmin/supabase 0.13.8 → 0.15.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/package.json +7 -7
- package/src/audit-handler.ts +59 -10
- package/src/auth-contract.ts +169 -0
- package/src/auth-provider.ts +262 -123
- package/src/data-provider.ts +112 -178
- package/src/index.ts +16 -11
- package/src/live-provider.ts +193 -75
- package/src/permission-adapter.ts +28 -0
- package/src/rpc.ts +11 -29
- package/src/sdk-boundary.ts +79 -0
- package/src/supacloud.ts +220 -594
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createPermissionAccessControlProvider,
|
|
3
|
+
setAccessControlProvider,
|
|
4
|
+
type AccessControlProvider,
|
|
5
|
+
type PermissionProjectionOptions,
|
|
6
|
+
type PermissionSnapshot,
|
|
7
|
+
} from '@svadmin/core';
|
|
8
|
+
|
|
9
|
+
export type SupabasePermissionSnapshot = PermissionSnapshot;
|
|
10
|
+
export type SupabasePermissionAdapterOptions = PermissionProjectionOptions;
|
|
11
|
+
|
|
12
|
+
/** Convert a backend-verified Supabase/SupAuth snapshot into the UI hint provider. */
|
|
13
|
+
export function createSupabasePermissionAccessControlProvider(
|
|
14
|
+
snapshot: unknown,
|
|
15
|
+
options: SupabasePermissionAdapterOptions = {},
|
|
16
|
+
): AccessControlProvider {
|
|
17
|
+
return createPermissionAccessControlProvider(snapshot, options);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Register the adapter in one step; the result remains a UI-only projection. */
|
|
21
|
+
export function registerSupabasePermissionAccessControl(
|
|
22
|
+
snapshot: unknown,
|
|
23
|
+
options: SupabasePermissionAdapterOptions = {},
|
|
24
|
+
): AccessControlProvider {
|
|
25
|
+
const provider = createSupabasePermissionAccessControlProvider(snapshot, options);
|
|
26
|
+
setAccessControlProvider(provider);
|
|
27
|
+
return provider;
|
|
28
|
+
}
|
package/src/rpc.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
1
|
+
import { executeSupabaseRpc, inputObject } from './sdk-boundary';
|
|
3
2
|
|
|
4
3
|
export interface SupabaseRpcOptions {
|
|
5
4
|
/** Database schema where the RPC function resides (defaults to client schema or 'public') */
|
|
6
5
|
schema?: string;
|
|
7
|
-
/**
|
|
6
|
+
/** Use HEAD and omit result rows. */
|
|
8
7
|
head?: boolean;
|
|
9
8
|
/** Count algorithm to use */
|
|
10
9
|
count?: 'exact' | 'planned' | 'estimated';
|
|
@@ -21,11 +20,11 @@ export interface SupabaseRpcClient {
|
|
|
21
20
|
* @param options Additional Supabase RPC options (schema, head, count, get)
|
|
22
21
|
* @returns The returned data from the RPC call
|
|
23
22
|
*/
|
|
24
|
-
call:
|
|
23
|
+
call: (
|
|
25
24
|
functionName: string,
|
|
26
|
-
args?:
|
|
25
|
+
args?: Record<string, unknown>,
|
|
27
26
|
options?: SupabaseRpcOptions
|
|
28
|
-
) => Promise<
|
|
27
|
+
) => Promise<unknown>;
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
/**
|
|
@@ -35,35 +34,18 @@ export interface SupabaseRpcClient {
|
|
|
35
34
|
* @param defaultOptions Default options applied to all RPC calls
|
|
36
35
|
*/
|
|
37
36
|
export function createSupabaseRpc(
|
|
38
|
-
client:
|
|
37
|
+
client: unknown,
|
|
39
38
|
defaultOptions: SupabaseRpcOptions = {}
|
|
40
39
|
): SupabaseRpcClient {
|
|
40
|
+
inputObject(client, 'Supabase client');
|
|
41
41
|
return {
|
|
42
|
-
async call
|
|
42
|
+
async call(
|
|
43
43
|
functionName: string,
|
|
44
|
-
args?:
|
|
44
|
+
args?: Record<string, unknown>,
|
|
45
45
|
options: SupabaseRpcOptions = {}
|
|
46
|
-
): Promise<
|
|
46
|
+
): Promise<unknown> {
|
|
47
47
|
const mergedOptions = { ...defaultOptions, ...options };
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const { data, error } = await targetClient.rpc(
|
|
51
|
-
functionName,
|
|
52
|
-
args ?? ({} as TArgs),
|
|
53
|
-
{
|
|
54
|
-
head: mergedOptions.head,
|
|
55
|
-
count: mergedOptions.count,
|
|
56
|
-
get: mergedOptions.get,
|
|
57
|
-
}
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
if (error) {
|
|
61
|
-
throw new Error(
|
|
62
|
-
`[svadmin/supabase] RPC function "${functionName}" failed: ${error.message}`
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return data as TResult;
|
|
48
|
+
return executeSupabaseRpc(client, functionName, args === undefined ? {} : args, mergedOptions);
|
|
67
49
|
},
|
|
68
50
|
};
|
|
69
51
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { definedOptions } from '@svadmin/core/options';
|
|
2
|
+
import { decodeCustomResult, rejectProviderResponse } from '@svadmin/core/schema';
|
|
3
|
+
|
|
4
|
+
export function isObject(value: unknown): value is Record<string, unknown> {
|
|
5
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function inputObject(value: unknown, label: string): Record<string, unknown> {
|
|
9
|
+
if (!isObject(value)) throw new TypeError(`${label} must be an object`);
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function inputRecord(value: unknown, label: string): Record<string, unknown> {
|
|
14
|
+
const record = inputObject(value, label);
|
|
15
|
+
const prototype: unknown = Object.getPrototypeOf(record);
|
|
16
|
+
if (prototype !== null && prototype !== Object.prototype) throw new TypeError(`${label} must be a plain record`);
|
|
17
|
+
return record;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function metadataValue(meta: object | undefined, key: string): unknown {
|
|
21
|
+
return meta === undefined ? undefined : Reflect.get(meta, key);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function optionalString(meta: object | undefined, key: string): string | undefined {
|
|
25
|
+
const value = metadataValue(meta, key);
|
|
26
|
+
if (value === undefined) return undefined;
|
|
27
|
+
if (typeof value !== 'string' || !value.trim()) throw new TypeError(`Supabase ${key} must be a nonempty string`);
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function optionalBoolean(meta: object | undefined, key: string): boolean | undefined {
|
|
32
|
+
const value = metadataValue(meta, key);
|
|
33
|
+
if (value === undefined) return undefined;
|
|
34
|
+
if (typeof value !== 'boolean') throw new TypeError(`Supabase ${key} must be a boolean`);
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function optionalCount(meta: object | undefined): 'exact' | 'planned' | 'estimated' | undefined {
|
|
39
|
+
const value = metadataValue(meta, 'count');
|
|
40
|
+
if (value === undefined || value === 'exact' || value === 'planned' || value === 'estimated') return value;
|
|
41
|
+
throw new TypeError('Supabase count must be exact, planned, or estimated');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type SdkMethod = 'schema' | 'from' | 'rpc' | 'invoke' | 'select' | 'insert' | 'update' | 'delete' | 'eq' | 'order';
|
|
45
|
+
|
|
46
|
+
/** Dynamic SDK calls remain unknown until their response envelope is checked. */
|
|
47
|
+
export function callSdk(target: unknown, name: SdkMethod, args: readonly unknown[]): unknown {
|
|
48
|
+
if (!isObject(target)) throw new TypeError('Invalid Supabase client or query builder');
|
|
49
|
+
const method = target[name];
|
|
50
|
+
if (typeof method !== 'function') throw new TypeError(`Supabase SDK method ${name} is unavailable`);
|
|
51
|
+
return Reflect.apply(method, target, args);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function sdkData(request: unknown, operation: string, write: boolean): Promise<unknown> {
|
|
55
|
+
const result: unknown = await request;
|
|
56
|
+
if (!isObject(result) || !Object.hasOwn(result, 'error')) return rejectProviderResponse(write);
|
|
57
|
+
const error = result['error'];
|
|
58
|
+
if (error !== null) {
|
|
59
|
+
if (!isObject(error) || typeof error['message'] !== 'string') return rejectProviderResponse(write);
|
|
60
|
+
throw new Error(`[svadmin/supabase] ${operation} failed: ${error['message']}`);
|
|
61
|
+
}
|
|
62
|
+
return decodeCustomResult(result, write).data;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function executeSupabaseRpc(
|
|
66
|
+
client: unknown, functionName: string, args: unknown, options: object,
|
|
67
|
+
): Promise<unknown> {
|
|
68
|
+
if (typeof functionName !== 'string' || !functionName.trim()) throw new TypeError('Supabase RPC name cannot be empty');
|
|
69
|
+
const schema = optionalString(options, 'schema');
|
|
70
|
+
const rpcOptions = definedOptions({
|
|
71
|
+
head: optionalBoolean(options, 'head'),
|
|
72
|
+
get: optionalBoolean(options, 'get'),
|
|
73
|
+
count: optionalCount(options),
|
|
74
|
+
});
|
|
75
|
+
const variables = inputRecord(args, 'Supabase RPC arguments');
|
|
76
|
+
const target = schema ? callSdk(client, 'schema', [schema]) : client;
|
|
77
|
+
return sdkData(callSdk(target, 'rpc', [functionName, variables, rpcOptions]),
|
|
78
|
+
`RPC function "${functionName}"`, !(rpcOptions.get || rpcOptions.head));
|
|
79
|
+
}
|