openapi-typesafe-react-query 1.0.0-rc.6 → 1.0.0-rc.8

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/src/query-key.ts CHANGED
@@ -7,6 +7,17 @@
7
7
  * plain values, and the base URL the request goes to or the client's key scope, so the key names
8
8
  * what was fetched.
9
9
  */
10
+ import { bodyKey } from "./body-key.js";
11
+
12
+ /**
13
+ * Whether a value is an object literal, such as the `params` of an init and its records, which
14
+ * the fetch client reads by their members.
15
+ */
16
+ function isPlainRecord(value: unknown): value is Readonly<Record<string, unknown>> {
17
+ const prototype: unknown =
18
+ typeof value === "object" && value !== null ? Object.getPrototypeOf(value) : undefined;
19
+ return prototype === Object.prototype || prototype === null;
20
+ }
10
21
 
11
22
  /** Headers as a plain object, which TanStack Query hashes by its names and values. */
12
23
  type HeadersRecord = Readonly<Record<string, unknown>>;
@@ -19,23 +30,6 @@ export interface KeyClient {
19
30
  readonly keyScope?: string | undefined;
20
31
  }
21
32
 
22
- /** `[object Name]`: the type tag is between these indexes of `Object.prototype.toString`. */
23
- const TYPE_TAG_START = "[object ".length;
24
- const TYPE_TAG_END = -1;
25
-
26
- /**
27
- * Whether a value is of the class `name`: an instance of this realm's global class of that name
28
- * (React Native's classes have no type tag), or a value with its type tag (a class of another
29
- * realm or of the `undici` package), as the fetch client tells bodies (ADR-030).
30
- */
31
- function isClass(value: object, name: string): boolean {
32
- const type = (globalThis as Record<string, unknown>)[name];
33
- return (
34
- (typeof type === "function" && value instanceof type) ||
35
- Object.prototype.toString.call(value).slice(TYPE_TAG_START, TYPE_TAG_END) === name
36
- );
37
- }
38
-
39
33
  /**
40
34
  * The headers of an init as a plain object. A `Headers` object and a list of pairs, which are both
41
35
  * iterable, become the names and values the request sends: names in lowercase, a repeated one
@@ -57,98 +51,65 @@ function plainHeaders(headers: object): HeadersRecord {
57
51
  }
58
52
 
59
53
  /**
60
- * A Blob or a File in a key: its type, size, and the name of a File. Its bytes cannot be read while
61
- * the component renders, so two Blobs of the same type and size share an entry. The date of a File
62
- * is left out: `FormData.append` and `new File` set it to the current time, so a body built while
63
- * rendering got a new key, and a new request, on every render.
54
+ * The members of an object that are not `undefined`, or undefined when none is. A member set to
55
+ * `undefined` (`headers: token ? … : undefined`, `query: { cursor: undefined }`) sends the same
56
+ * request as none, and TanStack Query's hash leaves it out too, so a record of only such members
57
+ * sends nothing and must not count as one. `null`, `[]` and `{}` members stay: `null` removes a
58
+ * header, and a query serializer of the client may send them.
64
59
  */
65
- function blobKey(blob: Blob): object {
66
- const { name } = blob as Partial<File>;
67
- return { type: blob.type, size: blob.size, name };
68
- }
69
-
70
- /** The fields of a FormData in a key, in order, with a file as `blobKey`. */
71
- function formDataKey(form: FormData): unknown[] {
72
- // React Native's FormData has no `entries`, so its fields cannot be read.
73
- if (typeof form.entries !== "function") {
74
- return [];
75
- }
76
- return Array.from(form.entries(), ([name, value]) => [
77
- name,
78
- typeof value === "string" ? value : blobKey(value),
79
- ]);
80
- }
81
-
82
- /** The bytes of an ArrayBuffer or a typed array, or null for another value. */
83
- function bodyBytes(body: object): Uint8Array | null {
84
- if (ArrayBuffer.isView(body)) {
85
- return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
86
- }
87
- return isClass(body, "ArrayBuffer") ? new Uint8Array(body as ArrayBuffer) : null;
88
- }
89
-
90
- /** A body that JSON cannot hold, as a value TanStack Query hashes, and the name of its kind. */
91
- interface RawBodyKey {
92
- readonly body: unknown;
93
- readonly bodyType: "URLSearchParams" | "FormData" | "Blob" | "bytes";
60
+ function definedMembers(record: object): Record<string, unknown> | undefined {
61
+ const members = Object.entries(record).filter(([, value]) => value !== undefined);
62
+ return members.length === 0 ? undefined : Object.fromEntries(members);
94
63
  }
95
64
 
96
65
  /**
97
- * A body that JSON cannot hold by its content: a URLSearchParams as its text, a FormData as its
98
- * fields, a Blob as `blobKey` and bytes as their numbers. Undefined for any other body.
66
+ * The `params` of an init without the records that send nothing: `{}`, `{ query: {} }` and
67
+ * `{ query: { cursor: undefined } }` send the request of no `params`, since the fetch client reads
68
+ * a missing record as an empty one. Undefined when no record is left.
99
69
  */
100
- function rawBodyKey(body: unknown): RawBodyKey | undefined {
101
- if (typeof body !== "object" || body === null) {
102
- return undefined;
103
- }
104
- if (isClass(body, "URLSearchParams")) {
105
- return { body: String(body), bodyType: "URLSearchParams" };
106
- }
107
- if (isClass(body, "FormData")) {
108
- return { body: formDataKey(body as FormData), bodyType: "FormData" };
109
- }
110
- if (isClass(body, "Blob") || isClass(body, "File")) {
111
- return { body: blobKey(body as Blob), bodyType: "Blob" };
70
+ function paramsKey(params: unknown): unknown {
71
+ if (!isPlainRecord(params)) {
72
+ return params;
112
73
  }
113
- const bytes = bodyBytes(body);
114
- return bytes === null ? undefined : { body: Array.from(bytes), bodyType: "bytes" };
74
+ const records = Object.entries(params).map(([location, record]) => [
75
+ location,
76
+ isPlainRecord(record) ? definedMembers(record) : record,
77
+ ]);
78
+ return definedMembers(Object.fromEntries(records));
115
79
  }
116
80
 
117
- /**
118
- * The init fields that are not `undefined`. A field set to `undefined` (`headers: token ? … :
119
- * undefined`) sends the same request as none, so it must not make the init count as not empty.
120
- */
121
- function definedFields(fields: object): Record<string, unknown> {
122
- return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
81
+ /** The headers of an init as `plainHeaders`, or undefined when they set none. */
82
+ function headersKey(headers: unknown): unknown {
83
+ return typeof headers === "object" && headers !== null
84
+ ? definedMembers(plainHeaders(headers))
85
+ : headers;
123
86
  }
124
87
 
125
88
  /**
126
- * The init of a query key: the request init with its headers and body as plain values, and the
127
- * client's key scope, or its base URL when the init has none. The `signal` is left out: TanStack
128
- * Query gives each request its own. A body that JSON cannot hold also names its kind in
129
- * `bodyType`, a field no init has, so it never shares a key with a JSON body of the same content.
130
- * Undefined when nothing is left, so a call without an init and one with an empty init, or one
131
- * of only `undefined` fields, have the key `[method, path]`.
89
+ * The init of a query key: the request init with its params and headers without what sends
90
+ * nothing, its body as `bodyKey`, and the client's key scope, or its base URL when the init has
91
+ * none. The `signal` is left out: TanStack Query gives each request its own. Undefined when
92
+ * nothing is left, so a call without an init and one with an init that sends nothing more, such
93
+ * as `{}` or `{ params: { query: { cursor: undefined } } }`, have the key `[method, path]`.
132
94
  */
133
95
  export function queryKeyInit(init: unknown, client: KeyClient): object | undefined {
134
96
  const { signal: _signal, ...rest } = (init ?? {}) as {
135
97
  signal?: unknown;
98
+ params?: unknown;
136
99
  headers?: unknown;
137
100
  baseUrl?: unknown;
138
101
  body?: unknown;
139
102
  };
140
- const keyInit = definedFields(rest);
141
- if (typeof rest.headers === "object" && rest.headers !== null) {
142
- keyInit.headers = plainHeaders(rest.headers);
143
- }
144
- const rawBody = rawBodyKey(rest.body);
145
- if (rawBody !== undefined) {
146
- Object.assign(keyInit, rawBody);
147
- }
103
+ const keyInit: Record<string, unknown> = {
104
+ ...rest,
105
+ params: paramsKey(rest.params),
106
+ headers: headersKey(rest.headers),
107
+ ...(rest.body === undefined ? {} : bodyKey(rest.body)),
108
+ };
148
109
  if (client.keyScope !== undefined) {
149
110
  keyInit.keyScope = client.keyScope;
150
111
  } else if (!rest.baseUrl && client.baseUrl) {
151
112
  keyInit.baseUrl = client.baseUrl;
152
113
  }
153
- return Object.keys(keyInit).length === 0 ? undefined : keyInit;
114
+ return definedMembers(keyInit);
154
115
  }