shelving 1.172.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.172.0",
14
+ "version": "1.173.1",
15
15
  "repository": {
16
16
  "type": "git",
17
17
  "url": "git+https://github.com/dhoulb/shelving.git"
@@ -0,0 +1,13 @@
1
+ import type { AddressData } from "../util/geo.js";
2
+ import { DataSchema, type DataSchemaOptions } from "./DataSchema.js";
3
+ /** Allowed options for `AddressSchema` */
4
+ export interface AddressSchemaOptions extends Omit<DataSchemaOptions<AddressData>, "props"> {
5
+ }
6
+ /** Schema that validates a postal address. */
7
+ export declare class AddressSchema extends DataSchema<AddressData> {
8
+ constructor({ one, title, ...options }?: AddressSchemaOptions);
9
+ }
10
+ /** Valid postal address data. */
11
+ export declare const ADDRESS: AddressSchema;
12
+ /** Valid postal address data, or `null` */
13
+ export declare const NULLABLE_ADDRESS: import("./NullableSchema.js").NullableSchema<AddressData>;
@@ -0,0 +1,22 @@
1
+ import { COUNTRY } from "./CountrySchema.js";
2
+ import { DataSchema } from "./DataSchema.js";
3
+ import { NULLABLE } from "./NullableSchema.js";
4
+ import { StringSchema } from "./StringSchema.js";
5
+ const ADDRESS_PROPS = {
6
+ address1: new StringSchema({ title: "Address 1", max: 60, min: 1 }),
7
+ address2: new StringSchema({ title: "Address 2", max: 60, min: 0 }),
8
+ city: new StringSchema({ title: "City", min: 1, max: 60 }),
9
+ state: new StringSchema({ title: "State", min: 1, max: 60 }),
10
+ postcode: new StringSchema({ title: "Postcode", min: 1, max: 12, case: "upper" }),
11
+ country: COUNTRY,
12
+ };
13
+ /** Schema that validates a postal address. */
14
+ export class AddressSchema extends DataSchema {
15
+ constructor({ one = "address", title = "Address", ...options } = {}) {
16
+ super({ one, title, props: ADDRESS_PROPS, ...options });
17
+ }
18
+ }
19
+ /** Valid postal address data. */
20
+ export const ADDRESS = new AddressSchema({});
21
+ /** Valid postal address data, or `null` */
22
+ export const NULLABLE_ADDRESS = NULLABLE(ADDRESS);
@@ -0,0 +1,17 @@
1
+ import { type Country } from "../util/geo.js";
2
+ import { ChoiceSchema, type ChoiceSchemaOptions } from "./ChoiceSchema.js";
3
+ /** Allowed options for `CountrySchema` */
4
+ export interface CountrySchemaOptions extends Omit<ChoiceSchemaOptions<Country>, "options" | "value"> {
5
+ /** Country value, or `"detect"` to resolve from browser language. */
6
+ readonly value?: Country | "detect";
7
+ }
8
+ /** Schema that validates an ISO country code. */
9
+ export declare class CountrySchema extends ChoiceSchema<Country> {
10
+ readonly defaultValue: Country | "detect";
11
+ constructor({ one, title, value, ...options }?: CountrySchemaOptions);
12
+ validate(unsafeValue?: unknown): Country;
13
+ }
14
+ /** Valid country code, e.g. `GB` (required because falsy values are invalid). */
15
+ export declare const COUNTRY: CountrySchema;
16
+ /** Valid country code, e.g. `GB`, or `null` */
17
+ export declare const NULLABLE_COUNTRY: import("./NullableSchema.js").NullableSchema<"AF" | "AX" | "AL" | "DZ" | "AS" | "AD" | "AO" | "AI" | "AQ" | "AG" | "AR" | "AM" | "AW" | "AU" | "AT" | "AZ" | "BS" | "BH" | "BD" | "BB" | "BY" | "BE" | "BZ" | "BJ" | "BM" | "BT" | "BO" | "BA" | "BW" | "BV" | "BR" | "IO" | "BN" | "BG" | "BF" | "BI" | "KH" | "CM" | "CA" | "CV" | "KY" | "CF" | "TD" | "CL" | "CN" | "CX" | "CC" | "CO" | "KM" | "CG" | "CD" | "CK" | "CR" | "CI" | "HR" | "CU" | "CY" | "CZ" | "DK" | "DJ" | "DM" | "DO" | "EC" | "EG" | "SV" | "GQ" | "ER" | "EE" | "ET" | "FK" | "FO" | "FJ" | "FI" | "FR" | "GF" | "PF" | "TF" | "GA" | "GM" | "GE" | "DE" | "GH" | "GI" | "GR" | "GL" | "GD" | "GP" | "GU" | "GT" | "GG" | "GN" | "GW" | "GY" | "HT" | "HM" | "VA" | "HN" | "HK" | "HU" | "IS" | "IN" | "ID" | "IR" | "IQ" | "IE" | "IM" | "IL" | "IT" | "JM" | "JP" | "JE" | "JO" | "KZ" | "KE" | "KI" | "KR" | "KP" | "KW" | "KG" | "LA" | "LV" | "LB" | "LS" | "LR" | "LY" | "LI" | "LT" | "LU" | "MO" | "MK" | "MG" | "MW" | "MY" | "MV" | "ML" | "MT" | "MH" | "MQ" | "MR" | "MU" | "YT" | "MX" | "FM" | "MD" | "MC" | "MN" | "ME" | "MS" | "MA" | "MZ" | "MM" | "NA" | "NR" | "NP" | "NL" | "AN" | "NC" | "NZ" | "NI" | "NE" | "NG" | "NU" | "NF" | "MP" | "NO" | "OM" | "PK" | "PW" | "PS" | "PA" | "PG" | "PY" | "PE" | "PH" | "PN" | "PL" | "PT" | "PR" | "QA" | "RE" | "RO" | "RU" | "RW" | "BL" | "SH" | "KN" | "LC" | "MF" | "PM" | "VC" | "WS" | "SM" | "ST" | "SA" | "SN" | "RS" | "SC" | "SL" | "SG" | "SK" | "SI" | "SB" | "SO" | "ZA" | "GS" | "ES" | "LK" | "SD" | "SR" | "SJ" | "SZ" | "SE" | "CH" | "SY" | "TW" | "TJ" | "TZ" | "TH" | "TL" | "TG" | "TK" | "TO" | "TT" | "TN" | "TR" | "TM" | "TC" | "TV" | "UG" | "UA" | "AE" | "GB" | "US" | "UM" | "UY" | "UZ" | "VU" | "VE" | "VN" | "VG" | "VI" | "WF" | "EH" | "YE" | "ZM" | "ZW">;
@@ -0,0 +1,26 @@
1
+ import { COUNTRIES, getCountry } from "../util/geo.js";
2
+ import { isProp } from "../util/object.js";
3
+ import { ChoiceSchema } from "./ChoiceSchema.js";
4
+ import { NULLABLE } from "./NullableSchema.js";
5
+ /** Schema that validates an ISO country code. */
6
+ export class CountrySchema extends ChoiceSchema {
7
+ defaultValue;
8
+ constructor({ one = "country", title = "Country", value = "detect", ...options } = {}) {
9
+ super({ one, title, options: COUNTRIES, value: value === "detect" ? "GB" : value, ...options });
10
+ this.defaultValue = value;
11
+ }
12
+ validate(unsafeValue = this.defaultValue) {
13
+ const country = getCountry(unsafeValue);
14
+ if (country)
15
+ return super.validate(country);
16
+ if (unsafeValue === undefined || unsafeValue === null || unsafeValue === "" || unsafeValue === "detect")
17
+ throw "Required";
18
+ if (typeof unsafeValue === "string" && isProp(COUNTRIES, unsafeValue.toUpperCase()))
19
+ return super.validate(unsafeValue.toUpperCase());
20
+ return super.validate(unsafeValue);
21
+ }
22
+ }
23
+ /** Valid country code, e.g. `GB` (required because falsy values are invalid). */
24
+ export const COUNTRY = new CountrySchema({});
25
+ /** Valid country code, e.g. `GB`, or `null` */
26
+ export const NULLABLE_COUNTRY = NULLABLE(COUNTRY);
@@ -6,7 +6,6 @@ import type { SchemaOptions, Schemas } from "./Schema.js";
6
6
  import { Schema } from "./Schema.js";
7
7
  /** Allowed options for `PropsSchema` (a schema that has props). */
8
8
  export interface DataSchemaOptions<T extends Data> extends SchemaOptions {
9
- readonly id?: Schema<string>;
10
9
  readonly props: Schemas<T>;
11
10
  readonly value?: Partial<T> | undefined;
12
11
  }
package/schema/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
+ export * from "./AddressSchema.js";
1
2
  export * from "./ArraySchema.js";
2
3
  export * from "./BooleanSchema.js";
3
4
  export * from "./ChoiceSchema.js";
4
5
  export * from "./ColorSchema.js";
6
+ export * from "./CountrySchema.js";
5
7
  export * from "./DataSchema.js";
6
8
  export * from "./DateSchema.js";
7
9
  export * from "./DateTimeSchema.js";
package/schema/index.js CHANGED
@@ -1,7 +1,9 @@
1
+ export * from "./AddressSchema.js";
1
2
  export * from "./ArraySchema.js";
2
3
  export * from "./BooleanSchema.js";
3
4
  export * from "./ChoiceSchema.js";
4
5
  export * from "./ColorSchema.js";
6
+ export * from "./CountrySchema.js";
5
7
  export * from "./DataSchema.js";
6
8
  export * from "./DateSchema.js";
7
9
  export * from "./DateTimeSchema.js";
package/util/geo.d.ts ADDED
@@ -0,0 +1,271 @@
1
+ import type { AnyCaller } from "./function.js";
2
+ /** List of countries by two-letter ISO code. */
3
+ export declare const COUNTRIES: {
4
+ readonly AF: "Afghanistan";
5
+ readonly AX: "Aland Islands";
6
+ readonly AL: "Albania";
7
+ readonly DZ: "Algeria";
8
+ readonly AS: "American Samoa";
9
+ readonly AD: "Andorra";
10
+ readonly AO: "Angola";
11
+ readonly AI: "Anguilla";
12
+ readonly AQ: "Antarctica";
13
+ readonly AG: "Antigua and Barbuda";
14
+ readonly AR: "Argentina";
15
+ readonly AM: "Armenia";
16
+ readonly AW: "Aruba";
17
+ readonly AU: "Australia";
18
+ readonly AT: "Austria";
19
+ readonly AZ: "Azerbaijan";
20
+ readonly BS: "Bahamas";
21
+ readonly BH: "Bahrain";
22
+ readonly BD: "Bangladesh";
23
+ readonly BB: "Barbados";
24
+ readonly BY: "Belarus";
25
+ readonly BE: "Belgium";
26
+ readonly BZ: "Belize";
27
+ readonly BJ: "Benin";
28
+ readonly BM: "Bermuda";
29
+ readonly BT: "Bhutan";
30
+ readonly BO: "Bolivia";
31
+ readonly BA: "Bosnia and Herzegovina";
32
+ readonly BW: "Botswana";
33
+ readonly BV: "Bouvet Island";
34
+ readonly BR: "Brazil";
35
+ readonly IO: "British Indian Ocean Territory";
36
+ readonly BN: "Brunei Darussalam";
37
+ readonly BG: "Bulgaria";
38
+ readonly BF: "Burkina Faso";
39
+ readonly BI: "Burundi";
40
+ readonly KH: "Cambodia";
41
+ readonly CM: "Cameroon";
42
+ readonly CA: "Canada";
43
+ readonly CV: "Cape Verde";
44
+ readonly KY: "Cayman Islands";
45
+ readonly CF: "Central African Republic";
46
+ readonly TD: "Chad";
47
+ readonly CL: "Chile";
48
+ readonly CN: "China";
49
+ readonly CX: "Christmas Island";
50
+ readonly CC: "Cocos (Keeling) Islands";
51
+ readonly CO: "Colombia";
52
+ readonly KM: "Comoros";
53
+ readonly CG: "Congo";
54
+ readonly CD: "Congo, Democratic Republic";
55
+ readonly CK: "Cook Islands";
56
+ readonly CR: "Costa Rica";
57
+ readonly CI: "Cote D'Ivoire";
58
+ readonly HR: "Croatia";
59
+ readonly CU: "Cuba";
60
+ readonly CY: "Cyprus";
61
+ readonly CZ: "Czech Republic";
62
+ readonly DK: "Denmark";
63
+ readonly DJ: "Djibouti";
64
+ readonly DM: "Dominica";
65
+ readonly DO: "Dominican Republic";
66
+ readonly EC: "Ecuador";
67
+ readonly EG: "Egypt";
68
+ readonly SV: "El Salvador";
69
+ readonly GQ: "Equatorial Guinea";
70
+ readonly ER: "Eritrea";
71
+ readonly EE: "Estonia";
72
+ readonly ET: "Ethiopia";
73
+ readonly FK: "Falkland Islands";
74
+ readonly FO: "Faroe Islands";
75
+ readonly FJ: "Fiji";
76
+ readonly FI: "Finland";
77
+ readonly FR: "France";
78
+ readonly GF: "French Guiana";
79
+ readonly PF: "French Polynesia";
80
+ readonly TF: "French Southern Territories";
81
+ readonly GA: "Gabon";
82
+ readonly GM: "Gambia";
83
+ readonly GE: "Georgia";
84
+ readonly DE: "Germany";
85
+ readonly GH: "Ghana";
86
+ readonly GI: "Gibraltar";
87
+ readonly GR: "Greece";
88
+ readonly GL: "Greenland";
89
+ readonly GD: "Grenada";
90
+ readonly GP: "Guadeloupe";
91
+ readonly GU: "Guam";
92
+ readonly GT: "Guatemala";
93
+ readonly GG: "Guernsey";
94
+ readonly GN: "Guinea";
95
+ readonly GW: "Guinea-Bissau";
96
+ readonly GY: "Guyana";
97
+ readonly HT: "Haiti";
98
+ readonly HM: "Heard Island & Mcdonald Islands";
99
+ readonly VA: "Holy See (Vatican City State)";
100
+ readonly HN: "Honduras";
101
+ readonly HK: "Hong Kong";
102
+ readonly HU: "Hungary";
103
+ readonly IS: "Iceland";
104
+ readonly IN: "India";
105
+ readonly ID: "Indonesia";
106
+ readonly IR: "Iran, Islamic Republic Of";
107
+ readonly IQ: "Iraq";
108
+ readonly IE: "Ireland";
109
+ readonly IM: "Isle of Man";
110
+ readonly IL: "Israel";
111
+ readonly IT: "Italy";
112
+ readonly JM: "Jamaica";
113
+ readonly JP: "Japan";
114
+ readonly JE: "Jersey";
115
+ readonly JO: "Jordan";
116
+ readonly KZ: "Kazakhstan";
117
+ readonly KE: "Kenya";
118
+ readonly KI: "Kiribati";
119
+ readonly KR: "Korea";
120
+ readonly KP: "North Korea";
121
+ readonly KW: "Kuwait";
122
+ readonly KG: "Kyrgyzstan";
123
+ readonly LA: "Lao People's Democratic Republic";
124
+ readonly LV: "Latvia";
125
+ readonly LB: "Lebanon";
126
+ readonly LS: "Lesotho";
127
+ readonly LR: "Liberia";
128
+ readonly LY: "Libyan Arab Jamahiriya";
129
+ readonly LI: "Liechtenstein";
130
+ readonly LT: "Lithuania";
131
+ readonly LU: "Luxembourg";
132
+ readonly MO: "Macao";
133
+ readonly MK: "Macedonia";
134
+ readonly MG: "Madagascar";
135
+ readonly MW: "Malawi";
136
+ readonly MY: "Malaysia";
137
+ readonly MV: "Maldives";
138
+ readonly ML: "Mali";
139
+ readonly MT: "Malta";
140
+ readonly MH: "Marshall Islands";
141
+ readonly MQ: "Martinique";
142
+ readonly MR: "Mauritania";
143
+ readonly MU: "Mauritius";
144
+ readonly YT: "Mayotte";
145
+ readonly MX: "Mexico";
146
+ readonly FM: "Micronesia, Federated States Of";
147
+ readonly MD: "Moldova";
148
+ readonly MC: "Monaco";
149
+ readonly MN: "Mongolia";
150
+ readonly ME: "Montenegro";
151
+ readonly MS: "Montserrat";
152
+ readonly MA: "Morocco";
153
+ readonly MZ: "Mozambique";
154
+ readonly MM: "Myanmar";
155
+ readonly NA: "Namibia";
156
+ readonly NR: "Nauru";
157
+ readonly NP: "Nepal";
158
+ readonly NL: "Netherlands";
159
+ readonly AN: "Netherlands Antilles";
160
+ readonly NC: "New Caledonia";
161
+ readonly NZ: "New Zealand";
162
+ readonly NI: "Nicaragua";
163
+ readonly NE: "Niger";
164
+ readonly NG: "Nigeria";
165
+ readonly NU: "Niue";
166
+ readonly NF: "Norfolk Island";
167
+ readonly MP: "Northern Mariana Islands";
168
+ readonly NO: "Norway";
169
+ readonly OM: "Oman";
170
+ readonly PK: "Pakistan";
171
+ readonly PW: "Palau";
172
+ readonly PS: "Palestinian Territory, Occupied";
173
+ readonly PA: "Panama";
174
+ readonly PG: "Papua New Guinea";
175
+ readonly PY: "Paraguay";
176
+ readonly PE: "Peru";
177
+ readonly PH: "Philippines";
178
+ readonly PN: "Pitcairn";
179
+ readonly PL: "Poland";
180
+ readonly PT: "Portugal";
181
+ readonly PR: "Puerto Rico";
182
+ readonly QA: "Qatar";
183
+ readonly RE: "Reunion";
184
+ readonly RO: "Romania";
185
+ readonly RU: "Russian Federation";
186
+ readonly RW: "Rwanda";
187
+ readonly BL: "Saint Barthelemy";
188
+ readonly SH: "Saint Helena";
189
+ readonly KN: "Saint Kitts and Nevis";
190
+ readonly LC: "Saint Lucia";
191
+ readonly MF: "Saint Martin";
192
+ readonly PM: "Saint Pierre and Miquelon";
193
+ readonly VC: "Saint Vincent and Grenadines";
194
+ readonly WS: "Samoa";
195
+ readonly SM: "San Marino";
196
+ readonly ST: "Sao Tome and Principe";
197
+ readonly SA: "Saudi Arabia";
198
+ readonly SN: "Senegal";
199
+ readonly RS: "Serbia";
200
+ readonly SC: "Seychelles";
201
+ readonly SL: "Sierra Leone";
202
+ readonly SG: "Singapore";
203
+ readonly SK: "Slovakia";
204
+ readonly SI: "Slovenia";
205
+ readonly SB: "Solomon Islands";
206
+ readonly SO: "Somalia";
207
+ readonly ZA: "South Africa";
208
+ readonly GS: "South Georgia and Sandwich Isl.";
209
+ readonly ES: "Spain";
210
+ readonly LK: "Sri Lanka";
211
+ readonly SD: "Sudan";
212
+ readonly SR: "Suriname";
213
+ readonly SJ: "Svalbard and Jan Mayen";
214
+ readonly SZ: "Swaziland";
215
+ readonly SE: "Sweden";
216
+ readonly CH: "Switzerland";
217
+ readonly SY: "Syrian Arab Republic";
218
+ readonly TW: "Taiwan";
219
+ readonly TJ: "Tajikistan";
220
+ readonly TZ: "Tanzania";
221
+ readonly TH: "Thailand";
222
+ readonly TL: "Timor-Leste";
223
+ readonly TG: "Togo";
224
+ readonly TK: "Tokelau";
225
+ readonly TO: "Tonga";
226
+ readonly TT: "Trinidad and Tobago";
227
+ readonly TN: "Tunisia";
228
+ readonly TR: "Turkey";
229
+ readonly TM: "Turkmenistan";
230
+ readonly TC: "Turks and Caicos Islands";
231
+ readonly TV: "Tuvalu";
232
+ readonly UG: "Uganda";
233
+ readonly UA: "Ukraine";
234
+ readonly AE: "United Arab Emirates";
235
+ readonly GB: "United Kingdom";
236
+ readonly US: "United States";
237
+ readonly UM: "United States Outlying Islands";
238
+ readonly UY: "Uruguay";
239
+ readonly UZ: "Uzbekistan";
240
+ readonly VU: "Vanuatu";
241
+ readonly VE: "Venezuela";
242
+ readonly VN: "Vietnam";
243
+ readonly VG: "Virgin Islands, British";
244
+ readonly VI: "Virgin Islands, U.S.";
245
+ readonly WF: "Wallis and Futuna";
246
+ readonly EH: "Western Sahara";
247
+ readonly YE: "Yemen";
248
+ readonly ZM: "Zambia";
249
+ readonly ZW: "Zimbabwe";
250
+ };
251
+ /** Country code string. */
252
+ export type Country = keyof typeof COUNTRIES;
253
+ /** Things that can possibly be a country. */
254
+ export type PossibleCountry = Country | "detect";
255
+ /** Parse a country string, or detect a browser country from `navigator.language`. */
256
+ export declare function getCountry(value?: unknown): Country | undefined;
257
+ /** Parse a country string, or detect a browser country from `navigator.language`, or throw `RequiredError` */
258
+ export declare function requireCountry(value?: unknown, caller?: AnyCaller): Country;
259
+ /** Format a country code into its full country name. */
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;
package/util/geo.js ADDED
@@ -0,0 +1,282 @@
1
+ import { RequiredError } from "../error/RequiredError.js";
2
+ import { isProp } from "./object.js";
3
+ /** List of countries by two-letter ISO code. */
4
+ export const COUNTRIES = {
5
+ AF: "Afghanistan",
6
+ AX: "Aland Islands",
7
+ AL: "Albania",
8
+ DZ: "Algeria",
9
+ AS: "American Samoa",
10
+ AD: "Andorra",
11
+ AO: "Angola",
12
+ AI: "Anguilla",
13
+ AQ: "Antarctica",
14
+ AG: "Antigua and Barbuda",
15
+ AR: "Argentina",
16
+ AM: "Armenia",
17
+ AW: "Aruba",
18
+ AU: "Australia",
19
+ AT: "Austria",
20
+ AZ: "Azerbaijan",
21
+ BS: "Bahamas",
22
+ BH: "Bahrain",
23
+ BD: "Bangladesh",
24
+ BB: "Barbados",
25
+ BY: "Belarus",
26
+ BE: "Belgium",
27
+ BZ: "Belize",
28
+ BJ: "Benin",
29
+ BM: "Bermuda",
30
+ BT: "Bhutan",
31
+ BO: "Bolivia",
32
+ BA: "Bosnia and Herzegovina",
33
+ BW: "Botswana",
34
+ BV: "Bouvet Island",
35
+ BR: "Brazil",
36
+ IO: "British Indian Ocean Territory",
37
+ BN: "Brunei Darussalam",
38
+ BG: "Bulgaria",
39
+ BF: "Burkina Faso",
40
+ BI: "Burundi",
41
+ KH: "Cambodia",
42
+ CM: "Cameroon",
43
+ CA: "Canada",
44
+ CV: "Cape Verde",
45
+ KY: "Cayman Islands",
46
+ CF: "Central African Republic",
47
+ TD: "Chad",
48
+ CL: "Chile",
49
+ CN: "China",
50
+ CX: "Christmas Island",
51
+ CC: "Cocos (Keeling) Islands",
52
+ CO: "Colombia",
53
+ KM: "Comoros",
54
+ CG: "Congo",
55
+ CD: "Congo, Democratic Republic",
56
+ CK: "Cook Islands",
57
+ CR: "Costa Rica",
58
+ CI: "Cote D'Ivoire",
59
+ HR: "Croatia",
60
+ CU: "Cuba",
61
+ CY: "Cyprus",
62
+ CZ: "Czech Republic",
63
+ DK: "Denmark",
64
+ DJ: "Djibouti",
65
+ DM: "Dominica",
66
+ DO: "Dominican Republic",
67
+ EC: "Ecuador",
68
+ EG: "Egypt",
69
+ SV: "El Salvador",
70
+ GQ: "Equatorial Guinea",
71
+ ER: "Eritrea",
72
+ EE: "Estonia",
73
+ ET: "Ethiopia",
74
+ FK: "Falkland Islands",
75
+ FO: "Faroe Islands",
76
+ FJ: "Fiji",
77
+ FI: "Finland",
78
+ FR: "France",
79
+ GF: "French Guiana",
80
+ PF: "French Polynesia",
81
+ TF: "French Southern Territories",
82
+ GA: "Gabon",
83
+ GM: "Gambia",
84
+ GE: "Georgia",
85
+ DE: "Germany",
86
+ GH: "Ghana",
87
+ GI: "Gibraltar",
88
+ GR: "Greece",
89
+ GL: "Greenland",
90
+ GD: "Grenada",
91
+ GP: "Guadeloupe",
92
+ GU: "Guam",
93
+ GT: "Guatemala",
94
+ GG: "Guernsey",
95
+ GN: "Guinea",
96
+ GW: "Guinea-Bissau",
97
+ GY: "Guyana",
98
+ HT: "Haiti",
99
+ HM: "Heard Island & Mcdonald Islands",
100
+ VA: "Holy See (Vatican City State)",
101
+ HN: "Honduras",
102
+ HK: "Hong Kong",
103
+ HU: "Hungary",
104
+ IS: "Iceland",
105
+ IN: "India",
106
+ ID: "Indonesia",
107
+ IR: "Iran, Islamic Republic Of",
108
+ IQ: "Iraq",
109
+ IE: "Ireland",
110
+ IM: "Isle of Man",
111
+ IL: "Israel",
112
+ IT: "Italy",
113
+ JM: "Jamaica",
114
+ JP: "Japan",
115
+ JE: "Jersey",
116
+ JO: "Jordan",
117
+ KZ: "Kazakhstan",
118
+ KE: "Kenya",
119
+ KI: "Kiribati",
120
+ KR: "Korea",
121
+ KP: "North Korea",
122
+ KW: "Kuwait",
123
+ KG: "Kyrgyzstan",
124
+ LA: "Lao People's Democratic Republic",
125
+ LV: "Latvia",
126
+ LB: "Lebanon",
127
+ LS: "Lesotho",
128
+ LR: "Liberia",
129
+ LY: "Libyan Arab Jamahiriya",
130
+ LI: "Liechtenstein",
131
+ LT: "Lithuania",
132
+ LU: "Luxembourg",
133
+ MO: "Macao",
134
+ MK: "Macedonia",
135
+ MG: "Madagascar",
136
+ MW: "Malawi",
137
+ MY: "Malaysia",
138
+ MV: "Maldives",
139
+ ML: "Mali",
140
+ MT: "Malta",
141
+ MH: "Marshall Islands",
142
+ MQ: "Martinique",
143
+ MR: "Mauritania",
144
+ MU: "Mauritius",
145
+ YT: "Mayotte",
146
+ MX: "Mexico",
147
+ FM: "Micronesia, Federated States Of",
148
+ MD: "Moldova",
149
+ MC: "Monaco",
150
+ MN: "Mongolia",
151
+ ME: "Montenegro",
152
+ MS: "Montserrat",
153
+ MA: "Morocco",
154
+ MZ: "Mozambique",
155
+ MM: "Myanmar",
156
+ NA: "Namibia",
157
+ NR: "Nauru",
158
+ NP: "Nepal",
159
+ NL: "Netherlands",
160
+ AN: "Netherlands Antilles",
161
+ NC: "New Caledonia",
162
+ NZ: "New Zealand",
163
+ NI: "Nicaragua",
164
+ NE: "Niger",
165
+ NG: "Nigeria",
166
+ NU: "Niue",
167
+ NF: "Norfolk Island",
168
+ MP: "Northern Mariana Islands",
169
+ NO: "Norway",
170
+ OM: "Oman",
171
+ PK: "Pakistan",
172
+ PW: "Palau",
173
+ PS: "Palestinian Territory, Occupied",
174
+ PA: "Panama",
175
+ PG: "Papua New Guinea",
176
+ PY: "Paraguay",
177
+ PE: "Peru",
178
+ PH: "Philippines",
179
+ PN: "Pitcairn",
180
+ PL: "Poland",
181
+ PT: "Portugal",
182
+ PR: "Puerto Rico",
183
+ QA: "Qatar",
184
+ RE: "Reunion",
185
+ RO: "Romania",
186
+ RU: "Russian Federation",
187
+ RW: "Rwanda",
188
+ BL: "Saint Barthelemy",
189
+ SH: "Saint Helena",
190
+ KN: "Saint Kitts and Nevis",
191
+ LC: "Saint Lucia",
192
+ MF: "Saint Martin",
193
+ PM: "Saint Pierre and Miquelon",
194
+ VC: "Saint Vincent and Grenadines",
195
+ WS: "Samoa",
196
+ SM: "San Marino",
197
+ ST: "Sao Tome and Principe",
198
+ SA: "Saudi Arabia",
199
+ SN: "Senegal",
200
+ RS: "Serbia",
201
+ SC: "Seychelles",
202
+ SL: "Sierra Leone",
203
+ SG: "Singapore",
204
+ SK: "Slovakia",
205
+ SI: "Slovenia",
206
+ SB: "Solomon Islands",
207
+ SO: "Somalia",
208
+ ZA: "South Africa",
209
+ GS: "South Georgia and Sandwich Isl.",
210
+ ES: "Spain",
211
+ LK: "Sri Lanka",
212
+ SD: "Sudan",
213
+ SR: "Suriname",
214
+ SJ: "Svalbard and Jan Mayen",
215
+ SZ: "Swaziland",
216
+ SE: "Sweden",
217
+ CH: "Switzerland",
218
+ SY: "Syrian Arab Republic",
219
+ TW: "Taiwan",
220
+ TJ: "Tajikistan",
221
+ TZ: "Tanzania",
222
+ TH: "Thailand",
223
+ TL: "Timor-Leste",
224
+ TG: "Togo",
225
+ TK: "Tokelau",
226
+ TO: "Tonga",
227
+ TT: "Trinidad and Tobago",
228
+ TN: "Tunisia",
229
+ TR: "Turkey",
230
+ TM: "Turkmenistan",
231
+ TC: "Turks and Caicos Islands",
232
+ TV: "Tuvalu",
233
+ UG: "Uganda",
234
+ UA: "Ukraine",
235
+ AE: "United Arab Emirates",
236
+ GB: "United Kingdom",
237
+ US: "United States",
238
+ UM: "United States Outlying Islands",
239
+ UY: "Uruguay",
240
+ UZ: "Uzbekistan",
241
+ VU: "Vanuatu",
242
+ VE: "Venezuela",
243
+ VN: "Vietnam",
244
+ VG: "Virgin Islands, British",
245
+ VI: "Virgin Islands, U.S.",
246
+ WF: "Wallis and Futuna",
247
+ EH: "Western Sahara",
248
+ YE: "Yemen",
249
+ ZM: "Zambia",
250
+ ZW: "Zimbabwe",
251
+ };
252
+ /** Parse a country string, or detect a browser country from `navigator.language`. */
253
+ export function getCountry(value = "detect") {
254
+ if (value === "detect") {
255
+ if (typeof navigator === "object") {
256
+ const code = navigator?.language?.slice(-2).toUpperCase();
257
+ if (code && isProp(COUNTRIES, code))
258
+ return code;
259
+ }
260
+ }
261
+ else if (typeof value === "string") {
262
+ const code = value.toUpperCase();
263
+ if (code && isProp(COUNTRIES, code))
264
+ return code;
265
+ }
266
+ }
267
+ /** Parse a country string, or detect a browser country from `navigator.language`, or throw `RequiredError` */
268
+ export function requireCountry(value, caller = requireCountry) {
269
+ const country = getCountry(value);
270
+ if (!country)
271
+ throw new RequiredError("Must be country", { received: value, caller });
272
+ return country;
273
+ }
274
+ /** Format a country code into its full country name. */
275
+ export function formatCountry(country) {
276
+ const code = country.toUpperCase();
277
+ return isProp(COUNTRIES, code) ? COUNTRIES[code] : country;
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
@@ -26,6 +26,7 @@ export * from "./filter.js";
26
26
  export * from "./focus.js";
27
27
  export * from "./format.js";
28
28
  export * from "./function.js";
29
+ export * from "./geo.js";
29
30
  export * from "./hash.js";
30
31
  export * from "./http.js";
31
32
  export * from "./hydrate.js";
package/util/index.js CHANGED
@@ -26,6 +26,7 @@ export * from "./filter.js";
26
26
  export * from "./focus.js";
27
27
  export * from "./format.js";
28
28
  export * from "./function.js";
29
+ export * from "./geo.js";
29
30
  export * from "./hash.js";
30
31
  export * from "./http.js";
31
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"... }`