shelving 1.178.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.
- package/api/cache/EndpointCache.d.ts +1 -1
- package/api/cache/EndpointCache.js +1 -1
- package/api/endpoint/Endpoint.d.ts +1 -20
- package/api/endpoint/Endpoint.js +1 -47
- package/api/index.d.ts +3 -1
- package/api/index.js +3 -1
- package/api/provider/ClientAPIProvider.d.ts +16 -0
- package/api/provider/ClientAPIProvider.js +37 -4
- package/api/provider/DebugAPIProvider.d.ts +8 -0
- package/api/provider/DebugAPIProvider.js +16 -0
- package/api/provider/MockAPIProvider.d.ts +2 -2
- package/api/provider/MockAPIProvider.js +4 -4
- package/api/provider/ThroughAPIProvider.d.ts +2 -3
- package/api/provider/ValidationAPIProvider.d.ts +8 -0
- package/api/provider/ValidationAPIProvider.js +26 -0
- package/cloudflare/CloudflareKVProvider.d.ts +71 -0
- package/cloudflare/CloudflareKVProvider.js +113 -0
- package/cloudflare/index.d.ts +1 -0
- package/cloudflare/index.js +1 -0
- package/db/collection/Collection.d.ts +27 -0
- package/db/collection/Collection.js +31 -0
- package/db/index.d.ts +11 -10
- package/db/index.js +11 -13
- package/db/provider/CacheDBProvider.d.ts +26 -0
- package/db/{CacheProvider.js → provider/CacheDBProvider.js} +14 -13
- package/db/provider/ChangesDBProvider.d.ts +28 -0
- package/db/provider/ChangesDBProvider.js +37 -0
- package/db/provider/DBProvider.d.ts +23 -0
- package/db/provider/DBProvider.js +33 -0
- package/db/provider/DebugDBProvider.d.ts +21 -0
- package/db/provider/DebugDBProvider.js +144 -0
- package/db/provider/MemoryDBProvider.d.ts +67 -0
- package/db/{MemoryProvider.js → provider/MemoryDBProvider.js} +42 -41
- package/db/provider/MockDBProvider.d.ts +30 -0
- package/db/provider/MockDBProvider.js +49 -0
- package/db/provider/ThroughDBProvider.d.ts +27 -0
- package/db/provider/ThroughDBProvider.js +52 -0
- package/db/provider/ValidationDBProvider.d.ts +20 -0
- package/db/provider/ValidationDBProvider.js +87 -0
- package/db/store/ItemStore.d.ts +27 -0
- package/db/{ItemStore.js → store/ItemStore.js} +10 -10
- package/db/store/QueryStore.d.ts +39 -0
- package/db/{QueryStore.js → store/QueryStore.js} +12 -12
- package/firestore/client/FirestoreClientProvider.d.ts +16 -15
- package/firestore/client/FirestoreClientProvider.js +27 -29
- package/firestore/lite/FirestoreLiteProvider.d.ts +16 -15
- package/firestore/lite/FirestoreLiteProvider.js +25 -27
- package/firestore/server/FirestoreServerProvider.d.ts +16 -15
- package/firestore/server/FirestoreServerProvider.js +34 -34
- package/package.json +2 -1
- package/react/createAPIContext.d.ts +1 -1
- package/react/createAPIContext.js +1 -1
- package/react/createDataContext.d.ts +12 -11
- package/react/createDataContext.js +7 -7
- package/schema/CountrySchema.d.ts +1 -1
- package/schema/DataSchema.d.ts +1 -5
- package/store/Store.d.ts +0 -1
- package/store/Store.js +0 -4
- package/test/index.d.ts +22 -25
- package/test/index.js +4 -4
- package/util/data.d.ts +0 -4
- package/util/query.d.ts +2 -2
- package/db/CacheProvider.d.ts +0 -25
- package/db/Change.d.ts +0 -76
- package/db/Change.js +0 -59
- package/db/ChangesProvider.d.ts +0 -31
- package/db/ChangesProvider.js +0 -73
- package/db/DebugProvider.d.ts +0 -35
- package/db/DebugProvider.js +0 -277
- package/db/ItemStore.d.ts +0 -26
- package/db/MemoryProvider.d.ts +0 -66
- package/db/Provider.d.ts +0 -120
- package/db/Provider.js +0 -64
- package/db/QueryStore.d.ts +0 -38
- package/db/ThroughProvider.d.ts +0 -46
- package/db/ThroughProvider.js +0 -104
- package/db/ValidationProvider.d.ts +0 -49
- package/db/ValidationProvider.js +0 -157
- /package/api/{cache → store}/EndpointStore.d.ts +0 -0
- /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 "
|
|
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 "
|
|
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 {
|
|
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
|
}
|
package/api/endpoint/Endpoint.js
CHANGED
|
@@ -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", {
|
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 {
|
|
2
|
-
import {
|
|
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 =
|
|
47
|
+
const request = this.getRequest(endpoint, payload, mergeRequestOptions(this.options, options), caller);
|
|
15
48
|
const response = await fetch(request);
|
|
16
|
-
return
|
|
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
|
+
}
|
|
@@ -27,8 +27,8 @@ export declare class MockAPIProvider extends ClientAPIProvider {
|
|
|
27
27
|
constructor({ handler, ...options }: MockAPIProviderOptions);
|
|
28
28
|
/**
|
|
29
29
|
* Log a `fetch()` call without using the network.
|
|
30
|
-
* - If `getResult` is configured, its return value is
|
|
31
|
-
* - Otherwise `undefined` is
|
|
30
|
+
* - If `getResult` is configured, its return value is returned as-is (no schema validation).
|
|
31
|
+
* - Otherwise `undefined` is returned.
|
|
32
32
|
*/
|
|
33
33
|
fetch<P, R>(endpoint: Endpoint<P, R>, payload: P, _options?: RequestOptions, caller?: AnyCaller): Promise<R>;
|
|
34
34
|
}
|
|
@@ -10,14 +10,14 @@ export class MockAPIProvider extends ClientAPIProvider {
|
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
12
|
* Log a `fetch()` call without using the network.
|
|
13
|
-
* - If `getResult` is configured, its return value is
|
|
14
|
-
* - Otherwise `undefined` is
|
|
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 =
|
|
18
|
+
const request = this.getRequest(endpoint, payload, options, caller);
|
|
19
19
|
const response = await this.handler(request);
|
|
20
|
-
const result = await
|
|
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:
|
|
12
|
-
constructor(source:
|
|
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,71 @@
|
|
|
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
|
+
list(options?: {
|
|
15
|
+
prefix?: string;
|
|
16
|
+
limit?: number;
|
|
17
|
+
cursor?: string;
|
|
18
|
+
}): Promise<{
|
|
19
|
+
keys: readonly {
|
|
20
|
+
name: string;
|
|
21
|
+
}[];
|
|
22
|
+
list_complete: boolean;
|
|
23
|
+
cursor?: string;
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Cloudflare Workers KV database provider.
|
|
28
|
+
*
|
|
29
|
+
* Items are stored as JSON values under keys formatted as `collection:id`.
|
|
30
|
+
* The `KVNamespace` object is provided by the Cloudflare Workers runtime environment.
|
|
31
|
+
*
|
|
32
|
+
* ### Supported
|
|
33
|
+
* - Single item operations: `getItem`, `setItem`, `addItem`, `updateItem`, `deleteItem`.
|
|
34
|
+
* - Query operations: `getQuery`, `setQuery`, `updateQuery`, `deleteQuery`, `countQuery`.
|
|
35
|
+
* - All filter operators: equality, not, in, out, contains, gt, gte, lt, lte.
|
|
36
|
+
* - Sorting (`$order`) and limiting (`$limit`).
|
|
37
|
+
* - ID generation: `addItem()` generates a UUID v4 identifier automatically.
|
|
38
|
+
*
|
|
39
|
+
* ### Not supported
|
|
40
|
+
* - **Realtime subscriptions:** `getItemSequence()` and `getQuerySequence()` throw `UnimplementedError`.
|
|
41
|
+
* KV has no change feed or push notification mechanism.
|
|
42
|
+
*
|
|
43
|
+
* ### Performance limitations
|
|
44
|
+
* - **Querying is expensive:** KV has no native query support. Every `getQuery()` call lists all keys
|
|
45
|
+
* in the collection (paginated, 1000 keys per page), fetches each value individually, then applies
|
|
46
|
+
* filtering, sorting, and limiting in-memory. Avoid large collections where possible.
|
|
47
|
+
* - **Non-atomic updates:** `updateItem()` performs a read-modify-write cycle. Concurrent writes to the
|
|
48
|
+
* same item may cause one write to be lost.
|
|
49
|
+
* - **Eventual consistency:** KV is eventually consistent — reads may return stale data, particularly
|
|
50
|
+
* shortly after writes. This also affects `updateItem`, `setQuery`, `updateQuery`, and `deleteQuery`
|
|
51
|
+
* since they read before writing.
|
|
52
|
+
* - **No bulk get:** Each item value must be fetched individually — there is no multi-get API.
|
|
53
|
+
* Query operations that match N items require N+1 KV requests (one `list` + N `get` calls per page).
|
|
54
|
+
*/
|
|
55
|
+
export declare class CloudflareKVProvider extends DBProvider<string> {
|
|
56
|
+
private readonly _kv;
|
|
57
|
+
constructor(kv: KVNamespace);
|
|
58
|
+
getItem<T extends Data>({ name }: Collection<string, string, T>, id: string): Promise<OptionalItem<string, T>>;
|
|
59
|
+
getItemSequence<T extends Data>(_c: Collection<string, string, T>, _id: string): AsyncIterable<OptionalItem<string, T>>;
|
|
60
|
+
addItem<T extends Data>({ name }: Collection<string, string, T>, data: T): Promise<string>;
|
|
61
|
+
setItem<T extends Data>({ name }: Collection<string, string, T>, id: string, data: T): Promise<void>;
|
|
62
|
+
updateItem<T extends Data>(c: Collection<string, string, T>, id: string, updates: Updates<T>): Promise<void>;
|
|
63
|
+
deleteItem<T extends Data>({ name }: Collection<string, string, T>, id: string): Promise<void>;
|
|
64
|
+
getQuery<T extends Data>({ name }: Collection<string, string, T>, q?: ItemQuery<string, T>): Promise<Items<string, T>>;
|
|
65
|
+
getQuerySequence<T extends Data>(_c: Collection<string, string, T>, _q?: ItemQuery<string, T>): AsyncIterable<Items<string, T>>;
|
|
66
|
+
setQuery<T extends Data>(c: Collection<string, string, T>, q: ItemQuery<string, T>, data: T): Promise<void>;
|
|
67
|
+
updateQuery<T extends Data>(c: Collection<string, string, T>, q: ItemQuery<string, T>, updates: Updates<T>): Promise<void>;
|
|
68
|
+
deleteQuery<T extends Data>(c: Collection<string, string, T>, q: ItemQuery<string, T>): Promise<void>;
|
|
69
|
+
/** Fetch all items in a collection, paginating through `kv.list()`. */
|
|
70
|
+
private _getAllItems;
|
|
71
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { DBProvider } from "../db/provider/DBProvider.js";
|
|
2
|
+
import { UnimplementedError } from "../error/UnimplementedError.js";
|
|
3
|
+
import { requireArray } from "../util/array.js";
|
|
4
|
+
import { getItem } from "../util/item.js";
|
|
5
|
+
import { queryItems } from "../util/query.js";
|
|
6
|
+
import { updateData } from "../util/update.js";
|
|
7
|
+
import { randomUUID } from "../util/uuid.js";
|
|
8
|
+
function _getKey(collection, id) {
|
|
9
|
+
return `${collection}:${id}`;
|
|
10
|
+
}
|
|
11
|
+
function _getPrefix(collection) {
|
|
12
|
+
return `${collection}:`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Cloudflare Workers KV database provider.
|
|
16
|
+
*
|
|
17
|
+
* Items are stored as JSON values under keys formatted as `collection:id`.
|
|
18
|
+
* The `KVNamespace` object is provided by the Cloudflare Workers runtime environment.
|
|
19
|
+
*
|
|
20
|
+
* ### Supported
|
|
21
|
+
* - Single item operations: `getItem`, `setItem`, `addItem`, `updateItem`, `deleteItem`.
|
|
22
|
+
* - Query operations: `getQuery`, `setQuery`, `updateQuery`, `deleteQuery`, `countQuery`.
|
|
23
|
+
* - All filter operators: equality, not, in, out, contains, gt, gte, lt, lte.
|
|
24
|
+
* - Sorting (`$order`) and limiting (`$limit`).
|
|
25
|
+
* - ID generation: `addItem()` generates a UUID v4 identifier automatically.
|
|
26
|
+
*
|
|
27
|
+
* ### Not supported
|
|
28
|
+
* - **Realtime subscriptions:** `getItemSequence()` and `getQuerySequence()` throw `UnimplementedError`.
|
|
29
|
+
* KV has no change feed or push notification mechanism.
|
|
30
|
+
*
|
|
31
|
+
* ### Performance limitations
|
|
32
|
+
* - **Querying is expensive:** KV has no native query support. Every `getQuery()` call lists all keys
|
|
33
|
+
* in the collection (paginated, 1000 keys per page), fetches each value individually, then applies
|
|
34
|
+
* filtering, sorting, and limiting in-memory. Avoid large collections where possible.
|
|
35
|
+
* - **Non-atomic updates:** `updateItem()` performs a read-modify-write cycle. Concurrent writes to the
|
|
36
|
+
* same item may cause one write to be lost.
|
|
37
|
+
* - **Eventual consistency:** KV is eventually consistent — reads may return stale data, particularly
|
|
38
|
+
* shortly after writes. This also affects `updateItem`, `setQuery`, `updateQuery`, and `deleteQuery`
|
|
39
|
+
* since they read before writing.
|
|
40
|
+
* - **No bulk get:** Each item value must be fetched individually — there is no multi-get API.
|
|
41
|
+
* Query operations that match N items require N+1 KV requests (one `list` + N `get` calls per page).
|
|
42
|
+
*/
|
|
43
|
+
export class CloudflareKVProvider extends DBProvider {
|
|
44
|
+
_kv;
|
|
45
|
+
constructor(kv) {
|
|
46
|
+
super();
|
|
47
|
+
this._kv = kv;
|
|
48
|
+
}
|
|
49
|
+
async getItem({ name }, id) {
|
|
50
|
+
const data = (await this._kv.get(_getKey(name, id), { type: "json" }));
|
|
51
|
+
if (data)
|
|
52
|
+
return getItem(id, data);
|
|
53
|
+
}
|
|
54
|
+
getItemSequence(_c, _id) {
|
|
55
|
+
throw new UnimplementedError("CloudflareKVProvider does not support realtime subscriptions");
|
|
56
|
+
}
|
|
57
|
+
async addItem({ name }, data) {
|
|
58
|
+
const id = randomUUID();
|
|
59
|
+
await this._kv.put(_getKey(name, id), JSON.stringify(data));
|
|
60
|
+
return id;
|
|
61
|
+
}
|
|
62
|
+
async setItem({ name }, id, data) {
|
|
63
|
+
await this._kv.put(_getKey(name, id), JSON.stringify(data));
|
|
64
|
+
}
|
|
65
|
+
async updateItem(c, id, updates) {
|
|
66
|
+
const existing = await this.getItem(c, id);
|
|
67
|
+
if (existing)
|
|
68
|
+
await this.setItem(c, id, updateData(existing, updates));
|
|
69
|
+
}
|
|
70
|
+
async deleteItem({ name }, id) {
|
|
71
|
+
await this._kv.delete(_getKey(name, id));
|
|
72
|
+
}
|
|
73
|
+
async getQuery({ name }, q) {
|
|
74
|
+
const all = await this._getAllItems(name);
|
|
75
|
+
return q ? requireArray(queryItems(all, q)) : all;
|
|
76
|
+
}
|
|
77
|
+
getQuerySequence(_c, _q) {
|
|
78
|
+
throw new UnimplementedError("CloudflareKVProvider does not support realtime subscriptions");
|
|
79
|
+
}
|
|
80
|
+
async setQuery(c, q, data) {
|
|
81
|
+
const items = await this.getQuery(c, q);
|
|
82
|
+
await Promise.all(items.map(item => this.setItem(c, item.id, data)));
|
|
83
|
+
}
|
|
84
|
+
async updateQuery(c, q, updates) {
|
|
85
|
+
const items = await this.getQuery(c, q);
|
|
86
|
+
await Promise.all(items.map(item => this.setItem(c, item.id, updateData(item, updates))));
|
|
87
|
+
}
|
|
88
|
+
async deleteQuery(c, q) {
|
|
89
|
+
const items = await this.getQuery(c, q);
|
|
90
|
+
await Promise.all(items.map(item => this._kv.delete(_getKey(c.name, item.id))));
|
|
91
|
+
}
|
|
92
|
+
/** Fetch all items in a collection, paginating through `kv.list()`. */
|
|
93
|
+
async _getAllItems(collection) {
|
|
94
|
+
const prefix = _getPrefix(collection);
|
|
95
|
+
const items = [];
|
|
96
|
+
let cursor;
|
|
97
|
+
do {
|
|
98
|
+
const result = await this._kv.list(cursor ? { prefix, cursor } : { prefix });
|
|
99
|
+
const values = await Promise.all(result.keys.map(async (key) => {
|
|
100
|
+
const id = key.name.slice(prefix.length);
|
|
101
|
+
const data = (await this._kv.get(key.name, { type: "json" }));
|
|
102
|
+
if (data)
|
|
103
|
+
return getItem(id, data);
|
|
104
|
+
}));
|
|
105
|
+
for (const item of values) {
|
|
106
|
+
if (item)
|
|
107
|
+
items.push(item);
|
|
108
|
+
}
|
|
109
|
+
cursor = result.list_complete ? undefined : result.cursor;
|
|
110
|
+
} while (cursor);
|
|
111
|
+
return items;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./CloudflareKVProvider.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./CloudflareKVProvider.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { DataSchema } from "../../schema/DataSchema.js";
|
|
2
|
+
import { NumberSchema } from "../../schema/NumberSchema.js";
|
|
3
|
+
import type { Schema, Schemas } from "../../schema/Schema.js";
|
|
4
|
+
import type { Data } from "../../util/data.js";
|
|
5
|
+
import type { Identifier, Item } from "../../util/item.js";
|
|
6
|
+
/** Default identifier schema (integer). */
|
|
7
|
+
export declare const ID: NumberSchema;
|
|
8
|
+
/** Declarative definition of a database collection/table. */
|
|
9
|
+
export declare class Collection<K extends string, I extends Identifier, T extends Data> extends DataSchema<T> {
|
|
10
|
+
/** Collection name (used as the table/collection key). */
|
|
11
|
+
readonly name: K;
|
|
12
|
+
/** Schema for the identifier type. */
|
|
13
|
+
readonly id: Schema<I>;
|
|
14
|
+
/** Schema for a complete item (id + data). */
|
|
15
|
+
readonly item: DataSchema<Item<I, T>>;
|
|
16
|
+
constructor(name: K, id: Schema<I>, data: Schemas<T> | DataSchema<T>);
|
|
17
|
+
}
|
|
18
|
+
/** Shortcut factory for creating a Collection. */
|
|
19
|
+
export declare function COLLECTION<K extends string, I extends Identifier, T extends Data>(name: K, id: Schema<I>, data: Schemas<T> | DataSchema<T>): Collection<K, I, T>;
|
|
20
|
+
/** A readonly array of Collection instances. */
|
|
21
|
+
export type Collections = ReadonlyArray<Collection<string, Identifier, Data>>;
|
|
22
|
+
/** Extract the union of collection key strings from a Collections type. */
|
|
23
|
+
export type CollectionKeys<C extends Collections> = C[number]["name"];
|
|
24
|
+
/** Convert a Collections array type to a Database-style object mapping. */
|
|
25
|
+
export type CollectionsDatabase<C extends Collections> = {
|
|
26
|
+
[E in C[number] as E extends Collection<infer K, any, any> ? K : never]: E extends Collection<string, any, infer T> ? T : never;
|
|
27
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { DataSchema, ITEM } from "../../schema/DataSchema.js";
|
|
2
|
+
import { NumberSchema } from "../../schema/NumberSchema.js";
|
|
3
|
+
/** Default identifier schema (integer). */
|
|
4
|
+
export const ID = new NumberSchema({
|
|
5
|
+
step: 1,
|
|
6
|
+
min: Number.MIN_SAFE_INTEGER,
|
|
7
|
+
max: Number.MAX_SAFE_INTEGER,
|
|
8
|
+
value: 0,
|
|
9
|
+
one: "ID",
|
|
10
|
+
title: "ID",
|
|
11
|
+
});
|
|
12
|
+
/** Declarative definition of a database collection/table. */
|
|
13
|
+
export class Collection extends DataSchema {
|
|
14
|
+
/** Collection name (used as the table/collection key). */
|
|
15
|
+
name;
|
|
16
|
+
/** Schema for the identifier type. */
|
|
17
|
+
id;
|
|
18
|
+
/** Schema for a complete item (id + data). */
|
|
19
|
+
item;
|
|
20
|
+
constructor(name, id, data) {
|
|
21
|
+
const dataSchema = data instanceof DataSchema ? data : new DataSchema({ props: data });
|
|
22
|
+
super({ ...dataSchema, props: dataSchema.props });
|
|
23
|
+
this.name = name;
|
|
24
|
+
this.id = id;
|
|
25
|
+
this.item = ITEM(id, dataSchema);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Shortcut factory for creating a Collection. */
|
|
29
|
+
export function COLLECTION(name, id, data) {
|
|
30
|
+
return new Collection(name, id, data);
|
|
31
|
+
}
|
package/db/index.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
export * from "./
|
|
2
|
-
export * from "./
|
|
3
|
-
export * from "./
|
|
4
|
-
export * from "./
|
|
5
|
-
export * from "./
|
|
6
|
-
export * from "./
|
|
7
|
-
export * from "./
|
|
8
|
-
export * from "./
|
|
9
|
-
export * from "./
|
|
10
|
-
export * from "./
|
|
1
|
+
export * from "./collection/Collection.js";
|
|
2
|
+
export * from "./provider/CacheDBProvider.js";
|
|
3
|
+
export * from "./provider/ChangesDBProvider.js";
|
|
4
|
+
export * from "./provider/DBProvider.js";
|
|
5
|
+
export * from "./provider/DebugDBProvider.js";
|
|
6
|
+
export * from "./provider/MemoryDBProvider.js";
|
|
7
|
+
export * from "./provider/MockDBProvider.js";
|
|
8
|
+
export * from "./provider/ThroughDBProvider.js";
|
|
9
|
+
export * from "./provider/ValidationDBProvider.js";
|
|
10
|
+
export * from "./store/ItemStore.js";
|
|
11
|
+
export * from "./store/QueryStore.js";
|