@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 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:把 ModulePlan 落到数据库 —— 账本幂等 + advisory lock + 单事务 + catalog 回读验证。
3
- * 边界与 plan 一致:只管模块声明的可重复 SQL 对象,不做表结构 migration。
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
- /** 实际执行了 SQL 的 step 名 */
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
- /** 应用后在 catalog 中确认存在的对象 */
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 读取:通过注入的 QueryExecutor 从 PostgreSQL 系统目录读取真实状态。
3
- * 所有 SQL 参数化($1 = schemas 数组),默认只读 public schema。
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
- * 可选事务封装:提供时 apply 用它包裹 begin/commit/rollback;
10
- * 缺省时退化为在 executor 上顺序执行 begin/commit/rollback 语句(mock 友好)。
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'(未被 disable) */
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
- /** 从 proconfig(text[])中提取 search_path=... 配置,无则 null */
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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Lint:对 SQL 源文本做正则级静态分析,无需数据库连接。
2
+ * Lint: Regex-based static analysis of SQL source text without requiring database connection.
3
3
  */
4
4
  import type { DatabaseModule } from './module.js';
5
5
  export interface LintIssue {
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Manifest:把若干 DatabaseModule 汇总为可序列化的数据库治理清单,
3
- * 并提供 explainObject 做人类可读的对象解释。
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
- * 按名字解释一个对象:策略(name 或 table.name)、函数(schema.name)、
21
- * 触发器(name)、归属表(schema.name)、授权对象(schema.name,最后兜底)。
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
- * 模块声明:数据库治理的一等资源(RLS 策略 / RPC 函数 / 触发器 / 授权)。
3
- * driver 无关,纯声明层。
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
- /** 策略名,如 cases_select */
7
+ /** Policy name, e.g. cases_select */
8
8
  name: string;
9
- /** 带 schema 的表名:public.cases */
9
+ /** Schema-qualified table name: public.cases */
10
10
  table: string;
11
11
  operation: PolicyOperation;
12
- /** 适用角色,如 ['authenticated'] */
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
- /** 带 schema 的函数名:public.case_create */
20
+ /** Schema-qualified function name: public.case_create */
21
21
  name: string;
22
22
  source: string;
23
- /** 业务权限标识,如 case.create */
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
- /** 带 schema 的表名 */
33
+ /** Schema-qualified table name */
34
34
  table: string;
35
35
  source: string;
36
36
  }
37
37
  export interface GrantDecl {
38
- /** 带 schema 的对象名:public.cases */
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
- /** drizzle Table 的内部结构形状(仅类型层兼容,不 import drizzle-orm) */
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
- /** 归属表:drizzle 表对象或带 schema 表名均可 */
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
- /** 归一化为 'schema.name' 形式的表名 */
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:把模块声明的可重复 SQL 对象(函数/策略/触发器/授权)编译为有序执行计划。
3
- * 边界:只管可重复对象,不做表结构 migration —— 表结构仍走前向 migration。
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
- /** 对象标识:函数为 schema 限定名;策略/触发器为 table.name;授权为 object:privilege:role */
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
- /** 依赖序:function -> policy -> trigger -> grant */
25
+ /** Dependency order: function -> policy -> trigger -> grant */
26
26
  steps: PlanStep[];
27
- /** 全部 step sha256 的组合哈希 */
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>;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 对账:声明式 Manifest(DatabaseModule)与 PostgreSQL 真实 Catalog 比对。
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
- /** 无 error 级问题 */
16
+ /** True if no error-level issues */
17
17
  ok: boolean;
18
18
  }
19
- /** 'public.cases' → ['public', 'cases'];无 schema 前缀时默认 public */
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.2.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",