shelving 1.177.0 → 1.179.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.
Files changed (108) hide show
  1. package/api/cache/APICache.d.ts +23 -0
  2. package/api/cache/APICache.js +39 -0
  3. package/api/cache/EndpointCache.d.ts +26 -0
  4. package/api/cache/EndpointCache.js +50 -0
  5. package/api/endpoint/Endpoint.d.ts +100 -0
  6. package/api/endpoint/Endpoint.js +95 -0
  7. package/api/endpoint/util.d.ts +24 -0
  8. package/api/endpoint/util.js +61 -0
  9. package/api/index.d.ts +11 -0
  10. package/api/index.js +11 -0
  11. package/api/provider/APIProvider.d.ts +9 -0
  12. package/api/provider/APIProvider.js +2 -0
  13. package/api/provider/ClientAPIProvider.d.ts +37 -0
  14. package/api/provider/ClientAPIProvider.js +51 -0
  15. package/api/provider/DebugAPIProvider.d.ts +8 -0
  16. package/api/provider/DebugAPIProvider.js +16 -0
  17. package/api/provider/MockAPIProvider.d.ts +34 -0
  18. package/api/provider/MockAPIProvider.js +24 -0
  19. package/api/provider/ThroughAPIProvider.d.ts +13 -0
  20. package/api/provider/ThroughAPIProvider.js +13 -0
  21. package/api/provider/ValidationAPIProvider.d.ts +8 -0
  22. package/api/provider/ValidationAPIProvider.js +26 -0
  23. package/api/store/EndpointStore.d.ts +43 -0
  24. package/api/store/EndpointStore.js +117 -0
  25. package/cloudflare/CloudflareKVProvider.d.ts +71 -0
  26. package/cloudflare/CloudflareKVProvider.js +113 -0
  27. package/cloudflare/index.d.ts +1 -0
  28. package/cloudflare/index.js +1 -0
  29. package/db/collection/Collection.d.ts +27 -0
  30. package/db/collection/Collection.js +31 -0
  31. package/db/index.d.ts +11 -10
  32. package/db/index.js +11 -13
  33. package/db/provider/CacheDBProvider.d.ts +26 -0
  34. package/db/{CacheProvider.js → provider/CacheDBProvider.js} +14 -13
  35. package/db/provider/ChangesDBProvider.d.ts +28 -0
  36. package/db/provider/ChangesDBProvider.js +37 -0
  37. package/db/provider/DBProvider.d.ts +23 -0
  38. package/db/provider/DBProvider.js +33 -0
  39. package/db/provider/DebugDBProvider.d.ts +21 -0
  40. package/db/provider/DebugDBProvider.js +144 -0
  41. package/db/provider/MemoryDBProvider.d.ts +67 -0
  42. package/db/{MemoryProvider.js → provider/MemoryDBProvider.js} +42 -41
  43. package/db/provider/MockDBProvider.d.ts +30 -0
  44. package/db/provider/MockDBProvider.js +49 -0
  45. package/db/provider/ThroughDBProvider.d.ts +27 -0
  46. package/db/provider/ThroughDBProvider.js +52 -0
  47. package/db/provider/ValidationDBProvider.d.ts +20 -0
  48. package/db/provider/ValidationDBProvider.js +87 -0
  49. package/db/store/ItemStore.d.ts +27 -0
  50. package/db/{ItemStore.js → store/ItemStore.js} +10 -10
  51. package/db/store/QueryStore.d.ts +39 -0
  52. package/db/{QueryStore.js → store/QueryStore.js} +12 -12
  53. package/error/RequestError.d.ts +4 -0
  54. package/error/RequestError.js +7 -0
  55. package/firestore/client/FirestoreClientProvider.d.ts +16 -15
  56. package/firestore/client/FirestoreClientProvider.js +27 -29
  57. package/firestore/lite/FirestoreLiteProvider.d.ts +16 -15
  58. package/firestore/lite/FirestoreLiteProvider.js +25 -27
  59. package/firestore/server/FirestoreServerProvider.d.ts +16 -15
  60. package/firestore/server/FirestoreServerProvider.js +34 -34
  61. package/index.d.ts +1 -1
  62. package/index.js +1 -1
  63. package/package.json +5 -4
  64. package/react/createAPIContext.d.ts +19 -0
  65. package/react/createAPIContext.js +25 -0
  66. package/react/createDataContext.d.ts +12 -11
  67. package/react/createDataContext.js +7 -7
  68. package/react/index.d.ts +1 -0
  69. package/react/index.js +1 -0
  70. package/schema/CountrySchema.d.ts +1 -1
  71. package/schema/DataSchema.d.ts +1 -5
  72. package/store/Store.d.ts +12 -3
  73. package/store/Store.js +18 -8
  74. package/store/index.d.ts +0 -1
  75. package/store/index.js +0 -1
  76. package/test/index.d.ts +22 -25
  77. package/test/index.js +4 -4
  78. package/util/data.d.ts +0 -4
  79. package/util/http.d.ts +34 -15
  80. package/util/http.js +50 -28
  81. package/util/query.d.ts +2 -2
  82. package/util/string.js +1 -1
  83. package/util/url.d.ts +11 -0
  84. package/util/url.js +22 -0
  85. package/db/CacheProvider.d.ts +0 -25
  86. package/db/Change.d.ts +0 -76
  87. package/db/Change.js +0 -59
  88. package/db/ChangesProvider.d.ts +0 -31
  89. package/db/ChangesProvider.js +0 -73
  90. package/db/DebugProvider.d.ts +0 -35
  91. package/db/DebugProvider.js +0 -277
  92. package/db/ItemStore.d.ts +0 -26
  93. package/db/MemoryProvider.d.ts +0 -66
  94. package/db/Provider.d.ts +0 -120
  95. package/db/Provider.js +0 -64
  96. package/db/QueryStore.d.ts +0 -38
  97. package/db/ThroughProvider.d.ts +0 -46
  98. package/db/ThroughProvider.js +0 -104
  99. package/db/ValidationProvider.d.ts +0 -49
  100. package/db/ValidationProvider.js +0 -157
  101. package/endpoint/Endpoint.d.ts +0 -123
  102. package/endpoint/Endpoint.js +0 -178
  103. package/endpoint/index.d.ts +0 -2
  104. package/endpoint/index.js +0 -2
  105. package/endpoint/util.d.ts +0 -26
  106. package/endpoint/util.js +0 -26
  107. package/store/EndpointStore.d.ts +0 -24
  108. package/store/EndpointStore.js +0 -74
@@ -0,0 +1,23 @@
1
+ import type { Endpoint } from "../endpoint/Endpoint.js";
2
+ import type { APIProvider } from "../provider/APIProvider.js";
3
+ import { EndpointCache } from "./EndpointCache.js";
4
+ /**
5
+ * Cache of `EndpointCache` objects for multiple endpoints.
6
+ * - Use `get(endpoint)` to retrieve or create the `EndpointCache` for a given endpoint, then `get(payload)` on that to get a specific `EndpointStore`.
7
+ */
8
+ export declare class APICache implements Disposable {
9
+ private readonly _caches;
10
+ readonly provider: APIProvider;
11
+ constructor(provider: APIProvider);
12
+ /** Get (or create) the `EndpointCache` for the given endpoint. */
13
+ get<P, R>(endpoint: Endpoint<P, R>): EndpointCache<P, R>;
14
+ /** Invalidate a specific store for an endpoint. */
15
+ invalidate<P, R>(endpoint: Endpoint<P, R>, payload: P): void;
16
+ /** Invalidate all stores for an endpoint. */
17
+ invalidateAll<P, R>(endpoint: Endpoint<P, R>): void;
18
+ /** Trigger a refetch on a specific store for an endpoint. */
19
+ refetch<P, R>(endpoint: Endpoint<P, R>, payload: P): void;
20
+ /** Trigger a refetch on all stores for an endpoint. */
21
+ refetchAll<P, R>(endpoint: Endpoint<P, R>): void;
22
+ [Symbol.dispose](): void;
23
+ }
@@ -0,0 +1,39 @@
1
+ import { setMapItem } from "../../util/map.js";
2
+ import { EndpointCache } from "./EndpointCache.js";
3
+ /**
4
+ * Cache of `EndpointCache` objects for multiple endpoints.
5
+ * - Use `get(endpoint)` to retrieve or create the `EndpointCache` for a given endpoint, then `get(payload)` on that to get a specific `EndpointStore`.
6
+ */
7
+ export class APICache {
8
+ _caches = new Map();
9
+ provider;
10
+ constructor(provider) {
11
+ this.provider = provider;
12
+ }
13
+ /** Get (or create) the `EndpointCache` for the given endpoint. */
14
+ get(endpoint) {
15
+ return this._caches.get(endpoint) || setMapItem(this._caches, endpoint, new EndpointCache(endpoint, this.provider));
16
+ }
17
+ /** Invalidate a specific store for an endpoint. */
18
+ invalidate(endpoint, payload) {
19
+ this._caches.get(endpoint)?.invalidate(payload);
20
+ }
21
+ /** Invalidate all stores for an endpoint. */
22
+ invalidateAll(endpoint) {
23
+ this._caches.get(endpoint)?.invalidateAll();
24
+ }
25
+ /** Trigger a refetch on a specific store for an endpoint. */
26
+ refetch(endpoint, payload) {
27
+ this._caches.get(endpoint)?.refetch(payload);
28
+ }
29
+ /** Trigger a refetch on all stores for an endpoint. */
30
+ refetchAll(endpoint) {
31
+ this._caches.get(endpoint)?.refetchAll();
32
+ }
33
+ // Implement Disposable.
34
+ [Symbol.dispose]() {
35
+ for (const cache of this._caches.values())
36
+ cache[Symbol.dispose]();
37
+ this._caches.clear();
38
+ }
39
+ }
@@ -0,0 +1,26 @@
1
+ import type { Endpoint } from "../endpoint/Endpoint.js";
2
+ import type { APIProvider } from "../provider/APIProvider.js";
3
+ import { EndpointStore } from "../store/EndpointStore.js";
4
+ /**
5
+ * Cache of `EndpointStore` objects for a single endpoint, keyed by serialized payload.
6
+ * - Use `get(payload)` to retrieve or create the `EndpointStore` for a given payload.
7
+ */
8
+ export declare class EndpointCache<P, R> implements Disposable {
9
+ private readonly _stores;
10
+ readonly endpoint: Endpoint<P, R>;
11
+ readonly provider: APIProvider;
12
+ constructor(endpoint: Endpoint<P, R>, provider: APIProvider);
13
+ /** Get (or create) the `EndpointStore` for the given payload. */
14
+ get(payload: P): EndpointStore<P, R>;
15
+ /** Invalidate a specific store. */
16
+ invalidate(payload: P): void;
17
+ /** Invalidate all stores. */
18
+ invalidateAll(): void;
19
+ /** Trigger a refetch on a specific store. */
20
+ refetch(payload: P): void;
21
+ /** Trigger a refetch on all stores. */
22
+ refetchAll(): void;
23
+ [Symbol.dispose](): void;
24
+ }
25
+ /** Any endpoint cache. */
26
+ export type AnyEndpointCache = EndpointCache<any, any>;
@@ -0,0 +1,50 @@
1
+ import { setMapItem } from "../../util/map.js";
2
+ import { EndpointStore } from "../store/EndpointStore.js";
3
+ /** Serialize a payload to a stable string key for use in a `Map`. */
4
+ function _serializePayload(payload) {
5
+ if (payload === undefined)
6
+ return "";
7
+ return JSON.stringify(payload);
8
+ }
9
+ /**
10
+ * Cache of `EndpointStore` objects for a single endpoint, keyed by serialized payload.
11
+ * - Use `get(payload)` to retrieve or create the `EndpointStore` for a given payload.
12
+ */
13
+ export class EndpointCache {
14
+ _stores = new Map();
15
+ endpoint;
16
+ provider;
17
+ constructor(endpoint, provider) {
18
+ this.endpoint = endpoint;
19
+ this.provider = provider;
20
+ }
21
+ /** Get (or create) the `EndpointStore` for the given payload. */
22
+ get(payload) {
23
+ const key = _serializePayload(payload);
24
+ return this._stores.get(key) || setMapItem(this._stores, key, new EndpointStore(this.endpoint, payload, this.provider));
25
+ }
26
+ /** Invalidate a specific store. */
27
+ invalidate(payload) {
28
+ this.get(payload)?.invalidate();
29
+ }
30
+ /** Invalidate all stores. */
31
+ invalidateAll() {
32
+ for (const store of this._stores.values())
33
+ store.invalidate();
34
+ }
35
+ /** Trigger a refetch on a specific store. */
36
+ refetch(payload) {
37
+ this.get(payload)?.fetch();
38
+ }
39
+ /** Trigger a refetch on all stores. */
40
+ refetchAll() {
41
+ for (const store of this._stores.values())
42
+ store.fetch();
43
+ }
44
+ // Implement Disposable.
45
+ [Symbol.dispose]() {
46
+ for (const store of this._stores.values())
47
+ store[Symbol.dispose]();
48
+ this._stores.clear();
49
+ }
50
+ }
@@ -0,0 +1,100 @@
1
+ import { type Schema } from "../../schema/Schema.js";
2
+ import type { ImmutableArray } from "../../util/array.js";
3
+ import { type Data } from "../../util/data.js";
4
+ import type { AnyCaller, Arguments } from "../../util/function.js";
5
+ import type { RequestMethod, RequestParams } from "../../util/http.js";
6
+ import type { AbsolutePath } from "../../util/path.js";
7
+ import { type TemplatePlaceholders } from "../../util/template.js";
8
+ import type { EndpointCallback, EndpointHandler } from "./util.js";
9
+ /**
10
+ * An abstract API resource definition, used to specify types for e.g. serverless functions.
11
+ *
12
+ * @param method The method of the endpoint, e.g. `GET`
13
+ * @param path Endpoint path, possibly including placeholders e.g. `/users/{id}`
14
+ * @param payload A `Schema` for the payload of the endpoint.
15
+ * @param result A `Schema` for the result of the endpoint.
16
+ */
17
+ export declare class Endpoint<P, R> {
18
+ /** Endpoint method. */
19
+ readonly method: RequestMethod;
20
+ /** Endpoint path, possibly including placeholders e.g. `/users/{id}` */
21
+ readonly path: AbsolutePath;
22
+ /** Endpoint path, possibly including placeholders e.g. `/users/{id}` */
23
+ readonly placeholders: TemplatePlaceholders;
24
+ /** Payload schema. */
25
+ readonly payload: Schema<P>;
26
+ /** Result schema. */
27
+ readonly result: Schema<R>;
28
+ constructor(method: RequestMethod, path: AbsolutePath, payload: Schema<P>, result: Schema<R>);
29
+ /**
30
+ * Render the path for this endpoint with the given payload.
31
+ * - Path might contain `{placeholder}` values that are replaced with values from `payload`.
32
+ *
33
+ * @returns URL string combining `base` with this endpoint's path, with any `{placeholders}` rendered and `?query` params added.
34
+ *
35
+ * @throws {RequiredError} if this endpoint's path has `{placeholders}` but `payload` is not a data object.
36
+ */
37
+ renderPath(payload: P, caller?: AnyCaller): AbsolutePath;
38
+ /**
39
+ * Match a method/path pair against this endpoint and return any matched `{placeholder}` params.
40
+ */
41
+ match(method: RequestMethod, path: AbsolutePath, caller?: AnyCaller): RequestParams | undefined;
42
+ /**
43
+ * Create an endpoint handler pairing for this endpoint.
44
+ * @param callback The callback function that implements the logic for this endpoint by receiving the payload and returning the response.
45
+ * @returns An `EndpointHandler` object combining this endpoint and the callback into a single typed object.
46
+ */
47
+ handler<A extends Arguments = []>(callback: EndpointCallback<P, R, A>): EndpointHandler<P, R, A>;
48
+ /** Convert to string, e.g. `GET /user/{id}` */
49
+ toString(): string;
50
+ }
51
+ /** Any endpoint. */
52
+ export type AnyEndpoint = Endpoint<any, any>;
53
+ /** List of endpoints. */
54
+ export type Endpoints = ImmutableArray<AnyEndpoint>;
55
+ /** Extract the payload type from a `Endpoint`. */
56
+ export type PayloadType<X extends Endpoint<unknown, unknown>> = X extends Endpoint<infer Y, unknown> ? Y : never;
57
+ /** Extract the result type from a `Endpoint`. */
58
+ export type EndpointType<X extends Endpoint<unknown, unknown>> = X extends Endpoint<unknown, infer Y> ? Y : never;
59
+ /**
60
+ * Represent a HEAD request to a specified path, with validated payload and return types.
61
+ * "The HEAD method requests a representation of the specified resource. Requests using HEAD should only retrieve data and should not contain a request content."
62
+ */
63
+ export declare function HEAD<P extends Data, R>(path: AbsolutePath, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>;
64
+ export declare function HEAD<P extends Data>(path: AbsolutePath, payload: Schema<P>): Endpoint<P, undefined>;
65
+ export declare function HEAD<R>(path: AbsolutePath, payload: undefined, result: Schema<R>): Endpoint<undefined, R>;
66
+ /**
67
+ * Represent a GET request to a specified path, with validated payload and return types.
68
+ * "The GET method requests a representation of the specified resource. Requests using GET should only retrieve data and should not contain a request content."
69
+ */
70
+ export declare function GET<P extends Data, R>(path: AbsolutePath, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>;
71
+ export declare function GET<P extends Data>(path: AbsolutePath, payload: Schema<P>): Endpoint<P, undefined>;
72
+ export declare function GET<R>(path: AbsolutePath, payload: undefined, result: Schema<R>): Endpoint<undefined, R>;
73
+ /**
74
+ * Represent a POST request to a specified path, with validated payload and return types.
75
+ * "The POST method submits an entity to the specified resource, often causing a change in state or side effects on the server.
76
+ */
77
+ export declare function POST<P, R>(path: AbsolutePath, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>;
78
+ export declare function POST<P>(path: AbsolutePath, payload: Schema<P>): Endpoint<P, undefined>;
79
+ export declare function POST<R>(path: AbsolutePath, payload: undefined, result: Schema<R>): Endpoint<undefined, R>;
80
+ /**
81
+ * Represent a PUT request to a specified path, with validated payload and return types.
82
+ * "The PUT method replaces all current representations of the target resource with the request content."
83
+ */
84
+ export declare function PUT<P, R>(path: AbsolutePath, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>;
85
+ export declare function PUT<P>(path: AbsolutePath, payload: Schema<P>): Endpoint<P, undefined>;
86
+ export declare function PUT<R>(path: AbsolutePath, payload: undefined, result: Schema<R>): Endpoint<undefined, R>;
87
+ /**
88
+ * Represent a PATCH request to a specified path, with validated payload and return types.
89
+ * "The PATCH method applies partial modifications to a resource."
90
+ */
91
+ export declare function PATCH<P, R>(path: AbsolutePath, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>;
92
+ export declare function PATCH<P>(path: AbsolutePath, payload: Schema<P>): Endpoint<P, undefined>;
93
+ export declare function PATCH<R>(path: AbsolutePath, payload: undefined, result: Schema<R>): Endpoint<undefined, R>;
94
+ /**
95
+ * Represent a DELETE request to a specified path, with validated payload and return types.
96
+ * "The DELETE method deletes the specified resource."
97
+ */
98
+ export declare function DELETE<P, R>(path: AbsolutePath, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>;
99
+ export declare function DELETE<P>(path: AbsolutePath, payload: Schema<P>): Endpoint<P, undefined>;
100
+ export declare function DELETE<R>(path: AbsolutePath, payload: undefined, result: Schema<R>): Endpoint<undefined, R>;
@@ -0,0 +1,95 @@
1
+ import { RequiredError } from "../../error/RequiredError.js";
2
+ import { UNDEFINED } from "../../schema/Schema.js";
3
+ import { isData } from "../../util/data.js";
4
+ import { getPlaceholders, matchTemplate, renderTemplate } from "../../util/template.js";
5
+ /**
6
+ * An abstract API resource definition, used to specify types for e.g. serverless functions.
7
+ *
8
+ * @param method The method of the endpoint, e.g. `GET`
9
+ * @param path Endpoint path, possibly including placeholders e.g. `/users/{id}`
10
+ * @param payload A `Schema` for the payload of the endpoint.
11
+ * @param result A `Schema` for the result of the endpoint.
12
+ */
13
+ export class Endpoint {
14
+ /** Endpoint method. */
15
+ method;
16
+ /** Endpoint path, possibly including placeholders e.g. `/users/{id}` */
17
+ path;
18
+ /** Endpoint path, possibly including placeholders e.g. `/users/{id}` */
19
+ placeholders;
20
+ /** Payload schema. */
21
+ payload;
22
+ /** Result schema. */
23
+ result;
24
+ constructor(method, path, payload, result) {
25
+ this.method = method;
26
+ this.path = path;
27
+ this.placeholders = getPlaceholders(path);
28
+ this.payload = payload;
29
+ this.result = result;
30
+ }
31
+ /**
32
+ * Render the path for this endpoint with the given payload.
33
+ * - Path might contain `{placeholder}` values that are replaced with values from `payload`.
34
+ *
35
+ * @returns URL string combining `base` with this endpoint's path, with any `{placeholders}` rendered and `?query` params added.
36
+ *
37
+ * @throws {RequiredError} if this endpoint's path has `{placeholders}` but `payload` is not a data object.
38
+ */
39
+ renderPath(payload, caller = this.renderPath) {
40
+ // Placeholders.
41
+ if (this.placeholders.length) {
42
+ assertPlaceholderPayload(payload, this, caller);
43
+ return renderTemplate(this.path, payload, caller);
44
+ }
45
+ // No placeholders.
46
+ return this.path;
47
+ }
48
+ /**
49
+ * Match a method/path pair against this endpoint and return any matched `{placeholder}` params.
50
+ */
51
+ match(method, path, caller = this.match) {
52
+ if (method !== this.method)
53
+ return undefined;
54
+ return matchTemplate(this.path, path, caller);
55
+ }
56
+ /**
57
+ * Create an endpoint handler pairing for this endpoint.
58
+ * @param callback The callback function that implements the logic for this endpoint by receiving the payload and returning the response.
59
+ * @returns An `EndpointHandler` object combining this endpoint and the callback into a single typed object.
60
+ */
61
+ handler(callback) {
62
+ return { endpoint: this, callback };
63
+ }
64
+ /** Convert to string, e.g. `GET /user/{id}` */
65
+ toString() {
66
+ return `${this.method} ${this.path}`;
67
+ }
68
+ }
69
+ export function HEAD(path, payload = UNDEFINED, result = UNDEFINED) {
70
+ return new Endpoint("HEAD", path, payload, result);
71
+ }
72
+ export function GET(path, payload = UNDEFINED, result = UNDEFINED) {
73
+ return new Endpoint("GET", path, payload, result);
74
+ }
75
+ export function POST(path, payload = UNDEFINED, result = UNDEFINED) {
76
+ return new Endpoint("POST", path, payload, result);
77
+ }
78
+ export function PUT(path, payload = UNDEFINED, result = UNDEFINED) {
79
+ return new Endpoint("PUT", path, payload, result);
80
+ }
81
+ export function PATCH(path, payload = UNDEFINED, result = UNDEFINED) {
82
+ return new Endpoint("PATCH", path, payload, result);
83
+ }
84
+ export function DELETE(path, payload = UNDEFINED, result = UNDEFINED) {
85
+ return new Endpoint("DELETE", path, payload, result);
86
+ }
87
+ /** Assert that an endpoint with `{placeholders}` only allows data payloads. */
88
+ function assertPlaceholderPayload(payload, endpoint, caller = assertPlaceholderPayload) {
89
+ if (!isData(payload))
90
+ throw new RequiredError("Payload for request with URL {placeholders} must be data object", {
91
+ endpoint,
92
+ received: payload,
93
+ caller,
94
+ });
95
+ }
@@ -0,0 +1,24 @@
1
+ import type { Arguments } from "../../util/function.js";
2
+ import { type PossibleURL } from "../../util/url.js";
3
+ import type { Endpoint } from "./Endpoint.js";
4
+ /**
5
+ * A function that handles an endpoint request, with a payload and returns a result.
6
+ * - `payload` has already been validated against the endpoint payload schema before the callback is invoked.
7
+ * - `request` is always the original incoming request object.
8
+ */
9
+ export type EndpointCallback<P, R, A extends Arguments = []> = (payload: P, request: Request, ...args: A) => R | Response | Promise<R | Response>;
10
+ /** A typed endpoint definition paired with its implementation callback. */
11
+ export interface EndpointHandler<P = unknown, R = unknown, A extends Arguments = []> {
12
+ readonly endpoint: Endpoint<P, R>;
13
+ readonly callback: EndpointCallback<P, R, A>;
14
+ }
15
+ export type AnyEndpointHandler<A extends Arguments = []> = EndpointHandler<any, any, A>;
16
+ /** A collection of endpoint handlers that can be matched and invoked by `handleEndpoints()`. */
17
+ export type EndpointHandlers<A extends Arguments = []> = Iterable<AnyEndpointHandler<A>>;
18
+ /**
19
+ * Handle a `Request` with the first matching endpoint handler after stripping any base-path prefix from the request pathname.
20
+ * - The original `Request` object is passed through to the callback unchanged.
21
+ * - Path params and query params are merged before payload validation.
22
+ * @param base The base URL for the API, e.g. `https://myapi.com/`
23
+ */
24
+ export declare function handleEndpoints<A extends Arguments = []>(request: Request, base: PossibleURL, handlers: EndpointHandlers<A>, ...args: A): Promise<Response>;
@@ -0,0 +1,61 @@
1
+ import { MethodNotAllowedError, NotFoundError } from "../../error/RequestError.js";
2
+ import { ValueError } from "../../error/ValueError.js";
3
+ import { getDictionary } from "../../util/dictionary.js";
4
+ import { getRequestContent, getResponse, isRequestMethod } from "../../util/http.js";
5
+ import { isPlainObject } from "../../util/object.js";
6
+ import { requireURL } from "../../util/url.js";
7
+ /**
8
+ * Handle a `Request` with the first matching endpoint handler after stripping any base-path prefix from the request pathname.
9
+ * - The original `Request` object is passed through to the callback unchanged.
10
+ * - Path params and query params are merged before payload validation.
11
+ * @param base The base URL for the API, e.g. `https://myapi.com/`
12
+ */
13
+ export function handleEndpoints(request, base, handlers, ...args) {
14
+ const caller = handleEndpoints;
15
+ const { url, method } = request;
16
+ if (!isRequestMethod(method))
17
+ throw new MethodNotAllowedError("Unsupported request method", { received: method, caller });
18
+ const { origin: baseOrigin, pathname: basePath } = requireURL(base, undefined, caller);
19
+ const { origin: requestOrigin, pathname: requestPath, searchParams } = requireURL(url, base, caller);
20
+ if (baseOrigin !== requestOrigin)
21
+ throw new NotFoundError("No matching origin", { expected: baseOrigin, received: requestOrigin, caller });
22
+ const targetPath = _stripPathPrefix(requestPath, basePath, caller);
23
+ for (const handler of handlers) {
24
+ const pathParams = handler.endpoint.match(method, targetPath, caller);
25
+ if (!pathParams)
26
+ continue;
27
+ const params = searchParams.size ? { ...getDictionary(searchParams), ...pathParams } : pathParams;
28
+ return handleEndpoint(handler, params, request, args, handleEndpoints);
29
+ }
30
+ throw new NotFoundError("No matching endpoint", { received: targetPath, caller });
31
+ }
32
+ /**
33
+ * Validate and invoke an endpoint callback after the routing layer has already matched URL params.
34
+ */
35
+ async function handleEndpoint({ endpoint, callback },
36
+ /** Params we already matched/parsed from the URL. */
37
+ params, request, args, caller = handleEndpoint) {
38
+ const content = await getRequestContent(request, caller);
39
+ const unsafePayload = content === undefined ? params : isPlainObject(content) ? { ...content, ...params } : content;
40
+ const payload = endpoint.payload.validate(unsafePayload);
41
+ const unsafeResult = await callback(payload, request, ...args);
42
+ try {
43
+ return getResponse(endpoint.result.validate(unsafeResult));
44
+ }
45
+ catch (thrown) {
46
+ if (typeof thrown === "string")
47
+ throw new ValueError(`Invalid result for ${endpoint.toString()}:\n${thrown}`, { endpoint, callback, cause: thrown, caller });
48
+ throw thrown;
49
+ }
50
+ }
51
+ /** Strip a prefix like `/a/b` from a path like `/a/b/c/d` to produce a remainder path like `/c/d`. */
52
+ function _stripPathPrefix(path, prefix, caller) {
53
+ prefix = prefix === "/" ? "/" : prefix.replace(/\/$/, "");
54
+ if (prefix === "/")
55
+ return path;
56
+ if (path === prefix)
57
+ return "/";
58
+ if (path.startsWith(`${prefix}/`))
59
+ return path.slice(prefix.length);
60
+ throw new NotFoundError("No matching endpoint", { received: path, expected: prefix, caller });
61
+ }
package/api/index.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ export * from "./cache/APICache.js";
2
+ export * from "./cache/EndpointCache.js";
3
+ export * from "./endpoint/Endpoint.js";
4
+ export * from "./endpoint/util.js";
5
+ export * from "./provider/APIProvider.js";
6
+ export * from "./provider/ClientAPIProvider.js";
7
+ export * from "./provider/DebugAPIProvider.js";
8
+ export * from "./provider/MockAPIProvider.js";
9
+ export * from "./provider/ThroughAPIProvider.js";
10
+ export * from "./provider/ValidationAPIProvider.js";
11
+ export * from "./store/EndpointStore.js";
package/api/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export * from "./cache/APICache.js";
2
+ export * from "./cache/EndpointCache.js";
3
+ export * from "./endpoint/Endpoint.js";
4
+ export * from "./endpoint/util.js";
5
+ export * from "./provider/APIProvider.js";
6
+ export * from "./provider/ClientAPIProvider.js";
7
+ export * from "./provider/DebugAPIProvider.js";
8
+ export * from "./provider/MockAPIProvider.js";
9
+ export * from "./provider/ThroughAPIProvider.js";
10
+ export * from "./provider/ValidationAPIProvider.js";
11
+ export * from "./store/EndpointStore.js";
@@ -0,0 +1,9 @@
1
+ import type { AnyCaller } from "../../util/function.js";
2
+ import type { RequestOptions } from "../../util/http.js";
3
+ import type { Endpoint } from "../endpoint/Endpoint.js";
4
+ export declare abstract class APIProvider {
5
+ /**
6
+ * Perform a fetch to an endpoint via this provider and validate the returned response against the endpoint result schema.
7
+ */
8
+ abstract fetch<P, R>(endpoint: Endpoint<P, R>, payload: P, options?: RequestOptions, caller?: AnyCaller): Promise<R>;
9
+ }
@@ -0,0 +1,2 @@
1
+ export class APIProvider {
2
+ }
@@ -0,0 +1,37 @@
1
+ import type { AnyCaller } from "../../util/function.js";
2
+ import { type RequestOptions } from "../../util/http.js";
3
+ import { type PossibleURL, type URLString } from "../../util/url.js";
4
+ import type { Endpoint } from "../endpoint/Endpoint.js";
5
+ import type { APIProvider } from "./APIProvider.js";
6
+ /** Options for an `APIProvider`. */
7
+ export interface ClientAPIProviderOptions {
8
+ /** The common base URL for all rendered endpoint requests. */
9
+ readonly url: PossibleURL;
10
+ /** Options used for HTTP requests created with `this.getRequest()` and `this.fetch()` */
11
+ readonly options?: RequestOptions;
12
+ }
13
+ /** Provider for API endpoints rooted at a common base URL. */
14
+ export declare class ClientAPIProvider implements APIProvider {
15
+ /** The common base URL for all rendered endpoint requests. */
16
+ readonly url: URLString;
17
+ /** Default options used for HTTP requests created with `this.getRequest()` and `this.fetch()` */
18
+ readonly options: RequestOptions;
19
+ constructor({ url, options }: ClientAPIProviderOptions);
20
+ /**
21
+ * Create a `Request` that targets this endpoint with a given base URL.
22
+ * - Path `{placeholders}` are rendered from the payload.
23
+ * - For `GET` and `HEAD`, remaining payload fields are appended as `?query` params.
24
+ * - For all other requests, payload is sent as the body.
25
+ *
26
+ * @throws {RequiredError} if this endpoint's path has `{placeholders}` but `payload` is not a data object.
27
+ * @throws {RequiredError} if this is a `HEAD` or `GET` request but `payload` is not a data object.
28
+ */
29
+ getRequest<P, R>(endpoint: Endpoint<P, R>, payload: P, options?: RequestOptions, caller?: AnyCaller): Request;
30
+ /**
31
+ * Parse an HTTP `Response` for this endpoint.
32
+ * - Non-2xx responses become `ResponseError`.
33
+ * - Does not validate the result against the endpoint schema — use `ValidationAPIProvider` for that.
34
+ */
35
+ parseResponse<P, R>(_endpoint: Endpoint<P, R>, response: Response, caller?: AnyCaller): Promise<R>;
36
+ fetch<P, R>(endpoint: Endpoint<P, R>, payload: P, options?: RequestOptions, caller?: AnyCaller): Promise<R>;
37
+ }
@@ -0,0 +1,51 @@
1
+ import { ResponseError } from "../../error/ResponseError.js";
2
+ import { getMessage } from "../../util/error.js";
3
+ import { getRequest, getResponseContent, mergeRequestOptions } from "../../util/http.js";
4
+ import { omitProps } from "../../util/object.js";
5
+ import { requireBaseURL, requireURL } from "../../util/url.js";
6
+ /** Provider for API endpoints rooted at a common base URL. */
7
+ export class ClientAPIProvider {
8
+ /** The common base URL for all rendered endpoint requests. */
9
+ url;
10
+ /** Default options used for HTTP requests created with `this.getRequest()` and `this.fetch()` */
11
+ options;
12
+ constructor({ url, options = {} }) {
13
+ this.url = requireBaseURL(url, undefined, ClientAPIProvider);
14
+ this.options = options;
15
+ }
16
+ /**
17
+ * Create a `Request` that targets this endpoint with a given base URL.
18
+ * - Path `{placeholders}` are rendered from the payload.
19
+ * - For `GET` and `HEAD`, remaining payload fields are appended as `?query` params.
20
+ * - For all other requests, payload is sent as the body.
21
+ *
22
+ * @throws {RequiredError} if this endpoint's path has `{placeholders}` but `payload` is not a data object.
23
+ * @throws {RequiredError} if this is a `HEAD` or `GET` request but `payload` is not a data object.
24
+ */
25
+ getRequest(endpoint, payload, options, caller = this.getRequest) {
26
+ // Render the path into the base URL.
27
+ const url = requireURL(`.${endpoint.renderPath(payload, caller)}`, requireBaseURL(this.url, undefined, caller), caller).href;
28
+ // Placeholders are rendered into the path so get omitted from the body payload.
29
+ if (endpoint.placeholders.length)
30
+ return getRequest(endpoint.method, url, omitProps(payload, ...endpoint.placeholders), options);
31
+ // No placeholders.
32
+ return getRequest(endpoint.method, url, payload, options);
33
+ }
34
+ /**
35
+ * Parse an HTTP `Response` for this endpoint.
36
+ * - Non-2xx responses become `ResponseError`.
37
+ * - Does not validate the result against the endpoint schema — use `ValidationAPIProvider` for that.
38
+ */
39
+ async parseResponse(_endpoint, response, caller = this.parseResponse) {
40
+ const { ok, status } = response;
41
+ const content = await getResponseContent(response, caller);
42
+ if (!ok)
43
+ throw new ResponseError(getMessage(content) ?? `Error ${status}`, { code: status, cause: response, caller });
44
+ return content;
45
+ }
46
+ async fetch(endpoint, payload, options, caller = this.fetch) {
47
+ const request = this.getRequest(endpoint, payload, mergeRequestOptions(this.options, options), caller);
48
+ const response = await fetch(request);
49
+ return this.parseResponse(endpoint, response, caller);
50
+ }
51
+ }
@@ -0,0 +1,8 @@
1
+ import type { AnyCaller } from "../../util/function.js";
2
+ import type { RequestOptions } from "../../util/http.js";
3
+ import type { Endpoint } from "../endpoint/Endpoint.js";
4
+ import { ThroughAPIProvider } from "./ThroughAPIProvider.js";
5
+ /** Provider that logs API operations to the console. */
6
+ export declare class DebugAPIProvider extends ThroughAPIProvider {
7
+ fetch<P, R>(endpoint: Endpoint<P, R>, payload: P, options?: RequestOptions, caller?: AnyCaller): Promise<R>;
8
+ }
@@ -0,0 +1,16 @@
1
+ import { ThroughAPIProvider } from "./ThroughAPIProvider.js";
2
+ /** Provider that logs API operations to the console. */
3
+ export class DebugAPIProvider extends ThroughAPIProvider {
4
+ async fetch(endpoint, payload, options, caller = this.fetch) {
5
+ try {
6
+ console.debug("⋯ FETCH", endpoint.method, endpoint.path, payload);
7
+ const result = await super.fetch(endpoint, payload, options, caller);
8
+ console.debug("↩ FETCH", endpoint.method, endpoint.path, result);
9
+ return result;
10
+ }
11
+ catch (reason) {
12
+ console.error("✘ FETCH", endpoint.method, endpoint.path, payload, reason);
13
+ throw reason;
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,34 @@
1
+ import type { AnyCaller } from "../../util/function.js";
2
+ import { type RequestHandler, type RequestOptions } from "../../util/http.js";
3
+ import type { AnyEndpoint, Endpoint } from "../endpoint/Endpoint.js";
4
+ import { ClientAPIProvider, type ClientAPIProviderOptions } from "./ClientAPIProvider.js";
5
+ /** A structured log entry emitted by `MockAPIProvider` for one of its provider operations. */
6
+ export type MockAPICall = {
7
+ readonly type: "fetch";
8
+ readonly endpoint: AnyEndpoint;
9
+ readonly options: RequestOptions;
10
+ readonly payload: unknown;
11
+ readonly request: Request;
12
+ readonly response: Response;
13
+ readonly result: unknown;
14
+ };
15
+ /**
16
+ * Construction options for a `MockAPIProvider`.
17
+ * - Accepts the normal `APIProvider` options.
18
+ */
19
+ export interface MockAPIProviderOptions extends ClientAPIProviderOptions {
20
+ /** Implement this handler to mock the the request/response input/output. */
21
+ readonly handler: RequestHandler;
22
+ }
23
+ /** Provider that logs API calls without sending network requests. */
24
+ export declare class MockAPIProvider extends ClientAPIProvider {
25
+ readonly calls: MockAPICall[];
26
+ readonly handler: RequestHandler;
27
+ constructor({ handler, ...options }: MockAPIProviderOptions);
28
+ /**
29
+ * Log a `fetch()` call without using the network.
30
+ * - If `getResult` is configured, its return value is returned as-is (no schema validation).
31
+ * - Otherwise `undefined` is returned.
32
+ */
33
+ fetch<P, R>(endpoint: Endpoint<P, R>, payload: P, _options?: RequestOptions, caller?: AnyCaller): Promise<R>;
34
+ }
@@ -0,0 +1,24 @@
1
+ import { mergeRequestOptions } from "../../util/http.js";
2
+ import { ClientAPIProvider } from "./ClientAPIProvider.js";
3
+ /** Provider that logs API calls without sending network requests. */
4
+ export class MockAPIProvider extends ClientAPIProvider {
5
+ calls = [];
6
+ handler;
7
+ constructor({ handler, ...options }) {
8
+ super(options);
9
+ this.handler = handler;
10
+ }
11
+ /**
12
+ * Log a `fetch()` call without using the network.
13
+ * - If `getResult` is configured, its return value is returned as-is (no schema validation).
14
+ * - Otherwise `undefined` is returned.
15
+ */
16
+ async fetch(endpoint, payload, _options = {}, caller = this.fetch) {
17
+ const options = mergeRequestOptions(this.options, _options);
18
+ const request = this.getRequest(endpoint, payload, options, caller);
19
+ const response = await this.handler(request);
20
+ const result = await this.parseResponse(endpoint, response, caller);
21
+ this.calls.push({ type: "fetch", endpoint, payload, options, request, response, result });
22
+ return result;
23
+ }
24
+ }