Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ payjp.charges.create({
currency: 'jpy',
card: 'token_id_by_Checkout_or_payjp.js'
}).then(console.log).catch(console.error);

payjp.charges.create({
amount: 1000,
currency: 'jpy',
customer: 'cus_xxx',
metadata: { user_id: 123 }
}).then(console.log).catch(console.error);
```

Typescript
Expand Down
31 changes: 29 additions & 2 deletions src/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,33 @@ export default class Resource {
return Math.ceil((delay / 2) * (1 + Math.random()));
}

private isPlainObject(value: unknown): value is Record<string, unknown> {
return Object.prototype.toString.call(value) === "[object Object]";
}

private appendFormValue(params: URLSearchParams, key: string, value: unknown): void {
if (value === undefined) {
return;
}

if (this.isPlainObject(value)) {
for (const [nestedKey, nestedValue] of Object.entries(value)) {
this.appendFormValue(params, `${key}[${nestedKey}]`, nestedValue);
}
return;
}

params.append(key, String(value));
}

private serializeQuery(query: object): URLSearchParams {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(query)) {
this.appendFormValue(params, key, value);
}
return params;
}

protected request<I>(
method: string,
endpoint: string,
Expand All @@ -56,14 +83,14 @@ export default class Resource {
// Set query parameters or request body
if (method === "GET" || method === "DELETE") {
// For GET and DELETE, add query parameters to URL
const params = new URLSearchParams(query as Record<string, string>);
const params = this.serializeQuery(query);
const queryString = params.toString();
if (queryString) {
url = `${url}?${queryString}`;
}
} else {
// For POST and PUT, send as request body
const body = new URLSearchParams(query as Record<string, string>);
const body = this.serializeQuery(query);
fetchOptions.body = body.toString();
}

Expand Down
120 changes: 120 additions & 0 deletions test/requestor.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,74 @@ describe("HTTP Requestor", () => {
});
});
});
it("serializes nested object values for POST bodies", (done) => {
const dummy = {
amount: 100,
metadata: { user: 121899 },
};
const status = 200;
const server = http
.createServer((msg, res) => {
server.close();
assert.strictEqual(msg.method, "POST");
assert.strictEqual(msg.url, "/v1/charges");
let rawData = "";
msg.on("data", (chunk) => {
rawData += chunk;
});
msg.on("end", () => {
assert.strictEqual(rawData, "amount=100&metadata%5Buser%5D=121899");
const body = JSON.stringify(dummy);
res.writeHead(status, {
"Content-Length": Buffer.byteLength(body),
"Content-Type": "application/json",
});
res.end(body);
});
})
.listen(() => {
const apibase = `http://localhost:${server.address().port}/v1`;
const payjp = new Payjp(apikey, { apibase });
payjp.charges.create(dummy).then((r) => {
assert.deepStrictEqual(r, dummy);
done();
});
});
});
it("keeps flat bracket notation keys for POST bodies", (done) => {
const dummy = {
amount: 100,
"metadata[user]": "test",
};
const status = 200;
const server = http
.createServer((msg, res) => {
server.close();
assert.strictEqual(msg.method, "POST");
assert.strictEqual(msg.url, "/v1/charges");
let rawData = "";
msg.on("data", (chunk) => {
rawData += chunk;
});
msg.on("end", () => {
assert.strictEqual(rawData, "amount=100&metadata%5Buser%5D=test");
const body = JSON.stringify(dummy);
res.writeHead(status, {
"Content-Length": Buffer.byteLength(body),
"Content-Type": "application/json",
});
res.end(body);
});
})
.listen(() => {
const apibase = `http://localhost:${server.address().port}/v1`;
const payjp = new Payjp(apikey, { apibase });
payjp.charges.create(dummy).then((r) => {
assert.deepStrictEqual(r, dummy);
done();
});
});
});
it("return 400 by GET", (done) => {
const dummy = { object: "payjp" };
const status = 400;
Expand Down Expand Up @@ -145,6 +213,58 @@ describe("HTTP Requestor", () => {
});
});
});
it("serializes nested object values for GET query strings", (done) => {
const dummy = {
metadata: { user: 121899 },
};
const status = 200;
const server = http
.createServer((msg, res) => {
server.close();
assert.strictEqual(msg.method, "GET");
assert.strictEqual(msg.url, "/v1/charges?metadata%5Buser%5D=121899");
const body = JSON.stringify({});
res.writeHead(status, {
"Content-Length": Buffer.byteLength(body),
"Content-Type": "application/json",
});
res.end(body);
})
.listen(() => {
const apibase = `http://localhost:${server.address().port}/v1`;
const payjp = new Payjp(apikey, { apibase });
payjp.charges.request("GET", "charges", dummy).then((r) => {
assert.deepStrictEqual(r, {});
done();
});
});
});
it("keeps flat bracket notation keys for GET query strings", (done) => {
const dummy = {
"metadata[user]": "test",
};
const status = 200;
const server = http
.createServer((msg, res) => {
server.close();
assert.strictEqual(msg.method, "GET");
assert.strictEqual(msg.url, "/v1/charges?metadata%5Buser%5D=test");
const body = JSON.stringify({});
res.writeHead(status, {
"Content-Length": Buffer.byteLength(body),
"Content-Type": "application/json",
});
res.end(body);
})
.listen(() => {
const apibase = `http://localhost:${server.address().port}/v1`;
const payjp = new Payjp(apikey, { apibase });
payjp.charges.request("GET", "charges", dummy).then((r) => {
assert.deepStrictEqual(r, {});
done();
});
});
});
it("return 200 by DELETE, but not json", (done) => {
const status = 200;
const server = http
Expand Down
Loading