@emeryld/rrroutes-client 2.5.6 → 2.5.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/README.md +18 -2
- package/dist/index.cjs +125 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +125 -19
- package/dist/index.mjs.map +1 -1
- package/dist/routesV3.client.shared.d.ts +6 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -222,7 +222,9 @@ const updateUser = buildRoute('updateUser', {}, { name: 'profile' }) // debug na
|
|
|
222
222
|
|
|
223
223
|
### 8) File uploads (FormData)
|
|
224
224
|
|
|
225
|
-
If a leaf has `bodyFiles` set in its contract, the client automatically converts the body to `FormData
|
|
225
|
+
If a leaf has `bodyFiles` set in its contract, the client automatically converts the body to `FormData`.
|
|
226
|
+
For each declared file field name, pass files using `file${name}` in the input body.
|
|
227
|
+
The raw field name is also accepted for backward compatibility.
|
|
226
228
|
|
|
227
229
|
```ts
|
|
228
230
|
const uploadAvatar = client.build(
|
|
@@ -231,7 +233,21 @@ const uploadAvatar = client.build(
|
|
|
231
233
|
|
|
232
234
|
await uploadAvatar.fetch(
|
|
233
235
|
{ params: { userId: 'u_1' } },
|
|
234
|
-
{
|
|
236
|
+
{
|
|
237
|
+
// bodyFiles: [{ name: 'avatar', maxCount: 1 }]
|
|
238
|
+
fileavatar: new File([blob], 'avatar.png', { type: 'image/png' }),
|
|
239
|
+
// any non-file body fields still go here and are validated by bodySchema
|
|
240
|
+
note: 'profile image',
|
|
241
|
+
},
|
|
242
|
+
)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
For multi-file fields (`maxCount > 1`), pass `Blob[]` or `FileList`:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
await uploadAvatar.fetch(
|
|
249
|
+
{ params: { userId: 'u_1' } },
|
|
250
|
+
{ filephotos: [fileA, fileB] }, // bodyFiles: [{ name: 'photos', maxCount: 5 }]
|
|
235
251
|
)
|
|
236
252
|
```
|
|
237
253
|
|
package/dist/index.cjs
CHANGED
|
@@ -160,25 +160,101 @@ function buildUrl(leaf, baseUrl, params, query) {
|
|
|
160
160
|
const url = `${baseUrl ?? ""}${path}${toSearchString(normalizedQuery)}`;
|
|
161
161
|
return { url, normalizedQuery, normalizedParams };
|
|
162
162
|
}
|
|
163
|
-
function
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
163
|
+
function isBlobLike(value) {
|
|
164
|
+
return typeof Blob !== "undefined" && value instanceof Blob;
|
|
165
|
+
}
|
|
166
|
+
function assertFileFieldValue(sourceKey, targetField, value) {
|
|
167
|
+
if (value == null) return;
|
|
168
|
+
if (isBlobLike(value)) return;
|
|
169
|
+
if (typeof FileList !== "undefined" && value instanceof FileList) {
|
|
170
|
+
for (const item of Array.from(value)) {
|
|
171
|
+
if (!isBlobLike(item)) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`Multipart field "${sourceKey}" must contain Blob/File values for "${targetField}".`
|
|
174
|
+
);
|
|
171
175
|
}
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (Array.isArray(value)) {
|
|
180
|
+
for (const item of value) {
|
|
181
|
+
if (!isBlobLike(item)) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Multipart field "${sourceKey}" must contain Blob/File values for "${targetField}".`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
throw new Error(
|
|
190
|
+
`Multipart field "${sourceKey}" must be Blob/File, Blob[] or FileList.`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
function splitMultipartBody(body, fields) {
|
|
194
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || !fields) {
|
|
195
|
+
return {
|
|
196
|
+
regularBody: body,
|
|
197
|
+
multipartFiles: {}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const fieldByInputKey = /* @__PURE__ */ new Map();
|
|
201
|
+
for (const field of fields) {
|
|
202
|
+
fieldByInputKey.set(`file_${field.name}`, field.name);
|
|
203
|
+
fieldByInputKey.set(field.name, field.name);
|
|
204
|
+
}
|
|
205
|
+
const regularBody = {};
|
|
206
|
+
const multipartFiles = {};
|
|
207
|
+
for (const [key, value] of Object.entries(body)) {
|
|
208
|
+
const resolvedFieldName = fieldByInputKey.get(key);
|
|
209
|
+
if (!resolvedFieldName) {
|
|
210
|
+
regularBody[key] = value;
|
|
172
211
|
continue;
|
|
173
212
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
213
|
+
assertFileFieldValue(key, resolvedFieldName, value);
|
|
214
|
+
multipartFiles[resolvedFieldName] = value;
|
|
215
|
+
}
|
|
216
|
+
return { regularBody, multipartFiles };
|
|
217
|
+
}
|
|
218
|
+
var isReactNativeFile = (v) => v && typeof v === "object" && typeof v.uri === "string" && typeof v.name === "string" && typeof v.type === "string";
|
|
219
|
+
var isFile = (v) => typeof File !== "undefined" && v instanceof File;
|
|
220
|
+
var isBlob = (v) => typeof Blob !== "undefined" && v instanceof Blob;
|
|
221
|
+
function toFormData(body, bodyFiles = []) {
|
|
222
|
+
const fd = new FormData();
|
|
223
|
+
const fileKeys = new Set(
|
|
224
|
+
bodyFiles.flatMap((f) => [f.name, `file_${f.name}`])
|
|
225
|
+
);
|
|
226
|
+
const appendFile = (fieldName, value) => {
|
|
227
|
+
if (value == null) return;
|
|
228
|
+
if (isReactNativeFile(value)) {
|
|
229
|
+
fd.append(fieldName, value);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (isFile(value)) {
|
|
233
|
+
fd.append(fieldName, value, value.name);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (isBlob(value)) {
|
|
237
|
+
fd.append(fieldName, value, "upload");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
throw new Error(`Invalid upload value for ${fieldName}`);
|
|
241
|
+
};
|
|
242
|
+
for (const [key, value] of Object.entries(body)) {
|
|
243
|
+
if (fileKeys.has(key)) {
|
|
244
|
+
if (Array.isArray(value)) {
|
|
245
|
+
for (const v of value) appendFile(key, v);
|
|
246
|
+
} else {
|
|
247
|
+
appendFile(key, value);
|
|
178
248
|
}
|
|
179
249
|
continue;
|
|
180
250
|
}
|
|
181
|
-
fd.append(
|
|
251
|
+
if (typeof value === "string") fd.append(key, value);
|
|
252
|
+
else if (typeof value === "number" || typeof value === "boolean")
|
|
253
|
+
fd.append(key, String(value));
|
|
254
|
+
else if (value == null) {
|
|
255
|
+
} else {
|
|
256
|
+
fd.append(key, JSON.stringify(value));
|
|
257
|
+
}
|
|
182
258
|
}
|
|
183
259
|
return fd;
|
|
184
260
|
}
|
|
@@ -308,9 +384,19 @@ function buildGetLeaf(leaf, rqOpts, env) {
|
|
|
308
384
|
const acceptsBody = Boolean(leafCfg.bodySchema || isMultipart);
|
|
309
385
|
const requiresBody = options?.requireBody ?? (!acceptsBody ? false : true);
|
|
310
386
|
if (typeof options?.body !== "undefined") {
|
|
311
|
-
const
|
|
387
|
+
const { regularBody, multipartFiles } = splitMultipartBody(
|
|
388
|
+
options.body,
|
|
389
|
+
leafCfg.bodyFiles
|
|
390
|
+
);
|
|
391
|
+
const normalizedBody = leafCfg.bodySchema ? (0, import_rrroutes_contract2.lowProfileParse)(leafCfg.bodySchema, regularBody) : regularBody;
|
|
312
392
|
if (isMultipart && normalizedBody && typeof normalizedBody === "object") {
|
|
313
|
-
payload = toFormData(
|
|
393
|
+
payload = toFormData(
|
|
394
|
+
{
|
|
395
|
+
...normalizedBody,
|
|
396
|
+
...multipartFiles
|
|
397
|
+
},
|
|
398
|
+
leafCfg.bodyFiles
|
|
399
|
+
);
|
|
314
400
|
} else {
|
|
315
401
|
payload = normalizedBody;
|
|
316
402
|
}
|
|
@@ -571,9 +657,19 @@ function buildInfiniteGetLeaf(leaf, rqOpts, env) {
|
|
|
571
657
|
const acceptsBody = Boolean(leafCfg.bodySchema || isMultipart);
|
|
572
658
|
const requiresBody = options?.requireBody ?? (!acceptsBody ? false : true);
|
|
573
659
|
if (typeof options?.body !== "undefined") {
|
|
574
|
-
const
|
|
660
|
+
const { regularBody, multipartFiles } = splitMultipartBody(
|
|
661
|
+
options.body,
|
|
662
|
+
leafCfg.bodyFiles
|
|
663
|
+
);
|
|
664
|
+
const normalizedBody = leafCfg.bodySchema ? (0, import_rrroutes_contract3.lowProfileParse)(leafCfg.bodySchema, regularBody) : regularBody;
|
|
575
665
|
if (isMultipart && normalizedBody && typeof normalizedBody === "object") {
|
|
576
|
-
payload = toFormData(
|
|
666
|
+
payload = toFormData(
|
|
667
|
+
{
|
|
668
|
+
...normalizedBody,
|
|
669
|
+
...multipartFiles
|
|
670
|
+
},
|
|
671
|
+
leafCfg.bodyFiles
|
|
672
|
+
);
|
|
577
673
|
} else {
|
|
578
674
|
payload = normalizedBody;
|
|
579
675
|
}
|
|
@@ -869,9 +965,19 @@ function buildMutationLeaf(leaf, rqOpts, env) {
|
|
|
869
965
|
const acceptsBody = Boolean(leafCfg.bodySchema || isMultipart);
|
|
870
966
|
const requiresBody = options?.requireBody ?? (!acceptsBody ? false : true);
|
|
871
967
|
if (typeof options?.body !== "undefined") {
|
|
872
|
-
const
|
|
968
|
+
const { regularBody, multipartFiles } = splitMultipartBody(
|
|
969
|
+
options.body,
|
|
970
|
+
leafCfg.bodyFiles
|
|
971
|
+
);
|
|
972
|
+
const normalizedBody = leafCfg.bodySchema ? (0, import_rrroutes_contract4.lowProfileParse)(leafCfg.bodySchema, regularBody) : regularBody;
|
|
873
973
|
if (isMultipart && normalizedBody && typeof normalizedBody === "object") {
|
|
874
|
-
payload = toFormData(
|
|
974
|
+
payload = toFormData(
|
|
975
|
+
{
|
|
976
|
+
...normalizedBody,
|
|
977
|
+
...multipartFiles
|
|
978
|
+
},
|
|
979
|
+
leafCfg.bodyFiles
|
|
980
|
+
);
|
|
875
981
|
} else {
|
|
876
982
|
payload = normalizedBody;
|
|
877
983
|
}
|