@supacloud/db 0.2.0 → 0.5.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/README.md +34 -0
- package/dist/apply.d.ts +5 -5
- package/dist/catalog.d.ts +6 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +54 -0
- package/dist/lint.d.ts +1 -1
- package/dist/manifest.d.ts +4 -4
- package/dist/module.d.ts +14 -14
- package/dist/plan.d.ts +7 -7
- package/dist/reconcile.d.ts +4 -4
- package/dist/rpc.d.ts +34 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,6 +79,40 @@ const userDb = await database.forUser(requireTrustedIdentity(requestContext));
|
|
|
79
79
|
const workerDb = await database.forService("scheduled-worker");
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
+
### Typed RPC boundary
|
|
83
|
+
|
|
84
|
+
`createRpcClient(transport, contracts)` validates arguments before transport and results
|
|
85
|
+
before returning them. Names, arguments and results are inferred from the registry;
|
|
86
|
+
callers cannot supply an arbitrary result type. It uses the supplied client without
|
|
87
|
+
changing its identity, RLS policy or credentials.
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { createRpcClient, defineRpcContract } from '@supacloud/db';
|
|
91
|
+
|
|
92
|
+
const rpc = createRpcClient(userDb, {
|
|
93
|
+
case_create: defineRpcContract({
|
|
94
|
+
args: decodeCreateCaseArgs,
|
|
95
|
+
result: decodeCreatedCase,
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
const result = await rpc.call('case_create', { title: 'Investigation' });
|
|
99
|
+
if (result.ok) {
|
|
100
|
+
console.log(result.data);
|
|
101
|
+
} else {
|
|
102
|
+
handleDatabaseFailure(result.error);
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Decoders must accept `unknown`, validate it at runtime and throw on invalid values.
|
|
107
|
+
They may project extra fields away. Schema libraries such as TypeBox can supply
|
|
108
|
+
these functions; this driver-independent package adds no schema-library dependency.
|
|
109
|
+
Database failures bypass result decoding. Contract failures throw `RpcContractError`
|
|
110
|
+
with a phase and no raw payload or decoder error. Calls are never retried automatically,
|
|
111
|
+
including result-validation failures after a potentially committed command.
|
|
112
|
+
|
|
113
|
+
This is an explicit contract boundary, not inference of JSONB fields from SQL,
|
|
114
|
+
authorization, migration execution or automatic HTTP client generation.
|
|
115
|
+
|
|
82
116
|
## 诊断码
|
|
83
117
|
|
|
84
118
|
### 对账(reconcile)
|
package/dist/apply.d.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Apply
|
|
3
|
-
*
|
|
2
|
+
* Apply: Executes ModulePlan against database - ledger idempotency + advisory lock + single transaction + catalog verification.
|
|
3
|
+
* Boundary matches plan: handles repeatable SQL objects declared by modules, not table migrations.
|
|
4
4
|
*/
|
|
5
5
|
import { type QueryExecutor } from './catalog.js';
|
|
6
6
|
import type { ModulePlan } from './plan.js';
|
|
7
7
|
export interface ApplyResult {
|
|
8
8
|
module: string;
|
|
9
|
-
/**
|
|
9
|
+
/** Names of steps whose SQL was executed */
|
|
10
10
|
applied: string[];
|
|
11
|
-
/** ledger
|
|
11
|
+
/** Steps skipped because ledger hash matched */
|
|
12
12
|
skipped: string[];
|
|
13
|
-
/**
|
|
13
|
+
/** Objects confirmed to exist in catalog after apply */
|
|
14
14
|
verified: string[];
|
|
15
15
|
failed?: {
|
|
16
16
|
step: string;
|
package/dist/catalog.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Catalog
|
|
3
|
-
*
|
|
2
|
+
* Catalog reading: Reads real state from PostgreSQL system catalogs via injected QueryExecutor.
|
|
3
|
+
* All SQL is parameterized ($1 = schemas array), defaulting to public schema.
|
|
4
4
|
*/
|
|
5
5
|
import type { PolicyOperation } from './module.js';
|
|
6
6
|
export interface QueryExecutor {
|
|
7
7
|
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Optional transaction wrapper: When provided, apply wraps begin/commit/rollback with it;
|
|
10
|
+
* when omitted, falls back to sequential begin/commit/rollback on executor (mock-friendly).
|
|
11
11
|
*/
|
|
12
12
|
transaction?<T>(fn: (executor: QueryExecutor) => Promise<T>): Promise<T>;
|
|
13
13
|
}
|
|
@@ -37,7 +37,7 @@ export interface CatalogTrigger {
|
|
|
37
37
|
schema: string;
|
|
38
38
|
table: string;
|
|
39
39
|
name: string;
|
|
40
|
-
/** tgenabled !== 'D'
|
|
40
|
+
/** tgenabled !== 'D' (not disabled) */
|
|
41
41
|
enabled: boolean;
|
|
42
42
|
}
|
|
43
43
|
export interface CatalogGrant {
|
|
@@ -53,6 +53,6 @@ export interface DatabaseCatalog {
|
|
|
53
53
|
triggers: CatalogTrigger[];
|
|
54
54
|
grants: CatalogGrant[];
|
|
55
55
|
}
|
|
56
|
-
/**
|
|
56
|
+
/** Extracts search_path=... setting from proconfig (text[]), or null if unset */
|
|
57
57
|
export declare function extractSearchPath(config: string[] | null): string | null;
|
|
58
58
|
export declare function readCatalog(executor: QueryExecutor, schemas?: string[]): Promise<DatabaseCatalog>;
|
package/dist/index.d.ts
CHANGED
|
@@ -6,3 +6,4 @@ export { planModule, type ModulePlan, type PlanStep } from './plan.js';
|
|
|
6
6
|
export { applyModulePlan, type ApplyResult } from './apply.js';
|
|
7
7
|
export { buildDatabaseManifest, explainObject, type DatabaseManifest, type DatabaseManifestModule, } from './manifest.js';
|
|
8
8
|
export { createDatabaseAccessBoundary, DatabaseAccessError, type AuthenticatedDatabaseIdentity, type DatabaseAccessBoundary, type DatabaseAccessBoundaryOptions, type DatabaseAccessErrorCode, } from './access.js';
|
|
9
|
+
export { createRpcClient, defineRpcContract, RpcContractError, type RpcArgs, type RpcCallResult, type RpcContract, type RpcDecoder, type RpcResult, type RpcTransport, } from './rpc.js';
|
package/dist/index.js
CHANGED
|
@@ -682,12 +682,66 @@ function createDatabaseAccessBoundary(options) {
|
|
|
682
682
|
}
|
|
683
683
|
};
|
|
684
684
|
}
|
|
685
|
+
// src/rpc.ts
|
|
686
|
+
class RpcContractError extends Error {
|
|
687
|
+
rpcName;
|
|
688
|
+
phase;
|
|
689
|
+
code = "RPC_CONTRACT_INVALID";
|
|
690
|
+
constructor(rpcName, phase) {
|
|
691
|
+
super(`RPC contract validation failed (${phase})`);
|
|
692
|
+
this.rpcName = rpcName;
|
|
693
|
+
this.phase = phase;
|
|
694
|
+
this.name = "RpcContractError";
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
function defineRpcContract(contract) {
|
|
698
|
+
return Object.freeze({ ...contract });
|
|
699
|
+
}
|
|
700
|
+
function createRpcClient(transport, contracts) {
|
|
701
|
+
const registered = new Map(Object.entries(contracts).map(([name, contract]) => [
|
|
702
|
+
name,
|
|
703
|
+
defineRpcContract(contract)
|
|
704
|
+
]));
|
|
705
|
+
return {
|
|
706
|
+
async call(name, args) {
|
|
707
|
+
const contract = registered.get(name);
|
|
708
|
+
if (!contract)
|
|
709
|
+
throw new RpcContractError(name, "registration");
|
|
710
|
+
let decodedArgs;
|
|
711
|
+
try {
|
|
712
|
+
decodedArgs = contract.args(args);
|
|
713
|
+
if (!decodedArgs || typeof decodedArgs !== "object" || Array.isArray(decodedArgs)) {
|
|
714
|
+
throw new TypeError("RPC arguments must decode to an object");
|
|
715
|
+
}
|
|
716
|
+
} catch {
|
|
717
|
+
throw new RpcContractError(name, "args");
|
|
718
|
+
}
|
|
719
|
+
const response = await transport.rpc(name, decodedArgs);
|
|
720
|
+
if (!response || typeof response !== "object" || Array.isArray(response) || !Object.hasOwn(response, "error")) {
|
|
721
|
+
throw new RpcContractError(name, "result");
|
|
722
|
+
}
|
|
723
|
+
if (response.error != null)
|
|
724
|
+
return { ok: false, data: null, error: response.error };
|
|
725
|
+
if (!Object.hasOwn(response, "data"))
|
|
726
|
+
throw new RpcContractError(name, "result");
|
|
727
|
+
try {
|
|
728
|
+
const data = contract.result(response.data);
|
|
729
|
+
return { ok: true, data, error: null };
|
|
730
|
+
} catch {
|
|
731
|
+
throw new RpcContractError(name, "result");
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
}
|
|
685
736
|
export {
|
|
686
737
|
DatabaseAccessError,
|
|
738
|
+
RpcContractError,
|
|
687
739
|
applyModulePlan,
|
|
688
740
|
buildDatabaseManifest,
|
|
689
741
|
createDatabaseAccessBoundary,
|
|
742
|
+
createRpcClient,
|
|
690
743
|
defineDatabaseModule,
|
|
744
|
+
defineRpcContract,
|
|
691
745
|
explainObject,
|
|
692
746
|
extractSearchPath,
|
|
693
747
|
lintModule,
|
package/dist/lint.d.ts
CHANGED
package/dist/manifest.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Manifest
|
|
3
|
-
*
|
|
2
|
+
* Manifest: Aggregates multiple DatabaseModules into a serializable database governance inventory,
|
|
3
|
+
* and provides explainObject for human-readable object explanation.
|
|
4
4
|
*/
|
|
5
5
|
import type { DatabaseModule, FunctionDecl, GrantDecl, PolicyDecl, TriggerDecl } from './module.js';
|
|
6
6
|
export interface DatabaseManifestModule {
|
|
@@ -17,7 +17,7 @@ export interface DatabaseManifest {
|
|
|
17
17
|
}
|
|
18
18
|
export declare function buildDatabaseManifest(modules: DatabaseModule[]): DatabaseManifest;
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
20
|
+
* Explains an object by name: policy (name or table.name), function (schema.name),
|
|
21
|
+
* trigger (name), owning table (schema.name), or granted object (schema.name fallback).
|
|
22
22
|
*/
|
|
23
23
|
export declare function explainObject(manifest: DatabaseManifest, name: string): string;
|
package/dist/module.d.ts
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Module declaration: First-class resources for database governance (RLS policies / RPC functions / triggers / grants).
|
|
3
|
+
* Driver-agnostic, pure declaration layer.
|
|
4
4
|
*/
|
|
5
5
|
export type PolicyOperation = 'select' | 'insert' | 'update' | 'delete' | 'all';
|
|
6
6
|
export interface PolicyDecl {
|
|
7
|
-
/**
|
|
7
|
+
/** Policy name, e.g. cases_select */
|
|
8
8
|
name: string;
|
|
9
|
-
/**
|
|
9
|
+
/** Schema-qualified table name: public.cases */
|
|
10
10
|
table: string;
|
|
11
11
|
operation: PolicyOperation;
|
|
12
|
-
/**
|
|
12
|
+
/** Target roles, e.g. ['authenticated'] */
|
|
13
13
|
roles: string[];
|
|
14
|
-
/** SQL
|
|
14
|
+
/** Relative path to SQL source file */
|
|
15
15
|
source: string;
|
|
16
|
-
/**
|
|
16
|
+
/** Relative path to test file */
|
|
17
17
|
tests?: string[];
|
|
18
18
|
}
|
|
19
19
|
export interface FunctionDecl {
|
|
20
|
-
/**
|
|
20
|
+
/** Schema-qualified function name: public.case_create */
|
|
21
21
|
name: string;
|
|
22
22
|
source: string;
|
|
23
|
-
/**
|
|
23
|
+
/** Business permission identifier, e.g. case.create */
|
|
24
24
|
permission?: string;
|
|
25
25
|
transaction?: 'required' | 'none';
|
|
26
26
|
security: 'invoker' | 'definer';
|
|
@@ -30,18 +30,18 @@ export interface FunctionDecl {
|
|
|
30
30
|
}
|
|
31
31
|
export interface TriggerDecl {
|
|
32
32
|
name: string;
|
|
33
|
-
/**
|
|
33
|
+
/** Schema-qualified table name */
|
|
34
34
|
table: string;
|
|
35
35
|
source: string;
|
|
36
36
|
}
|
|
37
37
|
export interface GrantDecl {
|
|
38
|
-
/**
|
|
38
|
+
/** Schema-qualified object name: public.cases */
|
|
39
39
|
object: string;
|
|
40
40
|
privilege: string;
|
|
41
41
|
role: string;
|
|
42
42
|
source: string;
|
|
43
43
|
}
|
|
44
|
-
/**
|
|
44
|
+
/** Internal structure shape of Drizzle Table (type-level compatibility only, does not import drizzle-orm) */
|
|
45
45
|
export interface DrizzleTableLike {
|
|
46
46
|
_: {
|
|
47
47
|
name: string;
|
|
@@ -51,7 +51,7 @@ export interface DrizzleTableLike {
|
|
|
51
51
|
export type TableRef = string | DrizzleTableLike;
|
|
52
52
|
export interface DatabaseModuleOptions {
|
|
53
53
|
name: string;
|
|
54
|
-
/**
|
|
54
|
+
/** Owning tables: Drizzle table object or schema-qualified table name */
|
|
55
55
|
tables?: TableRef[];
|
|
56
56
|
policies?: PolicyDecl[];
|
|
57
57
|
functions?: FunctionDecl[];
|
|
@@ -60,7 +60,7 @@ export interface DatabaseModuleOptions {
|
|
|
60
60
|
}
|
|
61
61
|
export interface DatabaseModule {
|
|
62
62
|
name: string;
|
|
63
|
-
/**
|
|
63
|
+
/** Normalized to 'schema.name' table name */
|
|
64
64
|
tables: string[];
|
|
65
65
|
policies: PolicyDecl[];
|
|
66
66
|
functions: FunctionDecl[];
|
package/dist/plan.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Plan
|
|
3
|
-
*
|
|
2
|
+
* Plan: Compiles repeatable SQL objects declared by modules (functions/policies/triggers/grants) into ordered execution plans.
|
|
3
|
+
* Boundary: Handles repeatable objects only; table schema migrations continue forward migrations.
|
|
4
4
|
*/
|
|
5
5
|
import type { DatabaseModule } from './module.js';
|
|
6
6
|
export interface PlanStep {
|
|
7
7
|
kind: 'function' | 'policy' | 'trigger' | 'grant';
|
|
8
|
-
/**
|
|
8
|
+
/** Object identifier: functions use schema-qualified name; policies/triggers use table.name; grants use object:privilege:role */
|
|
9
9
|
name: string;
|
|
10
|
-
/**
|
|
10
|
+
/** Relative module path */
|
|
11
11
|
source: string;
|
|
12
|
-
/**
|
|
12
|
+
/** Source file content hash */
|
|
13
13
|
sha256: string;
|
|
14
14
|
sql: string;
|
|
15
15
|
risk: Array<{
|
|
@@ -22,9 +22,9 @@ export interface ModulePlan {
|
|
|
22
22
|
version: 1;
|
|
23
23
|
module: string;
|
|
24
24
|
createdAt: string;
|
|
25
|
-
/**
|
|
25
|
+
/** Dependency order: function -> policy -> trigger -> grant */
|
|
26
26
|
steps: PlanStep[];
|
|
27
|
-
/**
|
|
27
|
+
/** Combined hash of all step sha256 hashes */
|
|
28
28
|
digest: string;
|
|
29
29
|
}
|
|
30
30
|
export declare function planModule(module: DatabaseModule, readFile: (path: string) => Promise<string>): Promise<ModulePlan>;
|
package/dist/reconcile.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Reconciliation: Compares declarative Manifest (DatabaseModule) with PostgreSQL live Catalog.
|
|
3
3
|
*/
|
|
4
4
|
import type { DatabaseCatalog } from './catalog.js';
|
|
5
5
|
import type { DatabaseModule } from './module.js';
|
|
@@ -7,15 +7,15 @@ export interface ReconcileIssue {
|
|
|
7
7
|
severity: 'error' | 'warn';
|
|
8
8
|
code: string;
|
|
9
9
|
message: string;
|
|
10
|
-
/**
|
|
10
|
+
/** Target object name */
|
|
11
11
|
object: string;
|
|
12
12
|
}
|
|
13
13
|
export interface ReconcileReport {
|
|
14
14
|
module: string;
|
|
15
15
|
issues: ReconcileIssue[];
|
|
16
|
-
/**
|
|
16
|
+
/** True if no error-level issues */
|
|
17
17
|
ok: boolean;
|
|
18
18
|
}
|
|
19
|
-
/** 'public.cases'
|
|
19
|
+
/** 'public.cases' -> ['public', 'cases']; defaults to public when schema prefix is absent */
|
|
20
20
|
export declare function splitQualifiedName(name: string): [string, string];
|
|
21
21
|
export declare function reconcileModule(module: DatabaseModule, catalog: DatabaseCatalog): ReconcileReport;
|
package/dist/rpc.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type RpcDecoder<T> = (value: unknown) => T;
|
|
2
|
+
export interface RpcContract<Args extends Record<string, unknown>, Result> {
|
|
3
|
+
args: RpcDecoder<Args>;
|
|
4
|
+
result: RpcDecoder<Result>;
|
|
5
|
+
}
|
|
6
|
+
export type RpcArgs<Contract> = Contract extends RpcContract<infer Args, unknown> ? Args : never;
|
|
7
|
+
export type RpcResult<Contract> = Contract extends RpcContract<Record<string, unknown>, infer Result> ? Result : never;
|
|
8
|
+
export interface RpcTransport {
|
|
9
|
+
rpc(name: string, args: Record<string, unknown>): PromiseLike<{
|
|
10
|
+
data: unknown;
|
|
11
|
+
error: unknown;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
export type RpcCallResult<T> = {
|
|
15
|
+
ok: true;
|
|
16
|
+
data: T;
|
|
17
|
+
error: null;
|
|
18
|
+
} | {
|
|
19
|
+
ok: false;
|
|
20
|
+
data: null;
|
|
21
|
+
error: unknown;
|
|
22
|
+
};
|
|
23
|
+
export declare class RpcContractError extends Error {
|
|
24
|
+
readonly rpcName: string;
|
|
25
|
+
readonly phase: 'registration' | 'args' | 'result';
|
|
26
|
+
readonly code = "RPC_CONTRACT_INVALID";
|
|
27
|
+
constructor(rpcName: string, phase: 'registration' | 'args' | 'result');
|
|
28
|
+
}
|
|
29
|
+
export declare function defineRpcContract<Args extends Record<string, unknown>, Result>(contract: RpcContract<Args, Result>): Readonly<RpcContract<Args, Result>>;
|
|
30
|
+
type Contracts = Record<string, RpcContract<Record<string, unknown>, unknown>>;
|
|
31
|
+
export declare function createRpcClient<const Registry extends Contracts>(transport: RpcTransport, contracts: Registry): {
|
|
32
|
+
call<Name extends Extract<keyof Registry, string>>(name: Name, args: RpcArgs<Registry[NoInfer<Name>]>): Promise<RpcCallResult<RpcResult<Registry[Name]>>>;
|
|
33
|
+
};
|
|
34
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supacloud/db",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Database governance layer for SupaCloud: RLS policies, RPC functions and grants as first-class resources, with manifest/catalog reconciliation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|