Skip to content

Commit 3872362

Browse files
committed
feat(api): give the admin app its own generated client and mark the CRM operations x-internal (#1286)
Synced from sferarc/pgbeam@4c9525c
1 parent 43e63d3 commit 3872362

58 files changed

Lines changed: 98 additions & 3781 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/client.ts

Lines changed: 71 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,45 @@
1-
import {
2-
type ApiOperations,
3-
operationsByPath,
4-
operationsByTag,
5-
type RequestMap,
6-
type Route,
7-
} from "./generated/operations-map.gen";
1+
import { type ApiClient, operationsByPath, operationsByTag } from "./generated/operations-map.gen";
82
import {
93
type FetcherOptions,
104
fetcher,
115
type OnResponseHook,
126
type RetryConfig,
137
} from "./utils/fetcher";
148

9+
export type { ApiClient };
10+
11+
/** A single operation's HTTP method and path template. */
12+
export interface OperationMeta {
13+
method: string;
14+
path: string;
15+
}
16+
17+
/**
18+
* The dispatch tables the client drives: operations grouped by tag (for
19+
* `api.<tag>.<operationId>()`) and keyed by route (for `api.request()`).
20+
*
21+
* Defaults to the registry generated from the public API bundle. A client
22+
* generated from a different bundle passes its own registry and its own `api`
23+
* type, so it reuses this transport (auth, retries, error mapping) instead of
24+
* forking it. See `@pgbeam/sdk-internal`.
25+
*/
26+
export interface OperationRegistry {
27+
byTag: Record<string, Record<string, OperationMeta>>;
28+
byPath: Record<string, OperationMeta>;
29+
}
30+
31+
const publicRegistry: OperationRegistry = {
32+
byTag: operationsByTag,
33+
byPath: operationsByPath,
34+
};
35+
36+
/** Params accepted by every generated operation, before the surface types narrow them. */
37+
interface CallParams {
38+
pathParams?: FetcherOptions["pathParams"];
39+
queryParams?: FetcherOptions["queryParams"];
40+
body?: unknown;
41+
}
42+
1543
export interface PgBeamClientOptions {
1644
/** JWT token, or async function that resolves one (for lazy/refreshing tokens). */
1745
token: string | null | (() => Promise<string | null>);
@@ -23,32 +51,18 @@ export interface PgBeamClientOptions {
2351
onResponse?: OnResponseHook;
2452
/** Retry configuration. Default: { maxRetries: 5 }. Set false to disable. */
2553
retry?: RetryConfig | false;
54+
/** Operation registry to dispatch on. Defaults to the public API's. */
55+
operations?: OperationRegistry;
2656
}
2757

28-
/** Type-safe client with tag-based proxy access and .request() method. */
29-
export type ApiClient = ApiOperations & {
30-
/**
31-
* Type-safe request by route string.
32-
*
33-
* @example
34-
* const projects = await api.request('GET /v1/projects', { queryParams: { org_id } });
35-
* const project = await api.request('GET /v1/projects/{project_id}', { pathParams: { project_id } });
36-
*/
37-
request: <K extends Route>(
38-
route: K,
39-
...args: RequestMap[K]["params"] extends undefined
40-
? [params?: undefined]
41-
: [params: RequestMap[K]["params"]]
42-
) => Promise<RequestMap[K]["response"]>;
43-
};
44-
45-
export class PgBeamClient {
58+
export class PgBeamClient<TApi = ApiClient> {
4659
private _baseUrl: string;
4760
private _tokenOrFn: string | null | (() => Promise<string | null>);
4861
private _fetchImpl?: typeof globalThis.fetch;
4962
private _onResponse?: OnResponseHook;
5063
private _retry?: RetryConfig;
51-
private _api?: ApiClient;
64+
private _operations: OperationRegistry;
65+
private _api?: TApi;
5266

5367
constructor(options: PgBeamClientOptions) {
5468
this._baseUrl = options.baseUrl;
@@ -57,6 +71,7 @@ export class PgBeamClient {
5771
this._onResponse = options.onResponse;
5872
this._retry =
5973
options.retry === false ? { maxRetries: 0 } : (options.retry ?? { maxRetries: 5 });
74+
this._operations = options.operations ?? publicRegistry;
6075
}
6176

6277
private async _resolveToken(): Promise<string | null> {
@@ -66,71 +81,57 @@ export class PgBeamClient {
6681
return this._tokenOrFn;
6782
}
6883

69-
private _call(method: string, path: string) {
70-
return async (params: Record<string, unknown> = {}) => {
71-
const token = await this._resolveToken();
72-
return fetcher({
73-
method,
74-
path,
75-
pathParams: params.pathParams as FetcherOptions["pathParams"],
76-
queryParams: params.queryParams as FetcherOptions["queryParams"],
77-
body: params.body,
78-
baseUrl: this._baseUrl,
79-
token,
80-
fetchImpl: this._fetchImpl,
81-
onResponse: this._onResponse,
82-
retry: this._retry,
83-
});
84-
};
84+
private async _send(meta: OperationMeta, params: CallParams): Promise<unknown> {
85+
const token = await this._resolveToken();
86+
return fetcher({
87+
method: meta.method,
88+
path: meta.path,
89+
pathParams: params.pathParams,
90+
queryParams: params.queryParams,
91+
body: params.body,
92+
baseUrl: this._baseUrl,
93+
token,
94+
fetchImpl: this._fetchImpl,
95+
onResponse: this._onResponse,
96+
retry: this._retry,
97+
});
98+
}
99+
100+
private _call(meta: OperationMeta) {
101+
return (params: CallParams = {}) => this._send(meta, params);
85102
}
86103

87104
/** Access API operations via tag-based namespaces or `.request()`. */
88-
get api(): ApiClient {
105+
get api(): TApi {
89106
if (this._api) return this._api;
90107

91-
const request = async <K extends Route>(
92-
route: K,
93-
params?: RequestMap[K]["params"],
94-
): Promise<RequestMap[K]["response"]> => {
95-
const meta = operationsByPath[route as keyof typeof operationsByPath];
96-
if (!meta) throw new Error(`Unknown route: ${String(route)}`);
97-
const token = await this._resolveToken();
98-
return fetcher({
99-
method: meta.method,
100-
path: meta.path,
101-
pathParams: (params as Record<string, unknown> | undefined)
102-
?.pathParams as FetcherOptions["pathParams"],
103-
queryParams: (params as Record<string, unknown> | undefined)
104-
?.queryParams as FetcherOptions["queryParams"],
105-
body: (params as Record<string, unknown> | undefined)?.body,
106-
baseUrl: this._baseUrl,
107-
token,
108-
fetchImpl: this._fetchImpl,
109-
onResponse: this._onResponse,
110-
retry: this._retry,
111-
});
108+
// Async so an unknown route rejects rather than throwing synchronously,
109+
// which is what callers awaiting .request() expect.
110+
const request = async (route: string, params: CallParams = {}) => {
111+
const meta = this._operations.byPath[route];
112+
if (!meta) throw new Error(`Unknown route: ${route}`);
113+
return this._send(meta, params);
112114
};
113115

114-
this._api = new Proxy({} as ApiClient, {
116+
this._api = new Proxy({} as TApi, {
115117
get: (_, prop: string) => {
116118
if (prop === "request") return request;
117119

118-
const tagOps = operationsByTag[prop as keyof typeof operationsByTag];
120+
const tagOps = this._operations.byTag[prop];
119121
if (!tagOps) return undefined;
120122

121123
return new Proxy(
122124
{},
123125
{
124126
get: (_, method: string) => {
125-
const ops = tagOps as Record<string, { method: string; path: string }>;
126-
const opMeta = ops[method];
127+
const opMeta = tagOps[method];
127128
if (!opMeta) return undefined;
128-
return this._call(opMeta.method, opMeta.path);
129+
return this._call(opMeta);
129130
},
130131
},
131132
);
132133
},
133-
}) as ApiClient;
134+
}) as TApi;
134135

135136
return this._api;
136137
}

0 commit comments

Comments
 (0)