@fragno-dev/core 0.0.1
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/.turbo/turbo-build.log +61 -0
- package/.turbo/turbo-types$colon$check.log +2 -0
- package/dist/api/api.d.ts +2 -0
- package/dist/api/api.js +3 -0
- package/dist/api-CBDGZiLC.d.ts +278 -0
- package/dist/api-CBDGZiLC.d.ts.map +1 -0
- package/dist/api-DgHfYjq2.js +54 -0
- package/dist/api-DgHfYjq2.js.map +1 -0
- package/dist/client/client.d.ts +3 -0
- package/dist/client/client.js +6 -0
- package/dist/client/client.svelte.d.ts +33 -0
- package/dist/client/client.svelte.d.ts.map +1 -0
- package/dist/client/client.svelte.js +123 -0
- package/dist/client/client.svelte.js.map +1 -0
- package/dist/client/react.d.ts +58 -0
- package/dist/client/react.d.ts.map +1 -0
- package/dist/client/react.js +80 -0
- package/dist/client/react.js.map +1 -0
- package/dist/client/vanilla.d.ts +61 -0
- package/dist/client/vanilla.d.ts.map +1 -0
- package/dist/client/vanilla.js +136 -0
- package/dist/client/vanilla.js.map +1 -0
- package/dist/client/vue.d.ts +39 -0
- package/dist/client/vue.d.ts.map +1 -0
- package/dist/client/vue.js +108 -0
- package/dist/client/vue.js.map +1 -0
- package/dist/client-DWjxKDnE.js +703 -0
- package/dist/client-DWjxKDnE.js.map +1 -0
- package/dist/client-XFdAy-IQ.d.ts +287 -0
- package/dist/client-XFdAy-IQ.d.ts.map +1 -0
- package/dist/integrations/astro.d.ts +18 -0
- package/dist/integrations/astro.d.ts.map +1 -0
- package/dist/integrations/astro.js +16 -0
- package/dist/integrations/astro.js.map +1 -0
- package/dist/integrations/next-js.d.ts +15 -0
- package/dist/integrations/next-js.d.ts.map +1 -0
- package/dist/integrations/next-js.js +17 -0
- package/dist/integrations/next-js.js.map +1 -0
- package/dist/integrations/react-ssr.d.ts +19 -0
- package/dist/integrations/react-ssr.d.ts.map +1 -0
- package/dist/integrations/react-ssr.js +38 -0
- package/dist/integrations/react-ssr.js.map +1 -0
- package/dist/integrations/svelte-kit.d.ts +21 -0
- package/dist/integrations/svelte-kit.d.ts.map +1 -0
- package/dist/integrations/svelte-kit.js +18 -0
- package/dist/integrations/svelte-kit.js.map +1 -0
- package/dist/mod.d.ts +3 -0
- package/dist/mod.js +177 -0
- package/dist/mod.js.map +1 -0
- package/dist/route-Bp6eByhz.js +331 -0
- package/dist/route-Bp6eByhz.js.map +1 -0
- package/dist/ssr-tJHqcNSw.js +48 -0
- package/dist/ssr-tJHqcNSw.js.map +1 -0
- package/package.json +127 -0
- package/src/api/api.test.ts +140 -0
- package/src/api/api.ts +106 -0
- package/src/api/error.ts +47 -0
- package/src/api/fragment.test.ts +509 -0
- package/src/api/fragment.ts +277 -0
- package/src/api/internal/path-runtime.test.ts +121 -0
- package/src/api/internal/path-type.test.ts +602 -0
- package/src/api/internal/path.ts +322 -0
- package/src/api/internal/response-stream.ts +118 -0
- package/src/api/internal/route.test.ts +56 -0
- package/src/api/internal/route.ts +9 -0
- package/src/api/request-input-context.test.ts +437 -0
- package/src/api/request-input-context.ts +201 -0
- package/src/api/request-middleware.test.ts +544 -0
- package/src/api/request-middleware.ts +126 -0
- package/src/api/request-output-context.test.ts +626 -0
- package/src/api/request-output-context.ts +175 -0
- package/src/api/route.test.ts +176 -0
- package/src/api/route.ts +152 -0
- package/src/client/client-builder.test.ts +264 -0
- package/src/client/client-error.test.ts +15 -0
- package/src/client/client-error.ts +141 -0
- package/src/client/client-types.test.ts +493 -0
- package/src/client/client.ssr.test.ts +173 -0
- package/src/client/client.svelte.test.ts +837 -0
- package/src/client/client.svelte.ts +278 -0
- package/src/client/client.test.ts +1690 -0
- package/src/client/client.ts +1035 -0
- package/src/client/component.test.svelte +21 -0
- package/src/client/internal/ndjson-streaming.test.ts +457 -0
- package/src/client/internal/ndjson-streaming.ts +248 -0
- package/src/client/react.test.ts +947 -0
- package/src/client/react.ts +241 -0
- package/src/client/vanilla.test.ts +867 -0
- package/src/client/vanilla.ts +265 -0
- package/src/client/vue.test.ts +754 -0
- package/src/client/vue.ts +242 -0
- package/src/http/http-status.ts +60 -0
- package/src/integrations/astro.ts +17 -0
- package/src/integrations/next-js.ts +31 -0
- package/src/integrations/react-ssr.ts +40 -0
- package/src/integrations/svelte-kit.ts +41 -0
- package/src/mod.ts +20 -0
- package/src/util/async.test.ts +85 -0
- package/src/util/async.ts +96 -0
- package/src/util/content-type.test.ts +136 -0
- package/src/util/content-type.ts +84 -0
- package/src/util/nanostores.test.ts +28 -0
- package/src/util/nanostores.ts +65 -0
- package/src/util/ssr.ts +75 -0
- package/src/util/types-util.ts +16 -0
- package/tsconfig.json +10 -0
- package/tsdown.config.ts +21 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { test, expect, expectTypeOf, describe } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createClientBuilder } from "./client";
|
|
4
|
+
import { addRoute } from "../api/api";
|
|
5
|
+
import { defineFragment } from "../api/fragment";
|
|
6
|
+
import type { FragnoPublicClientConfig } from "../api/fragment";
|
|
7
|
+
|
|
8
|
+
// Test route configurations
|
|
9
|
+
const testFragment = defineFragment("test-fragment");
|
|
10
|
+
const testRoutes = [
|
|
11
|
+
// GET routes
|
|
12
|
+
addRoute({
|
|
13
|
+
method: "GET",
|
|
14
|
+
path: "/home",
|
|
15
|
+
outputSchema: z.string(),
|
|
16
|
+
handler: async (_ctx, { json }) => json("ok"),
|
|
17
|
+
}),
|
|
18
|
+
addRoute({
|
|
19
|
+
method: "GET",
|
|
20
|
+
path: "/users",
|
|
21
|
+
outputSchema: z.array(z.object({ id: z.number(), name: z.string() })),
|
|
22
|
+
handler: async (_ctx, { json }) => json([{ id: 1, name: "" }]),
|
|
23
|
+
}),
|
|
24
|
+
addRoute({
|
|
25
|
+
method: "GET",
|
|
26
|
+
path: "/users/:id" as const,
|
|
27
|
+
outputSchema: z.object({ id: z.number(), name: z.string() }),
|
|
28
|
+
handler: async ({ pathParams }, { json }) => json({ id: Number(pathParams.id), name: "" }),
|
|
29
|
+
}),
|
|
30
|
+
addRoute({
|
|
31
|
+
method: "GET",
|
|
32
|
+
path: "/ai-config",
|
|
33
|
+
outputSchema: z.object({
|
|
34
|
+
apiProvider: z.enum(["openai", "anthropic"]),
|
|
35
|
+
model: z.string(),
|
|
36
|
+
systemPrompt: z.string(),
|
|
37
|
+
}),
|
|
38
|
+
handler: async (_ctx, { json }) =>
|
|
39
|
+
json({
|
|
40
|
+
apiProvider: "openai" as const,
|
|
41
|
+
model: "gpt-4o",
|
|
42
|
+
systemPrompt: "",
|
|
43
|
+
}),
|
|
44
|
+
}),
|
|
45
|
+
// Non-GET routes (should not be available for hooks)
|
|
46
|
+
addRoute({
|
|
47
|
+
method: "POST",
|
|
48
|
+
path: "/users",
|
|
49
|
+
inputSchema: z.object({ name: z.string() }),
|
|
50
|
+
outputSchema: z.object({ id: z.number(), name: z.string() }),
|
|
51
|
+
handler: async (_ctx, { json }) => json({ id: 1, name: "" }),
|
|
52
|
+
}),
|
|
53
|
+
addRoute({
|
|
54
|
+
method: "PUT",
|
|
55
|
+
path: "/users/:id",
|
|
56
|
+
inputSchema: z.object({ name: z.string() }),
|
|
57
|
+
handler: async (_ctx, { empty }) => {
|
|
58
|
+
return empty();
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
addRoute({
|
|
62
|
+
method: "DELETE",
|
|
63
|
+
path: "/users/:id",
|
|
64
|
+
handler: async (_ctx, { empty }) => {
|
|
65
|
+
return empty();
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
68
|
+
] as const;
|
|
69
|
+
|
|
70
|
+
const testPublicConfig: FragnoPublicClientConfig = {
|
|
71
|
+
baseUrl: "http://localhost:3000",
|
|
72
|
+
mountRoute: "/api",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Empty fragment config for edge case testing
|
|
76
|
+
const _emptyFragment = defineFragment("empty-fragment");
|
|
77
|
+
const _emptyRoutes = [] as const;
|
|
78
|
+
|
|
79
|
+
// Fragment config with no GET routes
|
|
80
|
+
const noGetFragment = defineFragment("no-get-fragment");
|
|
81
|
+
const noGetRoutes = [
|
|
82
|
+
addRoute({
|
|
83
|
+
method: "POST",
|
|
84
|
+
path: "/create",
|
|
85
|
+
handler: async (_ctx, { json }) => json({}),
|
|
86
|
+
}),
|
|
87
|
+
addRoute({
|
|
88
|
+
method: "DELETE",
|
|
89
|
+
path: "/delete/:id",
|
|
90
|
+
handler: async (_ctx, { empty }) => {
|
|
91
|
+
return empty();
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
] as const;
|
|
95
|
+
|
|
96
|
+
describe("Hook builder (createHookBuilder) and createFragmentHook", () => {
|
|
97
|
+
describe("basic functionality", () => {
|
|
98
|
+
test("should create builder object", () => {
|
|
99
|
+
const builder = createClientBuilder(testFragment, testPublicConfig, testRoutes);
|
|
100
|
+
expectTypeOf(builder.createHook).toBeFunction();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("should create hook for valid GET route", () => {
|
|
104
|
+
const builder = createClientBuilder(testFragment, testPublicConfig, testRoutes);
|
|
105
|
+
const hook = builder.createHook("/users");
|
|
106
|
+
|
|
107
|
+
expect(hook).toHaveProperty("route");
|
|
108
|
+
expect(hook).toHaveProperty("store");
|
|
109
|
+
expect(hook.route.path).toBe("/users");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("should create multiple hooks independently", () => {
|
|
113
|
+
const builder = createClientBuilder(testFragment, testPublicConfig, testRoutes);
|
|
114
|
+
const usersHook = builder.createHook("/users");
|
|
115
|
+
const userHook = builder.createHook("/users/:id");
|
|
116
|
+
const aiHook = builder.createHook("/ai-config");
|
|
117
|
+
|
|
118
|
+
expect(usersHook.route.path).toBe("/users");
|
|
119
|
+
expect(userHook.route.path).toBe("/users/:id");
|
|
120
|
+
expect(aiHook.route.path).toBe("/ai-config");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("error handling", () => {
|
|
125
|
+
test("should throw error for non-existent route", () => {
|
|
126
|
+
const builder = createClientBuilder(testFragment, testPublicConfig, testRoutes);
|
|
127
|
+
|
|
128
|
+
expect(() => {
|
|
129
|
+
// @ts-expect-error - Testing runtime error for invalid path
|
|
130
|
+
builder.createHook("/nonexistent");
|
|
131
|
+
}).toThrow("Route '/nonexistent' not found or is not a GET route with an output schema.");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("should throw error for fragment with no GET routes", () => {
|
|
135
|
+
const builder = createClientBuilder(noGetFragment, testPublicConfig, noGetRoutes);
|
|
136
|
+
|
|
137
|
+
expect(() => {
|
|
138
|
+
// @ts-expect-error - Testing runtime error for no GET routes
|
|
139
|
+
builder.createHook("/create");
|
|
140
|
+
}).toThrow("Route '/create' not found or is not a GET route with an output schema.");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("should handle complex route paths", () => {
|
|
144
|
+
const complexFragment = defineFragment("complex-fragment");
|
|
145
|
+
const complexRoutes = [
|
|
146
|
+
addRoute({
|
|
147
|
+
method: "GET",
|
|
148
|
+
path: "/api/v1/users/:userId/posts/:postId/comments/:commentId",
|
|
149
|
+
outputSchema: z.object({ id: z.number(), content: z.string() }),
|
|
150
|
+
handler: async (_ctx, { json }) => json({ id: 1, content: "" }),
|
|
151
|
+
}),
|
|
152
|
+
addRoute({
|
|
153
|
+
method: "GET",
|
|
154
|
+
path: "/files/**:filepath",
|
|
155
|
+
outputSchema: z.string(),
|
|
156
|
+
handler: async (_ctx, { json }) => json("file"),
|
|
157
|
+
}),
|
|
158
|
+
] as const;
|
|
159
|
+
|
|
160
|
+
const builder = createClientBuilder(complexFragment, testPublicConfig, complexRoutes);
|
|
161
|
+
const commentHook = builder.createHook(
|
|
162
|
+
"/api/v1/users/:userId/posts/:postId/comments/:commentId",
|
|
163
|
+
);
|
|
164
|
+
const fileHook = builder.createHook("/files/**:filepath");
|
|
165
|
+
|
|
166
|
+
expect(commentHook.route.path).toBe(
|
|
167
|
+
"/api/v1/users/:userId/posts/:postId/comments/:commentId",
|
|
168
|
+
);
|
|
169
|
+
expect(fileHook.route.path).toBe("/files/**:filepath");
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("type safety tests", () => {
|
|
175
|
+
test("should only allow valid GET route paths", () => {
|
|
176
|
+
const builder = createClientBuilder(testFragment, testPublicConfig, testRoutes);
|
|
177
|
+
|
|
178
|
+
// These should compile (valid GET routes)
|
|
179
|
+
expect(() => builder.createHook("/home")).not.toThrow();
|
|
180
|
+
expect(() => builder.createHook("/users")).not.toThrow();
|
|
181
|
+
expect(() => builder.createHook("/users/:id")).not.toThrow();
|
|
182
|
+
expect(() => builder.createHook("/ai-config")).not.toThrow();
|
|
183
|
+
|
|
184
|
+
expectTypeOf(builder.createHook)
|
|
185
|
+
.parameter(0)
|
|
186
|
+
.toEqualTypeOf<"/home" | "/users" | "/users/:id" | "/ai-config">();
|
|
187
|
+
|
|
188
|
+
expect(() => {
|
|
189
|
+
// @ts-expect-error - Invalid path should not be allowed
|
|
190
|
+
builder.createHook("/non-existent");
|
|
191
|
+
}).toThrow();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("real-world usage scenarios", () => {
|
|
196
|
+
test("should work with Chatno-like configuration", () => {
|
|
197
|
+
const chatnoFragment = defineFragment("chatno");
|
|
198
|
+
const chatnoRoutes = [
|
|
199
|
+
addRoute({
|
|
200
|
+
method: "GET",
|
|
201
|
+
path: "/home",
|
|
202
|
+
handler: async (_ctx, { empty }) => {
|
|
203
|
+
return empty();
|
|
204
|
+
},
|
|
205
|
+
}),
|
|
206
|
+
addRoute({
|
|
207
|
+
method: "GET",
|
|
208
|
+
path: "/thing/**:path",
|
|
209
|
+
handler: async (_ctx, { empty }) => {
|
|
210
|
+
return empty();
|
|
211
|
+
},
|
|
212
|
+
}),
|
|
213
|
+
addRoute({
|
|
214
|
+
method: "POST",
|
|
215
|
+
path: "/echo",
|
|
216
|
+
inputSchema: z.object({ number: z.number() }),
|
|
217
|
+
outputSchema: z.string(),
|
|
218
|
+
handler: async (_ctx, { json }) => json(""),
|
|
219
|
+
}),
|
|
220
|
+
addRoute({
|
|
221
|
+
method: "GET",
|
|
222
|
+
path: "/ai-config",
|
|
223
|
+
outputSchema: z.object({
|
|
224
|
+
apiProvider: z.enum(["openai", "anthropic"]),
|
|
225
|
+
model: z.string(),
|
|
226
|
+
systemPrompt: z.string(),
|
|
227
|
+
}),
|
|
228
|
+
handler: async (_ctx, { json }) =>
|
|
229
|
+
json({
|
|
230
|
+
apiProvider: "openai" as const,
|
|
231
|
+
model: "gpt-4o",
|
|
232
|
+
systemPrompt: "",
|
|
233
|
+
}),
|
|
234
|
+
}),
|
|
235
|
+
] as const;
|
|
236
|
+
|
|
237
|
+
const builder = createClientBuilder(chatnoFragment, {}, chatnoRoutes);
|
|
238
|
+
const hook = builder.createHook("/ai-config");
|
|
239
|
+
|
|
240
|
+
expect(hook).toHaveProperty("route");
|
|
241
|
+
|
|
242
|
+
// Should not allow POST routes (compile-time) and should throw at runtime if forced
|
|
243
|
+
expect(() => {
|
|
244
|
+
// @ts-expect-error - POST route should not be allowed
|
|
245
|
+
builder.createHook("/echo");
|
|
246
|
+
}).toThrow();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("should handle different public configs", () => {
|
|
250
|
+
const configs = [
|
|
251
|
+
{},
|
|
252
|
+
{ baseUrl: "https://api.example.com" },
|
|
253
|
+
{ mountRoute: "/v1" },
|
|
254
|
+
{ baseUrl: "https://api.example.com", mountRoute: "/v1" },
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
configs.forEach((config) => {
|
|
258
|
+
const builder = createClientBuilder(testFragment, config, testRoutes);
|
|
259
|
+
const result = builder.createHook("/users");
|
|
260
|
+
|
|
261
|
+
expect(result).toHaveProperty("store");
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { test, expect, describe } from "vitest";
|
|
2
|
+
import { FragnoClientApiError } from "./client-error";
|
|
3
|
+
import { FragnoApiError } from "../api/error";
|
|
4
|
+
|
|
5
|
+
describe("Error Conversion", () => {
|
|
6
|
+
test("should convert API error to client error", async () => {
|
|
7
|
+
const apiError = new FragnoApiError({ message: "API error", code: "API_ERROR" }, 500);
|
|
8
|
+
const apiResponse = apiError.toResponse();
|
|
9
|
+
const clientError = await FragnoClientApiError.fromResponse(apiResponse);
|
|
10
|
+
expect(clientError).toBeInstanceOf(FragnoClientApiError);
|
|
11
|
+
expect(clientError.message).toBe("API error");
|
|
12
|
+
expect(clientError.code).toBe("API_ERROR");
|
|
13
|
+
expect(clientError.status).toBe(500);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { StatusCode } from "../http/http-status";
|
|
2
|
+
|
|
3
|
+
export type FragnoErrorOptions = {
|
|
4
|
+
cause?: Error | unknown;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Base error class for all Fragno client errors.
|
|
9
|
+
*/
|
|
10
|
+
export abstract class FragnoClientError<TCode extends string = string> extends Error {
|
|
11
|
+
readonly #code: TCode;
|
|
12
|
+
|
|
13
|
+
constructor(message: string, code: TCode, options: FragnoErrorOptions = {}) {
|
|
14
|
+
super(message, { cause: options.cause });
|
|
15
|
+
this.name = "FragnoClientError";
|
|
16
|
+
this.#code = code;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get code(): TCode | (string & {}) {
|
|
20
|
+
return this.#code;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class FragnoClientFetchError extends FragnoClientError<
|
|
25
|
+
"NO_BODY" | "NETWORK_ERROR" | "ABORT_ERROR"
|
|
26
|
+
> {
|
|
27
|
+
constructor(
|
|
28
|
+
message: string,
|
|
29
|
+
code: "NO_BODY" | "NETWORK_ERROR" | "ABORT_ERROR",
|
|
30
|
+
options: FragnoErrorOptions = {},
|
|
31
|
+
) {
|
|
32
|
+
super(message, code, options);
|
|
33
|
+
this.name = "FragnoClientFetchError";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static fromUnknownFetchError(error: unknown): FragnoClientFetchError {
|
|
37
|
+
if (!(error instanceof Error)) {
|
|
38
|
+
return new FragnoClientFetchNetworkError("Network request failed", { cause: error });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (error.name === "AbortError") {
|
|
42
|
+
return new FragnoClientFetchAbortError("Request was aborted", { cause: error });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return new FragnoClientFetchNetworkError("Network request failed", { cause: error });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Error thrown when a network request fails (e.g., no internet connection, DNS failure).
|
|
51
|
+
*/
|
|
52
|
+
export class FragnoClientFetchNetworkError extends FragnoClientFetchError {
|
|
53
|
+
constructor(message: string = "Network request failed", options: FragnoErrorOptions = {}) {
|
|
54
|
+
super(message, "NETWORK_ERROR", options);
|
|
55
|
+
this.name = "FragnoClientFetchNetworkError";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Error thrown when a request is aborted (e.g., user cancels request, timeout).
|
|
61
|
+
*/
|
|
62
|
+
export class FragnoClientFetchAbortError extends FragnoClientFetchError {
|
|
63
|
+
constructor(message: string = "Request was aborted", options: FragnoErrorOptions = {}) {
|
|
64
|
+
super(message, "ABORT_ERROR", options);
|
|
65
|
+
this.name = "FragnoClientFetchAbortError";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Error thrown when the API result is unexpected, e.g. no json is returned.
|
|
71
|
+
*/
|
|
72
|
+
export class FragnoClientUnknownApiError extends FragnoClientError<"UNKNOWN_API_ERROR"> {
|
|
73
|
+
readonly #status: StatusCode;
|
|
74
|
+
|
|
75
|
+
constructor(
|
|
76
|
+
message: string = "Unknown API error",
|
|
77
|
+
status: StatusCode,
|
|
78
|
+
options: FragnoErrorOptions = {},
|
|
79
|
+
) {
|
|
80
|
+
super(message, "UNKNOWN_API_ERROR", options);
|
|
81
|
+
this.name = "FragnoClientUnknownApiError";
|
|
82
|
+
this.#status = status;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get status(): StatusCode {
|
|
86
|
+
return this.#status;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class FragnoClientApiError<
|
|
91
|
+
TErrorCode extends string = string,
|
|
92
|
+
> extends FragnoClientError<TErrorCode> {
|
|
93
|
+
readonly #status: StatusCode;
|
|
94
|
+
|
|
95
|
+
constructor(
|
|
96
|
+
{ message, code }: { message: string; code: TErrorCode },
|
|
97
|
+
status: StatusCode,
|
|
98
|
+
options: FragnoErrorOptions = {},
|
|
99
|
+
) {
|
|
100
|
+
super(message, code, options);
|
|
101
|
+
this.name = "FragnoClientApiError";
|
|
102
|
+
this.#status = status;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get status(): StatusCode {
|
|
106
|
+
return this.#status;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The error code returned by the API.
|
|
111
|
+
*
|
|
112
|
+
* The type is `TErrorCode` (the set of known error codes for this route), but may also be a string
|
|
113
|
+
* for forward compatibility with future error codes.
|
|
114
|
+
*/
|
|
115
|
+
get code(): TErrorCode | (string & {}) {
|
|
116
|
+
return super.code as TErrorCode | (string & {});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
static async fromResponse<TErrorCode extends string = string>(
|
|
120
|
+
response: Response,
|
|
121
|
+
): Promise<FragnoClientApiError<TErrorCode> | FragnoClientUnknownApiError> {
|
|
122
|
+
const unknown = await response.json();
|
|
123
|
+
const status = response.status as StatusCode;
|
|
124
|
+
|
|
125
|
+
if (!("message" in unknown || "code" in unknown)) {
|
|
126
|
+
return new FragnoClientUnknownApiError("Unknown API error", status);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!(typeof unknown.message === "string" && typeof unknown.code === "string")) {
|
|
130
|
+
return new FragnoClientUnknownApiError("Unknown API error", status);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return new FragnoClientApiError(
|
|
134
|
+
{
|
|
135
|
+
message: unknown.message,
|
|
136
|
+
code: unknown.code as TErrorCode,
|
|
137
|
+
},
|
|
138
|
+
status,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|