shelving 1.167.2 → 1.168.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{api → endpoint}/Endpoint.d.ts +22 -29
- package/{api → endpoint}/Endpoint.js +2 -69
- package/{api → endpoint}/util.js +6 -7
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/markup/rule/link.js +6 -6
- package/markup/util/options.d.ts +6 -10
- package/package.json +5 -5
- package/schema/URISchema.d.ts +21 -0
- package/schema/URISchema.js +38 -0
- package/schema/URLSchema.d.ts +24 -0
- package/schema/{LinkSchema.js → URLSchema.js} +14 -14
- package/schema/index.d.ts +2 -1
- package/schema/index.js +2 -3
- package/store/EndpointStore.d.ts +24 -0
- package/store/EndpointStore.js +74 -0
- package/store/URLStore.d.ts +4 -3
- package/store/URLStore.js +13 -12
- package/store/index.d.ts +1 -0
- package/store/index.js +1 -0
- package/util/format.d.ts +2 -2
- package/util/format.js +6 -4
- package/util/http.d.ts +22 -0
- package/util/http.js +59 -0
- package/util/index.d.ts +1 -1
- package/util/index.js +1 -1
- package/util/uri.d.ts +94 -0
- package/util/uri.js +119 -0
- package/util/url.d.ts +47 -37
- package/util/url.js +21 -95
- package/schema/LinkSchema.d.ts +0 -27
- package/util/link.d.ts +0 -71
- package/util/link.js +0 -44
- /package/{api → endpoint}/index.d.ts +0 -0
- /package/{api → endpoint}/index.js +0 -0
- /package/{api → endpoint}/util.d.ts +0 -0
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import { type Schema } from "../schema/Schema.js";
|
|
2
2
|
import type { AnyCaller } from "../util/function.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type RequestMethod, type RequestOptions } from "../util/http.js";
|
|
4
|
+
import type { URLString } from "../util/url.js";
|
|
4
5
|
import type { EndpointCallback, EndpointHandler } from "./util.js";
|
|
5
|
-
/** HTTP request methods. */
|
|
6
|
-
export type EndpointMethod = EndpointBodyMethod | EndpointHeadMethod;
|
|
7
|
-
/** HTTP request methods that have no body. */
|
|
8
|
-
export type EndpointHeadMethod = "HEAD" | "GET";
|
|
9
|
-
/** HTTP request methods that have a body. */
|
|
10
|
-
export type EndpointBodyMethod = "POST" | "PUT" | "PATCH" | "DELETE";
|
|
11
|
-
/** Configurable options for endpoint. */
|
|
12
|
-
export type EndpointOptions = Omit<RequestInit, "method" | "body">;
|
|
13
6
|
/**
|
|
14
7
|
* An abstract API resource definition, used to specify types for e.g. serverless functions.
|
|
15
8
|
*
|
|
@@ -20,14 +13,14 @@ export type EndpointOptions = Omit<RequestInit, "method" | "body">;
|
|
|
20
13
|
*/
|
|
21
14
|
export declare class Endpoint<P, R> {
|
|
22
15
|
/** Endpoint method. */
|
|
23
|
-
readonly method:
|
|
16
|
+
readonly method: RequestMethod;
|
|
24
17
|
/** Endpoint URL, possibly including placeholders e.g. `https://api.mysite.com/users/{id}` */
|
|
25
|
-
readonly url:
|
|
18
|
+
readonly url: URLString;
|
|
26
19
|
/** Payload schema. */
|
|
27
20
|
readonly payload: Schema<P>;
|
|
28
21
|
/** Result schema. */
|
|
29
22
|
readonly result: Schema<R>;
|
|
30
|
-
constructor(method:
|
|
23
|
+
constructor(method: RequestMethod, url: URLString, payload: Schema<P>, result: Schema<R>);
|
|
31
24
|
/**
|
|
32
25
|
* Return an `EndpointHandler` for this endpoint.
|
|
33
26
|
*
|
|
@@ -54,7 +47,7 @@ export declare class Endpoint<P, R> {
|
|
|
54
47
|
*
|
|
55
48
|
* @throws Feedback if the payload is invalid.
|
|
56
49
|
*/
|
|
57
|
-
request(payload: P, options?:
|
|
50
|
+
request(payload: P, options?: RequestOptions, caller?: AnyCaller): Request;
|
|
58
51
|
/**
|
|
59
52
|
* Validate an HTTP `Response` against this endpoint.
|
|
60
53
|
* @throws ResponseError if the response status is not ok (200-299)
|
|
@@ -70,7 +63,7 @@ export declare class Endpoint<P, R> {
|
|
|
70
63
|
* @throws ResponseError if the response status is not ok (200-299)
|
|
71
64
|
* @throws ResponseError if the response content is invalid.
|
|
72
65
|
*/
|
|
73
|
-
fetch(payload: P, options?:
|
|
66
|
+
fetch(payload: P, options?: RequestOptions, caller?: AnyCaller): Promise<R>;
|
|
74
67
|
/** Convert to string, e.g. `GET https://a.com/user/{id}` */
|
|
75
68
|
toString(): string;
|
|
76
69
|
}
|
|
@@ -82,34 +75,34 @@ export type EndpointType<X extends Endpoint<unknown, unknown>> = X extends Endpo
|
|
|
82
75
|
* Represent a GET request to a specified URL, with validated payload and return types.
|
|
83
76
|
* "The GET method requests a representation of the specified resource. Requests using GET should only retrieve data and should not contain a request content."
|
|
84
77
|
*/
|
|
85
|
-
export declare function GET<P, R>(url:
|
|
86
|
-
export declare function GET<P>(url:
|
|
87
|
-
export declare function GET<R>(url:
|
|
78
|
+
export declare function GET<P, R>(url: URLString, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>;
|
|
79
|
+
export declare function GET<P>(url: URLString, payload: Schema<P>): Endpoint<P, undefined>;
|
|
80
|
+
export declare function GET<R>(url: URLString, payload: undefined, result: Schema<R>): Endpoint<undefined, R>;
|
|
88
81
|
/**
|
|
89
82
|
* Represent a POST request to a specified URL, with validated payload and return types.
|
|
90
83
|
* "The POST method submits an entity to the specified resource, often causing a change in state or side effects on the server.
|
|
91
84
|
*/
|
|
92
|
-
export declare function POST<P, R>(url:
|
|
93
|
-
export declare function POST<P>(url:
|
|
94
|
-
export declare function POST<R>(url:
|
|
85
|
+
export declare function POST<P, R>(url: URLString, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>;
|
|
86
|
+
export declare function POST<P>(url: URLString, payload: Schema<P>): Endpoint<P, undefined>;
|
|
87
|
+
export declare function POST<R>(url: URLString, payload: undefined, result: Schema<R>): Endpoint<undefined, R>;
|
|
95
88
|
/**
|
|
96
89
|
* Represent a PUT request to a specified URL, with validated payload and return types.
|
|
97
90
|
* "The PUT method replaces all current representations of the target resource with the request content."
|
|
98
91
|
*/
|
|
99
|
-
export declare function PUT<P, R>(url:
|
|
100
|
-
export declare function PUT<P>(url:
|
|
101
|
-
export declare function PUT<R>(url:
|
|
92
|
+
export declare function PUT<P, R>(url: URLString, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>;
|
|
93
|
+
export declare function PUT<P>(url: URLString, payload: Schema<P>): Endpoint<P, undefined>;
|
|
94
|
+
export declare function PUT<R>(url: URLString, payload: undefined, result: Schema<R>): Endpoint<undefined, R>;
|
|
102
95
|
/**
|
|
103
96
|
* Represent a PATCH request to a specified URL, with validated payload and return types.
|
|
104
97
|
* "The PATCH method applies partial modifications to a resource."
|
|
105
98
|
*/
|
|
106
|
-
export declare function PATCH<P, R>(url:
|
|
107
|
-
export declare function PATCH<P>(url:
|
|
108
|
-
export declare function PATCH<R>(url:
|
|
99
|
+
export declare function PATCH<P, R>(url: URLString, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>;
|
|
100
|
+
export declare function PATCH<P>(url: URLString, payload: Schema<P>): Endpoint<P, undefined>;
|
|
101
|
+
export declare function PATCH<R>(url: URLString, payload: undefined, result: Schema<R>): Endpoint<undefined, R>;
|
|
109
102
|
/**
|
|
110
103
|
* Represent a DELETE request to a specified URL, with validated payload and return types.
|
|
111
104
|
* "The DELETE method deletes the specified resource."
|
|
112
105
|
*/
|
|
113
|
-
export declare function DELETE<P, R>(url:
|
|
114
|
-
export declare function DELETE<P>(url:
|
|
115
|
-
export declare function DELETE<R>(url:
|
|
106
|
+
export declare function DELETE<P, R>(url: URLString, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>;
|
|
107
|
+
export declare function DELETE<P>(url: URLString, payload: Schema<P>): Endpoint<P, undefined>;
|
|
108
|
+
export declare function DELETE<R>(url: URLString, payload: undefined, result: Schema<R>): Endpoint<undefined, R>;
|
|
@@ -2,9 +2,8 @@ import { ResponseError } from "../error/ResponseError.js";
|
|
|
2
2
|
import { UNDEFINED } from "../schema/Schema.js";
|
|
3
3
|
import { assertDictionary } from "../util/dictionary.js";
|
|
4
4
|
import { getMessage } from "../util/error.js";
|
|
5
|
-
import { getResponse, getResponseContent } from "../util/http.js";
|
|
5
|
+
import { getRequest, getResponse, getResponseContent } from "../util/http.js";
|
|
6
6
|
import { getPlaceholders, renderTemplate } from "../util/template.js";
|
|
7
|
-
import { omitURLParams, withURLParams } from "../util/url.js";
|
|
8
7
|
import { getValid } from "../util/validate.js";
|
|
9
8
|
/**
|
|
10
9
|
* An abstract API resource definition, used to specify types for e.g. serverless functions.
|
|
@@ -77,7 +76,7 @@ export class Endpoint {
|
|
|
77
76
|
* @throws Feedback if the payload is invalid.
|
|
78
77
|
*/
|
|
79
78
|
request(payload, options = {}, caller = this.request) {
|
|
80
|
-
return
|
|
79
|
+
return getRequest(this.method, this.url, this.payload.validate(payload), options, caller);
|
|
81
80
|
}
|
|
82
81
|
/**
|
|
83
82
|
* Validate an HTTP `Response` against this endpoint.
|
|
@@ -127,69 +126,3 @@ export function PATCH(url, payload = UNDEFINED, result = UNDEFINED) {
|
|
|
127
126
|
export function DELETE(url, payload = UNDEFINED, result = UNDEFINED) {
|
|
128
127
|
return new Endpoint("DELETE", url, payload, result);
|
|
129
128
|
}
|
|
130
|
-
/**
|
|
131
|
-
* Create a `Request` instance for a method/url and payload.
|
|
132
|
-
*
|
|
133
|
-
* - If `{placeholders}` are set in the URL, they are replaced by values from payload (will throw if `payload` is not a dictionary object).
|
|
134
|
-
* - If the method is `HEAD` or `GET`, the payload is sent as `?query` parameters in the URL.
|
|
135
|
-
* - If the method is anything else, the payload is sent in the body (either as JSON, string, or `FormData`).
|
|
136
|
-
*
|
|
137
|
-
* @throws ValueError if this is a `HEAD` or `GET` request but `body` is not a dictionary object.
|
|
138
|
-
* @throws ValueError if `{placeholders}` are set in the URL but `body` is not a dictionary object.
|
|
139
|
-
*/
|
|
140
|
-
function createRequest(method, url, payload, options = {}, caller = createRequest) {
|
|
141
|
-
// This is a head request, so ensure the payload is a dictionary object.
|
|
142
|
-
if (method === "GET" || method === "HEAD") {
|
|
143
|
-
assertDictionary(payload, caller);
|
|
144
|
-
return createHeadRequest(method, url, payload, options, caller);
|
|
145
|
-
}
|
|
146
|
-
// This is a normal body request.
|
|
147
|
-
return createBodyRequest(method, url, payload, options, caller);
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Create a body-less request to a URL.
|
|
151
|
-
* - Any `{placeholders}` in the URL will be rendered with values from `params`, and won't be set in `?query` parameters in the URL.
|
|
152
|
-
*/
|
|
153
|
-
function createHeadRequest(method, url, params, options = {}, caller = createHeadRequest) {
|
|
154
|
-
const placeholders = getPlaceholders(url);
|
|
155
|
-
// URL has `{placeholders}` to render, so rendere those to the URL and add all other params as `?query` params.
|
|
156
|
-
if (placeholders.length) {
|
|
157
|
-
const rendered = omitURLParams(withURLParams(renderTemplate(url, params, caller), params, caller), ...placeholders);
|
|
158
|
-
return new Request(rendered, { ...options, method });
|
|
159
|
-
}
|
|
160
|
-
// URL has no `{placeholders}`, so add all payload params to the URL.
|
|
161
|
-
return new Request(withURLParams(url, params, caller), { ...options, method });
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Create a body request to a URL.
|
|
165
|
-
* - Any `{placeholders}` in the URL will be rendered with values from `data`, and won't be set in the request body.
|
|
166
|
-
* - The payload is sent in the body (either as JSON, string, or `FormData`).
|
|
167
|
-
*
|
|
168
|
-
* @throws ValueError if `{placeholders}` are set in the URL but `body` is not a dictionary object.
|
|
169
|
-
*/
|
|
170
|
-
function createBodyRequest(method, url, body, options = {}, caller = createBodyRequest) {
|
|
171
|
-
const placeholders = getPlaceholders(url);
|
|
172
|
-
// If `{placeholders}` are set in the URL then body must be a dictionary object and is sent as JSON.
|
|
173
|
-
if (placeholders.length) {
|
|
174
|
-
assertDictionary(body, caller);
|
|
175
|
-
return createJSONRequest(method, url, body, options);
|
|
176
|
-
}
|
|
177
|
-
// `FormData` instances pass through unaltered and will set their own `Content-Type` with complex boundary information.
|
|
178
|
-
if (body instanceof FormData)
|
|
179
|
-
return createFormDataRequest(method, url, body, options);
|
|
180
|
-
if (typeof body === "string")
|
|
181
|
-
return createTextRequest(method, url, body, options);
|
|
182
|
-
return createJSONRequest(method, url, body, options); // JSON is the default.
|
|
183
|
-
}
|
|
184
|
-
/** Create a `FormData` request to a URL. */
|
|
185
|
-
function createFormDataRequest(method, url, body, options = {}) {
|
|
186
|
-
return new Request(url, { ...options, method, body });
|
|
187
|
-
}
|
|
188
|
-
/** Create a plain text request to a URL. */
|
|
189
|
-
function createTextRequest(method, url, body, { headers, ...options } = {}) {
|
|
190
|
-
return new Request(url, { ...options, headers: { ...headers, "Content-Type": "text/plain" }, method, body });
|
|
191
|
-
}
|
|
192
|
-
/** Create a JSON request to a URL. */
|
|
193
|
-
function createJSONRequest(method, url, body, { headers, ...options } = {}) {
|
|
194
|
-
return new Request(url, { ...options, headers: { ...headers, "Content-Type": "application/json" }, method, body: JSON.stringify(body) });
|
|
195
|
-
}
|
package/{api → endpoint}/util.js
RENAMED
|
@@ -15,10 +15,9 @@ import { getURL } from "../util/url.js";
|
|
|
15
15
|
*/
|
|
16
16
|
export function handleEndpoints(request, endpoints, caller = handleEndpoints) {
|
|
17
17
|
// Parse the URL of the request.
|
|
18
|
-
const
|
|
19
|
-
const url = getURL(requestUrl);
|
|
18
|
+
const url = getURL(request.url);
|
|
20
19
|
if (!url)
|
|
21
|
-
throw new RequestError("Invalid request URL", { received:
|
|
20
|
+
throw new RequestError("Invalid request URL", { received: request.url, caller });
|
|
22
21
|
const { origin, pathname, searchParams } = url;
|
|
23
22
|
// Iterate over the handlers and return the first one that matches the request.
|
|
24
23
|
for (const { endpoint, callback } of endpoints) {
|
|
@@ -33,15 +32,15 @@ export function handleEndpoints(request, endpoints, caller = handleEndpoints) {
|
|
|
33
32
|
// Make a simple dictionary object from the `{placeholder}` path params and the `?a=123` query params from the URL.
|
|
34
33
|
const combinedParams = searchParams.size ? { ...getDictionary(searchParams), ...pathParams } : pathParams;
|
|
35
34
|
// Get the response by calling the callback.
|
|
36
|
-
return handleEndpoint(endpoint, callback, combinedParams, request);
|
|
35
|
+
return handleEndpoint(endpoint, callback, combinedParams, request, caller);
|
|
37
36
|
}
|
|
38
37
|
// No handler matched the request.
|
|
39
|
-
throw new NotFoundError("No matching endpoint", { received:
|
|
38
|
+
throw new NotFoundError("No matching endpoint", { received: request.url, caller });
|
|
40
39
|
}
|
|
41
40
|
/** Handle an individual call to an endpoint callback. */
|
|
42
|
-
async function handleEndpoint(endpoint, callback, params, request) {
|
|
41
|
+
async function handleEndpoint(endpoint, callback, params, request, caller = handleEndpoint) {
|
|
43
42
|
// Extract a data object from the request body and validate it against the endpoint's payload type.
|
|
44
|
-
const content = await getRequestContent(request,
|
|
43
|
+
const content = await getRequestContent(request, caller);
|
|
45
44
|
// If content is undefined, it means the request has no body, so params are the only payload.
|
|
46
45
|
// - If the content is a plain object, merge if with the params.
|
|
47
46
|
// - If the content is anything else (e.g. string, number, array), return it directly (but you'll have no way to access the other params).
|
package/index.d.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* - Modules that use peer dependencies, like `shelving/react` and `shelving/firebase/client`
|
|
4
4
|
* - Modules that are for internal use, like `shelving/test`
|
|
5
5
|
*/
|
|
6
|
-
export * from "./api/index.js";
|
|
7
6
|
export * from "./db/index.js";
|
|
7
|
+
export * from "./endpoint/index.js";
|
|
8
8
|
export * from "./error/index.js";
|
|
9
9
|
export * from "./feedback/index.js";
|
|
10
10
|
export * from "./markup/index.js";
|
package/index.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* - Modules that use peer dependencies, like `shelving/react` and `shelving/firebase/client`
|
|
4
4
|
* - Modules that are for internal use, like `shelving/test`
|
|
5
5
|
*/
|
|
6
|
-
export * from "./api/index.js";
|
|
7
6
|
export * from "./db/index.js";
|
|
7
|
+
export * from "./endpoint/index.js";
|
|
8
8
|
export * from "./error/index.js";
|
|
9
9
|
export * from "./feedback/index.js";
|
|
10
10
|
// export * from "./firestore/client/index.js"; // Not exported.
|
package/markup/rule/link.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { getLink } from "../../util/link.js";
|
|
1
|
+
import { formatURI } from "../../util/format.js";
|
|
3
2
|
import { getRegExp } from "../../util/regexp.js";
|
|
3
|
+
import { getURI, HTTP_SCHEMES } from "../../util/uri.js";
|
|
4
4
|
import { getURL } from "../../util/url.js";
|
|
5
5
|
import { renderMarkup } from "../render.js";
|
|
6
6
|
import { REACT_ELEMENT_TYPE } from "../util/internal.js";
|
|
7
7
|
import { getMarkupRule } from "../util/rule.js";
|
|
8
8
|
/** Render `<a href="">` if the link is a valid one, or `<a>` (with no `href`) if it isn't. */
|
|
9
9
|
function renderLinkMarkupRule({ groups: { title, href: unsafeHref } }, options, key) {
|
|
10
|
-
const { base, schemes
|
|
11
|
-
const
|
|
12
|
-
const href =
|
|
13
|
-
const children = title ? renderMarkup(title, options, "link") :
|
|
10
|
+
const { base, schemes = HTTP_SCHEMES, rel } = options;
|
|
11
|
+
const uri = getURL(unsafeHref, base) ?? getURI(unsafeHref);
|
|
12
|
+
const href = uri && schemes.includes(uri.protocol) ? uri.href : undefined;
|
|
13
|
+
const children = title ? renderMarkup(title, options, "link") : uri ? formatURI(uri) : "";
|
|
14
14
|
return {
|
|
15
15
|
key,
|
|
16
16
|
$$typeof: REACT_ELEMENT_TYPE,
|
package/markup/util/options.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { URISchemes } from "../../util/uri.js";
|
|
2
|
+
import type { URLString } from "../../util/url.js";
|
|
2
3
|
import type { MarkupRules } from "./rule.js";
|
|
3
4
|
/** The current parsing options (represents the current state of the parsing). */
|
|
4
5
|
export type MarkupOptions = {
|
|
@@ -10,19 +11,14 @@ export type MarkupOptions = {
|
|
|
10
11
|
*/
|
|
11
12
|
readonly rel?: string | undefined;
|
|
12
13
|
/**
|
|
13
|
-
* Set the base URL that any relative
|
|
14
|
+
* Set the base URL that any relative URLs will be relative to
|
|
14
15
|
* @default `window.location.href` in browser environments, and `undefined` in server environments.
|
|
15
16
|
*/
|
|
16
|
-
readonly base?:
|
|
17
|
+
readonly base?: URLString | undefined;
|
|
17
18
|
/**
|
|
18
|
-
* Valid
|
|
19
|
+
* Valid URI schemes/protocols for URLs and URIs.
|
|
19
20
|
* @example ["http:", "https:"]
|
|
20
21
|
* @default ["http:", "https:"]
|
|
21
22
|
*/
|
|
22
|
-
readonly schemes?:
|
|
23
|
-
/**
|
|
24
|
-
* Valid URL hosts for links.
|
|
25
|
-
* @example ["google.com", "www.google.com"]
|
|
26
|
-
*/
|
|
27
|
-
readonly hosts?: LinkHosts | undefined;
|
|
23
|
+
readonly schemes?: URISchemes | undefined;
|
|
28
24
|
};
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"state-management",
|
|
12
12
|
"query-builder"
|
|
13
13
|
],
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.168.1",
|
|
15
15
|
"repository": "https://github.com/dhoulb/shelving",
|
|
16
16
|
"author": "Dave Houlbrooke <dave@shax.com>",
|
|
17
17
|
"license": "0BSD",
|
|
@@ -57,11 +57,11 @@
|
|
|
57
57
|
"build:test:unit": "bun test ./dist/**/*.test.js --bail"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@biomejs/biome": "^2.
|
|
60
|
+
"@biomejs/biome": "^2.3.8",
|
|
61
61
|
"@google-cloud/firestore": "^7.11.6",
|
|
62
|
-
"@types/bun": "^1.3.
|
|
63
|
-
"@types/react": "^19.2.
|
|
64
|
-
"@types/react-dom": "^19.2.
|
|
62
|
+
"@types/bun": "^1.3.3",
|
|
63
|
+
"@types/react": "^19.2.7",
|
|
64
|
+
"@types/react-dom": "^19.2.3",
|
|
65
65
|
"firebase": "^11.10.0",
|
|
66
66
|
"react": "^19.2.0",
|
|
67
67
|
"react-dom": "^19.2.0",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type URISchemes, type URIString } from "../util/uri.js";
|
|
2
|
+
import type { StringSchemaOptions } from "./StringSchema.js";
|
|
3
|
+
import { StringSchema } from "./StringSchema.js";
|
|
4
|
+
/** Allowed options for `URISchema` */
|
|
5
|
+
export interface URISchemaOptions extends Omit<StringSchemaOptions, "input" | "min" | "max" | "multiline"> {
|
|
6
|
+
readonly schemes?: URISchemes | undefined;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Type of `StringSchema` that defines a valid URI string.
|
|
10
|
+
* - Checks URI scheme against a whitelist (always).
|
|
11
|
+
* - URIs are limited to 512 characters, but generally these won't be data: URIs so this is a reasonable limit.
|
|
12
|
+
*/
|
|
13
|
+
export declare class URISchema extends StringSchema {
|
|
14
|
+
readonly schemes: URISchemes;
|
|
15
|
+
constructor({ one, title, schemes, ...options }: URISchemaOptions);
|
|
16
|
+
validate(unsafeValue: unknown): URIString;
|
|
17
|
+
}
|
|
18
|
+
/** Valid URI string, e.g. `https://www.google.com` */
|
|
19
|
+
export declare const URI_SCHEMA: URISchema;
|
|
20
|
+
/** Valid URI string, e.g. `https://www.google.com`, or `null` */
|
|
21
|
+
export declare const NULLABLE_URI_SCHEMA: import("./NullableSchema.js").NullableSchema<`${string}:${string}`>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ValueFeedback } from "../feedback/Feedback.js";
|
|
2
|
+
import { getURI, HTTP_SCHEMES } from "../util/uri.js";
|
|
3
|
+
import { NULLABLE } from "./NullableSchema.js";
|
|
4
|
+
import { StringSchema } from "./StringSchema.js";
|
|
5
|
+
/**
|
|
6
|
+
* Type of `StringSchema` that defines a valid URI string.
|
|
7
|
+
* - Checks URI scheme against a whitelist (always).
|
|
8
|
+
* - URIs are limited to 512 characters, but generally these won't be data: URIs so this is a reasonable limit.
|
|
9
|
+
*/
|
|
10
|
+
export class URISchema extends StringSchema {
|
|
11
|
+
schemes;
|
|
12
|
+
constructor({ one = "URI", title = "URI", schemes = HTTP_SCHEMES, ...options }) {
|
|
13
|
+
super({
|
|
14
|
+
one,
|
|
15
|
+
title,
|
|
16
|
+
...options,
|
|
17
|
+
input: "url",
|
|
18
|
+
min: 1,
|
|
19
|
+
max: 512,
|
|
20
|
+
multiline: false,
|
|
21
|
+
});
|
|
22
|
+
this.schemes = schemes;
|
|
23
|
+
}
|
|
24
|
+
// Override to validate the URI and check the schemes and hosts against the whitelists.
|
|
25
|
+
validate(unsafeValue) {
|
|
26
|
+
const str = super.validate(unsafeValue);
|
|
27
|
+
const uri = getURI(str);
|
|
28
|
+
if (!uri)
|
|
29
|
+
throw new ValueFeedback(str ? "Invalid format" : "Required", str);
|
|
30
|
+
if (this.schemes && !this.schemes.includes(uri.protocol))
|
|
31
|
+
throw new ValueFeedback("Invalid URI scheme", str);
|
|
32
|
+
return uri.href;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Valid URI string, e.g. `https://www.google.com` */
|
|
36
|
+
export const URI_SCHEMA = new URISchema({});
|
|
37
|
+
/** Valid URI string, e.g. `https://www.google.com`, or `null` */
|
|
38
|
+
export const NULLABLE_URI_SCHEMA = NULLABLE(URI_SCHEMA);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type URISchemes } from "../util/uri.js";
|
|
2
|
+
import { type URLString } from "../util/url.js";
|
|
3
|
+
import type { StringSchemaOptions } from "./StringSchema.js";
|
|
4
|
+
import { StringSchema } from "./StringSchema.js";
|
|
5
|
+
/** Allowed options for `URLSchema` */
|
|
6
|
+
export interface URLSchemaOptions extends Omit<StringSchemaOptions, "input" | "min" | "max" | "multiline"> {
|
|
7
|
+
readonly base?: URL | URLString | undefined;
|
|
8
|
+
readonly schemes?: URISchemes | undefined;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Type of `StringSchema` that defines a valid URL string.
|
|
12
|
+
* - Checks URL scheme against a whitelist (always), and checks URL domain against a whitelist (optional).
|
|
13
|
+
* - URLs are limited to 512 characters, but generally these won't be data: URIs so this is a reasonable limit.
|
|
14
|
+
*/
|
|
15
|
+
export declare class URLSchema extends StringSchema {
|
|
16
|
+
readonly base: URLString | undefined;
|
|
17
|
+
readonly schemes: URISchemes;
|
|
18
|
+
constructor({ one, title, base, schemes, ...options }: URLSchemaOptions);
|
|
19
|
+
validate(unsafeValue: unknown): URLString;
|
|
20
|
+
}
|
|
21
|
+
/** Valid URL string, e.g. `https://www.google.com` */
|
|
22
|
+
export declare const URL_SCHEMA: URLSchema;
|
|
23
|
+
/** Valid URL string, e.g. `https://www.google.com`, or `null` */
|
|
24
|
+
export declare const NULLABLE_URL_SCHEMA: import("./NullableSchema.js").NullableSchema<`${string}://${string}`>;
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { ValueFeedback } from "../feedback/Feedback.js";
|
|
2
|
-
import {
|
|
2
|
+
import { HTTP_SCHEMES } from "../util/uri.js";
|
|
3
|
+
import { getURL } from "../util/url.js";
|
|
3
4
|
import { NULLABLE } from "./NullableSchema.js";
|
|
4
5
|
import { StringSchema } from "./StringSchema.js";
|
|
5
6
|
/**
|
|
6
|
-
* Type of `StringSchema` that defines a valid URL
|
|
7
|
+
* Type of `StringSchema` that defines a valid URL string.
|
|
7
8
|
* - Checks URL scheme against a whitelist (always), and checks URL domain against a whitelist (optional).
|
|
8
9
|
* - URLs are limited to 512 characters, but generally these won't be data: URIs so this is a reasonable limit.
|
|
9
|
-
* - Falsy values are converted to `""` empty string.
|
|
10
10
|
*/
|
|
11
|
-
export class
|
|
11
|
+
export class URLSchema extends StringSchema {
|
|
12
12
|
base;
|
|
13
13
|
schemes;
|
|
14
|
-
|
|
15
|
-
constructor({ one = "link", title = "Link", base, schemes, hosts, ...options }) {
|
|
14
|
+
constructor({ one = "URL", title = "URL", base, schemes = HTTP_SCHEMES, ...options }) {
|
|
16
15
|
super({
|
|
17
16
|
one,
|
|
18
17
|
title,
|
|
@@ -22,20 +21,21 @@ export class LinkSchema extends StringSchema {
|
|
|
22
21
|
max: 512,
|
|
23
22
|
multiline: false,
|
|
24
23
|
});
|
|
25
|
-
this.base = base;
|
|
24
|
+
this.base = getURL(base)?.href;
|
|
26
25
|
this.schemes = schemes;
|
|
27
|
-
this.hosts = hosts;
|
|
28
26
|
}
|
|
29
|
-
// Override to
|
|
27
|
+
// Override to validate the URL and check the schemes and hosts against the whitelists.
|
|
30
28
|
validate(unsafeValue) {
|
|
31
29
|
const str = super.validate(unsafeValue);
|
|
32
|
-
const url =
|
|
30
|
+
const url = getURL(str, this.base);
|
|
33
31
|
if (!url)
|
|
34
32
|
throw new ValueFeedback(str ? "Invalid format" : "Required", str);
|
|
33
|
+
if (this.schemes && !this.schemes.includes(url.protocol))
|
|
34
|
+
throw new ValueFeedback("Invalid URL scheme", str);
|
|
35
35
|
return url.href;
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
/** Valid
|
|
39
|
-
export const
|
|
40
|
-
/** Valid
|
|
41
|
-
export const
|
|
38
|
+
/** Valid URL string, e.g. `https://www.google.com` */
|
|
39
|
+
export const URL_SCHEMA = new URLSchema({});
|
|
40
|
+
/** Valid URL string, e.g. `https://www.google.com`, or `null` */
|
|
41
|
+
export const NULLABLE_URL_SCHEMA = NULLABLE(URL_SCHEMA);
|
package/schema/index.d.ts
CHANGED
|
@@ -10,7 +10,6 @@ export * from "./EmailSchema.js";
|
|
|
10
10
|
export * from "./EntitySchema.js";
|
|
11
11
|
export * from "./FileSchema.js";
|
|
12
12
|
export * from "./KeySchema.js";
|
|
13
|
-
export * from "./LinkSchema.js";
|
|
14
13
|
export * from "./NullableSchema.js";
|
|
15
14
|
export * from "./NumberSchema.js";
|
|
16
15
|
export * from "./OptionalSchema.js";
|
|
@@ -21,4 +20,6 @@ export * from "./SlugSchema.js";
|
|
|
21
20
|
export * from "./StringSchema.js";
|
|
22
21
|
export * from "./ThroughSchema.js";
|
|
23
22
|
export * from "./TimeSchema.js";
|
|
23
|
+
export * from "./URISchema.js";
|
|
24
|
+
export * from "./URLSchema.js";
|
|
24
25
|
export * from "./UUIDSchema.js";
|
package/schema/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// Field schemas.
|
|
2
1
|
export * from "./ArraySchema.js";
|
|
3
2
|
export * from "./BooleanSchema.js";
|
|
4
3
|
export * from "./ChoiceSchema.js";
|
|
@@ -11,7 +10,6 @@ export * from "./EmailSchema.js";
|
|
|
11
10
|
export * from "./EntitySchema.js";
|
|
12
11
|
export * from "./FileSchema.js";
|
|
13
12
|
export * from "./KeySchema.js";
|
|
14
|
-
export * from "./LinkSchema.js";
|
|
15
13
|
export * from "./NullableSchema.js";
|
|
16
14
|
export * from "./NumberSchema.js";
|
|
17
15
|
export * from "./OptionalSchema.js";
|
|
@@ -20,7 +18,8 @@ export * from "./RequiredSchema.js";
|
|
|
20
18
|
export * from "./Schema.js";
|
|
21
19
|
export * from "./SlugSchema.js";
|
|
22
20
|
export * from "./StringSchema.js";
|
|
23
|
-
// Utility schemas.
|
|
24
21
|
export * from "./ThroughSchema.js";
|
|
25
22
|
export * from "./TimeSchema.js";
|
|
23
|
+
export * from "./URISchema.js";
|
|
24
|
+
export * from "./URLSchema.js";
|
|
26
25
|
export * from "./UUIDSchema.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Endpoint } from "../endpoint/Endpoint.js";
|
|
2
|
+
import { NONE } from "../util/constants.js";
|
|
3
|
+
import { Store } from "./Store.js";
|
|
4
|
+
/**
|
|
5
|
+
* Store object that loads a result from an API endpoint and manages its state.
|
|
6
|
+
*
|
|
7
|
+
* @todo Needs support for `EndpointOptions` to set headers etc.
|
|
8
|
+
*/
|
|
9
|
+
export declare class EndpointStore<P, R> extends Store<R> {
|
|
10
|
+
static CANCELLED: symbol;
|
|
11
|
+
readonly endpoint: Endpoint<P, R>;
|
|
12
|
+
private _payload;
|
|
13
|
+
private _abort;
|
|
14
|
+
get payload(): P;
|
|
15
|
+
set payload(next: P);
|
|
16
|
+
/** Maximum age data can be before a fetch is triggered (defaults to 5 minutes). */
|
|
17
|
+
maxAge: number;
|
|
18
|
+
get value(): R;
|
|
19
|
+
set value(value: R | typeof NONE);
|
|
20
|
+
constructor(endpoint: Endpoint<P, R>, payload: P);
|
|
21
|
+
refetch(): Promise<void>;
|
|
22
|
+
/** Call the API now to fetch the data. */
|
|
23
|
+
private _call;
|
|
24
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { MINUTE, NONE } from "../util/constants.js";
|
|
2
|
+
import { isDeepEqual } from "../util/equal.js";
|
|
3
|
+
import { Store } from "./Store.js";
|
|
4
|
+
/**
|
|
5
|
+
* Store object that loads a result from an API endpoint and manages its state.
|
|
6
|
+
*
|
|
7
|
+
* @todo Needs support for `EndpointOptions` to set headers etc.
|
|
8
|
+
*/
|
|
9
|
+
export class EndpointStore extends Store {
|
|
10
|
+
static CANCELLED = Symbol("CANCELLED");
|
|
11
|
+
endpoint;
|
|
12
|
+
_payload;
|
|
13
|
+
_abort = undefined;
|
|
14
|
+
get payload() {
|
|
15
|
+
return this._payload;
|
|
16
|
+
}
|
|
17
|
+
set payload(next) {
|
|
18
|
+
const current = this._payload;
|
|
19
|
+
// Did the payload actually change?
|
|
20
|
+
if (!isDeepEqual(current, next)) {
|
|
21
|
+
this._payload = next;
|
|
22
|
+
// If there's already a fetch in progress, cancel it.
|
|
23
|
+
if (this._abort) {
|
|
24
|
+
this._abort.abort(EndpointStore.CANCELLED);
|
|
25
|
+
this._abort = undefined;
|
|
26
|
+
}
|
|
27
|
+
// Trigger a fetch.
|
|
28
|
+
this._call();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Maximum age data can be before a fetch is triggered (defaults to 5 minutes). */
|
|
32
|
+
maxAge = 5 * MINUTE;
|
|
33
|
+
// Override to possibly trigger a fetch when value is read.
|
|
34
|
+
get value() {
|
|
35
|
+
// Queue `this.call()` if...
|
|
36
|
+
// 1. Value is still loading.
|
|
37
|
+
// 2. Value is stale (older than maxAge).
|
|
38
|
+
if (this.loading || this.age > this.maxAge)
|
|
39
|
+
if (!this.reason)
|
|
40
|
+
this._call();
|
|
41
|
+
return super.value;
|
|
42
|
+
}
|
|
43
|
+
set value(value) {
|
|
44
|
+
super.value = value;
|
|
45
|
+
}
|
|
46
|
+
constructor(endpoint, payload) {
|
|
47
|
+
super(NONE);
|
|
48
|
+
this.endpoint = endpoint;
|
|
49
|
+
this.payload = payload;
|
|
50
|
+
}
|
|
51
|
+
refetch() {
|
|
52
|
+
return this._call();
|
|
53
|
+
}
|
|
54
|
+
/** Call the API now to fetch the data. */
|
|
55
|
+
async _call() {
|
|
56
|
+
if (this._abort)
|
|
57
|
+
return; // Already fetching.
|
|
58
|
+
this.reason = undefined; // Optimistically clear any error.
|
|
59
|
+
try {
|
|
60
|
+
this._abort = new AbortController();
|
|
61
|
+
const value = await this.endpoint.fetch(this._payload, { signal: this._abort.signal });
|
|
62
|
+
this.value = value;
|
|
63
|
+
}
|
|
64
|
+
catch (thrown) {
|
|
65
|
+
console.error(thrown);
|
|
66
|
+
if (thrown === EndpointStore.CANCELLED)
|
|
67
|
+
return; // Cancelled on purpose.
|
|
68
|
+
this.reason = thrown;
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
this._abort = undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
package/store/URLStore.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Data } from "../util/data.js";
|
|
2
|
-
import { type
|
|
2
|
+
import { type PossibleURIParams, type URIParams } from "../util/uri.js";
|
|
3
|
+
import { type PossibleURL, type URL } from "../util/url.js";
|
|
3
4
|
import { Store } from "./Store.js";
|
|
4
5
|
/** Store a URL, e.g. `https://top.com/a/b/c` */
|
|
5
6
|
export declare class URLStore extends Store<URL> {
|
|
@@ -20,7 +21,7 @@ export declare class URLStore extends Store<URL> {
|
|
|
20
21
|
/** Get the URL params as a string. */
|
|
21
22
|
get search(): string;
|
|
22
23
|
/** Get the URL params as a dictionary. */
|
|
23
|
-
get params():
|
|
24
|
+
get params(): URIParams;
|
|
24
25
|
/** Return a single param in this URL, or `undefined` if it could not be found. */
|
|
25
26
|
getParam(key: string): string | undefined;
|
|
26
27
|
/** Require a single param in this URL, or throw `RequiredError` if it could not be found. */
|
|
@@ -36,7 +37,7 @@ export declare class URLStore extends Store<URL> {
|
|
|
36
37
|
/** Return the current URL with an additional param. */
|
|
37
38
|
withParam(key: string, value: unknown): URL;
|
|
38
39
|
/** Return the current URL with an additional param. */
|
|
39
|
-
withParams(params:
|
|
40
|
+
withParams(params: PossibleURIParams): URL;
|
|
40
41
|
/** Return the current URL with an additional param. */
|
|
41
42
|
omitParams(...keys: string[]): URL;
|
|
42
43
|
/** Return the current URL with an additional param. */
|