0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Node.js】Stripeライブラリのlist系のAPIでoffsetが使えないの無理くりどうにかする

Posted at

はじめに

【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し続けるというもの。

そして、呼び出し側からlimitoffsetを以下のようにしてできるようにしている。

const customers = await stripe.customers.$list({ offset: 9, limit: 1 });

ただ、StripeのAPIを見ると分かるが、以下のようにパターン化されるので、それに応じてlimitoffsetの値を取り出したり、設定したりする必要があり、そこで変に複雑な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だろう)。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?