@supacloud/db 0.3.1 → 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/index.d.ts +1 -0
- package/dist/index.js +54 -0
- 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/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/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",
|