はじめに
【Node.js】Stripeライブラリの入出力がsnake_caseになっているのをどうにかしたいでは、入出力をcamelCaseにできるようにするための独自拡張をやってみた。
今回は、StripeのAPIにoffset
がない事に気づき、SQLに親しんでいる身としては使いにくい(starting_after
はどちらかというと、NoSQL等で使われるデータの取得を行う際のデータ位置を指定するものになっている)ので、無理くりoffset
を使えるようにする実装をやってみたいと思う。
※以下の実装を見ればわかるが、loopでデータを取得しているだけなので、StripeのAPIのレート上限に引っかかる可能性が高いので、PoCではいいかもだが、頻繁にlist取得するような実装をするのであれば、今回試しにやってみた実装はNGになると思われる。
ソースコード全体は以下。
一応、動くかのテストも書いてみたが、そのコードは以下。
実際にやってみる
import Stripe from 'stripe';
import { strict as assert } from 'assert';
import snakecaseKeys from 'snakecase-keys';
import camelcaseKeys from 'camelcase-keys';
import config from 'config';
const execute = async (options = {}) => {
// 省略
const { stripe, category, func, args } = options;
const snakecaseArgs = snakecaseKeys(args, {
deep: true,
exclude: ['stripeAccount']
});
const result = await stripe[category][func](...snakecaseArgs);
return camelcaseKeys(result, { deep: true });
};
export default () => {
assert.ok(
process.env.STRIPE_KEY_SECRET,
'env STRIPE_KEY_SECRET must be required'
);
const stripe = Stripe(process.env.STRIPE_KEY_SECRET);
// IF(入出力)をcamelCaseにした関数を別途定義(_〇〇 のように利用)
Object.keys(stripe)
.filter(
(category) => !category.startsWith('_') && !category.match(/^[A-Z].+/)
)
.forEach((category) => {
Object.keys(Object.getPrototypeOf(stripe[category]))
.filter((func) => typeof stripe[category][func] === 'function')
.forEach((func) => {
stripe[category][`$${func}`] = async (...origin) => {
const args = origin;
if (!func.startsWith('list')) {
const v = await execute({ stripe, category, func, args });
return v;
}
let limit = config.get('stripe.limit');
let offset = 0;
// 想定 [ 'hoge' ]
if (typeof args[0] === 'string') {
const option = args[1];
// 想定 [ 'hoge', {} ]
if (option) {
// 想定 [ 'hoge', { stripeAccount: 'connectAccountId' } ]
if ('stripeAccount' in option) {
args.splice(1, 0, { limit }); // [ 'hoge', { limit: 10 }, { stripeAccount: 'connectAccountId' } ]
}
// 想定 [ 'hoge', { } ] or [ 'hoge', { limit: 1 } ]
else {
limit = option.limit || limit;
option.limit = limit; // [ 'hoge', { limit: 10 } ]
offset = option.offset || offset;
delete option.offset;
}
}
// 想定 [ 'hoge' ]
else args.push({ limit }); // [ 'hoge', { limit: 10 } ]
}
// 想定 [ {} ]
if (typeof args[0] === 'object') {
const option = args[0];
// 想定 [ { stripeAccount: 'connectAccountId' } ]
if ('stripeAccount' in option) {
args.unshift({ limit }); // [ { limit: 10 }, { stripeAccount: 'connectAccountId' } ]
}
// 想定 [ {} ] or [ { limit: 1, offset: 1} ]
else {
limit = option.limit || limit;
option.limit = limit; // [ { limit: 10 } ]
offset = option.offset || offset;
delete option.offset;
}
}
// 想定 [ ]
if (!args[0]) args.push({ limit });
const datas = [];
for (;;) {
// eslint-disable-next-line no-await-in-loop
const { object, data, hasMore } = await execute({
stripe,
category,
func,
args
});
if (object !== 'list') throw new Error('expected list object');
data.forEach((v) => {
datas.push(v);
});
if (!data.length || !hasMore || datas.length >= offset + limit)
break;
// [ {} ] -> args[0], [ 'hoge' ] -> args[1]
const index = typeof args[0] === 'object' ? 0 : 1;
args[index].startingAfter = data[data.length - 1].id;
}
return datas.slice(offset).slice(0, limit);
};
});
});
return stripe;
};
考え方・どう実現しているか?
大枠の考え方としては単純で、データが取得したい数よりも少ない場合には、starting_afterでその後続のデータを取得し、配列にpushし続けるというもの。
そして、呼び出し側からlimit
・offset
を以下のようにしてできるようにしている。
const customers = await stripe.customers.$list({ offset: 9, limit: 1 });
ただ、StripeのAPIを見ると分かるが、以下のようにパターン化されるので、それに応じてlimit
・offset
の値を取り出したり、設定したりする必要があり、そこで変に複雑なif文を実装している。
- 第1引数
- stringが設定されるパターン
- object(JSON)が設定されるパターン
- なし(指定なしのパターン)
具体的には、以下のあたりのコードがそれ。
// 想定 [ 'hoge' ]
if (typeof args[0] === 'string') {
const option = args[1];
// 想定 [ 'hoge', {} ]
if (option) {
// 想定 [ 'hoge', { stripeAccount: 'connectAccountId' } ]
if ('stripeAccount' in option) {
args.splice(1, 0, { limit }); // [ 'hoge', { limit: 10 }, { stripeAccount: 'connectAccountId' } ]
}
// 想定 [ 'hoge', { } ] or [ 'hoge', { limit: 1 } ]
else {
limit = option.limit || limit;
option.limit = limit; // [ 'hoge', { limit: 10 } ]
offset = option.offset || offset;
delete option.offset;
}
}
// 想定 [ 'hoge' ]
else args.push({ limit }); // [ 'hoge', { limit: 10 } ]
}
// 想定 [ {} ]
if (typeof args[0] === 'object') {
const option = args[0];
// 想定 [ { stripeAccount: 'connectAccountId' } ]
if ('stripeAccount' in option) {
args.unshift({ limit }); // [ { limit: 10 }, { stripeAccount: 'connectAccountId' } ]
}
// 想定 [ {} ] or [ { limit: 1, offset: 1} ]
else {
limit = option.limit || limit;
option.limit = limit; // [ { limit: 10 } ]
offset = option.offset || offset;
delete option.offset;
}
}
// 想定 [ ]
if (!args[0]) args.push({ limit });
まとめとして
今回は無理くり、StripeのNode.jsのライブラリを拡張し、list系のAPIでoffsetを利用できるように実装してみた。実装自体はできるが、これがStripeのレート制限や実際の処理速度などを鑑みて、時にはNGになる事もある気がした(paymentMethods程度なら、各csutomerに対する支払方法が100になるような事は考えにくいにので、問題ないだろうが、customer自体は規模が大きくなれば万単位になりそうなので、そこで今回のような実装をしていたらNGだろう)。