shelving 1.178.0 → 1.179.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.
Files changed (83) hide show
  1. package/api/cache/EndpointCache.d.ts +1 -1
  2. package/api/cache/EndpointCache.js +1 -1
  3. package/api/endpoint/Endpoint.d.ts +1 -20
  4. package/api/endpoint/Endpoint.js +1 -47
  5. package/api/endpoint/util.d.ts +13 -3
  6. package/api/endpoint/util.js +15 -8
  7. package/api/index.d.ts +3 -1
  8. package/api/index.js +3 -1
  9. package/api/provider/ClientAPIProvider.d.ts +16 -0
  10. package/api/provider/ClientAPIProvider.js +37 -4
  11. package/api/provider/DebugAPIProvider.d.ts +8 -0
  12. package/api/provider/DebugAPIProvider.js +16 -0
  13. package/api/provider/MockAPIProvider.d.ts +4 -9
  14. package/api/provider/MockAPIProvider.js +5 -5
  15. package/api/provider/ThroughAPIProvider.d.ts +2 -3
  16. package/api/provider/ValidationAPIProvider.d.ts +8 -0
  17. package/api/provider/ValidationAPIProvider.js +26 -0
  18. package/cloudflare/CloudflareKVProvider.d.ts +53 -0
  19. package/cloudflare/CloudflareKVProvider.js +75 -0
  20. package/cloudflare/index.d.ts +1 -0
  21. package/cloudflare/index.js +1 -0
  22. package/db/collection/Collection.d.ts +27 -0
  23. package/db/collection/Collection.js +31 -0
  24. package/db/index.d.ts +11 -10
  25. package/db/index.js +11 -13
  26. package/db/provider/CacheDBProvider.d.ts +26 -0
  27. package/db/{CacheProvider.js → provider/CacheDBProvider.js} +16 -15
  28. package/db/provider/ChangesDBProvider.d.ts +28 -0
  29. package/db/provider/ChangesDBProvider.js +37 -0
  30. package/db/provider/DBProvider.d.ts +23 -0
  31. package/db/provider/DBProvider.js +33 -0
  32. package/db/provider/DebugDBProvider.d.ts +21 -0
  33. package/db/provider/DebugDBProvider.js +144 -0
  34. package/db/provider/MemoryDBProvider.d.ts +65 -0
  35. package/db/provider/MemoryDBProvider.js +208 -0
  36. package/db/provider/MockDBProvider.d.ts +30 -0
  37. package/db/provider/MockDBProvider.js +49 -0
  38. package/db/provider/ThroughDBProvider.d.ts +27 -0
  39. package/db/provider/ThroughDBProvider.js +52 -0
  40. package/db/provider/ValidationDBProvider.d.ts +20 -0
  41. package/db/provider/ValidationDBProvider.js +87 -0
  42. package/db/store/ItemStore.d.ts +27 -0
  43. package/db/{ItemStore.js → store/ItemStore.js} +14 -16
  44. package/db/store/QueryStore.d.ts +39 -0
  45. package/db/{QueryStore.js → store/QueryStore.js} +16 -18
  46. package/firestore/client/FirestoreClientProvider.d.ts +16 -15
  47. package/firestore/client/FirestoreClientProvider.js +27 -29
  48. package/firestore/lite/FirestoreLiteProvider.d.ts +16 -15
  49. package/firestore/lite/FirestoreLiteProvider.js +25 -27
  50. package/firestore/server/FirestoreServerProvider.d.ts +16 -15
  51. package/firestore/server/FirestoreServerProvider.js +34 -34
  52. package/package.json +35 -34
  53. package/react/createAPIContext.d.ts +1 -1
  54. package/react/createAPIContext.js +1 -1
  55. package/react/createDataContext.d.ts +12 -11
  56. package/react/createDataContext.js +7 -7
  57. package/schema/CountrySchema.d.ts +1 -1
  58. package/schema/DataSchema.d.ts +1 -5
  59. package/store/Store.d.ts +0 -1
  60. package/store/Store.js +0 -4
  61. package/test/index.d.ts +22 -25
  62. package/test/index.js +4 -4
  63. package/util/data.d.ts +0 -4
  64. package/util/query.d.ts +2 -2
  65. package/db/CacheProvider.d.ts +0 -25
  66. package/db/Change.d.ts +0 -76
  67. package/db/Change.js +0 -59
  68. package/db/ChangesProvider.d.ts +0 -31
  69. package/db/ChangesProvider.js +0 -73
  70. package/db/DebugProvider.d.ts +0 -35
  71. package/db/DebugProvider.js +0 -277
  72. package/db/ItemStore.d.ts +0 -26
  73. package/db/MemoryProvider.d.ts +0 -66
  74. package/db/MemoryProvider.js +0 -248
  75. package/db/Provider.d.ts +0 -120
  76. package/db/Provider.js +0 -64
  77. package/db/QueryStore.d.ts +0 -38
  78. package/db/ThroughProvider.d.ts +0 -46
  79. package/db/ThroughProvider.js +0 -104
  80. package/db/ValidationProvider.d.ts +0 -49
  81. package/db/ValidationProvider.js +0 -157
  82. /package/api/{cache → store}/EndpointStore.d.ts +0 -0
  83. /package/api/{cache → store}/EndpointStore.js +0 -0
@@ -1,6 +1,6 @@
1
1
  import type { Endpoint } from "../endpoint/Endpoint.js";
2
2
  import type { APIProvider } from "../provider/APIProvider.js";
3
- import { EndpointStore } from "./EndpointStore.js";
3
+ import { EndpointStore } from "../store/EndpointStore.js";
4
4
  /**
5
5
  * Cache of `EndpointStore` objects for a single endpoint, keyed by serialized payload.
6
6
  * - Use `get(payload)` to retrieve or create the `EndpointStore` for a given payload.
@@ -1,5 +1,5 @@
1
1
  import { setMapItem } from "../../util/map.js";
2
- import { EndpointStore } from "./EndpointStore.js";
2
+ import { EndpointStore } from "../store/EndpointStore.js";
3
3
  /** Serialize a payload to a stable string key for use in a `Map`. */
4
4
  function _serializePayload(payload) {
5
5
  if (payload === undefined)
@@ -2,10 +2,9 @@ import { type Schema } from "../../schema/Schema.js";
2
2
  import type { ImmutableArray } from "../../util/array.js";
3
3
  import { type Data } from "../../util/data.js";
4
4
  import type { AnyCaller, Arguments } from "../../util/function.js";
5
- import { type RequestMethod, type RequestOptions, type RequestParams } from "../../util/http.js";
5
+ import type { RequestMethod, RequestParams } from "../../util/http.js";
6
6
  import type { AbsolutePath } from "../../util/path.js";
7
7
  import { type TemplatePlaceholders } from "../../util/template.js";
8
- import { type PossibleURL } from "../../util/url.js";
9
8
  import type { EndpointCallback, EndpointHandler } from "./util.js";
10
9
  /**
11
10
  * An abstract API resource definition, used to specify types for e.g. serverless functions.
@@ -36,18 +35,6 @@ export declare class Endpoint<P, R> {
36
35
  * @throws {RequiredError} if this endpoint's path has `{placeholders}` but `payload` is not a data object.
37
36
  */
38
37
  renderPath(payload: P, caller?: AnyCaller): AbsolutePath;
39
- /**
40
- * Create a `Request` that targets this endpoint with a given base URL.
41
- * - Path `{placeholders}` are rendered from the payload.
42
- * - For `GET` and `HEAD`, remaining payload fields are appended as `?query` params.
43
- * - For all other requests, payload is sent as the body.
44
- *
45
- * @param base The base URL where this endpoint is targeted. Base URL conversion rules from `getBaseURL()` apply so only the `origin` and `pathname` are kept and always has a trailing slash.
46
- *
47
- * @throws {RequiredError} if this endpoint's path has `{placeholders}` but `payload` is not a data object.
48
- * @throws {RequiredError} if this is a `HEAD` or `GET` request but `payload` is not a data object.
49
- */
50
- getRequest(base: PossibleURL, payload: P, options?: RequestOptions, caller?: AnyCaller): Request;
51
38
  /**
52
39
  * Match a method/path pair against this endpoint and return any matched `{placeholder}` params.
53
40
  */
@@ -58,12 +45,6 @@ export declare class Endpoint<P, R> {
58
45
  * @returns An `EndpointHandler` object combining this endpoint and the callback into a single typed object.
59
46
  */
60
47
  handler<A extends Arguments = []>(callback: EndpointCallback<P, R, A>): EndpointHandler<P, R, A>;
61
- /**
62
- * Parse an HTTP `Response` for this endpoint and validate it against the result schema.
63
- * - Non-2xx responses become `ResponseError`.
64
- * - Invalid success payloads also become `ResponseError`.
65
- */
66
- parseResponse(response: Response, caller?: AnyCaller): Promise<R>;
67
48
  /** Convert to string, e.g. `GET /user/{id}` */
68
49
  toString(): string;
69
50
  }
@@ -1,12 +1,7 @@
1
1
  import { RequiredError } from "../../error/RequiredError.js";
2
- import { ResponseError } from "../../error/ResponseError.js";
3
2
  import { UNDEFINED } from "../../schema/Schema.js";
4
3
  import { isData } from "../../util/data.js";
5
- import { getMessage } from "../../util/error.js";
6
- import { getRequest, getResponseContent } from "../../util/http.js";
7
- import { omitProps } from "../../util/object.js";
8
4
  import { getPlaceholders, matchTemplate, renderTemplate } from "../../util/template.js";
9
- import { requireBaseURL, requireURL } from "../../util/url.js";
10
5
  /**
11
6
  * An abstract API resource definition, used to specify types for e.g. serverless functions.
12
7
  *
@@ -50,28 +45,6 @@ export class Endpoint {
50
45
  // No placeholders.
51
46
  return this.path;
52
47
  }
53
- /**
54
- * Create a `Request` that targets this endpoint with a given base URL.
55
- * - Path `{placeholders}` are rendered from the payload.
56
- * - For `GET` and `HEAD`, remaining payload fields are appended as `?query` params.
57
- * - For all other requests, payload is sent as the body.
58
- *
59
- * @param base The base URL where this endpoint is targeted. Base URL conversion rules from `getBaseURL()` apply so only the `origin` and `pathname` are kept and always has a trailing slash.
60
- *
61
- * @throws {RequiredError} if this endpoint's path has `{placeholders}` but `payload` is not a data object.
62
- * @throws {RequiredError} if this is a `HEAD` or `GET` request but `payload` is not a data object.
63
- */
64
- getRequest(base, payload, options, caller = this.getRequest) {
65
- // Render the path into the base URL.
66
- const url = requireURL(`.${this.renderPath(payload, caller)}`, requireBaseURL(base, undefined, caller), caller).href;
67
- // Placeholders are rendered into the path so get omitted from the body payload.
68
- if (this.placeholders.length) {
69
- assertPlaceholderPayload(payload, this, caller);
70
- return getRequest(this.method, url, omitProps(payload, ...this.placeholders), options);
71
- }
72
- // No placeholders.
73
- return getRequest(this.method, url, payload, options);
74
- }
75
48
  /**
76
49
  * Match a method/path pair against this endpoint and return any matched `{placeholder}` params.
77
50
  */
@@ -88,26 +61,6 @@ export class Endpoint {
88
61
  handler(callback) {
89
62
  return { endpoint: this, callback };
90
63
  }
91
- /**
92
- * Parse an HTTP `Response` for this endpoint and validate it against the result schema.
93
- * - Non-2xx responses become `ResponseError`.
94
- * - Invalid success payloads also become `ResponseError`.
95
- */
96
- async parseResponse(response, caller = this.parseResponse) {
97
- const { ok, status } = response;
98
- const content = await getResponseContent(response, caller);
99
- if (!ok)
100
- throw new ResponseError(getMessage(content) ?? `Error ${status}`, { code: status, cause: response, caller });
101
- try {
102
- return this.result.validate(content);
103
- }
104
- catch (thrown) {
105
- // Thrown string indicates a validation error in the returned response.
106
- if (typeof thrown === "string")
107
- throw new ResponseError(`Invalid result for ${this.toString()}:\n${thrown}`, { endpoint: this, code: 422, caller });
108
- throw thrown;
109
- }
110
- }
111
64
  /** Convert to string, e.g. `GET /user/{id}` */
112
65
  toString() {
113
66
  return `${this.method} ${this.path}`;
@@ -131,6 +84,7 @@ export function PATCH(path, payload = UNDEFINED, result = UNDEFINED) {
131
84
  export function DELETE(path, payload = UNDEFINED, result = UNDEFINED) {
132
85
  return new Endpoint("DELETE", path, payload, result);
133
86
  }
87
+ /** Assert that an endpoint with `{placeholders}` only allows data payloads. */
134
88
  function assertPlaceholderPayload(payload, endpoint, caller = assertPlaceholderPayload) {
135
89
  if (!isData(payload))
136
90
  throw new RequiredError("Payload for request with URL {placeholders} must be data object", {
@@ -3,8 +3,13 @@ import { type PossibleURL } from "../../util/url.js";
3
3
  import type { Endpoint } from "./Endpoint.js";
4
4
  /**
5
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.
6
+ *
7
+ * @param payload The payload for the callback combining the `{placeholders}`, `?search` params, and body content (this has been validated against the Endpoint's payload schema).
8
+ * @param request The original incoming request object.
9
+ * @param args Any additional arguments to pass into the endpoint callback
10
+ *
11
+ * @returns {Response} Returning a `Response` object (this will pass back to the client without validation).
12
+ * @returns {R} Returning the return type of the handler (this will be validated against the Endpoint's result schema).
8
13
  */
9
14
  export type EndpointCallback<P, R, A extends Arguments = []> = (payload: P, request: Request, ...args: A) => R | Response | Promise<R | Response>;
10
15
  /** A typed endpoint definition paired with its implementation callback. */
@@ -12,6 +17,7 @@ export interface EndpointHandler<P = unknown, R = unknown, A extends Arguments =
12
17
  readonly endpoint: Endpoint<P, R>;
13
18
  readonly callback: EndpointCallback<P, R, A>;
14
19
  }
20
+ /** Any endpoint handler. */
15
21
  export type AnyEndpointHandler<A extends Arguments = []> = EndpointHandler<any, any, A>;
16
22
  /** A collection of endpoint handlers that can be matched and invoked by `handleEndpoints()`. */
17
23
  export type EndpointHandlers<A extends Arguments = []> = Iterable<AnyEndpointHandler<A>>;
@@ -19,6 +25,10 @@ export type EndpointHandlers<A extends Arguments = []> = Iterable<AnyEndpointHan
19
25
  * Handle a `Request` with the first matching endpoint handler after stripping any base-path prefix from the request pathname.
20
26
  * - The original `Request` object is passed through to the callback unchanged.
21
27
  * - Path params and query params are merged before payload validation.
22
- * @param base The base URL for the API, e.g. `https://myapi.com/`
28
+ *
29
+ * @param request The input request to handle.
30
+ *
31
+ * @param base The base URL for the API, e.g. `https://myapi.com/a/b`
32
+ * - `pathname` of this URL gets trimmed from `request.path` to form the target path when matching against endpoints, e.g. `/a/b/c/d` will produce `/c/d` for matching.
23
33
  */
24
34
  export declare function handleEndpoints<A extends Arguments = []>(request: Request, base: PossibleURL, handlers: EndpointHandlers<A>, ...args: A): Promise<Response>;
@@ -8,7 +8,11 @@ import { requireURL } from "../../util/url.js";
8
8
  * Handle a `Request` with the first matching endpoint handler after stripping any base-path prefix from the request pathname.
9
9
  * - The original `Request` object is passed through to the callback unchanged.
10
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/`
11
+ *
12
+ * @param request The input request to handle.
13
+ *
14
+ * @param base The base URL for the API, e.g. `https://myapi.com/a/b`
15
+ * - `pathname` of this URL gets trimmed from `request.path` to form the target path when matching against endpoints, e.g. `/a/b/c/d` will produce `/c/d` for matching.
12
16
  */
13
17
  export function handleEndpoints(request, base, handlers, ...args) {
14
18
  const caller = handleEndpoints;
@@ -18,27 +22,31 @@ export function handleEndpoints(request, base, handlers, ...args) {
18
22
  const { origin: baseOrigin, pathname: basePath } = requireURL(base, undefined, caller);
19
23
  const { origin: requestOrigin, pathname: requestPath, searchParams } = requireURL(url, base, caller);
20
24
  if (baseOrigin !== requestOrigin)
21
- throw new NotFoundError("No matching origin", { expected: baseOrigin, received: requestOrigin, caller });
22
- const targetPath = _stripPathPrefix(requestPath, basePath, caller);
25
+ throw new NotFoundError("No matching base origin", { expected: baseOrigin, received: requestOrigin, caller });
26
+ const targetPath = _stripPathPrefix(requestPath, basePath);
27
+ if (!targetPath)
28
+ throw new NotFoundError("No matching base path", { received: requestPath, expected: basePath, caller });
23
29
  for (const handler of handlers) {
24
30
  const pathParams = handler.endpoint.match(method, targetPath, caller);
25
31
  if (!pathParams)
26
32
  continue;
27
33
  const params = searchParams.size ? { ...getDictionary(searchParams), ...pathParams } : pathParams;
28
- return handleEndpoint(handler, params, request, args, handleEndpoints);
34
+ return _handleEndpoint(handler, params, request, args, handleEndpoints);
29
35
  }
30
36
  throw new NotFoundError("No matching endpoint", { received: targetPath, caller });
31
37
  }
32
38
  /**
33
39
  * Validate and invoke an endpoint callback after the routing layer has already matched URL params.
34
40
  */
35
- async function handleEndpoint({ endpoint, callback },
41
+ async function _handleEndpoint({ endpoint, callback },
36
42
  /** Params we already matched/parsed from the URL. */
37
- params, request, args, caller = handleEndpoint) {
43
+ params, request, args, caller = _handleEndpoint) {
38
44
  const content = await getRequestContent(request, caller);
39
45
  const unsafePayload = content === undefined ? params : isPlainObject(content) ? { ...content, ...params } : content;
40
46
  const payload = endpoint.payload.validate(unsafePayload);
41
47
  const unsafeResult = await callback(payload, request, ...args);
48
+ if (unsafeResult instanceof Response)
49
+ return unsafeResult;
42
50
  try {
43
51
  return getResponse(endpoint.result.validate(unsafeResult));
44
52
  }
@@ -49,7 +57,7 @@ params, request, args, caller = handleEndpoint) {
49
57
  }
50
58
  }
51
59
  /** 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) {
60
+ function _stripPathPrefix(path, prefix) {
53
61
  prefix = prefix === "/" ? "/" : prefix.replace(/\/$/, "");
54
62
  if (prefix === "/")
55
63
  return path;
@@ -57,5 +65,4 @@ function _stripPathPrefix(path, prefix, caller) {
57
65
  return "/";
58
66
  if (path.startsWith(`${prefix}/`))
59
67
  return path.slice(prefix.length);
60
- throw new NotFoundError("No matching endpoint", { received: path, expected: prefix, caller });
61
68
  }
package/api/index.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  export * from "./cache/APICache.js";
2
2
  export * from "./cache/EndpointCache.js";
3
- export * from "./cache/EndpointStore.js";
4
3
  export * from "./endpoint/Endpoint.js";
5
4
  export * from "./endpoint/util.js";
6
5
  export * from "./provider/APIProvider.js";
7
6
  export * from "./provider/ClientAPIProvider.js";
7
+ export * from "./provider/DebugAPIProvider.js";
8
8
  export * from "./provider/MockAPIProvider.js";
9
9
  export * from "./provider/ThroughAPIProvider.js";
10
+ export * from "./provider/ValidationAPIProvider.js";
11
+ export * from "./store/EndpointStore.js";
package/api/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  export * from "./cache/APICache.js";
2
2
  export * from "./cache/EndpointCache.js";
3
- export * from "./cache/EndpointStore.js";
4
3
  export * from "./endpoint/Endpoint.js";
5
4
  export * from "./endpoint/util.js";
6
5
  export * from "./provider/APIProvider.js";
7
6
  export * from "./provider/ClientAPIProvider.js";
7
+ export * from "./provider/DebugAPIProvider.js";
8
8
  export * from "./provider/MockAPIProvider.js";
9
9
  export * from "./provider/ThroughAPIProvider.js";
10
+ export * from "./provider/ValidationAPIProvider.js";
11
+ export * from "./store/EndpointStore.js";
@@ -17,5 +17,21 @@ export declare class ClientAPIProvider implements APIProvider {
17
17
  /** Default options used for HTTP requests created with `this.getRequest()` and `this.fetch()` */
18
18
  readonly options: RequestOptions;
19
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>;
20
36
  fetch<P, R>(endpoint: Endpoint<P, R>, payload: P, options?: RequestOptions, caller?: AnyCaller): Promise<R>;
21
37
  }
@@ -1,5 +1,8 @@
1
- import { mergeRequestOptions } from "../../util/http.js";
2
- import { requireBaseURL } from "../../util/url.js";
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";
3
6
  /** Provider for API endpoints rooted at a common base URL. */
4
7
  export class ClientAPIProvider {
5
8
  /** The common base URL for all rendered endpoint requests. */
@@ -10,9 +13,39 @@ export class ClientAPIProvider {
10
13
  this.url = requireBaseURL(url, undefined, ClientAPIProvider);
11
14
  this.options = options;
12
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
+ }
13
46
  async fetch(endpoint, payload, options, caller = this.fetch) {
14
- const request = endpoint.getRequest(this.url, payload, mergeRequestOptions(this.options, options), caller);
47
+ const request = this.getRequest(endpoint, payload, mergeRequestOptions(this.options, options), caller);
15
48
  const response = await fetch(request);
16
- return endpoint.parseResponse(response, caller);
49
+ return this.parseResponse(endpoint, response, caller);
17
50
  }
18
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
+ }
@@ -12,23 +12,18 @@ export type MockAPICall = {
12
12
  readonly response: Response;
13
13
  readonly result: unknown;
14
14
  };
15
- /**
16
- * Construction options for a `MockAPIProvider`.
17
- * - Accepts the normal `APIProvider` options.
18
- */
15
+ /** Construction options for a `MockAPIProvider`. */
19
16
  export interface MockAPIProviderOptions extends ClientAPIProviderOptions {
20
- /** Implement this handler to mock the the request/response input/output. */
21
- readonly handler: RequestHandler;
22
17
  }
23
18
  /** Provider that logs API calls without sending network requests. */
24
19
  export declare class MockAPIProvider extends ClientAPIProvider {
25
20
  readonly calls: MockAPICall[];
26
21
  readonly handler: RequestHandler;
27
- constructor({ handler, ...options }: MockAPIProviderOptions);
22
+ constructor(handler: RequestHandler, options?: MockAPIProviderOptions);
28
23
  /**
29
24
  * Log a `fetch()` call without using the network.
30
- * - If `getResult` is configured, its return value is validated against the endpoint result schema.
31
- * - Otherwise `undefined` is validated against the endpoint result schema.
25
+ * - If `getResult` is configured, its return value is returned as-is (no schema validation).
26
+ * - Otherwise `undefined` is returned.
32
27
  */
33
28
  fetch<P, R>(endpoint: Endpoint<P, R>, payload: P, _options?: RequestOptions, caller?: AnyCaller): Promise<R>;
34
29
  }
@@ -4,20 +4,20 @@ import { ClientAPIProvider } from "./ClientAPIProvider.js";
4
4
  export class MockAPIProvider extends ClientAPIProvider {
5
5
  calls = [];
6
6
  handler;
7
- constructor({ handler, ...options }) {
7
+ constructor(handler, options = { url: "https://api.mock.com" }) {
8
8
  super(options);
9
9
  this.handler = handler;
10
10
  }
11
11
  /**
12
12
  * Log a `fetch()` call without using the network.
13
- * - If `getResult` is configured, its return value is validated against the endpoint result schema.
14
- * - Otherwise `undefined` is validated against the endpoint result schema.
13
+ * - If `getResult` is configured, its return value is returned as-is (no schema validation).
14
+ * - Otherwise `undefined` is returned.
15
15
  */
16
16
  async fetch(endpoint, payload, _options = {}, caller = this.fetch) {
17
17
  const options = mergeRequestOptions(this.options, _options);
18
- const request = endpoint.getRequest(this.url, payload, options, caller);
18
+ const request = this.getRequest(endpoint, payload, options, caller);
19
19
  const response = await this.handler(request);
20
- const result = await endpoint.parseResponse(response);
20
+ const result = await this.parseResponse(endpoint, response, caller);
21
21
  this.calls.push({ type: "fetch", endpoint, payload, options, request, response, result });
22
22
  return result;
23
23
  }
@@ -2,13 +2,12 @@ import type { AnyCaller } from "../../util/function.js";
2
2
  import type { RequestOptions } from "../../util/http.js";
3
3
  import type { Endpoint } from "../endpoint/Endpoint.js";
4
4
  import type { APIProvider } from "./APIProvider.js";
5
- import type { ClientAPIProvider } from "./ClientAPIProvider.js";
6
5
  /**
7
6
  * Provider wrapper that delegates API operations to a source provider.
8
7
  * - Extend this when you want to intercept only selected API operations, such as injecting auth headers or logging.
9
8
  */
10
9
  export declare class ThroughAPIProvider implements APIProvider {
11
- readonly source: ClientAPIProvider;
12
- constructor(source: ClientAPIProvider);
10
+ readonly source: APIProvider;
11
+ constructor(source: APIProvider);
13
12
  fetch<P, R>(endpoint: Endpoint<P, R>, payload: P, options?: RequestOptions, caller?: AnyCaller): Promise<R>;
14
13
  }
@@ -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
+ /** Validate an asynchronous source provider (source can have any type because validation guarantees the type). */
6
+ export declare class ValidationAPIProvider extends ThroughAPIProvider {
7
+ fetch<P, R>(endpoint: Endpoint<P, R>, payload: P, options?: RequestOptions, caller?: AnyCaller): Promise<R>;
8
+ }
@@ -0,0 +1,26 @@
1
+ import { ResponseError } from "../../error/ResponseError.js";
2
+ import { ThroughAPIProvider } from "./ThroughAPIProvider.js";
3
+ /** Validate an asynchronous source provider (source can have any type because validation guarantees the type). */
4
+ export class ValidationAPIProvider extends ThroughAPIProvider {
5
+ async fetch(endpoint, payload, options, caller = this.fetch) {
6
+ // Validate payload — let thrown strings bubble up as user-readable messages for e.g. form handlers.
7
+ const validPayload = _validatePayload(endpoint, payload);
8
+ // Call through to source (raw transport, no schema validation).
9
+ const content = await this.source.fetch(endpoint, validPayload, options, caller);
10
+ // Validate result — wrap in ResponseError as this is a server/transport problem, not user error.
11
+ return _validateResult(endpoint, content, caller);
12
+ }
13
+ }
14
+ function _validatePayload(endpoint, payload) {
15
+ return endpoint.payload.validate(payload);
16
+ }
17
+ function _validateResult(endpoint, content, caller) {
18
+ try {
19
+ return endpoint.result.validate(content);
20
+ }
21
+ catch (thrown) {
22
+ if (typeof thrown === "string")
23
+ throw new ResponseError(`Invalid result for ${endpoint}:\n${thrown}`, { endpoint, code: 422, caller });
24
+ throw thrown;
25
+ }
26
+ }
@@ -0,0 +1,53 @@
1
+ import type { Collection } from "../db/collection/Collection.js";
2
+ import { DBProvider } from "../db/provider/DBProvider.js";
3
+ import type { Data } from "../util/data.js";
4
+ import type { Items, OptionalItem } from "../util/item.js";
5
+ import type { ItemQuery } from "../util/query.js";
6
+ import type { Updates } from "../util/update.js";
7
+ /** Minimal interface matching Cloudflare Workers KV namespace runtime object. */
8
+ export interface KVNamespace {
9
+ get(key: string, options: {
10
+ type: "json";
11
+ }): Promise<unknown>;
12
+ put(key: string, value: string): Promise<void>;
13
+ delete(key: string): Promise<void>;
14
+ }
15
+ /**
16
+ * Cloudflare Workers KV database provider.
17
+ *
18
+ * Items are stored as JSON values under keys formatted as `collection:id`.
19
+ * The `KVNamespace` object is provided by the Cloudflare Workers runtime environment.
20
+ *
21
+ * ### Supported
22
+ * - Single item operations: `getItem`, `setItem`, `addItem`, `updateItem`, `deleteItem`.
23
+ * - ID generation: `addItem()` generates a UUID v4 identifier automatically.
24
+ *
25
+ * ### Not supported
26
+ * - **Realtime subscriptions:** `getItemSequence()` and `getQuerySequence()` throw `UnimplementedError`.
27
+ * KV has no change feed or push notification mechanism.
28
+ * - **Updates:** `updateItem()` and `updateQuery()` throw `UnimplementedError`.
29
+ * - **Collection queries:** `getQuery()`, `setQuery()`, `deleteQuery()`, and `countQuery()` are not supported.
30
+ * KV does not expose efficient filtering, sorting, or collection scans, so this provider avoids
31
+ * the old "read everything and filter in memory" behavior.
32
+ *
33
+ * ### Performance limitations
34
+ * - **Single-key store only:** This provider is intentionally limited to direct key reads and writes.
35
+ * If you need collection queries, filtering, sorting, or bulk mutations, use a different backend.
36
+ * - **Eventual consistency:** KV is eventually consistent, so reads may briefly return stale values
37
+ * shortly after writes.
38
+ */
39
+ export declare class CloudflareKVProvider extends DBProvider<string> {
40
+ private readonly _kv;
41
+ constructor(kv: KVNamespace);
42
+ getItem<T extends Data>({ name }: Collection<string, string, T>, id: string): Promise<OptionalItem<string, T>>;
43
+ getItemSequence<T extends Data>(_c: Collection<string, string, T>, _id: string): AsyncIterable<OptionalItem<string, T>>;
44
+ addItem<T extends Data>({ name }: Collection<string, string, T>, data: T): Promise<string>;
45
+ setItem<T extends Data>({ name }: Collection<string, string, T>, id: string, data: T): Promise<void>;
46
+ updateItem<T extends Data>(_c: Collection<string, string, T>, _id: string, _updates: Updates<T>): Promise<void>;
47
+ deleteItem<T extends Data>({ name }: Collection<string, string, T>, id: string): Promise<void>;
48
+ getQuery<T extends Data>(_c: Collection<string, string, T>, q?: ItemQuery<string, T>): Promise<Items<string, T>>;
49
+ getQuerySequence<T extends Data>(_c: Collection<string, string, T>, _q?: ItemQuery<string, T>): AsyncIterable<Items<string, T>>;
50
+ setQuery<T extends Data>(_c: Collection<string, string, T>, _q: ItemQuery<string, T>, _data: T): Promise<void>;
51
+ updateQuery<T extends Data>(_c: Collection<string, string, T>, _q: ItemQuery<string, T>, _updates: Updates<T>): Promise<void>;
52
+ deleteQuery<T extends Data>(c: Collection<string, string, T>, q: ItemQuery<string, T>): Promise<void>;
53
+ }
@@ -0,0 +1,75 @@
1
+ import { DBProvider } from "../db/provider/DBProvider.js";
2
+ import { UnimplementedError } from "../error/UnimplementedError.js";
3
+ import { getItem } from "../util/item.js";
4
+ import { randomUUID } from "../util/uuid.js";
5
+ /**
6
+ * Cloudflare Workers KV database provider.
7
+ *
8
+ * Items are stored as JSON values under keys formatted as `collection:id`.
9
+ * The `KVNamespace` object is provided by the Cloudflare Workers runtime environment.
10
+ *
11
+ * ### Supported
12
+ * - Single item operations: `getItem`, `setItem`, `addItem`, `updateItem`, `deleteItem`.
13
+ * - ID generation: `addItem()` generates a UUID v4 identifier automatically.
14
+ *
15
+ * ### Not supported
16
+ * - **Realtime subscriptions:** `getItemSequence()` and `getQuerySequence()` throw `UnimplementedError`.
17
+ * KV has no change feed or push notification mechanism.
18
+ * - **Updates:** `updateItem()` and `updateQuery()` throw `UnimplementedError`.
19
+ * - **Collection queries:** `getQuery()`, `setQuery()`, `deleteQuery()`, and `countQuery()` are not supported.
20
+ * KV does not expose efficient filtering, sorting, or collection scans, so this provider avoids
21
+ * the old "read everything and filter in memory" behavior.
22
+ *
23
+ * ### Performance limitations
24
+ * - **Single-key store only:** This provider is intentionally limited to direct key reads and writes.
25
+ * If you need collection queries, filtering, sorting, or bulk mutations, use a different backend.
26
+ * - **Eventual consistency:** KV is eventually consistent, so reads may briefly return stale values
27
+ * shortly after writes.
28
+ */
29
+ export class CloudflareKVProvider extends DBProvider {
30
+ _kv;
31
+ constructor(kv) {
32
+ super();
33
+ this._kv = kv;
34
+ }
35
+ async getItem({ name }, id) {
36
+ const data = (await this._kv.get(_getKey(name, id), { type: "json" }));
37
+ if (data)
38
+ return getItem(id, data);
39
+ }
40
+ getItemSequence(_c, _id) {
41
+ throw new UnimplementedError("CloudflareKVProvider does not support realtime subscriptions");
42
+ }
43
+ async addItem({ name }, data) {
44
+ const id = randomUUID();
45
+ await this._kv.put(_getKey(name, id), JSON.stringify(data));
46
+ return id;
47
+ }
48
+ async setItem({ name }, id, data) {
49
+ await this._kv.put(_getKey(name, id), JSON.stringify(data));
50
+ }
51
+ async updateItem(_c, _id, _updates) {
52
+ throw new UnimplementedError("CloudflareKVProvider does not support updates to items");
53
+ }
54
+ async deleteItem({ name }, id) {
55
+ await this._kv.delete(_getKey(name, id));
56
+ }
57
+ async getQuery(_c, q) {
58
+ throw new UnimplementedError("CloudflareKVProvider does not support querying items");
59
+ }
60
+ getQuerySequence(_c, _q) {
61
+ throw new UnimplementedError("CloudflareKVProvider does not support realtime subscriptions");
62
+ }
63
+ async setQuery(_c, _q, _data) {
64
+ throw new UnimplementedError("CloudflareKVProvider does not support querying items");
65
+ }
66
+ async updateQuery(_c, _q, _updates) {
67
+ throw new UnimplementedError("CloudflareKVProvider does not support updates to items");
68
+ }
69
+ async deleteQuery(c, q) {
70
+ throw new UnimplementedError("CloudflareKVProvider does not support querying items");
71
+ }
72
+ }
73
+ function _getKey(collection, id) {
74
+ return `${collection}:${id}`;
75
+ }
@@ -0,0 +1 @@
1
+ export * from "./CloudflareKVProvider.js";
@@ -0,0 +1 @@
1
+ export * from "./CloudflareKVProvider.js";