diff --git a/lib/StripeMethod.js b/lib/StripeMethod.js index ace8febfe5..4c6c544dd2 100644 --- a/lib/StripeMethod.js +++ b/lib/StripeMethod.js @@ -11,6 +11,8 @@ const makeAutoPaginationMethods = require('./autoPagination') * @param [spec.method='GET'] Request Method (POST, GET, DELETE, PUT) * @param [spec.path=''] Path to be appended to the API BASE_PATH, joined with * the instance's path (e.g. 'charges' or 'customers') + * @param [spec.fullPath=''] Fully qualified path to the method (eg. /v1/a/b/c). + * If this is specified, path should not be specified. * @param [spec.urlParams=[]] Array of required arguments in the order that they * must be passed by the consumer of the API. Subsequent optional arguments are * optionally passed through a hash (Object) as the penultimate argument @@ -20,11 +22,16 @@ const makeAutoPaginationMethods = require('./autoPagination') * @param [spec.host] Hostname for the request. */ function stripeMethod(spec) { + if (spec.path !== undefined && spec.fullPath !== undefined) { + throw new Error( + `Method spec specified both a 'path' (${spec.path}) and a 'fullPath' (${spec.fullPath}).` + ); + } return function(...args) { const callback = typeof args[args.length - 1] == 'function' && args.pop(); spec.urlParams = utils.extractUrlParams( - this.createResourcePathWithSymbols(spec.path || '') + spec.fullPath || this.createResourcePathWithSymbols(spec.path || '') ); const requestPromise = utils.callbackifyPromiseWithTimeout( diff --git a/lib/makeRequest.js b/lib/makeRequest.js index 8944b30bde..89a1c6bd30 100644 --- a/lib/makeRequest.js +++ b/lib/makeRequest.js @@ -4,11 +4,20 @@ const utils = require('./utils'); function getRequestOpts(self, requestArgs, spec, overrideData) { // Extract spec values with defaults. - const commandPath = utils.makeURLInterpolator(spec.path || ''); const requestMethod = (spec.method || 'GET').toUpperCase(); const urlParams = spec.urlParams || []; const encode = spec.encode || ((data) => data); - const path = self.createResourcePathWithSymbols(spec.path); + + const isUsingFullPath = !!spec.fullPath; + const commandPath = utils.makeURLInterpolator( + isUsingFullPath ? spec.fullPath : spec.path || '' + ); + + // When using fullPath, we ignore the resource path as it should already be + // fully qualified. + const path = isUsingFullPath + ? spec.fullPath + : self.createResourcePathWithSymbols(spec.path); // Don't mutate args externally. const args = [].slice.call(requestArgs); @@ -39,7 +48,11 @@ function getRequestOpts(self, requestArgs, spec, overrideData) { ); } - const requestPath = self.createFullPath(commandPath, urlData); + // When using full path, we can just invoke the URL interpolator directly + // as we don't need to use the resource to create a full path. + const requestPath = isUsingFullPath + ? commandPath(urlData) + : self.createFullPath(commandPath, urlData); const headers = Object.assign(options.headers, spec.headers); if (spec.validator) { diff --git a/test/StripeResource.spec.js b/test/StripeResource.spec.js index 18b1f2a585..2f5ec6ccf9 100644 --- a/test/StripeResource.spec.js +++ b/test/StripeResource.spec.js @@ -642,6 +642,38 @@ describe('StripeResource', () => { }); }); + describe('method with fullPath', () => { + it('interpolates in parameters', (callback) => { + const handleRequest = (req, res) => { + expect(req.url).to.equal('/v1/parent/hello/child/world'); + + // Write back JSON to close out the server. + res.write('{}'); + res.end(); + }; + + testUtils.getTestServerStripe( + {}, + handleRequest, + (err, stripe, closeServer) => { + const resource = new (StripeResource.extend({ + test: stripeMethod({ + method: 'GET', + fullPath: '/v1/parent/{param1}/child/{param2}', + }), + }))(stripe); + + return resource.test('hello', 'world', (err, res) => { + closeServer(); + // Spot check that we received a response. + expect(res).to.deep.equal({}); + return callback(err); + }); + } + ); + }); + }); + describe('streaming', () => { /** * Defines a fake resource with a `pdf` method diff --git a/test/makeRequest.spec.js b/test/makeRequest.spec.js index 93fe5e67f1..c333594d2c 100644 --- a/test/makeRequest.spec.js +++ b/test/makeRequest.spec.js @@ -3,6 +3,7 @@ require('../testUtils'); const makeRequest = require('../lib/makeRequest'); +const utils = require('../lib/utils'); const expect = require('chai').expect; describe('makeRequest', () => { @@ -26,4 +27,42 @@ describe('makeRequest', () => { ]); }); }); + + describe('makeRequest with fullPath', () => { + it('handles urlData', async () => { + const args = ['hello', 'world']; + + const fullPath = '/v1/parent/{param1}/child/{param2}'; + const spec = { + fullPath, + urlParams: utils.extractUrlParams(fullPath), + }; + + let actualPath; + const mockSelf = { + // These two methods shouldn't be called when using a fullPath, as they + // don't rely on the resource path. + createResourcePathWithSymbols: () => { + throw new Error('Unexpected call to createResourcePathWithSymbols.'); + }, + createFullPath: () => { + throw new Error('Unexpected call to createFullPath.'); + }, + _request: ( + _method, + _host, + path, + _body, + _auth, + _headers, + requestCallback + ) => { + actualPath = path; + requestCallback(null, 'response'); + }, + }; + await makeRequest(mockSelf, args, spec, {}); + expect(actualPath).to.equal('/v1/parent/hello/child/world'); + }); + }); }); diff --git a/types/lib.d.ts b/types/lib.d.ts index 45d396a03b..cb090ab5df 100644 --- a/types/lib.d.ts +++ b/types/lib.d.ts @@ -16,7 +16,8 @@ declare module 'stripe' { >(spec: T): StripeResource & T; static method(spec: { method: string; - path: string; + path?: string; + fullPath?: string; methodType?: 'list'; }): (...args: any[]) => object; //eslint-disable-line @typescript-eslint/no-explicit-any static BASIC_METHODS: { diff --git a/types/test/typescriptTest.ts b/types/test/typescriptTest.ts index d93e3bdeff..9c763be32f 100644 --- a/types/test/typescriptTest.ts +++ b/types/test/typescriptTest.ts @@ -180,6 +180,10 @@ Stripe.StripeResource.extend({ method: 'create', path: 'foo', }), + fooFullPath: Stripe.StripeResource.method({ + method: 'create', + fullPath: '/v1/full/path', + }), }); const maxBufferedRequestMetrics: number =