liaise 5.0.1 → 5.0.2
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/CHANGELOG.md +24 -0
- package/MIGRATION.md +10 -0
- package/README.md +3 -1
- package/dist/utils/path-params.d.ts +6 -4
- package/dist/utils/path-params.js +32 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [5.0.2] — 2026-10-04
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **A path parameter with no usable value is refused instead of sent.** A token
|
|
13
|
+
was filled with `String(value)` unchecked, so `getUser({ id: undefined })` on
|
|
14
|
+
`/users/:id` fetched `/users/undefined`, and `null`, `''`, an object or an array
|
|
15
|
+
built `/users/null`, `/users/`, `/users/%5Bobject%20Object%5D` or `/users/1%2C2`.
|
|
16
|
+
The usual cause is a component rendering before the id has loaded. Such a call
|
|
17
|
+
now returns an error Result (`kind: 'network'`, a `TypeError` naming each bad
|
|
18
|
+
param, e.g. `Path parameter "id" is undefined in path "/users/:id", so the call
|
|
19
|
+
was not sent.`) and nothing reaches the server. Accepted values are non-empty
|
|
20
|
+
strings, finite numbers, bigints and booleans; a `Date` is refused with a hint
|
|
21
|
+
to convert it first (`toISOString()` or `getTime()`), as in a query string.
|
|
22
|
+
`NaN` and `Infinity` are refused too. See
|
|
23
|
+
[MIGRATION.md](./MIGRATION.md#upgrading-to-502).
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- The README's size figures are re-measured with `npm run size`: about 5.8 kB
|
|
28
|
+
gzipped for a REST-only import, 6.9 kB for the core entry, 8.0 kB with all
|
|
29
|
+
middleware (the new check and its error messages add about 0.2 kB).
|
|
30
|
+
|
|
8
31
|
## [5.0.1] — 2026-10-03
|
|
9
32
|
|
|
10
33
|
A bug-fix release from an audit of 5.0.0. Nothing in the API changes; a few calls
|
|
@@ -1008,6 +1031,7 @@ Initial release of the rewritten client. Reconstructed from the release commit
|
|
|
1008
1031
|
`ArrayBuffer` and strings
|
|
1009
1032
|
- Response parsing as `json`, `text`, `blob`, `arrayBuffer` or `formData`
|
|
1010
1033
|
|
|
1034
|
+
[5.0.2]: https://github.com/iremlopsum/liaise/compare/v5.0.1...v5.0.2
|
|
1011
1035
|
[5.0.1]: https://github.com/iremlopsum/liaise/compare/v5.0.0...v5.0.1
|
|
1012
1036
|
[5.0.0]: https://github.com/iremlopsum/liaise/compare/v4.4.3...v5.0.0
|
|
1013
1037
|
[4.4.3]: https://github.com/iremlopsum/liaise/compare/v4.4.2...v4.4.3
|
package/MIGRATION.md
CHANGED
|
@@ -7,6 +7,16 @@ For the full record of what changed in each release, see [CHANGELOG.md](./CHANGE
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## Upgrading to 5.0.2
|
|
11
|
+
|
|
12
|
+
No code changes needed. A call whose path parameter is `undefined`, `null`, an
|
|
13
|
+
empty string, an object, an array, a `Date`, a function, a symbol, `NaN` or `Infinity` now returns an
|
|
14
|
+
error Result instead of being sent to a URL like `/users/undefined`. Those calls
|
|
15
|
+
were already hitting the wrong URL; now they say so. If you relied on a literal
|
|
16
|
+
`null` or `undefined` segment, pass the string (`'null'`) explicitly.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
10
20
|
## Upgrading to 5.0.1
|
|
11
21
|
|
|
12
22
|
No code changes for most callers. Six things you may observe.
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Runtime-agnostic, type-safe HTTP client for REST and GraphQL. Built on standard
|
|
|
11
11
|
- **Composable middleware** — retry, cache, dedupe, auth, logging — applied at global, per-endpoint, or per-call level
|
|
12
12
|
- **Types by inference** — declare params and response once on the endpoint definition; types flow to every call site automatically
|
|
13
13
|
- **Runtime-agnostic** — Node.js 20+, browsers, Bun, Deno, Cloudflare Workers, React Native (its built-in `fetch`; not tested in CI) — any environment with `fetch`
|
|
14
|
-
- **Tiny** — about **5.
|
|
14
|
+
- **Tiny** — about **5.8 kB gzipped** for a REST-only import, 6.9 kB for the core entry, 8.0 kB with all middleware (measured by `npm run size`); tree-shaking drops what you do not import
|
|
15
15
|
|
|
16
16
|
```
|
|
17
17
|
npm install liaise
|
|
@@ -135,6 +135,8 @@ const getItem = new Request<{ orgId: string; id: string }, Item>({
|
|
|
135
135
|
await api.getItem({ orgId: 'acme', id: '42' })
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
+
A path parameter must be a non-empty string, a finite number, a bigint or a boolean. Anything else (`undefined`, `null`, `''`, an object, an array, a `Date`, `NaN`) is refused before the request is sent, with an error Result naming the parameter. This catches the common front-end mistake of calling before an id has loaded: `getItem({ orgId: 'acme', id: undefined })` returns an error instead of fetching `/orgs/acme/items/undefined`.
|
|
139
|
+
|
|
138
140
|
#### `responseType`
|
|
139
141
|
|
|
140
142
|
Controls how the response body is parsed. Defaults to `'json'`.
|
|
@@ -56,10 +56,12 @@ export declare class FragmentError extends TypeError {
|
|
|
56
56
|
* This is the main URL construction function used by the request engine.
|
|
57
57
|
* It handles the full lifecycle from path template to final URL.
|
|
58
58
|
*
|
|
59
|
-
* **Path param matching**
|
|
60
|
-
*
|
|
61
|
-
* `:idExtra`.
|
|
62
|
-
*
|
|
59
|
+
* **Path param matching** scans the template once for `:name` tokens
|
|
60
|
+
* (`[a-zA-Z0-9_]+`, greedy), so a param key `id` fills `:id` but never part of
|
|
61
|
+
* `:idExtra`. A token is filled only from a non-empty string, a finite number,
|
|
62
|
+
* a bigint or a boolean; `undefined`, `null`, `''`, objects, arrays and Dates
|
|
63
|
+
* throw a TypeError naming the param, so a call is never sent to
|
|
64
|
+
* '/users/undefined'.
|
|
63
65
|
*
|
|
64
66
|
* **Query string rules** (when `asQuery` is true):
|
|
65
67
|
* - Primitives: `{ page: 1 }` → `?page=1`
|
|
@@ -25,17 +25,44 @@ export class FragmentError extends TypeError {
|
|
|
25
25
|
this.resolvedUrl = resolvedUrl;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
function unusableSegment(value) {
|
|
29
|
+
if (value === undefined)
|
|
30
|
+
return 'undefined';
|
|
31
|
+
if (value === null)
|
|
32
|
+
return 'null';
|
|
33
|
+
if (value === '')
|
|
34
|
+
return 'an empty string';
|
|
35
|
+
if (typeof value === 'number' && !Number.isFinite(value))
|
|
36
|
+
return String(value);
|
|
37
|
+
if (value instanceof Date)
|
|
38
|
+
return 'a Date (convert it first, e.g. date.toISOString() or date.getTime())';
|
|
39
|
+
if (Array.isArray(value))
|
|
40
|
+
return 'an array';
|
|
41
|
+
if (typeof value === 'object')
|
|
42
|
+
return 'an object';
|
|
43
|
+
if (typeof value === 'function' || typeof value === 'symbol')
|
|
44
|
+
return `a ${typeof value}`;
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
28
47
|
export function buildUrl(baseUrl, path, params, asQuery = false) {
|
|
29
48
|
const fragmentIn = path.includes('#') ? { where: 'path', value: path } : baseUrl.includes('#') ? { where: 'baseUrl', value: baseUrl } : null;
|
|
30
49
|
let resolvedPath = path;
|
|
31
50
|
const remaining = {};
|
|
32
51
|
const lookup = new Map(Object.entries(params));
|
|
33
52
|
const consumed = new Set();
|
|
53
|
+
const unusable = [];
|
|
34
54
|
resolvedPath = resolvedPath.replace(/:([a-zA-Z0-9_]+)/g, (token, name) => {
|
|
35
55
|
if (!lookup.has(name))
|
|
36
56
|
return token;
|
|
37
57
|
consumed.add(name);
|
|
38
|
-
|
|
58
|
+
const value = lookup.get(name);
|
|
59
|
+
const problem = unusableSegment(value);
|
|
60
|
+
if (problem) {
|
|
61
|
+
if (!unusable.some(entry => entry.startsWith(`"${name}"`)))
|
|
62
|
+
unusable.push(`"${name}" is ${problem}`);
|
|
63
|
+
return token;
|
|
64
|
+
}
|
|
65
|
+
return encodeURIComponent(String(value));
|
|
39
66
|
});
|
|
40
67
|
for (const [key, value] of lookup) {
|
|
41
68
|
if (!consumed.has(key))
|
|
@@ -46,6 +73,10 @@ export function buildUrl(baseUrl, path, params, asQuery = false) {
|
|
|
46
73
|
throw new FragmentError(`A URL fragment is never sent to the server, so it cannot appear in a ${fragmentIn.where}. ` +
|
|
47
74
|
`Remove "${fragment}" from "${fragmentIn.value}".`, joinUrl(baseUrl, resolvedPath));
|
|
48
75
|
}
|
|
76
|
+
if (unusable.length > 0) {
|
|
77
|
+
throw new TypeError(`Path parameter ${unusable.join(', ')} in path "${path}", so the call was not sent. ` +
|
|
78
|
+
'A path parameter must be a non-empty string, a finite number, a bigint or a boolean.');
|
|
79
|
+
}
|
|
49
80
|
const unresolved = resolvedPath
|
|
50
81
|
.split('/')
|
|
51
82
|
.map(segment => /^:[a-zA-Z0-9_]+/.exec(segment)?.[0])
|