diff --git a/CHANGELOG.md b/CHANGELOG.md index e10bea4640..288432ee6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 17.2.0 - 2024-10-09 +* [#2201](https://github.com/stripe/stripe-node/pull/2201) Add fetchRelatedObject to V2 Events if needed + * `fetchRelatedObject` is added to events retrieved using `stripe.v2.core.events` and can be used to easily fetch the Stripe object related to a retrieved event + ## 17.2.0-beta.2 - 2024-10-08 * [#2180](https://github.com/stripe/stripe-node/pull/2180) Update generated code for beta * Add support for `submit_card` test helper method on resource `Issuing.Card` diff --git a/examples/snippets/example_template.ts b/examples/snippets/example_template.ts new file mode 100644 index 0000000000..08c1fe65c3 --- /dev/null +++ b/examples/snippets/example_template.ts @@ -0,0 +1,21 @@ +/** + * example_template.py - This is a template for defining new examples. It is not intended to be used directly. + + * + + * In this example, we: + * - + * - + */ + +import {Stripe} from 'stripe'; + +const apiKey = '{{API_KEY}}'; + +console.log('Hello World'); +// const client = new Stripe(apiKey); +// client.v2.... diff --git a/examples/snippets/meter_event_stream.ts b/examples/snippets/meter_event_stream.ts index 6039ede351..fec60cce78 100644 --- a/examples/snippets/meter_event_stream.ts +++ b/examples/snippets/meter_event_stream.ts @@ -1,3 +1,15 @@ +/** + * meter_event_stream.ts - Use the high-throughput meter event stream to report create billing meter events. + * + * In this example, we: + * - create a meter event session and store the session's authentication token + * - define an event with a payload + * - use the meterEventStream service to create an event stream that reports this event + * + * This example expects a billing meter with an event_name of 'alpaca_ai_tokens'. If you have + * a different meter event name, you can change it before running this example. + */ + import {Stripe} from 'stripe'; const apiKey = '{{API_KEY}}'; diff --git a/examples/snippets/new_example.ts b/examples/snippets/new_example.ts deleted file mode 100644 index e521280c83..0000000000 --- a/examples/snippets/new_example.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {Stripe} from 'stripe'; - -const apiKey = '{{API_KEY}}'; - -console.log('Hello World'); -// const client = new Stripe(apiKey); -// client.v2.... diff --git a/examples/snippets/package.json b/examples/snippets/package.json index ba3738f172..386c40adb4 100644 --- a/examples/snippets/package.json +++ b/examples/snippets/package.json @@ -6,7 +6,7 @@ "license": "ISC", "dependencies": { "express": "^4.21.0", - "stripe": "file:../../", + "stripe": "file:../..", "ts-node": "^10.9.2", "typescript": "^5.6.2" } diff --git a/examples/snippets/stripe_webhook_handler.js b/examples/snippets/thinevent_webhook_handler.js similarity index 63% rename from examples/snippets/stripe_webhook_handler.js rename to examples/snippets/thinevent_webhook_handler.js index bc75e62683..86c56f1b66 100644 --- a/examples/snippets/stripe_webhook_handler.js +++ b/examples/snippets/thinevent_webhook_handler.js @@ -1,3 +1,15 @@ +/** + * thinevent_webhook_handler.js - receive and process thin events like the + * v1.billing.meter.error_report_triggered event. + * In this example, we: + * - create a Stripe client object called client + * - use client.parseThinEvent to parse the received thin event webhook body + * - call client.v2.core.events.retrieve to retrieve the full event object + * - if it is a v1.billing.meter.error_report_triggered event type, call + * event.fetchRelatedObject to retrieve the Billing Meter object associated + * with the event. + */ + const express = require('express'); const {Stripe} = require('stripe'); @@ -20,11 +32,10 @@ app.post( // Fetch the event data to understand the failure const event = await client.v2.core.events.retrieve(thinEvent.id); if (event.type == 'v1.billing.meter.error_report_triggered') { - const meter = await client.billing.meters.retrieve( - event.related_object.id - ); + const meter = await event.fetchRelatedObject(); const meterId = meter.id; console.log(`Success! ${meterId}`); + // Record the failures and alert your team // Add your logic here } diff --git a/examples/snippets/yarn.lock b/examples/snippets/yarn.lock index 622721e44e..31868864fa 100644 --- a/examples/snippets/yarn.lock +++ b/examples/snippets/yarn.lock @@ -48,9 +48,9 @@ integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== "@types/node@>=8.1.0": - version "22.6.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.6.1.tgz#e531a45f4d78f14a8468cb9cdc29dc9602afc7ac" - integrity sha512-V48tCfcKb/e6cVUigLAaJDAILdMP0fUW6BidkPK4GpGjXcfbnoHasCZDwz3N3yVt5we2RHm4XTQCpv0KJz9zqw== + version "22.7.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.5.tgz#cfde981727a7ab3611a481510b473ae54442b92b" + integrity sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ== dependencies: undici-types "~6.19.2" @@ -524,7 +524,7 @@ statuses@2.0.1: integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== "stripe@file:../..": - version "16.12.0" + version "17.0.0" dependencies: "@types/node" ">=8.1.0" qs "^6.11.0" diff --git a/src/resources/V2/Core/Events.ts b/src/resources/V2/Core/Events.ts index e0bba400ce..cbfa40a317 100644 --- a/src/resources/V2/Core/Events.ts +++ b/src/resources/V2/Core/Events.ts @@ -1,12 +1,64 @@ -// File generated from our OpenAPI spec - +// This file is manually maintained import {StripeResource} from '../../../StripeResource.js'; + const stripeMethod = StripeResource.method; + export const Events = StripeResource.extend({ - retrieve: stripeMethod({method: 'GET', fullPath: '/v2/core/events/{id}'}), - list: stripeMethod({ - method: 'GET', - fullPath: '/v2/core/events', - methodType: 'list', - }), + retrieve(...args: any[]) { + const transformResponseData = (response: any): any => { + return this.addFetchRelatedObjectIfNeeded(response); + }; + return stripeMethod({ + method: 'GET', + fullPath: '/v2/core/events/{id}', + transformResponseData, + }).apply(this, args); + }, + + list(...args: any[]) { + const transformResponseData = (response: any): any => { + return { + ...response, + data: response.data.map(this.addFetchRelatedObjectIfNeeded.bind(this)), + }; + }; + return stripeMethod({ + method: 'GET', + fullPath: '/v2/core/events', + methodType: 'list', + transformResponseData, + }).apply(this, args); + }, + + /** + * @private + * + * For internal use in stripe-node. + * + * @param pulledEvent The retrieved event object + * @returns The retrieved event object with a fetchRelatedObject method, + * if pulledEvent.related_object is valid (non-null and has a url) + */ + addFetchRelatedObjectIfNeeded(pulledEvent: any) { + if (!pulledEvent.related_object || !pulledEvent.related_object.url) { + return pulledEvent; + } + return { + ...pulledEvent, + fetchRelatedObject: (): Promise => + // call stripeMethod with 'this' resource to fetch + // the related object. 'this' is needed to construct + // and send the request, but the method spec controls + // the url endpoint and method, so it doesn't matter + // that 'this' is an Events resource object here + stripeMethod({ + method: 'GET', + fullPath: pulledEvent.related_object.url, + }).apply(this, [ + { + stripeAccount: pulledEvent.context, + }, + ]), + }; + }, }); diff --git a/test/resources/V2/Core/Events.spec.js b/test/resources/V2/Core/Events.spec.js new file mode 100644 index 0000000000..c565b22626 --- /dev/null +++ b/test/resources/V2/Core/Events.spec.js @@ -0,0 +1,209 @@ +'use strict'; + +const testUtils = require('../../../testUtils.js'); +const expect = require('chai').expect; + +const stripe = testUtils.getSpyableStripe(); + +const v2EventPayloadWithoutRelatedObject = ` + { + "context": "context", + "created": "1970-01-12T21:42:34.472Z", + "id": "obj_123", + "livemode": true, + "object":"v2.core.event", + "reason": + { + "type": "request", + "request": + { + "id": "obj_123", + "idempotency_key": "idempotency_key" + } + }, + "type": "type" + } +`; + +const v2EventPayloadWithRelatedObject = ` + { + "context": "context", + "created": "1970-01-12T21:42:34.472Z", + "id": "obj_123", + "livemode": true, + "object":"v2.core.event", + "reason": + { + "type": "request", + "request": + { + "id": "obj_123", + "idempotency_key": "idempotency_key" + } + }, + "type": "type", + "related_object": + { + "id": "obj_123", + "type": "thing", + "url": "/v1/things/obj_123" + } + } +`; + +describe('V2 Core Events Resource', () => { + describe('retrieve', () => { + it('Sends the correct request', () => { + stripe.v2.core.events.retrieve('eventIdBaz'); + expect(stripe.LAST_REQUEST).to.deep.equal({ + method: 'GET', + url: '/v2/core/events/eventIdBaz', + headers: {}, + data: null, + settings: {}, + }); + }); + + it('Does not have fetchRelatedObject if not needed', async () => { + const mockStripe = testUtils.createMockClient([ + { + method: 'GET', + path: '/v2/core/events/ll_123', + response: v2EventPayloadWithoutRelatedObject, + }, + ]); + const event = await mockStripe.v2.core.events.retrieve('ll_123'); + expect(event).ok; + expect(event.fetchRelatedObject).to.be.undefined; + }); + + it('Has fetchRelatedObject if needed', async () => { + const mockStripe = testUtils.createMockClient([ + { + method: 'GET', + path: '/v2/core/events/ll_123', + response: v2EventPayloadWithRelatedObject, + }, + ]); + const event = await mockStripe.v2.core.events.retrieve('ll_123'); + expect(event).ok; + expect(event.fetchRelatedObject).ok; + }); + + it('Can call fetchRelatedObject', async () => { + const mockStripe = testUtils.createMockClient([ + { + method: 'GET', + path: '/v2/core/events/ll_123', + response: v2EventPayloadWithRelatedObject, + }, + { + method: 'GET', + path: '/v1/things/obj_123', + response: '{"id": "obj_123"}', + }, + ]); + const event = await mockStripe.v2.core.events.retrieve('ll_123'); + expect(event).ok; + expect(event.fetchRelatedObject).ok; + const obj = await event.fetchRelatedObject(); + expect(obj.id).to.equal('obj_123'); + }); + }); + + describe('list', () => { + it('Sends the correct request', () => { + stripe.v2.core.events.list({object_id: 'foo'}); + expect(stripe.LAST_REQUEST).to.deep.equal({ + method: 'GET', + url: '/v2/core/events?object_id=foo', + headers: {}, + data: null, + settings: {}, + }); + }); + + it('Does not have fetchRelatedObject if not needed', async () => { + const mockStripe = testUtils.createMockClient([ + { + method: 'GET', + path: '/v2/core/events?object_id=foo', + response: `{ + "data": [ + ${v2EventPayloadWithoutRelatedObject}, + ${v2EventPayloadWithoutRelatedObject}, + ${v2EventPayloadWithoutRelatedObject} + ], + "next_page_url": null + }`, + }, + ]); + const resp = await mockStripe.v2.core.events.list({object_id: 'foo'}); + expect(resp).ok; + expect(resp.data.length).is.equal(3); + for (const event of resp.data) { + expect(event.fetchRelatedObject).not.ok; + } + }); + + it('Has fetchRelatedObject if needed', async () => { + const mockStripe = testUtils.createMockClient([ + { + method: 'GET', + path: '/v2/core/events?object_id=foo', + response: `{ + "data": [ + ${v2EventPayloadWithRelatedObject}, + ${v2EventPayloadWithRelatedObject}, + ${v2EventPayloadWithRelatedObject} + ], + "next_page_url": null + }`, + }, + ]); + const resp = await mockStripe.v2.core.events.list({object_id: 'foo'}); + expect(resp).ok; + expect(resp.data.length).is.equal(3); + for (const event of resp.data) { + expect(event.fetchRelatedObject).ok; + } + }); + + it('Has fetchRelatedObject added to autoPaginate results', async () => { + const mockStripe = testUtils.createMockClient([ + { + method: 'GET', + path: '/v2/core/events?object_id=foo', + response: `{ + "data": [ + ${v2EventPayloadWithRelatedObject}, + ${v2EventPayloadWithRelatedObject}, + ${v2EventPayloadWithRelatedObject} + ], + "next_page_url": "/next_page" + }`, + }, + { + method: 'GET', + path: '/next_page', + response: `{ + "data": [ + ${v2EventPayloadWithRelatedObject}, + ${v2EventPayloadWithRelatedObject}, + ${v2EventPayloadWithRelatedObject} + ], + "next_page_url": null + }`, + }, + ]); + const respProm = mockStripe.v2.core.events.list({object_id: 'foo'}); + expect(respProm).ok; + let totalEvents = 0; + await respProm.autoPagingEach(function(event) { + totalEvents += 1; + expect(event.fetchRelatedObject).ok; + }); + expect(totalEvents).is.equal(6); + }); + }); +}); diff --git a/test/testUtils.ts b/test/testUtils.ts index 454b273b46..f75ae08e87 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -158,7 +158,7 @@ export const createMockClient = ( throw new Error(`Unable to find a mock request for ${method} ${path}`); } - callback(null, Promise.resolve(JSON.parse(request.response))); + callback(null, JSON.parse(request.response)); }); }; diff --git a/types/V2/EventTypes.d.ts b/types/V2/EventTypes.d.ts index 4fe79efa87..a5a3110b45 100644 --- a/types/V2/EventTypes.d.ts +++ b/types/V2/EventTypes.d.ts @@ -16,8 +16,10 @@ declare module 'stripe' { type: 'v1.billing.meter.error_report_triggered'; // Retrieves data specific to this event. data: V1BillingMeterErrorReportTriggeredEvent.Data; - // Retrieves the object associated with the event. + // Object containing the reference to API resource relevant to the event. related_object: Event.RelatedObject; + // Retrieves the object associated with the event. + fetchRelatedObject(): Promise; } namespace V1BillingMeterErrorReportTriggeredEvent {