@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 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.1",
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",