shelving 1.173.0 → 1.173.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.
@@ -1,4 +1,5 @@
1
1
  import { type Schema } from "../schema/Schema.js";
2
+ import { type Data } from "../util/data.js";
2
3
  import type { AnyCaller } from "../util/function.js";
3
4
  import { type RequestMethod, type RequestOptions } from "../util/http.js";
4
5
  import type { URLString } from "../util/url.js";
@@ -40,7 +41,10 @@ export declare class Endpoint<P, R> {
40
41
  handle(callback: EndpointCallback<P, R>, unsafePayload: unknown, request: Request, caller?: AnyCaller): Promise<Response>;
41
42
  /**
42
43
  * Render the URL for this endpoint with the given payload.
43
- * - URL mioght contain `{placeholder}` values that are replaced with values from the payload.
44
+ * - URL might contain `{placeholder}` values that are replaced with values from the payload.
45
+ *
46
+ * @returns Rendered URL with `{placeholders}` rendered with values from `payload`
47
+ * @throws {RequiredError} if `{placeholders}` are set in the URL but `payload` is not a data object.
44
48
  */
45
49
  renderURL(payload: P, caller?: AnyCaller): string;
46
50
  /**
@@ -75,12 +79,23 @@ export declare class Endpoint<P, R> {
75
79
  export type PayloadType<X extends Endpoint<unknown, unknown>> = X extends Endpoint<infer Y, unknown> ? Y : never;
76
80
  /** Extract the result type from a `Endpoint`. */
77
81
  export type EndpointType<X extends Endpoint<unknown, unknown>> = X extends Endpoint<unknown, infer Y> ? Y : never;
82
+ /**
83
+ * Represent a HEAD request to a specified URL, with validated payload and return types.
84
+ * "The HEAD method requests a representation of the specified resource. Requests using HEAD should only retrieve data and should not contain a request content."
85
+ *
86
+ * - Because HEAD requests have no body payload can only be a data object (where props get sent as `{placeholders}` or `?query` params in the URL).
87
+ */
88
+ export declare function HEAD<P extends Data, R>(url: URLString, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>;
89
+ export declare function HEAD<P extends Data>(url: URLString, payload: Schema<P>): Endpoint<P, undefined>;
90
+ export declare function HEAD<R>(url: URLString, payload: undefined, result: Schema<R>): Endpoint<undefined, R>;
78
91
  /**
79
92
  * Represent a GET request to a specified URL, with validated payload and return types.
80
93
  * "The GET method requests a representation of the specified resource. Requests using GET should only retrieve data and should not contain a request content."
94
+ *
95
+ * - Because GET requests have no body payload can only be a data object (where props get sent as `{placeholders}` or `?query` params in the URL).
81
96
  */
82
- export declare function GET<P, R>(url: URLString, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>;
83
- export declare function GET<P>(url: URLString, payload: Schema<P>): Endpoint<P, undefined>;
97
+ export declare function GET<P extends Data, R>(url: URLString, payload?: Schema<P>, result?: Schema<R>): Endpoint<P, R>;
98
+ export declare function GET<P extends Data>(url: URLString, payload: Schema<P>): Endpoint<P, undefined>;
84
99
  export declare function GET<R>(url: URLString, payload: undefined, result: Schema<R>): Endpoint<undefined, R>;
85
100
  /**
86
101
  * Represent a POST request to a specified URL, with validated payload and return types.
@@ -1,10 +1,10 @@
1
1
  import { ResponseError } from "../error/ResponseError.js";
2
2
  import { ValueError } from "../error/ValueError.js";
3
3
  import { UNDEFINED } from "../schema/Schema.js";
4
- import { assertDictionary } from "../util/dictionary.js";
4
+ import { isData } from "../util/data.js";
5
5
  import { getMessage } from "../util/error.js";
6
6
  import { getRequest, getResponse, getResponseContent } from "../util/http.js";
7
- import { getPlaceholders, renderTemplate } from "../util/template.js";
7
+ import { renderTemplate } from "../util/template.js";
8
8
  /**
9
9
  * An abstract API resource definition, used to specify types for e.g. serverless functions.
10
10
  *
@@ -68,18 +68,13 @@ export class Endpoint {
68
68
  }
69
69
  /**
70
70
  * Render the URL for this endpoint with the given payload.
71
- * - URL mioght contain `{placeholder}` values that are replaced with values from the payload.
71
+ * - URL might contain `{placeholder}` values that are replaced with values from the payload.
72
+ *
73
+ * @returns Rendered URL with `{placeholders}` rendered with values from `payload`
74
+ * @throws {RequiredError} if `{placeholders}` are set in the URL but `payload` is not a data object.
72
75
  */
73
76
  renderURL(payload, caller = this.renderURL) {
74
- const { url } = this;
75
- // URL has `{placeholders}` to render.
76
- const placeholders = getPlaceholders(url);
77
- if (placeholders.length) {
78
- assertDictionary(payload, caller);
79
- return renderTemplate(url, payload, caller);
80
- }
81
- // URL has no `{placeholders}`
82
- return url;
77
+ return renderTemplate(this.url, isData(payload) ? payload : {}, caller); // Empty object in `renderTemplate()` will throw intended `RequiredError` for missing `{placeholder}`
83
78
  }
84
79
  /**
85
80
  * Get an HTTP `Request` object for this endpoint.
@@ -132,6 +127,9 @@ export class Endpoint {
132
127
  return `${this.method} ${this.url}`;
133
128
  }
134
129
  }
130
+ export function HEAD(url, payload = UNDEFINED, result = UNDEFINED) {
131
+ return new Endpoint("HEAD", url, payload, result);
132
+ }
135
133
  export function GET(url, payload = UNDEFINED, result = UNDEFINED) {
136
134
  return new Endpoint("GET", url, payload, result);
137
135
  }
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "state-management",
12
12
  "query-builder"
13
13
  ],
14
- "version": "1.173.0",
14
+ "version": "1.173.1",
15
15
  "repository": {
16
16
  "type": "git",
17
17
  "url": "git+https://github.com/dhoulb/shelving.git"
@@ -1,4 +1,4 @@
1
- import type { AddressData } from "../util/address.js";
1
+ import type { AddressData } from "../util/geo.js";
2
2
  import { DataSchema, type DataSchemaOptions } from "./DataSchema.js";
3
3
  /** Allowed options for `AddressSchema` */
4
4
  export interface AddressSchemaOptions extends Omit<DataSchemaOptions<AddressData>, "props"> {
@@ -1,4 +1,4 @@
1
- import { type Country } from "../util/country.js";
1
+ import { type Country } from "../util/geo.js";
2
2
  import { ChoiceSchema, type ChoiceSchemaOptions } from "./ChoiceSchema.js";
3
3
  /** Allowed options for `CountrySchema` */
4
4
  export interface CountrySchemaOptions extends Omit<ChoiceSchemaOptions<Country>, "options" | "value"> {
@@ -1,4 +1,4 @@
1
- import { COUNTRIES, getCountry } from "../util/country.js";
1
+ import { COUNTRIES, getCountry } from "../util/geo.js";
2
2
  import { isProp } from "../util/object.js";
3
3
  import { ChoiceSchema } from "./ChoiceSchema.js";
4
4
  import { NULLABLE } from "./NullableSchema.js";
@@ -258,3 +258,14 @@ export declare function getCountry(value?: unknown): Country | undefined;
258
258
  export declare function requireCountry(value?: unknown, caller?: AnyCaller): Country;
259
259
  /** Format a country code into its full country name. */
260
260
  export declare function formatCountry(country: string): string;
261
+ /** Valid shape for physical address data. */
262
+ export type AddressData = {
263
+ readonly address1: string;
264
+ readonly address2: string;
265
+ readonly city: string;
266
+ readonly state: string;
267
+ readonly postcode: string;
268
+ readonly country: Country;
269
+ };
270
+ /** Format address data into a single multiline string. */
271
+ export declare function formatAddress({ address1, address2, city, state, postcode, country }: AddressData): string;
@@ -276,3 +276,7 @@ export function formatCountry(country) {
276
276
  const code = country.toUpperCase();
277
277
  return isProp(COUNTRIES, code) ? COUNTRIES[code] : country;
278
278
  }
279
+ /** Format address data into a single multiline string. */
280
+ export function formatAddress({ address1, address2, city, state, postcode, country }) {
281
+ return `${address1}\n${address2 ? `${address2}\n` : ""}${city}\n${state}\n${postcode}\n${formatCountry(country)}`;
282
+ }
package/util/http.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { RequestError } from "../error/RequestError.js";
2
2
  import { ResponseError } from "../error/ResponseError.js";
3
- import { type ImmutableDictionary } from "./dictionary.js";
3
+ import { type Data } from "./data.js";
4
4
  import type { AnyCaller } from "./function.js";
5
5
  import type { URLString } from "./url.js";
6
6
  /** A handler function takes a `Request` and returns a `Response` (possibly asynchronously). */
@@ -62,12 +62,12 @@ export type RequestOptions = Omit<RequestInit, "method" | "body">;
62
62
  /**
63
63
  * Create a `Request` instance for a method/url and payload.
64
64
  *
65
- * - If `{placeholders}` are set in the URL, they are replaced by values from payload (will throw if `payload` is not a dictionary object).
65
+ * - If `{placeholders}` are set in the URL, they are replaced by values from payload (will throw if `payload` is not a data object).
66
66
  * - If the method is `HEAD` or `GET`, the payload is sent as `?query` parameters in the URL.
67
67
  * - If the method is anything else, the payload is sent in the body (plain text string, `FormData` object, or JSON for any other).
68
68
  *
69
- * @throws ValueError if this is a `HEAD` or `GET` request but `body` is not a dictionary object.
70
- * @throws ValueError if `{placeholders}` are set in the URL but `body` is not a dictionary object.
69
+ * @throws {RequiredError} if this is a `HEAD` or `GET` request but `payload` is not a data object.
70
+ * @throws {RequiredError} if `{placeholders}` are set in the URL but `payload` is not a data object.
71
71
  */
72
- export declare function getRequest(method: RequestHeadMethod, url: URLString, payload: ImmutableDictionary<unknown>, options?: RequestOptions, caller?: AnyCaller): Request;
72
+ export declare function getRequest(method: RequestHeadMethod, url: URLString, payload: Data, options?: RequestOptions, caller?: AnyCaller): Request;
73
73
  export declare function getRequest(method: RequestMethod, url: URLString, payload: unknown, options?: RequestOptions, caller?: AnyCaller): Request;
package/util/http.js CHANGED
@@ -1,9 +1,12 @@
1
1
  import { RequestError } from "../error/RequestError.js";
2
+ import { RequiredError } from "../error/RequiredError.js";
2
3
  import { ResponseError } from "../error/ResponseError.js";
3
- import { assertDictionary } from "./dictionary.js";
4
+ import { isData } from "./data.js";
4
5
  import { isError } from "./error.js";
6
+ import { isNullish } from "./null.js";
7
+ import { omitProps } from "./object.js";
5
8
  import { getPlaceholders, renderTemplate } from "./template.js";
6
- import { omitURIParams, withURIParams } from "./uri.js";
9
+ import { withURIParams } from "./uri.js";
7
10
  export async function _getMessageJSON(message, MessageError, caller) {
8
11
  const trimmed = (await message.text()).trim();
9
12
  if (!trimmed.length)
@@ -115,58 +118,40 @@ export function getErrorResponse(reason, debug = false) {
115
118
  return new Response(undefined, { status });
116
119
  }
117
120
  export function getRequest(method, url, payload, options = {}, caller = getRequest) {
118
- // This is a head request, so ensure the payload is a dictionary object.
119
- if (method === "GET" || method === "HEAD") {
120
- assertDictionary(payload, caller);
121
- return getHeadRequest(method, url, payload, options, caller);
122
- }
123
- // This is a normal body request.
124
- return getBodyRequest(method, url, payload, options, caller);
125
- }
126
- /**
127
- * Create a body-less request to a URL.
128
- * - Any `{placeholders}` in the URL will be rendered with values from `params`, and won't be set in `?query` parameters in the URL.
129
- */
130
- function getHeadRequest(method, url, params, options = {}, caller = getHeadRequest) {
121
+ // Render any `{placeholders}` in the URL string.
131
122
  const placeholders = getPlaceholders(url);
132
- // URL has `{placeholders}` to render, so rendere those to the URL and add all other params as `?query` params.
133
123
  if (placeholders.length) {
134
- const rendered = omitURIParams(withURIParams(renderTemplate(url, params, caller), params, caller), ...placeholders);
135
- return new Request(rendered, { ...options, method });
124
+ if (!isData(payload))
125
+ throw new RequiredError("Payload for request with URL {placeholders} must be data object", { received: payload, caller });
126
+ url = renderTemplate(url, payload, caller);
127
+ payload = omitProps(payload, ...placeholders);
136
128
  }
137
- // URL has no `{placeholders}`, so add all payload params to the URL.
138
- return new Request(withURIParams(url, params, caller), { ...options, method });
139
- }
140
- /**
141
- * Create a body request to a URL.
142
- * - Any `{placeholders}` in the URL will be rendered with values from `data`, and won't be set in the request body.
143
- * - The payload is sent in the body (either as JSON, string, or `FormData`).
144
- *
145
- * @throws ValueError if `{placeholders}` are set in the URL but `body` is not a dictionary object.
146
- */
147
- function getBodyRequest(method, url, body, options = {}, caller = getBodyRequest) {
148
- const placeholders = getPlaceholders(url);
149
- // If `{placeholders}` are set in the URL then body must be a dictionary object and is sent as JSON.
150
- if (placeholders.length) {
151
- assertDictionary(body, caller);
152
- return getJSONRequest(method, renderTemplate(url, body, caller), body, options);
129
+ // This is a body-less request, so ensure the payload is a data object and set the `?query=params` in the URL.
130
+ if (method === "GET" || method === "HEAD") {
131
+ if (!isData(payload))
132
+ throw new RequiredError(`Payload for ${method} request must be data object`, { received: payload, caller });
133
+ url = withURIParams(url, payload).href;
134
+ payload = undefined;
153
135
  }
154
- // `FormData` instances pass through unaltered and will set their own `Content-Type` with complex boundary information.
155
- if (body instanceof FormData)
156
- return getFormDataRequest(method, url, body, options);
157
- if (typeof body === "string")
158
- return getTextRequest(method, url, body, options);
159
- return getJSONRequest(method, url, body, options); // JSON is the default.
160
- }
161
- /** Create a `FormData` request to a URL. */
162
- function getFormDataRequest(method, url, body, options = {}) {
163
- return new Request(url, { ...options, method, body });
164
- }
165
- /** Create a plain text request to a URL. */
166
- function getTextRequest(method, url, body, { headers, ...options } = {}) {
167
- return new Request(url, { ...options, headers: { ...headers, "Content-Type": "text/plain" }, method, body });
168
- }
169
- /** Create a JSON request to a URL. */
170
- function getJSONRequest(method, url, body, { headers, ...options } = {}) {
171
- return new Request(url, { ...options, headers: { ...headers, "Content-Type": "application/json" }, method, body: JSON.stringify(body) });
136
+ // `null` or `undefined` payloads send no body.
137
+ if (isNullish(payload))
138
+ return new Request(url, { ...options, method, body: null });
139
+ // `FormData` instances in body pass through unaltered and will set their own `Content-Type` with complex boundary information
140
+ if (payload instanceof FormData)
141
+ return new Request(url, { ...options, method, body: payload });
142
+ // Strings are sent as plain text.
143
+ if (typeof payload === "string")
144
+ return new Request(url, {
145
+ ...options,
146
+ headers: { ...options.headers, "Content-Type": "text/plain" },
147
+ method,
148
+ body: payload,
149
+ });
150
+ // JSON is the default.
151
+ return new Request(url, {
152
+ ...options,
153
+ headers: { ...options.headers, "Content-Type": "application/json" },
154
+ method,
155
+ body: JSON.stringify(payload),
156
+ });
172
157
  }
package/util/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- export * from "./address.js";
2
1
  export * from "./ansi.js";
3
2
  export * from "./array.js";
4
3
  export * from "./async.js";
@@ -10,7 +9,6 @@ export * from "./callback.js";
10
9
  export * from "./class.js";
11
10
  export * from "./color.js";
12
11
  export * from "./constants.js";
13
- export * from "./country.js";
14
12
  export * from "./crypto.js";
15
13
  export * from "./data.js";
16
14
  export * from "./date.js";
@@ -28,6 +26,7 @@ export * from "./filter.js";
28
26
  export * from "./focus.js";
29
27
  export * from "./format.js";
30
28
  export * from "./function.js";
29
+ export * from "./geo.js";
31
30
  export * from "./hash.js";
32
31
  export * from "./http.js";
33
32
  export * from "./hydrate.js";
package/util/index.js CHANGED
@@ -1,4 +1,3 @@
1
- export * from "./address.js";
2
1
  export * from "./ansi.js";
3
2
  export * from "./array.js";
4
3
  export * from "./async.js";
@@ -10,7 +9,6 @@ export * from "./callback.js";
10
9
  export * from "./class.js";
11
10
  export * from "./color.js";
12
11
  export * from "./constants.js";
13
- export * from "./country.js";
14
12
  export * from "./crypto.js";
15
13
  export * from "./data.js";
16
14
  export * from "./date.js";
@@ -28,6 +26,7 @@ export * from "./filter.js";
28
26
  export * from "./focus.js";
29
27
  export * from "./format.js";
30
28
  export * from "./function.js";
29
+ export * from "./geo.js";
31
30
  export * from "./hash.js";
32
31
  export * from "./http.js";
33
32
  export * from "./hydrate.js";
@@ -13,13 +13,15 @@ import { type NotString, type PossibleString } from "./string.js";
13
13
  export type TemplateValues = PossibleString | ImmutableArray<unknown> | ImmutableDictionary<unknown> | ((placeholder: string) => string);
14
14
  /** The output of matching a template is a dictionary in `{ myPlaceholder: "value" }` format. */
15
15
  export type TemplateMatches = ImmutableDictionary<string>;
16
+ /** List of `{placeholders}` found in a template string. */
17
+ export type TemplatePlaceholders = ImmutableArray<string>;
16
18
  /**
17
19
  * Get list of placeholders named in a template string.
18
20
  *
19
21
  * @param template The template including template placeholders, e.g. `:name-${country}/{city}`
20
22
  * @returns Array of clean string names of found placeholders, e.g. `["name", "country", "city"]`
21
23
  */
22
- export declare function getPlaceholders(template: string): readonly string[];
24
+ export declare function getPlaceholders(template: string): TemplatePlaceholders;
23
25
  /**
24
26
  * Match a template against a target string.
25
27
  * - Turn ":year-:month" and "2016-06..." etc into `{ year: "2016"... }`
package/util/address.d.ts DELETED
@@ -1,12 +0,0 @@
1
- import { type Country } from "./country.js";
2
- /** Valid shape for postal address data. */
3
- export type AddressData = {
4
- readonly address1: string;
5
- readonly address2: string;
6
- readonly city: string;
7
- readonly state: string;
8
- readonly postcode: string;
9
- readonly country: Country;
10
- };
11
- /** Format address lines into a printable string. */
12
- export declare function formatAddress({ address1, address2, city, state, postcode, country }: AddressData): string;
package/util/address.js DELETED
@@ -1,5 +0,0 @@
1
- import { formatCountry } from "./country.js";
2
- /** Format address lines into a printable string. */
3
- export function formatAddress({ address1, address2, city, state, postcode, country }) {
4
- return `${address1}\n${address2 ? `${address2}\n` : ""}${city}\n${state}\n${postcode}\n${formatCountry(country)}`;
5
- }