Skip to content

Commit 99a5c3d

Browse files
committed
added support for assigning FormData as body to individual batch requests
1 parent 53bf1f4 commit 99a5c3d

20 files changed

+194
-45
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.24.0
2+
3+
- Added support for assigning `FormData` as body to individual batch requests ([pocketbase#6145](https://github.com/pocketbase/pocketbase/discussions/6145)).
4+
5+
16
## 0.23.0
27

38
- Added optional `pb.realtime.onDisconnect` hook function.

dist/pocketbase.cjs.d.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -1115,8 +1115,6 @@ declare class BatchService extends BaseService {
11151115
/**
11161116
* Sends the batch requests.
11171117
*
1118-
* Note: FormData as individual request body is not supported at the moment.
1119-
*
11201118
* @throws {ClientResponseError}
11211119
*/
11221120
send(options?: SendOptions): Promise<Array<BatchRequestResult>>;
@@ -1132,19 +1130,19 @@ declare class SubBatchService {
11321130
*/
11331131
upsert(bodyParams?: {
11341132
[key: string]: any;
1135-
}, options?: RecordOptions): void;
1133+
} | FormData, options?: RecordOptions): void;
11361134
/**
11371135
* Registers a record create request into the current batch queue.
11381136
*/
11391137
create(bodyParams?: {
11401138
[key: string]: any;
1141-
}, options?: RecordOptions): void;
1139+
} | FormData, options?: RecordOptions): void;
11421140
/**
11431141
* Registers a record update request into the current batch queue.
11441142
*/
11451143
update(id: string, bodyParams?: {
11461144
[key: string]: any;
1147-
}, options?: RecordOptions): void;
1145+
} | FormData, options?: RecordOptions): void;
11481146
/**
11491147
* Registers a record delete request into the current batch queue.
11501148
*/

dist/pocketbase.cjs.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/pocketbase.cjs.js.map

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/pocketbase.es.d.mts

+3-5
Original file line numberDiff line numberDiff line change
@@ -1158,8 +1158,6 @@ declare class BatchService extends BaseService {
11581158
/**
11591159
* Sends the batch requests.
11601160
*
1161-
* Note: FormData as individual request body is not supported at the moment.
1162-
*
11631161
* @throws {ClientResponseError}
11641162
*/
11651163
send(options?: SendOptions): Promise<Array<BatchRequestResult>>;
@@ -1175,19 +1173,19 @@ declare class SubBatchService {
11751173
*/
11761174
upsert(bodyParams?: {
11771175
[key: string]: any;
1178-
}, options?: RecordOptions): void;
1176+
} | FormData, options?: RecordOptions): void;
11791177
/**
11801178
* Registers a record create request into the current batch queue.
11811179
*/
11821180
create(bodyParams?: {
11831181
[key: string]: any;
1184-
}, options?: RecordOptions): void;
1182+
} | FormData, options?: RecordOptions): void;
11851183
/**
11861184
* Registers a record update request into the current batch queue.
11871185
*/
11881186
update(id: string, bodyParams?: {
11891187
[key: string]: any;
1190-
}, options?: RecordOptions): void;
1188+
} | FormData, options?: RecordOptions): void;
11911189
/**
11921190
* Registers a record delete request into the current batch queue.
11931191
*/

dist/pocketbase.es.d.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -1158,8 +1158,6 @@ declare class BatchService extends BaseService {
11581158
/**
11591159
* Sends the batch requests.
11601160
*
1161-
* Note: FormData as individual request body is not supported at the moment.
1162-
*
11631161
* @throws {ClientResponseError}
11641162
*/
11651163
send(options?: SendOptions): Promise<Array<BatchRequestResult>>;
@@ -1175,19 +1173,19 @@ declare class SubBatchService {
11751173
*/
11761174
upsert(bodyParams?: {
11771175
[key: string]: any;
1178-
}, options?: RecordOptions): void;
1176+
} | FormData, options?: RecordOptions): void;
11791177
/**
11801178
* Registers a record create request into the current batch queue.
11811179
*/
11821180
create(bodyParams?: {
11831181
[key: string]: any;
1184-
}, options?: RecordOptions): void;
1182+
} | FormData, options?: RecordOptions): void;
11851183
/**
11861184
* Registers a record update request into the current batch queue.
11871185
*/
11881186
update(id: string, bodyParams?: {
11891187
[key: string]: any;
1190-
}, options?: RecordOptions): void;
1188+
} | FormData, options?: RecordOptions): void;
11911189
/**
11921190
* Registers a record delete request into the current batch queue.
11931191
*/

dist/pocketbase.es.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/pocketbase.es.js.map

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/pocketbase.es.mjs

+1-1
Large diffs are not rendered by default.

dist/pocketbase.es.mjs.map

+1-1
Large diffs are not rendered by default.

dist/pocketbase.iife.d.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -1115,8 +1115,6 @@ declare class BatchService extends BaseService {
11151115
/**
11161116
* Sends the batch requests.
11171117
*
1118-
* Note: FormData as individual request body is not supported at the moment.
1119-
*
11201118
* @throws {ClientResponseError}
11211119
*/
11221120
send(options?: SendOptions): Promise<Array<BatchRequestResult>>;
@@ -1132,19 +1130,19 @@ declare class SubBatchService {
11321130
*/
11331131
upsert(bodyParams?: {
11341132
[key: string]: any;
1135-
}, options?: RecordOptions): void;
1133+
} | FormData, options?: RecordOptions): void;
11361134
/**
11371135
* Registers a record create request into the current batch queue.
11381136
*/
11391137
create(bodyParams?: {
11401138
[key: string]: any;
1141-
}, options?: RecordOptions): void;
1139+
} | FormData, options?: RecordOptions): void;
11421140
/**
11431141
* Registers a record update request into the current batch queue.
11441142
*/
11451143
update(id: string, bodyParams?: {
11461144
[key: string]: any;
1147-
}, options?: RecordOptions): void;
1145+
} | FormData, options?: RecordOptions): void;
11481146
/**
11491147
* Registers a record delete request into the current batch queue.
11501148
*/

dist/pocketbase.iife.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/pocketbase.iife.js.map

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/pocketbase.umd.d.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -1115,8 +1115,6 @@ declare class BatchService extends BaseService {
11151115
/**
11161116
* Sends the batch requests.
11171117
*
1118-
* Note: FormData as individual request body is not supported at the moment.
1119-
*
11201118
* @throws {ClientResponseError}
11211119
*/
11221120
send(options?: SendOptions): Promise<Array<BatchRequestResult>>;
@@ -1132,19 +1130,19 @@ declare class SubBatchService {
11321130
*/
11331131
upsert(bodyParams?: {
11341132
[key: string]: any;
1135-
}, options?: RecordOptions): void;
1133+
} | FormData, options?: RecordOptions): void;
11361134
/**
11371135
* Registers a record create request into the current batch queue.
11381136
*/
11391137
create(bodyParams?: {
11401138
[key: string]: any;
1141-
}, options?: RecordOptions): void;
1139+
} | FormData, options?: RecordOptions): void;
11421140
/**
11431141
* Registers a record update request into the current batch queue.
11441142
*/
11451143
update(id: string, bodyParams?: {
11461144
[key: string]: any;
1147-
}, options?: RecordOptions): void;
1145+
} | FormData, options?: RecordOptions): void;
11481146
/**
11491147
* Registers a record delete request into the current batch queue.
11501148
*/

dist/pocketbase.umd.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/pocketbase.umd.js.map

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "0.23.0",
2+
"version": "0.24.0",
33
"name": "pocketbase",
44
"description": "PocketBase JavaScript SDK",
55
"author": "Gani Georgiev",

src/services/BatchService.ts

+11-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { BaseService } from "@/services/BaseService";
2-
import { isFile } from "@/tools/formdata";
2+
import { isFile, isFormData, convertFormDataToObject } from "@/tools/formdata";
33
import {
44
SendOptions,
55
RecordOptions,
@@ -41,8 +41,6 @@ export class BatchService extends BaseService {
4141
/**
4242
* Sends the batch requests.
4343
*
44-
* Note: FormData as individual request body is not supported at the moment.
45-
*
4644
* @throws {ClientResponseError}
4745
*/
4846
async send(options?: SendOptions): Promise<Array<BatchRequestResult>> {
@@ -98,7 +96,7 @@ export class SubBatchService {
9896
*
9997
* The request will be executed as update if `bodyParams` have a valid existing record `id` value, otherwise - create.
10098
*/
101-
upsert(bodyParams?: { [key: string]: any }, options?: RecordOptions): void {
99+
upsert(bodyParams?: { [key: string]: any } | FormData, options?: RecordOptions): void {
102100
options = Object.assign(
103101
{
104102
body: bodyParams || {},
@@ -122,7 +120,7 @@ export class SubBatchService {
122120
/**
123121
* Registers a record create request into the current batch queue.
124122
*/
125-
create(bodyParams?: { [key: string]: any }, options?: RecordOptions): void {
123+
create(bodyParams?: { [key: string]: any } | FormData, options?: RecordOptions): void {
126124
options = Object.assign(
127125
{
128126
body: bodyParams || {},
@@ -148,7 +146,7 @@ export class SubBatchService {
148146
*/
149147
update(
150148
id: string,
151-
bodyParams?: { [key: string]: any },
149+
bodyParams?: { [key: string]: any } | FormData,
152150
options?: RecordOptions,
153151
): void {
154152
options = Object.assign(
@@ -210,8 +208,13 @@ export class SubBatchService {
210208

211209
// extract json and files body data
212210
// -----------------------------------------------------------
213-
for (const key in options.body) {
214-
const val = options.body[key];
211+
let body = options.body;
212+
if (isFormData(body)) {
213+
body = convertFormDataToObject(body)
214+
}
215+
216+
for (const key in body) {
217+
const val = body[key];
215218

216219
if (isFile(val)) {
217220
request.files[key] = request.files[key] || [];

src/tools/formdata.ts

+67
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,70 @@ export function convertToFormDataIfNeeded(body: any): any {
8585

8686
return form;
8787
}
88+
89+
/**
90+
* Converts the provided FormData instance into a plain object.
91+
*
92+
* For consistency with the server multipart/form-data inferring,
93+
* the following normalization rules are applied for plain multipart string values:
94+
* - "true" is converted to the json "true"
95+
* - "false" is converted to the json "false"
96+
* - numeric strings are converted to json number ONLY if the resulted
97+
* minimal number string representation is the same as the provided raw string
98+
* (aka. scientific notations, "Infinity", "0.0", "0001", etc. are kept as string)
99+
* - any other string (empty string too) is left as it is
100+
*/
101+
export function convertFormDataToObject(formData: FormData): { [key: string]: any } {
102+
let result: { [key: string]: any }= {};
103+
104+
formData.forEach((v, k) => {
105+
if (k === "@jsonPayload" && typeof v == "string") {
106+
try {
107+
let parsed = JSON.parse(v)
108+
Object.assign(result, parsed);
109+
} catch (err) {
110+
console.warn("@jsonPayload error:", err)
111+
}
112+
} else {
113+
if (typeof result[k] !== 'undefined') {
114+
if (!Array.isArray(result[k])) {
115+
result[k] = [result[k]]
116+
}
117+
result[k].push(inferFormDataValue(v));
118+
} else {
119+
result[k] = inferFormDataValue(v);
120+
}
121+
}
122+
});
123+
124+
return result;
125+
}
126+
127+
const inferNumberCharsRegex = /^[\-\.\d]+$/
128+
129+
function inferFormDataValue(value: any): any {
130+
if (typeof value != "string") {
131+
return value
132+
}
133+
134+
if (value == "true") {
135+
return true;
136+
}
137+
138+
if (value == "false") {
139+
return false;
140+
}
141+
142+
// note: expects the provided raw string to match exactly with the minimal string representation of the parsed number
143+
if (
144+
(value[0] === "-" || (value[0] >= '0' && value[0] <= '9')) &&
145+
inferNumberCharsRegex.test(value)
146+
) {
147+
let num = (+value)
148+
if (("" + num) === value) {
149+
return num
150+
}
151+
}
152+
153+
return value;
154+
}

tests/services/BatchService.spec.ts

+85-1
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,91 @@ describe("BatchService", function () {
210210
});
211211

212212
// restore
213-
(globalThis.global as any).product = false;
213+
(globalThis.global as any).HermesInternal = false;
214+
215+
assert.deepEqual(result as any, true);
216+
});
217+
218+
test("Should convert FormData to object on individual batch request level", async function () {
219+
const service = new BatchService(client);
220+
221+
fetchMock.on({
222+
method: "POST",
223+
url: service.client.buildURL("/api/batch") + "?q1=123",
224+
additionalMatcher: (_, config) => {
225+
if (
226+
// custom header is missing
227+
config?.headers?.["x-test"] != "123" ||
228+
// multipart/form-data requests shouldn't have explicitly set Content-Type
229+
config?.headers?.["Content-Type"] ||
230+
// the body should have been converted to FormData
231+
!(config.body instanceof FormData)
232+
) {
233+
return false;
234+
}
235+
236+
assert.equal(Array.from(config.body.keys()).length, 5);
237+
238+
assert.deepEqual(
239+
JSON.parse(config.body.get("@jsonPayload") as string),
240+
{
241+
requests: [
242+
{
243+
method: "POST",
244+
url: "/api/collections/%40test1/records?fields=1abc",
245+
body: {
246+
title: "test_title",
247+
number1: 123,
248+
number2: -123.456,
249+
number3: '0.0',
250+
number4: '10e100',
251+
bool1: true,
252+
bool2: false,
253+
options: ["a","b","c"],
254+
json_payload: 789,
255+
description: "new",
256+
json_array: [1,2,3],
257+
},
258+
},
259+
],
260+
},
261+
);
262+
263+
assert.equal(config.body.getAll("requests.0.files_one").length, 1);
264+
assert.equal(config.body.getAll("requests.0.files_many").length, 3);
265+
266+
return true;
267+
},
268+
replyCode: 200,
269+
replyBody: true,
270+
});
271+
272+
let formData = new FormData();
273+
formData.append("title", "test_title")
274+
formData.append("description", "old")
275+
formData.append("number1", "123")
276+
formData.append("number2", "-123.456")
277+
formData.append("number3", "0.0")
278+
formData.append("number4", "10e100")
279+
formData.append("bool1", "true")
280+
formData.append("bool2", "false")
281+
formData.append("options", "a")
282+
formData.append("options", "b")
283+
formData.append("options", "c")
284+
formData.append("files_one", new File(["test"], "test0.png"))
285+
formData.append("files_many", new File(["test"], "test1.png"))
286+
formData.append("files_many", new File(["test"], "test2.png"))
287+
formData.append("files_many", new File(["test"], "test3.png"))
288+
formData.append("@jsonPayload", `{"json_payload": 789, "description": "new", "json_array": [1,2,3]}`)
289+
290+
service.collection("@test1").create(formData, { fields: "1abc" });
291+
292+
const result = await service.send({
293+
q1: 123,
294+
headers: {
295+
"x-test": "123",
296+
},
297+
});
214298

215299
assert.deepEqual(result as any, true);
216300
});

0 commit comments

Comments
 (0)