@rustrak/client 0.18.1 → 0.19.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 +312 -123
- package/dist/_virtual/_rolldown/runtime.cjs +23 -0
- package/dist/client.cjs +86 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +18 -0
- package/dist/client.d.cts.map +1 -0
- package/dist/client.d.mts +18 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +86 -0
- package/dist/client.mjs.map +1 -0
- package/dist/config.d.cts +10 -0
- package/dist/config.d.cts.map +1 -0
- package/dist/config.d.mts +10 -0
- package/dist/config.d.mts.map +1 -0
- package/dist/errors.cjs +52 -0
- package/dist/errors.cjs.map +1 -0
- package/dist/errors.d.cts +77 -0
- package/dist/errors.d.cts.map +1 -0
- package/dist/errors.d.mts +77 -0
- package/dist/errors.d.mts.map +1 -0
- package/dist/errors.mjs +48 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/generated/client/client.gen.cjs +174 -0
- package/dist/generated/client/client.gen.cjs.map +1 -0
- package/dist/generated/client/client.gen.mjs +174 -0
- package/dist/generated/client/client.gen.mjs.map +1 -0
- package/dist/generated/client/types.gen.d.cts +71 -0
- package/dist/generated/client/types.gen.d.cts.map +1 -0
- package/dist/generated/client/types.gen.d.mts +71 -0
- package/dist/generated/client/types.gen.d.mts.map +1 -0
- package/dist/generated/client/utils.gen.cjs +184 -0
- package/dist/generated/client/utils.gen.cjs.map +1 -0
- package/dist/generated/client/utils.gen.d.cts +21 -0
- package/dist/generated/client/utils.gen.d.cts.map +1 -0
- package/dist/generated/client/utils.gen.d.mts +21 -0
- package/dist/generated/client/utils.gen.d.mts.map +1 -0
- package/dist/generated/client/utils.gen.mjs +177 -0
- package/dist/generated/client/utils.gen.mjs.map +1 -0
- package/dist/generated/client.gen.cjs +7 -0
- package/dist/generated/client.gen.cjs.map +1 -0
- package/dist/generated/client.gen.mjs +8 -0
- package/dist/generated/client.gen.mjs.map +1 -0
- package/dist/generated/core/auth.gen.cjs +12 -0
- package/dist/generated/core/auth.gen.cjs.map +1 -0
- package/dist/generated/core/auth.gen.d.cts +11 -0
- package/dist/generated/core/auth.gen.d.cts.map +1 -0
- package/dist/generated/core/auth.gen.d.mts +11 -0
- package/dist/generated/core/auth.gen.d.mts.map +1 -0
- package/dist/generated/core/auth.gen.mjs +12 -0
- package/dist/generated/core/auth.gen.mjs.map +1 -0
- package/dist/generated/core/bodySerializer.gen.cjs +21 -0
- package/dist/generated/core/bodySerializer.gen.cjs.map +1 -0
- package/dist/generated/core/bodySerializer.gen.d.cts +15 -0
- package/dist/generated/core/bodySerializer.gen.d.cts.map +1 -0
- package/dist/generated/core/bodySerializer.gen.d.mts +15 -0
- package/dist/generated/core/bodySerializer.gen.d.mts.map +1 -0
- package/dist/generated/core/bodySerializer.gen.mjs +20 -0
- package/dist/generated/core/bodySerializer.gen.mjs.map +1 -0
- package/dist/generated/core/pathSerializer.gen.cjs +87 -0
- package/dist/generated/core/pathSerializer.gen.cjs.map +1 -0
- package/dist/generated/core/pathSerializer.gen.d.cts +9 -0
- package/dist/generated/core/pathSerializer.gen.d.cts.map +1 -0
- package/dist/generated/core/pathSerializer.gen.d.mts +9 -0
- package/dist/generated/core/pathSerializer.gen.d.mts.map +1 -0
- package/dist/generated/core/pathSerializer.gen.mjs +82 -0
- package/dist/generated/core/pathSerializer.gen.mjs.map +1 -0
- package/dist/generated/core/serverSentEvents.gen.cjs +96 -0
- package/dist/generated/core/serverSentEvents.gen.cjs.map +1 -0
- package/dist/generated/core/serverSentEvents.gen.d.cts +25 -0
- package/dist/generated/core/serverSentEvents.gen.d.cts.map +1 -0
- package/dist/generated/core/serverSentEvents.gen.d.mts +25 -0
- package/dist/generated/core/serverSentEvents.gen.d.mts.map +1 -0
- package/dist/generated/core/serverSentEvents.gen.mjs +96 -0
- package/dist/generated/core/serverSentEvents.gen.mjs.map +1 -0
- package/dist/generated/core/types.gen.d.cts +27 -0
- package/dist/generated/core/types.gen.d.cts.map +1 -0
- package/dist/generated/core/types.gen.d.mts +27 -0
- package/dist/generated/core/types.gen.d.mts.map +1 -0
- package/dist/generated/core/utils.gen.cjs +81 -0
- package/dist/generated/core/utils.gen.cjs.map +1 -0
- package/dist/generated/core/utils.gen.mjs +78 -0
- package/dist/generated/core/utils.gen.mjs.map +1 -0
- package/dist/generated/sdk.gen.cjs +3138 -0
- package/dist/generated/sdk.gen.cjs.map +1 -0
- package/dist/generated/sdk.gen.d.cts +276 -0
- package/dist/generated/sdk.gen.d.cts.map +1 -0
- package/dist/generated/sdk.gen.d.mts +276 -0
- package/dist/generated/sdk.gen.d.mts.map +1 -0
- package/dist/generated/sdk.gen.mjs +3110 -0
- package/dist/generated/sdk.gen.mjs.map +1 -0
- package/dist/generated/types.gen.d.cts +3679 -0
- package/dist/generated/types.gen.d.cts.map +1 -0
- package/dist/generated/types.gen.d.mts +3679 -0
- package/dist/generated/types.gen.d.mts.map +1 -0
- package/dist/generated/zod.gen.cjs +3076 -0
- package/dist/generated/zod.gen.cjs.map +1 -0
- package/dist/generated/zod.gen.mjs +2618 -0
- package/dist/generated/zod.gen.mjs.map +1 -0
- package/dist/index.cjs +15 -3248
- package/dist/index.d.cts +6 -2037
- package/dist/index.d.mts +6 -0
- package/dist/index.mjs +4 -0
- package/dist/result.cjs +59 -0
- package/dist/result.cjs.map +1 -0
- package/dist/result.d.cts +15 -0
- package/dist/result.d.cts.map +1 -0
- package/dist/result.d.mts +15 -0
- package/dist/result.d.mts.map +1 -0
- package/dist/result.mjs +55 -0
- package/dist/result.mjs.map +1 -0
- package/dist/utils/http.cjs +339 -0
- package/dist/utils/http.cjs.map +1 -0
- package/dist/utils/http.mjs +336 -0
- package/dist/utils/http.mjs.map +1 -0
- package/package.json +17 -12
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.ts +0 -2037
- package/dist/index.js +0 -3232
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.cjs","names":["Rustrak","createClient","createConfig","createFetch","Ok","isTransportFailure","Err","transformHttpError","ZodError"],"sources":["../src/client.ts"],"sourcesContent":["import { ZodError } from 'zod';\nimport type { ClientConfig } from './config.js';\nimport type { RustrakError } from './errors.js';\nimport { createClient, createConfig } from './generated/client/index.js';\nimport { Rustrak } from './generated/sdk.gen.js';\nimport { Err, Ok, type Result } from './result.js';\nimport {\n createFetch,\n isTransportFailure,\n transformHttpError,\n} from './utils/http.js';\n\n/** What a caller may pass: the request's own parts, never client plumbing. */\ntype RequestKeys = 'path' | 'query' | 'body' | 'headers' | 'signal';\n\ntype Call<F> = F extends (options: infer O) => infer R\n ? (\n ...args: {} extends Pick<O, Extract<keyof O, RequestKeys>>\n ? [options?: Pick<O, Extract<keyof O, RequestKeys>>]\n : [options: Pick<O, Extract<keyof O, RequestKeys>>]\n ) => Promise<Result<Data<Awaited<R>>, RustrakError>>\n : never;\n\n/** The success half of the generated `{ data, error }` union. */\ntype Data<R> =\n Extract<R, { error: undefined }> extends { data: infer D } ? D : never;\n\ntype Resource<T> = { readonly [M in keyof T]: Call<T[M]> };\n\n/**\n * Every operation in `openapi.json`, grouped by tag:\n * `client.issues.list({ path: { project_id } })`.\n *\n * Generated in `src/generated/`; this file only changes what a call returns.\n */\nexport type RustrakClient = {\n readonly [R in keyof Rustrak]: Resource<Rustrak[R]>;\n};\n\n/**\n * Main Rustrak API client. Every method returns a `Result`; nothing throws for\n * an expected failure.\n *\n * @example\n * ```typescript\n * const client = new RustrakClient({\n * baseUrl: 'http://localhost:8080',\n * token: 'your-api-token',\n * });\n *\n * const issues = await client.issues.list({ path: { project_id: 1 } });\n * if (issues.success) console.log(issues.data.items);\n * ```\n */\nexport const RustrakClient = class {\n constructor(config: ClientConfig) {\n // biome-ignore lint/correctness/noConstructorReturn: the instance is the generated SDK with every method wrapped\n return Object.setPrototypeOf(createApi(config), new.target.prototype);\n }\n} as unknown as new (\n config: ClientConfig,\n) => RustrakClient;\n\nfunction createApi(config: ClientConfig): RustrakClient {\n const sdk = new Rustrak({\n client: createClient(\n createConfig({\n baseUrl: config.baseUrl,\n credentials: 'include',\n fetch: createFetch(config),\n // Every 2xx in the spec is JSON or empty. `auto` would hand back an\n // HTML page or a stream as a success without validating it.\n parseAs: 'json',\n headers: {\n // Without it a content-negotiating hop can answer with HTML.\n Accept: 'application/json',\n ...config.headers,\n ...(config.token && { Authorization: `Bearer ${config.token}` }),\n },\n }),\n ),\n });\n\n const api: Record<string, Record<string, unknown>> = {};\n for (const name of Object.getOwnPropertyNames(Rustrak.prototype)) {\n if (name === 'constructor') continue;\n const resource = sdk[name as keyof Rustrak] as unknown as Record<\n string,\n (options: object) => Promise<Outcome>\n >;\n const methods: Record<string, unknown> = {};\n for (const method of Object.getOwnPropertyNames(\n Object.getPrototypeOf(resource),\n )) {\n if (method === 'constructor') continue;\n methods[method] = async (options: { headers?: unknown } = {}) =>\n toResult(\n await resource[method]!({\n ...options,\n // A JSON write spreads these into an object, which keeps nothing\n // of a `Headers` or an array of pairs.\n headers:\n options.headers instanceof Headers ||\n Array.isArray(options.headers)\n ? Object.fromEntries(new Headers(options.headers))\n : options.headers,\n throwOnError: false,\n }),\n );\n }\n api[name] = methods;\n }\n return api as unknown as RustrakClient;\n}\n\ntype Outcome = {\n data?: unknown;\n error?: unknown;\n response?: Response;\n};\n\n/**\n * The boundary: the generated client's `{ data, error, response }` becomes a\n * `Result`.\n *\n * Only failures this file recognises become an `Err`. Anything else, a\n * `TypeError` from a genuine bug, is rethrown so it crashes where it can be\n * debugged instead of being laundered into `{ success: false }`.\n */\nfunction toResult({\n data,\n error,\n response,\n}: Outcome): Result<unknown, RustrakError> {\n if (error === undefined) return Ok(data);\n if (isTransportFailure(error)) return Err(error.rustrakError);\n if (response && !response.ok) {\n // `error` is the parsed body, or its text when it is not JSON.\n return Err(transformHttpError(response, error));\n }\n if (!response && error instanceof ZodError) {\n return Err({\n kind: 'invalid_request',\n message: 'The request input did not match the expected schema.',\n });\n }\n if (response && error instanceof SyntaxError) {\n return Err({\n kind: 'invalid_response',\n message: 'The server returned a body that is not valid JSON.',\n });\n }\n if (response && error instanceof ZodError) {\n // No Zod issues: they embed the offending response data.\n return Err({\n kind: 'invalid_response',\n message: 'The server response did not match the expected schema.',\n });\n }\n throw error;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAsDA,MAAa,gBAAgB,MAAM;CACjC,YAAY,QAAsB;EAEhC,OAAO,OAAO,eAAe,UAAU,MAAM,GAAG,WAAW,SAAS;CACtE;AACF;AAIA,SAAS,UAAU,QAAqC;CACtD,MAAM,MAAM,IAAIA,gBAAAA,QAAQ,EACtB,QAAQC,mBAAAA,aACNC,kBAAAA,aAAa;EACX,SAAS,OAAO;EAChB,aAAa;EACb,OAAOC,aAAAA,YAAY,MAAM;EAGzB,SAAS;EACT,SAAS;GAEP,QAAQ;GACR,GAAG,OAAO;GACV,GAAI,OAAO,SAAS,EAAE,eAAe,UAAU,OAAO,QAAQ;EAChE;CACF,CAAC,CACH,EACF,CAAC;CAED,MAAM,MAA+C,CAAC;CACtD,KAAK,MAAM,QAAQ,OAAO,oBAAoBH,gBAAAA,QAAQ,SAAS,GAAG;EAChE,IAAI,SAAS,eAAe;EAC5B,MAAM,WAAW,IAAI;EAIrB,MAAM,UAAmC,CAAC;EAC1C,KAAK,MAAM,UAAU,OAAO,oBAC1B,OAAO,eAAe,QAAQ,CAChC,GAAG;GACD,IAAI,WAAW,eAAe;GAC9B,QAAQ,UAAU,OAAO,UAAiC,CAAC,MACzD,SACE,MAAM,SAAS,OAAO,CAAE;IACtB,GAAG;IAGH,SACE,QAAQ,mBAAmB,WAC3B,MAAM,QAAQ,QAAQ,OAAO,IACzB,OAAO,YAAY,IAAI,QAAQ,QAAQ,OAAO,CAAC,IAC/C,QAAQ;IACd,cAAc;GAChB,CAAC,CACH;EACJ;EACA,IAAI,QAAQ;CACd;CACA,OAAO;AACT;;;;;;;;;AAgBA,SAAS,SAAS,EAChB,MACA,OACA,YACyC;CACzC,IAAI,UAAU,KAAA,GAAW,OAAOI,eAAAA,GAAG,IAAI;CACvC,IAAIC,aAAAA,mBAAmB,KAAK,GAAG,OAAOC,eAAAA,IAAI,MAAM,YAAY;CAC5D,IAAI,YAAY,CAAC,SAAS,IAExB,OAAOA,eAAAA,IAAIC,aAAAA,mBAAmB,UAAU,KAAK,CAAC;CAEhD,IAAI,CAAC,YAAY,iBAAiBC,IAAAA,UAChC,OAAOF,eAAAA,IAAI;EACT,MAAM;EACN,SAAS;CACX,CAAC;CAEH,IAAI,YAAY,iBAAiB,aAC/B,OAAOA,eAAAA,IAAI;EACT,MAAM;EACN,SAAS;CACX,CAAC;CAEH,IAAI,YAAY,iBAAiBE,IAAAA,UAE/B,OAAOF,eAAAA,IAAI;EACT,MAAM;EACN,SAAS;CACX,CAAC;CAEH,MAAM;AACR"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ClientConfig } from "./config.cjs";
|
|
2
|
+
import { RustrakError } from "./errors.cjs";
|
|
3
|
+
import { Rustrak } from "./generated/sdk.gen.cjs";
|
|
4
|
+
import { Result } from "./result.cjs";
|
|
5
|
+
//#region src/client.d.ts
|
|
6
|
+
type RequestKeys = 'path' | 'query' | 'body' | 'headers' | 'signal';
|
|
7
|
+
type Call<F> = F extends ((options: infer O) => infer R) ? (...args: {} extends Pick<O, Extract<keyof O, RequestKeys>> ? [options?: Pick<O, Extract<keyof O, RequestKeys>>] : [options: Pick<O, Extract<keyof O, RequestKeys>>]) => Promise<Result<Data<Awaited<R>>, RustrakError>> : never;
|
|
8
|
+
type Data<R> = Extract<R, {
|
|
9
|
+
error: undefined;
|
|
10
|
+
}> extends {
|
|
11
|
+
data: infer D;
|
|
12
|
+
} ? D : never;
|
|
13
|
+
type Resource<T> = { readonly [M in keyof T]: Call<T[M]>; };
|
|
14
|
+
export type RustrakClient = { readonly [R in keyof Rustrak]: Resource<Rustrak[R]>; };
|
|
15
|
+
export declare const RustrakClient: new (config: ClientConfig) => RustrakClient;
|
|
16
|
+
//#endregion
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=client.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.cts","names":[],"sources":["../src/client.ts"],"mappings":";;;;;KAaK;KAEA,KAAK,KAAK,YAAW,eAAe,YAAY,SAE5C,iBAAiB,KAAK,GAAG,cAAc,GAAG,iBACxC,UAAU,KAAK,GAAG,cAAc,GAAG,kBACnC,SAAS,KAAK,GAAG,cAAc,GAAG,mBACpC,QAAQ,OAAO,KAAK,QAAQ,KAAK;KAIrC,KAAK,KACR,QAAQ;EAAK;;EAA8B,YAAY;IAAM;KAE1D,SAAS,iBAAiB,WAAW,IAAI,KAAK,EAAE;YAQzC,4BACA,WAAW,UAAU,SAAS,QAAQ;qBAkBrC,oBAMX,QAAQ,iBACL"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ClientConfig } from "./config.mjs";
|
|
2
|
+
import { RustrakError } from "./errors.mjs";
|
|
3
|
+
import { Rustrak } from "./generated/sdk.gen.mjs";
|
|
4
|
+
import { Result } from "./result.mjs";
|
|
5
|
+
//#region src/client.d.ts
|
|
6
|
+
type RequestKeys = 'path' | 'query' | 'body' | 'headers' | 'signal';
|
|
7
|
+
type Call<F> = F extends ((options: infer O) => infer R) ? (...args: {} extends Pick<O, Extract<keyof O, RequestKeys>> ? [options?: Pick<O, Extract<keyof O, RequestKeys>>] : [options: Pick<O, Extract<keyof O, RequestKeys>>]) => Promise<Result<Data<Awaited<R>>, RustrakError>> : never;
|
|
8
|
+
type Data<R> = Extract<R, {
|
|
9
|
+
error: undefined;
|
|
10
|
+
}> extends {
|
|
11
|
+
data: infer D;
|
|
12
|
+
} ? D : never;
|
|
13
|
+
type Resource<T> = { readonly [M in keyof T]: Call<T[M]>; };
|
|
14
|
+
export type RustrakClient = { readonly [R in keyof Rustrak]: Resource<Rustrak[R]>; };
|
|
15
|
+
export declare const RustrakClient: new (config: ClientConfig) => RustrakClient;
|
|
16
|
+
//#endregion
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=client.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.mts","names":[],"sources":["../src/client.ts"],"mappings":";;;;;KAaK;KAEA,KAAK,KAAK,YAAW,eAAe,YAAY,SAE5C,iBAAiB,KAAK,GAAG,cAAc,GAAG,iBACxC,UAAU,KAAK,GAAG,cAAc,GAAG,kBACnC,SAAS,KAAK,GAAG,cAAc,GAAG,mBACpC,QAAQ,OAAO,KAAK,QAAQ,KAAK;KAIrC,KAAK,KACR,QAAQ;EAAK;;EAA8B,YAAY;IAAM;KAE1D,SAAS,iBAAiB,WAAW,IAAI,KAAK,EAAE;YAQzC,4BACA,WAAW,UAAU,SAAS,QAAQ;qBAkBrC,oBAMX,QAAQ,iBACL"}
|
package/dist/client.mjs
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createConfig } from "./generated/client/utils.gen.mjs";
|
|
2
|
+
import { createClient } from "./generated/client/client.gen.mjs";
|
|
3
|
+
import { Rustrak } from "./generated/sdk.gen.mjs";
|
|
4
|
+
import { Err, Ok } from "./result.mjs";
|
|
5
|
+
import { createFetch, isTransportFailure, transformHttpError } from "./utils/http.mjs";
|
|
6
|
+
import { ZodError } from "zod";
|
|
7
|
+
//#region src/client.ts
|
|
8
|
+
/**
|
|
9
|
+
* Main Rustrak API client. Every method returns a `Result`; nothing throws for
|
|
10
|
+
* an expected failure.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const client = new RustrakClient({
|
|
15
|
+
* baseUrl: 'http://localhost:8080',
|
|
16
|
+
* token: 'your-api-token',
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* const issues = await client.issues.list({ path: { project_id: 1 } });
|
|
20
|
+
* if (issues.success) console.log(issues.data.items);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
const RustrakClient = class {
|
|
24
|
+
constructor(config) {
|
|
25
|
+
return Object.setPrototypeOf(createApi(config), new.target.prototype);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
function createApi(config) {
|
|
29
|
+
const sdk = new Rustrak({ client: createClient(createConfig({
|
|
30
|
+
baseUrl: config.baseUrl,
|
|
31
|
+
credentials: "include",
|
|
32
|
+
fetch: createFetch(config),
|
|
33
|
+
parseAs: "json",
|
|
34
|
+
headers: {
|
|
35
|
+
Accept: "application/json",
|
|
36
|
+
...config.headers,
|
|
37
|
+
...config.token && { Authorization: `Bearer ${config.token}` }
|
|
38
|
+
}
|
|
39
|
+
})) });
|
|
40
|
+
const api = {};
|
|
41
|
+
for (const name of Object.getOwnPropertyNames(Rustrak.prototype)) {
|
|
42
|
+
if (name === "constructor") continue;
|
|
43
|
+
const resource = sdk[name];
|
|
44
|
+
const methods = {};
|
|
45
|
+
for (const method of Object.getOwnPropertyNames(Object.getPrototypeOf(resource))) {
|
|
46
|
+
if (method === "constructor") continue;
|
|
47
|
+
methods[method] = async (options = {}) => toResult(await resource[method]({
|
|
48
|
+
...options,
|
|
49
|
+
headers: options.headers instanceof Headers || Array.isArray(options.headers) ? Object.fromEntries(new Headers(options.headers)) : options.headers,
|
|
50
|
+
throwOnError: false
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
api[name] = methods;
|
|
54
|
+
}
|
|
55
|
+
return api;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The boundary: the generated client's `{ data, error, response }` becomes a
|
|
59
|
+
* `Result`.
|
|
60
|
+
*
|
|
61
|
+
* Only failures this file recognises become an `Err`. Anything else, a
|
|
62
|
+
* `TypeError` from a genuine bug, is rethrown so it crashes where it can be
|
|
63
|
+
* debugged instead of being laundered into `{ success: false }`.
|
|
64
|
+
*/
|
|
65
|
+
function toResult({ data, error, response }) {
|
|
66
|
+
if (error === void 0) return Ok(data);
|
|
67
|
+
if (isTransportFailure(error)) return Err(error.rustrakError);
|
|
68
|
+
if (response && !response.ok) return Err(transformHttpError(response, error));
|
|
69
|
+
if (!response && error instanceof ZodError) return Err({
|
|
70
|
+
kind: "invalid_request",
|
|
71
|
+
message: "The request input did not match the expected schema."
|
|
72
|
+
});
|
|
73
|
+
if (response && error instanceof SyntaxError) return Err({
|
|
74
|
+
kind: "invalid_response",
|
|
75
|
+
message: "The server returned a body that is not valid JSON."
|
|
76
|
+
});
|
|
77
|
+
if (response && error instanceof ZodError) return Err({
|
|
78
|
+
kind: "invalid_response",
|
|
79
|
+
message: "The server response did not match the expected schema."
|
|
80
|
+
});
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
export { RustrakClient };
|
|
85
|
+
|
|
86
|
+
//# sourceMappingURL=client.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["import { ZodError } from 'zod';\nimport type { ClientConfig } from './config.js';\nimport type { RustrakError } from './errors.js';\nimport { createClient, createConfig } from './generated/client/index.js';\nimport { Rustrak } from './generated/sdk.gen.js';\nimport { Err, Ok, type Result } from './result.js';\nimport {\n createFetch,\n isTransportFailure,\n transformHttpError,\n} from './utils/http.js';\n\n/** What a caller may pass: the request's own parts, never client plumbing. */\ntype RequestKeys = 'path' | 'query' | 'body' | 'headers' | 'signal';\n\ntype Call<F> = F extends (options: infer O) => infer R\n ? (\n ...args: {} extends Pick<O, Extract<keyof O, RequestKeys>>\n ? [options?: Pick<O, Extract<keyof O, RequestKeys>>]\n : [options: Pick<O, Extract<keyof O, RequestKeys>>]\n ) => Promise<Result<Data<Awaited<R>>, RustrakError>>\n : never;\n\n/** The success half of the generated `{ data, error }` union. */\ntype Data<R> =\n Extract<R, { error: undefined }> extends { data: infer D } ? D : never;\n\ntype Resource<T> = { readonly [M in keyof T]: Call<T[M]> };\n\n/**\n * Every operation in `openapi.json`, grouped by tag:\n * `client.issues.list({ path: { project_id } })`.\n *\n * Generated in `src/generated/`; this file only changes what a call returns.\n */\nexport type RustrakClient = {\n readonly [R in keyof Rustrak]: Resource<Rustrak[R]>;\n};\n\n/**\n * Main Rustrak API client. Every method returns a `Result`; nothing throws for\n * an expected failure.\n *\n * @example\n * ```typescript\n * const client = new RustrakClient({\n * baseUrl: 'http://localhost:8080',\n * token: 'your-api-token',\n * });\n *\n * const issues = await client.issues.list({ path: { project_id: 1 } });\n * if (issues.success) console.log(issues.data.items);\n * ```\n */\nexport const RustrakClient = class {\n constructor(config: ClientConfig) {\n // biome-ignore lint/correctness/noConstructorReturn: the instance is the generated SDK with every method wrapped\n return Object.setPrototypeOf(createApi(config), new.target.prototype);\n }\n} as unknown as new (\n config: ClientConfig,\n) => RustrakClient;\n\nfunction createApi(config: ClientConfig): RustrakClient {\n const sdk = new Rustrak({\n client: createClient(\n createConfig({\n baseUrl: config.baseUrl,\n credentials: 'include',\n fetch: createFetch(config),\n // Every 2xx in the spec is JSON or empty. `auto` would hand back an\n // HTML page or a stream as a success without validating it.\n parseAs: 'json',\n headers: {\n // Without it a content-negotiating hop can answer with HTML.\n Accept: 'application/json',\n ...config.headers,\n ...(config.token && { Authorization: `Bearer ${config.token}` }),\n },\n }),\n ),\n });\n\n const api: Record<string, Record<string, unknown>> = {};\n for (const name of Object.getOwnPropertyNames(Rustrak.prototype)) {\n if (name === 'constructor') continue;\n const resource = sdk[name as keyof Rustrak] as unknown as Record<\n string,\n (options: object) => Promise<Outcome>\n >;\n const methods: Record<string, unknown> = {};\n for (const method of Object.getOwnPropertyNames(\n Object.getPrototypeOf(resource),\n )) {\n if (method === 'constructor') continue;\n methods[method] = async (options: { headers?: unknown } = {}) =>\n toResult(\n await resource[method]!({\n ...options,\n // A JSON write spreads these into an object, which keeps nothing\n // of a `Headers` or an array of pairs.\n headers:\n options.headers instanceof Headers ||\n Array.isArray(options.headers)\n ? Object.fromEntries(new Headers(options.headers))\n : options.headers,\n throwOnError: false,\n }),\n );\n }\n api[name] = methods;\n }\n return api as unknown as RustrakClient;\n}\n\ntype Outcome = {\n data?: unknown;\n error?: unknown;\n response?: Response;\n};\n\n/**\n * The boundary: the generated client's `{ data, error, response }` becomes a\n * `Result`.\n *\n * Only failures this file recognises become an `Err`. Anything else, a\n * `TypeError` from a genuine bug, is rethrown so it crashes where it can be\n * debugged instead of being laundered into `{ success: false }`.\n */\nfunction toResult({\n data,\n error,\n response,\n}: Outcome): Result<unknown, RustrakError> {\n if (error === undefined) return Ok(data);\n if (isTransportFailure(error)) return Err(error.rustrakError);\n if (response && !response.ok) {\n // `error` is the parsed body, or its text when it is not JSON.\n return Err(transformHttpError(response, error));\n }\n if (!response && error instanceof ZodError) {\n return Err({\n kind: 'invalid_request',\n message: 'The request input did not match the expected schema.',\n });\n }\n if (response && error instanceof SyntaxError) {\n return Err({\n kind: 'invalid_response',\n message: 'The server returned a body that is not valid JSON.',\n });\n }\n if (response && error instanceof ZodError) {\n // No Zod issues: they embed the offending response data.\n return Err({\n kind: 'invalid_response',\n message: 'The server response did not match the expected schema.',\n });\n }\n throw error;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAsDA,MAAa,gBAAgB,MAAM;CACjC,YAAY,QAAsB;EAEhC,OAAO,OAAO,eAAe,UAAU,MAAM,GAAG,WAAW,SAAS;CACtE;AACF;AAIA,SAAS,UAAU,QAAqC;CACtD,MAAM,MAAM,IAAI,QAAQ,EACtB,QAAQ,aACN,aAAa;EACX,SAAS,OAAO;EAChB,aAAa;EACb,OAAO,YAAY,MAAM;EAGzB,SAAS;EACT,SAAS;GAEP,QAAQ;GACR,GAAG,OAAO;GACV,GAAI,OAAO,SAAS,EAAE,eAAe,UAAU,OAAO,QAAQ;EAChE;CACF,CAAC,CACH,EACF,CAAC;CAED,MAAM,MAA+C,CAAC;CACtD,KAAK,MAAM,QAAQ,OAAO,oBAAoB,QAAQ,SAAS,GAAG;EAChE,IAAI,SAAS,eAAe;EAC5B,MAAM,WAAW,IAAI;EAIrB,MAAM,UAAmC,CAAC;EAC1C,KAAK,MAAM,UAAU,OAAO,oBAC1B,OAAO,eAAe,QAAQ,CAChC,GAAG;GACD,IAAI,WAAW,eAAe;GAC9B,QAAQ,UAAU,OAAO,UAAiC,CAAC,MACzD,SACE,MAAM,SAAS,OAAO,CAAE;IACtB,GAAG;IAGH,SACE,QAAQ,mBAAmB,WAC3B,MAAM,QAAQ,QAAQ,OAAO,IACzB,OAAO,YAAY,IAAI,QAAQ,QAAQ,OAAO,CAAC,IAC/C,QAAQ;IACd,cAAc;GAChB,CAAC,CACH;EACJ;EACA,IAAI,QAAQ;CACd;CACA,OAAO;AACT;;;;;;;;;AAgBA,SAAS,SAAS,EAChB,MACA,OACA,YACyC;CACzC,IAAI,UAAU,KAAA,GAAW,OAAO,GAAG,IAAI;CACvC,IAAI,mBAAmB,KAAK,GAAG,OAAO,IAAI,MAAM,YAAY;CAC5D,IAAI,YAAY,CAAC,SAAS,IAExB,OAAO,IAAI,mBAAmB,UAAU,KAAK,CAAC;CAEhD,IAAI,CAAC,YAAY,iBAAiB,UAChC,OAAO,IAAI;EACT,MAAM;EACN,SAAS;CACX,CAAC;CAEH,IAAI,YAAY,iBAAiB,aAC/B,OAAO,IAAI;EACT,MAAM;EACN,SAAS;CACX,CAAC;CAEH,IAAI,YAAY,iBAAiB,UAE/B,OAAO,IAAI;EACT,MAAM;EACN,SAAS;CACX,CAAC;CAEH,MAAM;AACR"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.cts","names":[],"sources":["../src/config.ts"],"mappings":";iBAGiB;EAKf;EAOA;EAMA;EAMA;EAKA,UAAU"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.mts","names":[],"sources":["../src/config.ts"],"mappings":";iBAGiB;EAKf;EAOA;EAMA;EAMA;EAKA,UAAU"}
|
package/dist/errors.cjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
/**
|
|
3
|
+
* Every `code` a {@link FieldError} may carry, mirroring the `FieldErrorCode`
|
|
4
|
+
* enum in `apps/server/src/error.rs`.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately tiny and resource-agnostic. `tests/unit/app-error-contract.test.ts`
|
|
7
|
+
* parses that Rust enum and fails if this list stops matching it, so a variant
|
|
8
|
+
* added on the server cannot ship without a matching entry here.
|
|
9
|
+
*
|
|
10
|
+
* Exported as a value, not only a type, so a consumer can exhaustively switch
|
|
11
|
+
* or validate against it at runtime.
|
|
12
|
+
*/
|
|
13
|
+
const FIELD_ERROR_CODES = [
|
|
14
|
+
"required",
|
|
15
|
+
"invalid",
|
|
16
|
+
"already_exists",
|
|
17
|
+
"too_short",
|
|
18
|
+
"too_long",
|
|
19
|
+
"custom"
|
|
20
|
+
];
|
|
21
|
+
/**
|
|
22
|
+
* The fixed message every `server_error` carries. Exported so a consumer (or a
|
|
23
|
+
* test) can assert that nothing server-supplied survived the 5xx redaction.
|
|
24
|
+
*/
|
|
25
|
+
const SERVER_ERROR_MESSAGE = "The server failed to handle the request.";
|
|
26
|
+
/**
|
|
27
|
+
* The fixed message a `network` error carries when the request never reached
|
|
28
|
+
* the server. Fixed for the same reason as {@link SERVER_ERROR_MESSAGE}: the
|
|
29
|
+
* underlying message interpolates the request URL, which is the deployment's
|
|
30
|
+
* internal host and port. Read `reason` to branch, not `message`.
|
|
31
|
+
*/
|
|
32
|
+
const NETWORK_ERROR_MESSAGE = "The request could not reach the server.";
|
|
33
|
+
/** The fixed message a `network` error carries when the client timed out. */
|
|
34
|
+
const TIMEOUT_ERROR_MESSAGE = "The request timed out.";
|
|
35
|
+
/**
|
|
36
|
+
* Whether retrying the same call could plausibly succeed.
|
|
37
|
+
*
|
|
38
|
+
* `network` and `server_error` are transient by nature; `rate_limited` clears
|
|
39
|
+
* once its window rolls over. Everything else is deterministic: the same
|
|
40
|
+
* request will fail the same way.
|
|
41
|
+
*/
|
|
42
|
+
function isRetryable(error) {
|
|
43
|
+
return error.kind === "network" || error.kind === "server_error" || error.kind === "rate_limited";
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
exports.FIELD_ERROR_CODES = FIELD_ERROR_CODES;
|
|
47
|
+
exports.NETWORK_ERROR_MESSAGE = NETWORK_ERROR_MESSAGE;
|
|
48
|
+
exports.SERVER_ERROR_MESSAGE = SERVER_ERROR_MESSAGE;
|
|
49
|
+
exports.TIMEOUT_ERROR_MESSAGE = TIMEOUT_ERROR_MESSAGE;
|
|
50
|
+
exports.isRetryable = isRetryable;
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=errors.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.cjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * The one closed error union `@rustrak/client` reports.\n *\n * It mirrors `apps/server/src/error.rs` variant by variant, plus the three\n * failures that never reach the server (`invalid_request`, `network`,\n * `invalid_response`) and a catch-all so an unanticipated status is still\n * representable rather than unrepresentable.\n *\n * Every member is a plain object: no class, no prototype chain, no `Error`\n * instance. That is what lets a failed `Result` survive `structuredClone` and\n * therefore React's server/client boundary.\n *\n * `status` is `number`, never a literal union. Pinning literals would make a\n * proxy-generated 502 or a future status a type error at the call site rather\n * than a value the consumer can log.\n *\n * `fields` appears on every member that can carry an `AppError` body, because\n * `AppError::with_field` works on all eight Rust variants: a `Forbidden` or a\n * `NotFound` may name an input just as a `Conflict` does. Only the members\n * that never carry a server body at all (`server_error`, which discards it,\n * plus `invalid_request`, `network` and `invalid_response`, which never had\n * one) omit it. See {@link FieldError}.\n */\nexport type RustrakError =\n /**\n * 400. `AppError::Validation`: the server rejected the request.\n *\n * `fields` is present only when the server named the offending inputs; a\n * consumer that ignores it behaves exactly as it did before.\n */\n | {\n readonly kind: 'validation';\n readonly status: number;\n readonly message: string;\n readonly fields?: readonly FieldError[];\n }\n /** 401. `AppError::Unauthorized`: no session, or the session expired. */\n | {\n readonly kind: 'unauthenticated';\n readonly status: number;\n readonly message: string;\n readonly fields?: readonly FieldError[];\n }\n /** 403. `AppError::Forbidden`: authenticated, but not allowed. */\n | {\n readonly kind: 'forbidden';\n readonly status: number;\n readonly message: string;\n readonly fields?: readonly FieldError[];\n }\n /** 404. `AppError::NotFound`. */\n | {\n readonly kind: 'not_found';\n readonly status: number;\n readonly message: string;\n readonly fields?: readonly FieldError[];\n }\n /**\n * 409. `AppError::Conflict`.\n *\n * Where `fields` earns its keep: uniqueness is the thing only the server can\n * know, and a taken name or slug is a 409, not a 400.\n */\n | {\n readonly kind: 'conflict';\n readonly status: number;\n readonly message: string;\n readonly fields?: readonly FieldError[];\n }\n /** 410. Reserved for endpoints that retire a resource rather than delete it. */\n | {\n readonly kind: 'gone';\n readonly status: number;\n readonly message: string;\n readonly fields?: readonly FieldError[];\n }\n /** 413. `AppError::PayloadTooLarge`: envelope ingestion only. */\n | {\n readonly kind: 'payload_too_large';\n readonly status: number;\n readonly message: string;\n readonly fields?: readonly FieldError[];\n }\n /** 429. The ingest rate limiter (`routes/ingest.rs`), the one flat body. */\n | {\n readonly kind: 'rate_limited';\n readonly status: number;\n readonly message: string;\n /** Seconds to wait, from the `Retry-After` header, when the server sent one. */\n readonly retryAfter?: number;\n readonly fields?: readonly FieldError[];\n }\n /**\n * Any status below 500 that no other member claims. The catch-all that keeps\n * the union closed but total.\n *\n * Not only 4xx: a 3xx that reached the error path (a redirect ky did not\n * follow), a 408, an unenumerated 4xx, and an opaque `status: 0` all land\n * here. `status` is the field to read; `kind` only says \"no dedicated\n * handling exists for this one\".\n */\n | {\n readonly kind: 'client_error';\n readonly status: number;\n readonly message: string;\n readonly fields?: readonly FieldError[];\n }\n /**\n * The caller's own input failed a pre-flight check. Never reached the\n * network, so it has no status.\n */\n | { readonly kind: 'invalid_request'; readonly message: string }\n /**\n * Any 5xx. `AppError::Database` and `AppError::Internal` both land here, and\n * so does anything a reverse proxy generates.\n *\n * `message` is always {@link SERVER_ERROR_MESSAGE}: the server's body is\n * discarded at construction, inside the client, so no consumer can leak a\n * connection string or a filesystem path into a UI. `incidentId` is the\n * forward-compatible slot for a correlation id the server does not send yet.\n */\n | {\n readonly kind: 'server_error';\n readonly status: number;\n readonly message: string;\n readonly incidentId?: string;\n }\n /**\n * The request never got an HTTP response: DNS, connection refused, TLS,\n * timeout, abort. Also a connection that died while the body was still being\n * read, which is a transport failure even though a status line arrived.\n *\n * `message` is always {@link NETWORK_ERROR_MESSAGE} or\n * {@link TIMEOUT_ERROR_MESSAGE}, never the underlying error's own: fetch\n * builds that message by interpolating the request method and URL\n * (`Request failed due to a network error: GET http://rustrak.internal:8080/api/projects`),\n * so echoing it prints a self-hosted deployment's internal host and port into\n * any UI that renders `error.message`.\n *\n * Carries no `cause` for the same reason. `reason` is the machine-readable\n * discriminator a caller needs instead, derived from the error's `name` and\n * never from its message.\n */\n | {\n readonly kind: 'network';\n readonly message: string;\n /** `timeout` when the client gave up waiting; `unreachable` otherwise. */\n readonly reason: 'timeout' | 'unreachable';\n }\n /**\n * A 2xx arrived but the body is not what the schema promises: not JSON, or\n * JSON of the wrong shape.\n *\n * Carries no Zod issues: they embed the offending response data.\n */\n | { readonly kind: 'invalid_response'; readonly message: string };\n\n/** The `kind` discriminant, useful for exhaustive `switch` in consumers. */\nexport type RustrakErrorKind = RustrakError['kind'];\n\n/**\n * Every `code` a {@link FieldError} may carry, mirroring the `FieldErrorCode`\n * enum in `apps/server/src/error.rs`.\n *\n * Deliberately tiny and resource-agnostic. `tests/unit/app-error-contract.test.ts`\n * parses that Rust enum and fails if this list stops matching it, so a variant\n * added on the server cannot ship without a matching entry here.\n *\n * Exported as a value, not only a type, so a consumer can exhaustively switch\n * or validate against it at runtime.\n */\nexport const FIELD_ERROR_CODES = [\n 'required',\n 'invalid',\n 'already_exists',\n 'too_short',\n 'too_long',\n 'custom',\n] as const;\n\n/** The `code` of a {@link FieldError}. */\nexport type FieldErrorCode = (typeof FIELD_ERROR_CODES)[number];\n\n/**\n * One input the server rejected, named as data.\n *\n * `field` is a **dot path into the request body** (`slug`,\n * `credentials.webhook_url`), which is exactly what a form library's\n * `setError` takes.\n *\n * Select the copy shown to the user from `(field, code)` and never from\n * `message`: that is what makes it translatable. `message` is populated only\n * for `code: 'custom'`, where the code set genuinely cannot express the\n * reason, and is then rendered verbatim. The client strips a `message` that\n * arrives on any other code, so the rule holds even against a server that\n * breaks it.\n *\n * The same `(field, code)` pair can mean two things at two statuses:\n * `(role, 'invalid')` is \"not a role at all\" on a 400 and \"a real role, but\n * not acceptable right now\" on a 409. Branch on `kind` as well when the copy\n * has to differ.\n *\n * A `field` naming an input the form does not have must fall back to a\n * form-level error. Passing an unknown path to `setError` registers a\n * phantom field that nothing can ever clear.\n */\nexport type FieldError = {\n readonly field: string;\n readonly code: FieldErrorCode;\n readonly message?: string;\n};\n\n/**\n * The fixed message every `server_error` carries. Exported so a consumer (or a\n * test) can assert that nothing server-supplied survived the 5xx redaction.\n */\nexport const SERVER_ERROR_MESSAGE = 'The server failed to handle the request.';\n\n/**\n * The fixed message a `network` error carries when the request never reached\n * the server. Fixed for the same reason as {@link SERVER_ERROR_MESSAGE}: the\n * underlying message interpolates the request URL, which is the deployment's\n * internal host and port. Read `reason` to branch, not `message`.\n */\nexport const NETWORK_ERROR_MESSAGE = 'The request could not reach the server.';\n\n/** The fixed message a `network` error carries when the client timed out. */\nexport const TIMEOUT_ERROR_MESSAGE = 'The request timed out.';\n\n/**\n * Whether retrying the same call could plausibly succeed.\n *\n * `network` and `server_error` are transient by nature; `rate_limited` clears\n * once its window rolls over. Everything else is deterministic: the same\n * request will fail the same way.\n */\nexport function isRetryable(error: RustrakError): boolean {\n return (\n error.kind === 'network' ||\n error.kind === 'server_error' ||\n error.kind === 'rate_limited'\n );\n}\n"],"mappings":";;;;;;;;;;;;AA2KA,MAAa,oBAAoB;CAC/B;CACA;CACA;CACA;CACA;CACA;AACF;;;;;AAsCA,MAAa,uBAAuB;;;;;;;AAQpC,MAAa,wBAAwB;;AAGrC,MAAa,wBAAwB;;;;;;;;AASrC,SAAgB,YAAY,OAA8B;CACxD,OACE,MAAM,SAAS,aACf,MAAM,SAAS,kBACf,MAAM,SAAS;AAEnB"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
//#region src/errors.d.ts
|
|
2
|
+
export type RustrakError = {
|
|
3
|
+
readonly kind: 'validation';
|
|
4
|
+
readonly status: number;
|
|
5
|
+
readonly message: string;
|
|
6
|
+
readonly fields?: readonly FieldError[];
|
|
7
|
+
} | {
|
|
8
|
+
readonly kind: 'unauthenticated';
|
|
9
|
+
readonly status: number;
|
|
10
|
+
readonly message: string;
|
|
11
|
+
readonly fields?: readonly FieldError[];
|
|
12
|
+
} | {
|
|
13
|
+
readonly kind: 'forbidden';
|
|
14
|
+
readonly status: number;
|
|
15
|
+
readonly message: string;
|
|
16
|
+
readonly fields?: readonly FieldError[];
|
|
17
|
+
} | {
|
|
18
|
+
readonly kind: 'not_found';
|
|
19
|
+
readonly status: number;
|
|
20
|
+
readonly message: string;
|
|
21
|
+
readonly fields?: readonly FieldError[];
|
|
22
|
+
} | {
|
|
23
|
+
readonly kind: 'conflict';
|
|
24
|
+
readonly status: number;
|
|
25
|
+
readonly message: string;
|
|
26
|
+
readonly fields?: readonly FieldError[];
|
|
27
|
+
} | {
|
|
28
|
+
readonly kind: 'gone';
|
|
29
|
+
readonly status: number;
|
|
30
|
+
readonly message: string;
|
|
31
|
+
readonly fields?: readonly FieldError[];
|
|
32
|
+
} | {
|
|
33
|
+
readonly kind: 'payload_too_large';
|
|
34
|
+
readonly status: number;
|
|
35
|
+
readonly message: string;
|
|
36
|
+
readonly fields?: readonly FieldError[];
|
|
37
|
+
} | {
|
|
38
|
+
readonly kind: 'rate_limited';
|
|
39
|
+
readonly status: number;
|
|
40
|
+
readonly message: string;
|
|
41
|
+
readonly retryAfter?: number;
|
|
42
|
+
readonly fields?: readonly FieldError[];
|
|
43
|
+
} | {
|
|
44
|
+
readonly kind: 'client_error';
|
|
45
|
+
readonly status: number;
|
|
46
|
+
readonly message: string;
|
|
47
|
+
readonly fields?: readonly FieldError[];
|
|
48
|
+
} | {
|
|
49
|
+
readonly kind: 'invalid_request';
|
|
50
|
+
readonly message: string;
|
|
51
|
+
} | {
|
|
52
|
+
readonly kind: 'server_error';
|
|
53
|
+
readonly status: number;
|
|
54
|
+
readonly message: string;
|
|
55
|
+
readonly incidentId?: string;
|
|
56
|
+
} | {
|
|
57
|
+
readonly kind: 'network';
|
|
58
|
+
readonly message: string;
|
|
59
|
+
readonly reason: 'timeout' | 'unreachable';
|
|
60
|
+
} | {
|
|
61
|
+
readonly kind: 'invalid_response';
|
|
62
|
+
readonly message: string;
|
|
63
|
+
};
|
|
64
|
+
export type RustrakErrorKind = RustrakError['kind'];
|
|
65
|
+
export declare const FIELD_ERROR_CODES: readonly ["required", "invalid", "already_exists", "too_short", "too_long", "custom"];
|
|
66
|
+
export type FieldErrorCode = (typeof FIELD_ERROR_CODES)[number];
|
|
67
|
+
export type FieldError = {
|
|
68
|
+
readonly field: string;
|
|
69
|
+
readonly code: FieldErrorCode;
|
|
70
|
+
readonly message?: string;
|
|
71
|
+
};
|
|
72
|
+
export declare const SERVER_ERROR_MESSAGE = "The server failed to handle the request.";
|
|
73
|
+
export declare const NETWORK_ERROR_MESSAGE = "The request could not reach the server.";
|
|
74
|
+
export declare const TIMEOUT_ERROR_MESSAGE = "The request timed out.";
|
|
75
|
+
export declare function isRetryable(error: RustrakError): boolean;
|
|
76
|
+
//#endregion
|
|
77
|
+
//# sourceMappingURL=errors.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.cts","names":[],"sources":["../src/errors.ts"],"mappings":";YAuBY;WAQG;WACA;WACA;WACA,kBAAkB;;WAIlB;WACA;WACA;WACA,kBAAkB;;WAIlB;WACA;WACA;WACA,kBAAkB;;WAIlB;WACA;WACA;WACA,kBAAkB;;WASlB;WACA;WACA;WACA,kBAAkB;;WAIlB;WACA;WACA;WACA,kBAAkB;;WAIlB;WACA;WACA;WACA,kBAAkB;;WAIlB;WACA;WACA;WAEA;WACA,kBAAkB;;WAYlB;WACA;WACA;WACA,kBAAkB;;WAMlB;WAAkC;;WAWlC;WACA;WACA;WACA;;WAmBA;WACA;WAEA;;WAQA;WAAmC;;YAGtC,mBAAmB;qBAalB;YAUD,yBAAyB;YAyBzB;WACD;WACA,MAAM;WACN;;qBAOE;qBAQA;qBAGA;wBASG,YAAY,OAAO"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
//#region src/errors.d.ts
|
|
2
|
+
export type RustrakError = {
|
|
3
|
+
readonly kind: 'validation';
|
|
4
|
+
readonly status: number;
|
|
5
|
+
readonly message: string;
|
|
6
|
+
readonly fields?: readonly FieldError[];
|
|
7
|
+
} | {
|
|
8
|
+
readonly kind: 'unauthenticated';
|
|
9
|
+
readonly status: number;
|
|
10
|
+
readonly message: string;
|
|
11
|
+
readonly fields?: readonly FieldError[];
|
|
12
|
+
} | {
|
|
13
|
+
readonly kind: 'forbidden';
|
|
14
|
+
readonly status: number;
|
|
15
|
+
readonly message: string;
|
|
16
|
+
readonly fields?: readonly FieldError[];
|
|
17
|
+
} | {
|
|
18
|
+
readonly kind: 'not_found';
|
|
19
|
+
readonly status: number;
|
|
20
|
+
readonly message: string;
|
|
21
|
+
readonly fields?: readonly FieldError[];
|
|
22
|
+
} | {
|
|
23
|
+
readonly kind: 'conflict';
|
|
24
|
+
readonly status: number;
|
|
25
|
+
readonly message: string;
|
|
26
|
+
readonly fields?: readonly FieldError[];
|
|
27
|
+
} | {
|
|
28
|
+
readonly kind: 'gone';
|
|
29
|
+
readonly status: number;
|
|
30
|
+
readonly message: string;
|
|
31
|
+
readonly fields?: readonly FieldError[];
|
|
32
|
+
} | {
|
|
33
|
+
readonly kind: 'payload_too_large';
|
|
34
|
+
readonly status: number;
|
|
35
|
+
readonly message: string;
|
|
36
|
+
readonly fields?: readonly FieldError[];
|
|
37
|
+
} | {
|
|
38
|
+
readonly kind: 'rate_limited';
|
|
39
|
+
readonly status: number;
|
|
40
|
+
readonly message: string;
|
|
41
|
+
readonly retryAfter?: number;
|
|
42
|
+
readonly fields?: readonly FieldError[];
|
|
43
|
+
} | {
|
|
44
|
+
readonly kind: 'client_error';
|
|
45
|
+
readonly status: number;
|
|
46
|
+
readonly message: string;
|
|
47
|
+
readonly fields?: readonly FieldError[];
|
|
48
|
+
} | {
|
|
49
|
+
readonly kind: 'invalid_request';
|
|
50
|
+
readonly message: string;
|
|
51
|
+
} | {
|
|
52
|
+
readonly kind: 'server_error';
|
|
53
|
+
readonly status: number;
|
|
54
|
+
readonly message: string;
|
|
55
|
+
readonly incidentId?: string;
|
|
56
|
+
} | {
|
|
57
|
+
readonly kind: 'network';
|
|
58
|
+
readonly message: string;
|
|
59
|
+
readonly reason: 'timeout' | 'unreachable';
|
|
60
|
+
} | {
|
|
61
|
+
readonly kind: 'invalid_response';
|
|
62
|
+
readonly message: string;
|
|
63
|
+
};
|
|
64
|
+
export type RustrakErrorKind = RustrakError['kind'];
|
|
65
|
+
export declare const FIELD_ERROR_CODES: readonly ["required", "invalid", "already_exists", "too_short", "too_long", "custom"];
|
|
66
|
+
export type FieldErrorCode = (typeof FIELD_ERROR_CODES)[number];
|
|
67
|
+
export type FieldError = {
|
|
68
|
+
readonly field: string;
|
|
69
|
+
readonly code: FieldErrorCode;
|
|
70
|
+
readonly message?: string;
|
|
71
|
+
};
|
|
72
|
+
export declare const SERVER_ERROR_MESSAGE = "The server failed to handle the request.";
|
|
73
|
+
export declare const NETWORK_ERROR_MESSAGE = "The request could not reach the server.";
|
|
74
|
+
export declare const TIMEOUT_ERROR_MESSAGE = "The request timed out.";
|
|
75
|
+
export declare function isRetryable(error: RustrakError): boolean;
|
|
76
|
+
//#endregion
|
|
77
|
+
//# sourceMappingURL=errors.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.mts","names":[],"sources":["../src/errors.ts"],"mappings":";YAuBY;WAQG;WACA;WACA;WACA,kBAAkB;;WAIlB;WACA;WACA;WACA,kBAAkB;;WAIlB;WACA;WACA;WACA,kBAAkB;;WAIlB;WACA;WACA;WACA,kBAAkB;;WASlB;WACA;WACA;WACA,kBAAkB;;WAIlB;WACA;WACA;WACA,kBAAkB;;WAIlB;WACA;WACA;WACA,kBAAkB;;WAIlB;WACA;WACA;WAEA;WACA,kBAAkB;;WAYlB;WACA;WACA;WACA,kBAAkB;;WAMlB;WAAkC;;WAWlC;WACA;WACA;WACA;;WAmBA;WACA;WAEA;;WAQA;WAAmC;;YAGtC,mBAAmB;qBAalB;YAUD,yBAAyB;YAyBzB;WACD;WACA,MAAM;WACN;;qBAOE;qBAQA;qBAGA;wBASG,YAAY,OAAO"}
|
package/dist/errors.mjs
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
/**
|
|
3
|
+
* Every `code` a {@link FieldError} may carry, mirroring the `FieldErrorCode`
|
|
4
|
+
* enum in `apps/server/src/error.rs`.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately tiny and resource-agnostic. `tests/unit/app-error-contract.test.ts`
|
|
7
|
+
* parses that Rust enum and fails if this list stops matching it, so a variant
|
|
8
|
+
* added on the server cannot ship without a matching entry here.
|
|
9
|
+
*
|
|
10
|
+
* Exported as a value, not only a type, so a consumer can exhaustively switch
|
|
11
|
+
* or validate against it at runtime.
|
|
12
|
+
*/
|
|
13
|
+
const FIELD_ERROR_CODES = [
|
|
14
|
+
"required",
|
|
15
|
+
"invalid",
|
|
16
|
+
"already_exists",
|
|
17
|
+
"too_short",
|
|
18
|
+
"too_long",
|
|
19
|
+
"custom"
|
|
20
|
+
];
|
|
21
|
+
/**
|
|
22
|
+
* The fixed message every `server_error` carries. Exported so a consumer (or a
|
|
23
|
+
* test) can assert that nothing server-supplied survived the 5xx redaction.
|
|
24
|
+
*/
|
|
25
|
+
const SERVER_ERROR_MESSAGE = "The server failed to handle the request.";
|
|
26
|
+
/**
|
|
27
|
+
* The fixed message a `network` error carries when the request never reached
|
|
28
|
+
* the server. Fixed for the same reason as {@link SERVER_ERROR_MESSAGE}: the
|
|
29
|
+
* underlying message interpolates the request URL, which is the deployment's
|
|
30
|
+
* internal host and port. Read `reason` to branch, not `message`.
|
|
31
|
+
*/
|
|
32
|
+
const NETWORK_ERROR_MESSAGE = "The request could not reach the server.";
|
|
33
|
+
/** The fixed message a `network` error carries when the client timed out. */
|
|
34
|
+
const TIMEOUT_ERROR_MESSAGE = "The request timed out.";
|
|
35
|
+
/**
|
|
36
|
+
* Whether retrying the same call could plausibly succeed.
|
|
37
|
+
*
|
|
38
|
+
* `network` and `server_error` are transient by nature; `rate_limited` clears
|
|
39
|
+
* once its window rolls over. Everything else is deterministic: the same
|
|
40
|
+
* request will fail the same way.
|
|
41
|
+
*/
|
|
42
|
+
function isRetryable(error) {
|
|
43
|
+
return error.kind === "network" || error.kind === "server_error" || error.kind === "rate_limited";
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { FIELD_ERROR_CODES, NETWORK_ERROR_MESSAGE, SERVER_ERROR_MESSAGE, TIMEOUT_ERROR_MESSAGE, isRetryable };
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=errors.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * The one closed error union `@rustrak/client` reports.\n *\n * It mirrors `apps/server/src/error.rs` variant by variant, plus the three\n * failures that never reach the server (`invalid_request`, `network`,\n * `invalid_response`) and a catch-all so an unanticipated status is still\n * representable rather than unrepresentable.\n *\n * Every member is a plain object: no class, no prototype chain, no `Error`\n * instance. That is what lets a failed `Result` survive `structuredClone` and\n * therefore React's server/client boundary.\n *\n * `status` is `number`, never a literal union. Pinning literals would make a\n * proxy-generated 502 or a future status a type error at the call site rather\n * than a value the consumer can log.\n *\n * `fields` appears on every member that can carry an `AppError` body, because\n * `AppError::with_field` works on all eight Rust variants: a `Forbidden` or a\n * `NotFound` may name an input just as a `Conflict` does. Only the members\n * that never carry a server body at all (`server_error`, which discards it,\n * plus `invalid_request`, `network` and `invalid_response`, which never had\n * one) omit it. See {@link FieldError}.\n */\nexport type RustrakError =\n /**\n * 400. `AppError::Validation`: the server rejected the request.\n *\n * `fields` is present only when the server named the offending inputs; a\n * consumer that ignores it behaves exactly as it did before.\n */\n | {\n readonly kind: 'validation';\n readonly status: number;\n readonly message: string;\n readonly fields?: readonly FieldError[];\n }\n /** 401. `AppError::Unauthorized`: no session, or the session expired. */\n | {\n readonly kind: 'unauthenticated';\n readonly status: number;\n readonly message: string;\n readonly fields?: readonly FieldError[];\n }\n /** 403. `AppError::Forbidden`: authenticated, but not allowed. */\n | {\n readonly kind: 'forbidden';\n readonly status: number;\n readonly message: string;\n readonly fields?: readonly FieldError[];\n }\n /** 404. `AppError::NotFound`. */\n | {\n readonly kind: 'not_found';\n readonly status: number;\n readonly message: string;\n readonly fields?: readonly FieldError[];\n }\n /**\n * 409. `AppError::Conflict`.\n *\n * Where `fields` earns its keep: uniqueness is the thing only the server can\n * know, and a taken name or slug is a 409, not a 400.\n */\n | {\n readonly kind: 'conflict';\n readonly status: number;\n readonly message: string;\n readonly fields?: readonly FieldError[];\n }\n /** 410. Reserved for endpoints that retire a resource rather than delete it. */\n | {\n readonly kind: 'gone';\n readonly status: number;\n readonly message: string;\n readonly fields?: readonly FieldError[];\n }\n /** 413. `AppError::PayloadTooLarge`: envelope ingestion only. */\n | {\n readonly kind: 'payload_too_large';\n readonly status: number;\n readonly message: string;\n readonly fields?: readonly FieldError[];\n }\n /** 429. The ingest rate limiter (`routes/ingest.rs`), the one flat body. */\n | {\n readonly kind: 'rate_limited';\n readonly status: number;\n readonly message: string;\n /** Seconds to wait, from the `Retry-After` header, when the server sent one. */\n readonly retryAfter?: number;\n readonly fields?: readonly FieldError[];\n }\n /**\n * Any status below 500 that no other member claims. The catch-all that keeps\n * the union closed but total.\n *\n * Not only 4xx: a 3xx that reached the error path (a redirect ky did not\n * follow), a 408, an unenumerated 4xx, and an opaque `status: 0` all land\n * here. `status` is the field to read; `kind` only says \"no dedicated\n * handling exists for this one\".\n */\n | {\n readonly kind: 'client_error';\n readonly status: number;\n readonly message: string;\n readonly fields?: readonly FieldError[];\n }\n /**\n * The caller's own input failed a pre-flight check. Never reached the\n * network, so it has no status.\n */\n | { readonly kind: 'invalid_request'; readonly message: string }\n /**\n * Any 5xx. `AppError::Database` and `AppError::Internal` both land here, and\n * so does anything a reverse proxy generates.\n *\n * `message` is always {@link SERVER_ERROR_MESSAGE}: the server's body is\n * discarded at construction, inside the client, so no consumer can leak a\n * connection string or a filesystem path into a UI. `incidentId` is the\n * forward-compatible slot for a correlation id the server does not send yet.\n */\n | {\n readonly kind: 'server_error';\n readonly status: number;\n readonly message: string;\n readonly incidentId?: string;\n }\n /**\n * The request never got an HTTP response: DNS, connection refused, TLS,\n * timeout, abort. Also a connection that died while the body was still being\n * read, which is a transport failure even though a status line arrived.\n *\n * `message` is always {@link NETWORK_ERROR_MESSAGE} or\n * {@link TIMEOUT_ERROR_MESSAGE}, never the underlying error's own: fetch\n * builds that message by interpolating the request method and URL\n * (`Request failed due to a network error: GET http://rustrak.internal:8080/api/projects`),\n * so echoing it prints a self-hosted deployment's internal host and port into\n * any UI that renders `error.message`.\n *\n * Carries no `cause` for the same reason. `reason` is the machine-readable\n * discriminator a caller needs instead, derived from the error's `name` and\n * never from its message.\n */\n | {\n readonly kind: 'network';\n readonly message: string;\n /** `timeout` when the client gave up waiting; `unreachable` otherwise. */\n readonly reason: 'timeout' | 'unreachable';\n }\n /**\n * A 2xx arrived but the body is not what the schema promises: not JSON, or\n * JSON of the wrong shape.\n *\n * Carries no Zod issues: they embed the offending response data.\n */\n | { readonly kind: 'invalid_response'; readonly message: string };\n\n/** The `kind` discriminant, useful for exhaustive `switch` in consumers. */\nexport type RustrakErrorKind = RustrakError['kind'];\n\n/**\n * Every `code` a {@link FieldError} may carry, mirroring the `FieldErrorCode`\n * enum in `apps/server/src/error.rs`.\n *\n * Deliberately tiny and resource-agnostic. `tests/unit/app-error-contract.test.ts`\n * parses that Rust enum and fails if this list stops matching it, so a variant\n * added on the server cannot ship without a matching entry here.\n *\n * Exported as a value, not only a type, so a consumer can exhaustively switch\n * or validate against it at runtime.\n */\nexport const FIELD_ERROR_CODES = [\n 'required',\n 'invalid',\n 'already_exists',\n 'too_short',\n 'too_long',\n 'custom',\n] as const;\n\n/** The `code` of a {@link FieldError}. */\nexport type FieldErrorCode = (typeof FIELD_ERROR_CODES)[number];\n\n/**\n * One input the server rejected, named as data.\n *\n * `field` is a **dot path into the request body** (`slug`,\n * `credentials.webhook_url`), which is exactly what a form library's\n * `setError` takes.\n *\n * Select the copy shown to the user from `(field, code)` and never from\n * `message`: that is what makes it translatable. `message` is populated only\n * for `code: 'custom'`, where the code set genuinely cannot express the\n * reason, and is then rendered verbatim. The client strips a `message` that\n * arrives on any other code, so the rule holds even against a server that\n * breaks it.\n *\n * The same `(field, code)` pair can mean two things at two statuses:\n * `(role, 'invalid')` is \"not a role at all\" on a 400 and \"a real role, but\n * not acceptable right now\" on a 409. Branch on `kind` as well when the copy\n * has to differ.\n *\n * A `field` naming an input the form does not have must fall back to a\n * form-level error. Passing an unknown path to `setError` registers a\n * phantom field that nothing can ever clear.\n */\nexport type FieldError = {\n readonly field: string;\n readonly code: FieldErrorCode;\n readonly message?: string;\n};\n\n/**\n * The fixed message every `server_error` carries. Exported so a consumer (or a\n * test) can assert that nothing server-supplied survived the 5xx redaction.\n */\nexport const SERVER_ERROR_MESSAGE = 'The server failed to handle the request.';\n\n/**\n * The fixed message a `network` error carries when the request never reached\n * the server. Fixed for the same reason as {@link SERVER_ERROR_MESSAGE}: the\n * underlying message interpolates the request URL, which is the deployment's\n * internal host and port. Read `reason` to branch, not `message`.\n */\nexport const NETWORK_ERROR_MESSAGE = 'The request could not reach the server.';\n\n/** The fixed message a `network` error carries when the client timed out. */\nexport const TIMEOUT_ERROR_MESSAGE = 'The request timed out.';\n\n/**\n * Whether retrying the same call could plausibly succeed.\n *\n * `network` and `server_error` are transient by nature; `rate_limited` clears\n * once its window rolls over. Everything else is deterministic: the same\n * request will fail the same way.\n */\nexport function isRetryable(error: RustrakError): boolean {\n return (\n error.kind === 'network' ||\n error.kind === 'server_error' ||\n error.kind === 'rate_limited'\n );\n}\n"],"mappings":";;;;;;;;;;;;AA2KA,MAAa,oBAAoB;CAC/B;CACA;CACA;CACA;CACA;CACA;AACF;;;;;AAsCA,MAAa,uBAAuB;;;;;;;AAQpC,MAAa,wBAAwB;;AAGrC,MAAa,wBAAwB;;;;;;;;AASrC,SAAgB,YAAY,OAA8B;CACxD,OACE,MAAM,SAAS,aACf,MAAM,SAAS,kBACf,MAAM,SAAS;AAEnB"}
|