@makerx/graphql-core 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -405,6 +405,104 @@ The returned value is both an `AsyncIterator` and `AsyncIterable`, so it can be
405
405
  extractAnonymousOperationName('subscription { onUpdate { id } }') // 'subscription onUpdate'
406
406
  ```
407
407
 
408
+ ## GraphQL test helpers
409
+
410
+ The `@makerx/graphql-core/testing` submodule provides helpers for running GraphQL operations directly against a schema, without spinning up an HTTP or websocket server.
411
+
412
+ Bypassing the transport layer supports complete control over JWT payloads and other operation context inputs required to set up complex test scenarios.
413
+
414
+ This complements the `@makerx/graphql-apollo-server/testing` module, which exports `buildExecuteOperation` for running queries and mutations directly against an `ApolloServer` instance. Subscriptions normally flow over websockets via [graphql-ws](https://the-guild.dev/graphql/ws), so they need their own helper that calls `graphql.subscribe` directly against the schema.
415
+
416
+ ### buildSubscribeOperation
417
+
418
+ `buildSubscribeOperation` accepts a `GraphQLSchema` (or async schema factory) and a context creation function and returns a `subscribeOperation` function which:
419
+
420
+ - is strongly typed to the GraphQL context
421
+ - accepts `TypedDocumentNode` subscriptions to provide strong operation typing
422
+ - forwards any additional arguments to the supplied context creation function
423
+ - returns the subscription's `AsyncIterableIterator<ExecutionResult<TData>>` so tests can consume events via `for await` or manual `next()` calls
424
+ - throws if the subscription fails to produce an iterator (e.g. a validation error), surfacing the underlying errors
425
+
426
+ The shape of the context creation function — and the JWT/user factories used to drive it — depends on your GraphQL implementation, so the example below illustrates one common pattern using [Vitest test contexts](https://vitest.dev/guide/test-context), matching the [`buildExecuteOperation`](https://github.com/MakerXStudio/graphql-apollo-server#apollo-server-test-helpers) examples for queries and mutations.
427
+
428
+ #### Vitest GraphQL context example
429
+
430
+ Note: extend auth context (e.g. `buildJwt`, `buildUserJwt`) and other context as required — see the [graphql-apollo-server test helper docs](https://github.com/MakerXStudio/graphql-apollo-server#vitest-auth-context-example) for a complete auth fixture example.
431
+
432
+ `test/graphql.ts`
433
+
434
+ ```ts
435
+ import { buildSubscribeOperation } from '@makerx/graphql-core/testing'
436
+ import { test as baseTest } from './auth'
437
+
438
+ const createContext = async (jwtPayload?: JwtPayload): Promise<GraphQLContext> => {
439
+ const user = await findUpdateOrCreateUser(jwtPayload, randomUUID())
440
+ const baseContext: BaseContext = { user, logger, requestInfo, started: Date.now() }
441
+ const extraContext = await augmentContext(baseContext)
442
+ return { ...baseContext, ...extraContext }
443
+ }
444
+
445
+ export const test = baseTest
446
+ .extend('schema', { scope: 'worker' }, async ({}) => createSchema())
447
+ .extend('backgroundJobs', { scope: 'worker' }, backgroundJobsFixture)
448
+ .extend('executeOperation', { scope: 'worker' }, async ({ schema }, { onCleanup }) => {
449
+ const server = new ApolloServer<GraphQLContext>({ schema })
450
+ onCleanup(() => server.stop())
451
+ return buildExecuteOperation(server, createContext)
452
+ })
453
+ .extend('subscribeOperation', { scope: 'worker' }, async ({ schema }) => {
454
+ return buildSubscribeOperation(schema, createContext)
455
+ })
456
+ ```
457
+
458
+ #### job-status.subscription.test.ts
459
+
460
+ The test below uses the `graphql` template-literal tag from [GraphQL-Codegen](https://the-guild.dev/graphql/codegen/docs/getting-started) to produce strongly typed operations.
461
+
462
+ ```ts
463
+ import { describe, expect } from 'vitest'
464
+ import { graphql } from './gql'
465
+ import { test } from '../../../test/graphql'
466
+
467
+ const jobStatusSubscription = graphql(`
468
+ subscription JobStatus($jobId: ID!) {
469
+ jobStatus(jobId: $jobId) {
470
+ jobId
471
+ status
472
+ }
473
+ }
474
+ `)
475
+
476
+ describe('jobStatus subscription operation', () => {
477
+ test('anonymous subscribes fail', async ({ subscribeOperation }) => {
478
+ await expect(subscribeOperation({ subscription: jobStatusSubscription, variables: { jobId: 'job-1' } })).rejects.toThrow(
479
+ /Not authenticated/,
480
+ )
481
+ })
482
+
483
+ test('authenticated subscribers receive events until terminal status', async ({ subscribeOperation, buildUserJwt }) => {
484
+ const jwt = buildUserJwt()
485
+ const jobId = randomUUID()
486
+
487
+ const iterator = await subscribeOperation({ subscription: jobStatusSubscription, variables: { jobId } }, jwt)
488
+
489
+ // app-specific: trigger the events your subscription resolves over
490
+ // await addJob('demo', { demoPayload: 'subscription-test' }, { jobId })
491
+
492
+ const events: JobStatusSubscription[] = []
493
+ for await (const result of iterator) {
494
+ if (result.data) events.push(result.data)
495
+ }
496
+
497
+ expect(events.at(-1)?.jobStatus.status).toBe('Completed')
498
+ })
499
+ })
500
+ ```
501
+
502
+ The first argument is a `TypedSubscribeRequest` (`subscription`, `variables`, `operationName`); any further arguments are forwarded verbatim to the context creation function — in the example above, the optional `JwtPayload` produced by `buildUserJwt()`.
503
+
504
+ When pairing this helper with [`wrapSubscriptionIterator`](#wrapsubscriptioniterator), `eventIsFinal` will close the subscription once a terminal event arrives, ending the `for await` loop above without manual cleanup.
505
+
408
506
  ## \*Express peer dependency
409
507
 
410
508
  ApolloServer v3 standardises on the Express request representation.
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "**"
5
5
  ],
6
6
  "name": "@makerx/graphql-core",
7
- "version": "3.0.0",
7
+ "version": "3.1.0",
8
8
  "private": false,
9
9
  "description": "A set of core GraphQL utilities that MakerX uses to build GraphQL APIs",
10
10
  "author": "MakerX",
@@ -21,6 +21,7 @@
21
21
  "url": "git+https://github.com/MakerXStudio/graphql-core.git"
22
22
  },
23
23
  "peerDependencies": {
24
+ "@graphql-typed-document-node/core": ">=3.2.0",
24
25
  "@makerx/node-common": ">=1",
25
26
  "es-toolkit": ">=1",
26
27
  "express": ">=4",
@@ -74,6 +75,16 @@
74
75
  "types": "./subscriptions/index.d.cts",
75
76
  "default": "./subscriptions/index.js"
76
77
  }
78
+ },
79
+ "./testing": {
80
+ "import": {
81
+ "types": "./testing/index.d.mts",
82
+ "default": "./testing/index.mjs"
83
+ },
84
+ "require": {
85
+ "types": "./testing/index.d.cts",
86
+ "default": "./testing/index.js"
87
+ }
77
88
  }
78
89
  }
79
90
  }
@@ -11,7 +11,9 @@
11
11
  * @param iterator The async iterator to wrap.
12
12
  * @param initialPayload An optional initial payload to return on the first iteration. May be a single event or an array.
13
13
  * @param eventIsFinal An optional function to determine if an event is final (and iteration should end).
14
- * Note: only applied to events from the wrapped iterator, not to entries in `initialPayload`.
14
+ * The full `initialPayload` is always yielded; `eventIsFinal` is only checked against its final entry, after which
15
+ * the wrapped iterator is closed without ever being pulled from. For events from the wrapped iterator, every event
16
+ * is checked.
15
17
  * @returns An async iterator for the event data.
16
18
  */
17
19
  export declare function wrapSubscriptionIterator<TEventData>({ iterator: wrapped, initialPayload, eventIsFinal, }: {
@@ -11,7 +11,9 @@
11
11
  * @param iterator The async iterator to wrap.
12
12
  * @param initialPayload An optional initial payload to return on the first iteration. May be a single event or an array.
13
13
  * @param eventIsFinal An optional function to determine if an event is final (and iteration should end).
14
- * Note: only applied to events from the wrapped iterator, not to entries in `initialPayload`.
14
+ * The full `initialPayload` is always yielded; `eventIsFinal` is only checked against its final entry, after which
15
+ * the wrapped iterator is closed without ever being pulled from. For events from the wrapped iterator, every event
16
+ * is checked.
15
17
  * @returns An async iterator for the event data.
16
18
  */
17
19
  export declare function wrapSubscriptionIterator<TEventData>({ iterator: wrapped, initialPayload, eventIsFinal, }: {
@@ -11,7 +11,9 @@
11
11
  * @param iterator The async iterator to wrap.
12
12
  * @param initialPayload An optional initial payload to return on the first iteration. May be a single event or an array.
13
13
  * @param eventIsFinal An optional function to determine if an event is final (and iteration should end).
14
- * Note: only applied to events from the wrapped iterator, not to entries in `initialPayload`.
14
+ * The full `initialPayload` is always yielded; `eventIsFinal` is only checked against its final entry, after which
15
+ * the wrapped iterator is closed without ever being pulled from. For events from the wrapped iterator, every event
16
+ * is checked.
15
17
  * @returns An async iterator for the event data.
16
18
  */
17
19
  export declare function wrapSubscriptionIterator<TEventData>({ iterator: wrapped, initialPayload, eventIsFinal, }: {
@@ -13,7 +13,9 @@
13
13
  * @param iterator The async iterator to wrap.
14
14
  * @param initialPayload An optional initial payload to return on the first iteration. May be a single event or an array.
15
15
  * @param eventIsFinal An optional function to determine if an event is final (and iteration should end).
16
- * Note: only applied to events from the wrapped iterator, not to entries in `initialPayload`.
16
+ * The full `initialPayload` is always yielded; `eventIsFinal` is only checked against its final entry, after which
17
+ * the wrapped iterator is closed without ever being pulled from. For events from the wrapped iterator, every event
18
+ * is checked.
17
19
  * @returns An async iterator for the event data.
18
20
  */
19
21
  function wrapSubscriptionIterator({ iterator: wrapped, initialPayload, eventIsFinal, }) {
@@ -25,6 +27,10 @@ function wrapSubscriptionIterator({ iterator: wrapped, initialPayload, eventIsFi
25
27
  return { done: true, value: undefined };
26
28
  if (initialQueue.length > 0) {
27
29
  const value = initialQueue.shift();
30
+ if (initialQueue.length === 0 && eventIsFinal && eventIsFinal(value)) {
31
+ done = true;
32
+ await wrapped.return?.();
33
+ }
28
34
  return { done: false, value };
29
35
  }
30
36
  const next = await wrapped.next();
@@ -1 +1 @@
1
- {"version":3,"file":"subscription-iterator.js","sources":["../../src/subscriptions/subscription-iterator.ts"],"sourcesContent":["/**\n * Wraps an async iterator with options for:\n * - returning an initial payload (e.g. current state)\n * - eagerly ending iteration when a final event is received\n *\n * Providing an initial payload helps avoid race conditions when clients subscribe to events but miss recent state changes,\n * by providing an initial payload to be immediately returned, before subsequent events stream in.\n *\n * Eagerly ending iteration when a final event ensures subscriptions do not continue unnecessarily.\n *\n * @param iterator The async iterator to wrap.\n * @param initialPayload An optional initial payload to return on the first iteration. May be a single event or an array.\n * @param eventIsFinal An optional function to determine if an event is final (and iteration should end).\n * Note: only applied to events from the wrapped iterator, not to entries in `initialPayload`.\n * @returns An async iterator for the event data.\n */\nexport function wrapSubscriptionIterator<TEventData>({\n iterator: wrapped,\n initialPayload,\n eventIsFinal,\n}: {\n iterator: AsyncIterableIterator<TEventData>\n initialPayload?: TEventData | TEventData[]\n eventIsFinal?: (event: TEventData) => boolean\n}): AsyncIterator<TEventData> & AsyncIterable<TEventData> {\n let done = false\n const initialQueue: TEventData[] =\n initialPayload === undefined ? [] : Array.isArray(initialPayload) ? [...initialPayload] : [initialPayload]\n\n const iterator: AsyncIterator<TEventData> = {\n next: async () => {\n if (done) return { done: true, value: undefined }\n\n if (initialQueue.length > 0) {\n const value = initialQueue.shift() as TEventData\n return { done: false, value }\n }\n\n const next = await wrapped.next()\n if (!next.done && eventIsFinal && eventIsFinal(next.value)) {\n done = true\n await wrapped.return?.()\n }\n return next\n },\n return: async (value) => {\n done = true\n initialQueue.length = 0\n if (wrapped.return) return wrapped.return(value)\n return { done: true, value: value as TEventData }\n },\n throw: async (err) => {\n done = true\n initialQueue.length = 0\n if (wrapped.throw) return wrapped.throw(err)\n throw err\n },\n }\n\n return { ...iterator, [Symbol.asyncIterator]: () => iterator }\n}\n"],"names":[],"mappings":";;AAAA;;;;;;;;;;;;;;;AAeG;AACG,SAAU,wBAAwB,CAAa,EACnD,QAAQ,EAAE,OAAO,EACjB,cAAc,EACd,YAAY,GAKb,EAAA;IACC,IAAI,IAAI,GAAG,KAAK;AAChB,IAAA,MAAM,YAAY,GAChB,cAAc,KAAK,SAAS,GAAG,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,cAAc,CAAC,GAAG,CAAC,cAAc,CAAC;AAE5G,IAAA,MAAM,QAAQ,GAA8B;QAC1C,IAAI,EAAE,YAAW;AACf,YAAA,IAAI,IAAI;gBAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE;AAEjD,YAAA,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE;AAC3B,gBAAA,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,EAAgB;AAChD,gBAAA,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE;YAC/B;AAEA,YAAA,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE;AACjC,YAAA,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,YAAY,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE;gBAC1D,IAAI,GAAG,IAAI;AACX,gBAAA,MAAM,OAAO,CAAC,MAAM,IAAI;YAC1B;AACA,YAAA,OAAO,IAAI;QACb,CAAC;AACD,QAAA,MAAM,EAAE,OAAO,KAAK,KAAI;YACtB,IAAI,GAAG,IAAI;AACX,YAAA,YAAY,CAAC,MAAM,GAAG,CAAC;YACvB,IAAI,OAAO,CAAC,MAAM;AAAE,gBAAA,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC;YAChD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAmB,EAAE;QACnD,CAAC;AACD,QAAA,KAAK,EAAE,OAAO,GAAG,KAAI;YACnB,IAAI,GAAG,IAAI;AACX,YAAA,YAAY,CAAC,MAAM,GAAG,CAAC;YACvB,IAAI,OAAO,CAAC,KAAK;AAAE,gBAAA,OAAO,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC;AAC5C,YAAA,MAAM,GAAG;QACX,CAAC;KACF;AAED,IAAA,OAAO,EAAE,GAAG,QAAQ,EAAE,CAAC,MAAM,CAAC,aAAa,GAAG,MAAM,QAAQ,EAAE;AAChE;;;;"}
1
+ {"version":3,"file":"subscription-iterator.js","sources":["../../src/subscriptions/subscription-iterator.ts"],"sourcesContent":["/**\n * Wraps an async iterator with options for:\n * - returning an initial payload (e.g. current state)\n * - eagerly ending iteration when a final event is received\n *\n * Providing an initial payload helps avoid race conditions when clients subscribe to events but miss recent state changes,\n * by providing an initial payload to be immediately returned, before subsequent events stream in.\n *\n * Eagerly ending iteration when a final event ensures subscriptions do not continue unnecessarily.\n *\n * @param iterator The async iterator to wrap.\n * @param initialPayload An optional initial payload to return on the first iteration. May be a single event or an array.\n * @param eventIsFinal An optional function to determine if an event is final (and iteration should end).\n * The full `initialPayload` is always yielded; `eventIsFinal` is only checked against its final entry, after which\n * the wrapped iterator is closed without ever being pulled from. For events from the wrapped iterator, every event\n * is checked.\n * @returns An async iterator for the event data.\n */\nexport function wrapSubscriptionIterator<TEventData>({\n iterator: wrapped,\n initialPayload,\n eventIsFinal,\n}: {\n iterator: AsyncIterableIterator<TEventData>\n initialPayload?: TEventData | TEventData[]\n eventIsFinal?: (event: TEventData) => boolean\n}): AsyncIterator<TEventData> & AsyncIterable<TEventData> {\n let done = false\n const initialQueue: TEventData[] =\n initialPayload === undefined ? [] : Array.isArray(initialPayload) ? [...initialPayload] : [initialPayload]\n\n const iterator: AsyncIterator<TEventData> = {\n next: async () => {\n if (done) return { done: true, value: undefined }\n\n if (initialQueue.length > 0) {\n const value = initialQueue.shift() as TEventData\n if (initialQueue.length === 0 && eventIsFinal && eventIsFinal(value)) {\n done = true\n await wrapped.return?.()\n }\n return { done: false, value }\n }\n\n const next = await wrapped.next()\n if (!next.done && eventIsFinal && eventIsFinal(next.value)) {\n done = true\n await wrapped.return?.()\n }\n return next\n },\n return: async (value) => {\n done = true\n initialQueue.length = 0\n if (wrapped.return) return wrapped.return(value)\n return { done: true, value: value as TEventData }\n },\n throw: async (err) => {\n done = true\n initialQueue.length = 0\n if (wrapped.throw) return wrapped.throw(err)\n throw err\n },\n }\n\n return { ...iterator, [Symbol.asyncIterator]: () => iterator }\n}\n"],"names":[],"mappings":";;AAAA;;;;;;;;;;;;;;;;;AAiBG;AACG,SAAU,wBAAwB,CAAa,EACnD,QAAQ,EAAE,OAAO,EACjB,cAAc,EACd,YAAY,GAKb,EAAA;IACC,IAAI,IAAI,GAAG,KAAK;AAChB,IAAA,MAAM,YAAY,GAChB,cAAc,KAAK,SAAS,GAAG,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,cAAc,CAAC,GAAG,CAAC,cAAc,CAAC;AAE5G,IAAA,MAAM,QAAQ,GAA8B;QAC1C,IAAI,EAAE,YAAW;AACf,YAAA,IAAI,IAAI;gBAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE;AAEjD,YAAA,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE;AAC3B,gBAAA,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,EAAgB;AAChD,gBAAA,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,IAAI,YAAY,IAAI,YAAY,CAAC,KAAK,CAAC,EAAE;oBACpE,IAAI,GAAG,IAAI;AACX,oBAAA,MAAM,OAAO,CAAC,MAAM,IAAI;gBAC1B;AACA,gBAAA,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE;YAC/B;AAEA,YAAA,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE;AACjC,YAAA,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,YAAY,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE;gBAC1D,IAAI,GAAG,IAAI;AACX,gBAAA,MAAM,OAAO,CAAC,MAAM,IAAI;YAC1B;AACA,YAAA,OAAO,IAAI;QACb,CAAC;AACD,QAAA,MAAM,EAAE,OAAO,KAAK,KAAI;YACtB,IAAI,GAAG,IAAI;AACX,YAAA,YAAY,CAAC,MAAM,GAAG,CAAC;YACvB,IAAI,OAAO,CAAC,MAAM;AAAE,gBAAA,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC;YAChD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAmB,EAAE;QACnD,CAAC;AACD,QAAA,KAAK,EAAE,OAAO,GAAG,KAAI;YACnB,IAAI,GAAG,IAAI;AACX,YAAA,YAAY,CAAC,MAAM,GAAG,CAAC;YACvB,IAAI,OAAO,CAAC,KAAK;AAAE,gBAAA,OAAO,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC;AAC5C,YAAA,MAAM,GAAG;QACX,CAAC;KACF;AAED,IAAA,OAAO,EAAE,GAAG,QAAQ,EAAE,CAAC,MAAM,CAAC,aAAa,GAAG,MAAM,QAAQ,EAAE;AAChE;;;;"}
@@ -11,7 +11,9 @@
11
11
  * @param iterator The async iterator to wrap.
12
12
  * @param initialPayload An optional initial payload to return on the first iteration. May be a single event or an array.
13
13
  * @param eventIsFinal An optional function to determine if an event is final (and iteration should end).
14
- * Note: only applied to events from the wrapped iterator, not to entries in `initialPayload`.
14
+ * The full `initialPayload` is always yielded; `eventIsFinal` is only checked against its final entry, after which
15
+ * the wrapped iterator is closed without ever being pulled from. For events from the wrapped iterator, every event
16
+ * is checked.
15
17
  * @returns An async iterator for the event data.
16
18
  */
17
19
  function wrapSubscriptionIterator({ iterator: wrapped, initialPayload, eventIsFinal, }) {
@@ -23,6 +25,10 @@ function wrapSubscriptionIterator({ iterator: wrapped, initialPayload, eventIsFi
23
25
  return { done: true, value: undefined };
24
26
  if (initialQueue.length > 0) {
25
27
  const value = initialQueue.shift();
28
+ if (initialQueue.length === 0 && eventIsFinal && eventIsFinal(value)) {
29
+ done = true;
30
+ await wrapped.return?.();
31
+ }
26
32
  return { done: false, value };
27
33
  }
28
34
  const next = await wrapped.next();
@@ -1 +1 @@
1
- {"version":3,"file":"subscription-iterator.mjs","sources":["../../src/subscriptions/subscription-iterator.ts"],"sourcesContent":["/**\n * Wraps an async iterator with options for:\n * - returning an initial payload (e.g. current state)\n * - eagerly ending iteration when a final event is received\n *\n * Providing an initial payload helps avoid race conditions when clients subscribe to events but miss recent state changes,\n * by providing an initial payload to be immediately returned, before subsequent events stream in.\n *\n * Eagerly ending iteration when a final event ensures subscriptions do not continue unnecessarily.\n *\n * @param iterator The async iterator to wrap.\n * @param initialPayload An optional initial payload to return on the first iteration. May be a single event or an array.\n * @param eventIsFinal An optional function to determine if an event is final (and iteration should end).\n * Note: only applied to events from the wrapped iterator, not to entries in `initialPayload`.\n * @returns An async iterator for the event data.\n */\nexport function wrapSubscriptionIterator<TEventData>({\n iterator: wrapped,\n initialPayload,\n eventIsFinal,\n}: {\n iterator: AsyncIterableIterator<TEventData>\n initialPayload?: TEventData | TEventData[]\n eventIsFinal?: (event: TEventData) => boolean\n}): AsyncIterator<TEventData> & AsyncIterable<TEventData> {\n let done = false\n const initialQueue: TEventData[] =\n initialPayload === undefined ? [] : Array.isArray(initialPayload) ? [...initialPayload] : [initialPayload]\n\n const iterator: AsyncIterator<TEventData> = {\n next: async () => {\n if (done) return { done: true, value: undefined }\n\n if (initialQueue.length > 0) {\n const value = initialQueue.shift() as TEventData\n return { done: false, value }\n }\n\n const next = await wrapped.next()\n if (!next.done && eventIsFinal && eventIsFinal(next.value)) {\n done = true\n await wrapped.return?.()\n }\n return next\n },\n return: async (value) => {\n done = true\n initialQueue.length = 0\n if (wrapped.return) return wrapped.return(value)\n return { done: true, value: value as TEventData }\n },\n throw: async (err) => {\n done = true\n initialQueue.length = 0\n if (wrapped.throw) return wrapped.throw(err)\n throw err\n },\n }\n\n return { ...iterator, [Symbol.asyncIterator]: () => iterator }\n}\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;AAeG;AACG,SAAU,wBAAwB,CAAa,EACnD,QAAQ,EAAE,OAAO,EACjB,cAAc,EACd,YAAY,GAKb,EAAA;IACC,IAAI,IAAI,GAAG,KAAK;AAChB,IAAA,MAAM,YAAY,GAChB,cAAc,KAAK,SAAS,GAAG,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,cAAc,CAAC,GAAG,CAAC,cAAc,CAAC;AAE5G,IAAA,MAAM,QAAQ,GAA8B;QAC1C,IAAI,EAAE,YAAW;AACf,YAAA,IAAI,IAAI;gBAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE;AAEjD,YAAA,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE;AAC3B,gBAAA,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,EAAgB;AAChD,gBAAA,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE;YAC/B;AAEA,YAAA,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE;AACjC,YAAA,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,YAAY,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE;gBAC1D,IAAI,GAAG,IAAI;AACX,gBAAA,MAAM,OAAO,CAAC,MAAM,IAAI;YAC1B;AACA,YAAA,OAAO,IAAI;QACb,CAAC;AACD,QAAA,MAAM,EAAE,OAAO,KAAK,KAAI;YACtB,IAAI,GAAG,IAAI;AACX,YAAA,YAAY,CAAC,MAAM,GAAG,CAAC;YACvB,IAAI,OAAO,CAAC,MAAM;AAAE,gBAAA,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC;YAChD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAmB,EAAE;QACnD,CAAC;AACD,QAAA,KAAK,EAAE,OAAO,GAAG,KAAI;YACnB,IAAI,GAAG,IAAI;AACX,YAAA,YAAY,CAAC,MAAM,GAAG,CAAC;YACvB,IAAI,OAAO,CAAC,KAAK;AAAE,gBAAA,OAAO,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC;AAC5C,YAAA,MAAM,GAAG;QACX,CAAC;KACF;AAED,IAAA,OAAO,EAAE,GAAG,QAAQ,EAAE,CAAC,MAAM,CAAC,aAAa,GAAG,MAAM,QAAQ,EAAE;AAChE;;;;"}
1
+ {"version":3,"file":"subscription-iterator.mjs","sources":["../../src/subscriptions/subscription-iterator.ts"],"sourcesContent":["/**\n * Wraps an async iterator with options for:\n * - returning an initial payload (e.g. current state)\n * - eagerly ending iteration when a final event is received\n *\n * Providing an initial payload helps avoid race conditions when clients subscribe to events but miss recent state changes,\n * by providing an initial payload to be immediately returned, before subsequent events stream in.\n *\n * Eagerly ending iteration when a final event ensures subscriptions do not continue unnecessarily.\n *\n * @param iterator The async iterator to wrap.\n * @param initialPayload An optional initial payload to return on the first iteration. May be a single event or an array.\n * @param eventIsFinal An optional function to determine if an event is final (and iteration should end).\n * The full `initialPayload` is always yielded; `eventIsFinal` is only checked against its final entry, after which\n * the wrapped iterator is closed without ever being pulled from. For events from the wrapped iterator, every event\n * is checked.\n * @returns An async iterator for the event data.\n */\nexport function wrapSubscriptionIterator<TEventData>({\n iterator: wrapped,\n initialPayload,\n eventIsFinal,\n}: {\n iterator: AsyncIterableIterator<TEventData>\n initialPayload?: TEventData | TEventData[]\n eventIsFinal?: (event: TEventData) => boolean\n}): AsyncIterator<TEventData> & AsyncIterable<TEventData> {\n let done = false\n const initialQueue: TEventData[] =\n initialPayload === undefined ? [] : Array.isArray(initialPayload) ? [...initialPayload] : [initialPayload]\n\n const iterator: AsyncIterator<TEventData> = {\n next: async () => {\n if (done) return { done: true, value: undefined }\n\n if (initialQueue.length > 0) {\n const value = initialQueue.shift() as TEventData\n if (initialQueue.length === 0 && eventIsFinal && eventIsFinal(value)) {\n done = true\n await wrapped.return?.()\n }\n return { done: false, value }\n }\n\n const next = await wrapped.next()\n if (!next.done && eventIsFinal && eventIsFinal(next.value)) {\n done = true\n await wrapped.return?.()\n }\n return next\n },\n return: async (value) => {\n done = true\n initialQueue.length = 0\n if (wrapped.return) return wrapped.return(value)\n return { done: true, value: value as TEventData }\n },\n throw: async (err) => {\n done = true\n initialQueue.length = 0\n if (wrapped.throw) return wrapped.throw(err)\n throw err\n },\n }\n\n return { ...iterator, [Symbol.asyncIterator]: () => iterator }\n}\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;AAiBG;AACG,SAAU,wBAAwB,CAAa,EACnD,QAAQ,EAAE,OAAO,EACjB,cAAc,EACd,YAAY,GAKb,EAAA;IACC,IAAI,IAAI,GAAG,KAAK;AAChB,IAAA,MAAM,YAAY,GAChB,cAAc,KAAK,SAAS,GAAG,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,cAAc,CAAC,GAAG,CAAC,cAAc,CAAC;AAE5G,IAAA,MAAM,QAAQ,GAA8B;QAC1C,IAAI,EAAE,YAAW;AACf,YAAA,IAAI,IAAI;gBAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE;AAEjD,YAAA,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE;AAC3B,gBAAA,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,EAAgB;AAChD,gBAAA,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,IAAI,YAAY,IAAI,YAAY,CAAC,KAAK,CAAC,EAAE;oBACpE,IAAI,GAAG,IAAI;AACX,oBAAA,MAAM,OAAO,CAAC,MAAM,IAAI;gBAC1B;AACA,gBAAA,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE;YAC/B;AAEA,YAAA,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE;AACjC,YAAA,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,YAAY,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE;gBAC1D,IAAI,GAAG,IAAI;AACX,gBAAA,MAAM,OAAO,CAAC,MAAM,IAAI;YAC1B;AACA,YAAA,OAAO,IAAI;QACb,CAAC;AACD,QAAA,MAAM,EAAE,OAAO,KAAK,KAAI;YACtB,IAAI,GAAG,IAAI;AACX,YAAA,YAAY,CAAC,MAAM,GAAG,CAAC;YACvB,IAAI,OAAO,CAAC,MAAM;AAAE,gBAAA,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC;YAChD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAmB,EAAE;QACnD,CAAC;AACD,QAAA,KAAK,EAAE,OAAO,GAAG,KAAI;YACnB,IAAI,GAAG,IAAI;AACX,YAAA,YAAY,CAAC,MAAM,GAAG,CAAC;YACvB,IAAI,OAAO,CAAC,KAAK;AAAE,gBAAA,OAAO,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC;AAC5C,YAAA,MAAM,GAAG;QACX,CAAC;KACF;AAED,IAAA,OAAO,EAAE,GAAG,QAAQ,EAAE,CAAC,MAAM,CAAC,aAAa,GAAG,MAAM,QAAQ,EAAE;AAChE;;;;"}
@@ -0,0 +1 @@
1
+ export * from './subscribe-operation';
@@ -0,0 +1 @@
1
+ export * from './subscribe-operation.mjs';
@@ -0,0 +1 @@
1
+ export * from './subscribe-operation';
@@ -0,0 +1,8 @@
1
+ 'use strict';
2
+
3
+ var subscribeOperation = require('./subscribe-operation.js');
4
+
5
+
6
+
7
+ exports.buildSubscribeOperation = subscribeOperation.buildSubscribeOperation;
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;"}
@@ -0,0 +1,2 @@
1
+ export { buildSubscribeOperation } from './subscribe-operation.mjs';
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":""}
@@ -0,0 +1,31 @@
1
+ import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
2
+ import { type DocumentNode, type ExecutionResult, type GraphQLSchema } from 'graphql';
3
+ import type { AnyGraphqlContext } from '../context';
4
+ export type VariableValues = {
5
+ [key: string]: any;
6
+ };
7
+ export type TypedSubscribeRequest<TData = Record<string, unknown>, TVariables extends VariableValues = VariableValues> = {
8
+ subscription: string | DocumentNode | TypedDocumentNode<TData, TVariables>;
9
+ variables?: TVariables;
10
+ operationName?: string;
11
+ };
12
+ /**
13
+ * Returns a `subscribeOperation` test helper for the provided schema and context creation function.
14
+ *
15
+ * The returned function runs a subscription operation directly against the schema using `graphql.subscribe`,
16
+ * bypassing the websocket / `graphql-ws` transport. This gives tests complete control over JWT payloads
17
+ * and other context inputs, the same way `buildExecuteOperation` does for queries and mutations in
18
+ * `@makerx/graphql-apollo-server/testing`.
19
+ *
20
+ * The returned function:
21
+ * - is strongly typed to the GraphQL context
22
+ * - accepts `TypedDocumentNode` subscriptions to provide strong operation typing
23
+ * - forwards any additional arguments to the supplied context creation function
24
+ * - returns the subscription's `AsyncIterableIterator<ExecutionResult<TData>>` so tests can consume events via `for await` or manual `next()` calls
25
+ * - throws if the subscription fails to produce an iterator (e.g. a validation error), surfacing the underlying errors
26
+ *
27
+ * @param schema The executable `GraphQLSchema`, or a factory that resolves one (useful when schema construction is async).
28
+ * @param createContext A context creation function. Any args supplied after the request are forwarded to it,
29
+ * so test fixtures can drive auth/user state per-call (e.g. `(jwtPayload?) => Promise<GraphQLContext>`).
30
+ */
31
+ export declare function buildSubscribeOperation<TContext extends AnyGraphqlContext, TContextFunction extends (...args: any) => Promise<TContext>>(schema: GraphQLSchema | (() => Promise<GraphQLSchema>), createContext: TContextFunction): <TData = Record<string, unknown>, TVariables extends VariableValues = VariableValues>({ subscription, variables, operationName }: TypedSubscribeRequest<TData, TVariables>, ...createContextArgs: Parameters<TContextFunction>) => Promise<AsyncIterableIterator<ExecutionResult<TData>>>;
@@ -0,0 +1,31 @@
1
+ import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
2
+ import { type DocumentNode, type ExecutionResult, type GraphQLSchema } from 'graphql';
3
+ import type { AnyGraphqlContext } from '../context.mjs';
4
+ export type VariableValues = {
5
+ [key: string]: any;
6
+ };
7
+ export type TypedSubscribeRequest<TData = Record<string, unknown>, TVariables extends VariableValues = VariableValues> = {
8
+ subscription: string | DocumentNode | TypedDocumentNode<TData, TVariables>;
9
+ variables?: TVariables;
10
+ operationName?: string;
11
+ };
12
+ /**
13
+ * Returns a `subscribeOperation` test helper for the provided schema and context creation function.
14
+ *
15
+ * The returned function runs a subscription operation directly against the schema using `graphql.subscribe`,
16
+ * bypassing the websocket / `graphql-ws` transport. This gives tests complete control over JWT payloads
17
+ * and other context inputs, the same way `buildExecuteOperation` does for queries and mutations in
18
+ * `@makerx/graphql-apollo-server/testing`.
19
+ *
20
+ * The returned function:
21
+ * - is strongly typed to the GraphQL context
22
+ * - accepts `TypedDocumentNode` subscriptions to provide strong operation typing
23
+ * - forwards any additional arguments to the supplied context creation function
24
+ * - returns the subscription's `AsyncIterableIterator<ExecutionResult<TData>>` so tests can consume events via `for await` or manual `next()` calls
25
+ * - throws if the subscription fails to produce an iterator (e.g. a validation error), surfacing the underlying errors
26
+ *
27
+ * @param schema The executable `GraphQLSchema`, or a factory that resolves one (useful when schema construction is async).
28
+ * @param createContext A context creation function. Any args supplied after the request are forwarded to it,
29
+ * so test fixtures can drive auth/user state per-call (e.g. `(jwtPayload?) => Promise<GraphQLContext>`).
30
+ */
31
+ export declare function buildSubscribeOperation<TContext extends AnyGraphqlContext, TContextFunction extends (...args: any) => Promise<TContext>>(schema: GraphQLSchema | (() => Promise<GraphQLSchema>), createContext: TContextFunction): <TData = Record<string, unknown>, TVariables extends VariableValues = VariableValues>({ subscription, variables, operationName }: TypedSubscribeRequest<TData, TVariables>, ...createContextArgs: Parameters<TContextFunction>) => Promise<AsyncIterableIterator<ExecutionResult<TData>>>;
@@ -0,0 +1,31 @@
1
+ import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
2
+ import { type DocumentNode, type ExecutionResult, type GraphQLSchema } from 'graphql';
3
+ import type { AnyGraphqlContext } from '../context';
4
+ export type VariableValues = {
5
+ [key: string]: any;
6
+ };
7
+ export type TypedSubscribeRequest<TData = Record<string, unknown>, TVariables extends VariableValues = VariableValues> = {
8
+ subscription: string | DocumentNode | TypedDocumentNode<TData, TVariables>;
9
+ variables?: TVariables;
10
+ operationName?: string;
11
+ };
12
+ /**
13
+ * Returns a `subscribeOperation` test helper for the provided schema and context creation function.
14
+ *
15
+ * The returned function runs a subscription operation directly against the schema using `graphql.subscribe`,
16
+ * bypassing the websocket / `graphql-ws` transport. This gives tests complete control over JWT payloads
17
+ * and other context inputs, the same way `buildExecuteOperation` does for queries and mutations in
18
+ * `@makerx/graphql-apollo-server/testing`.
19
+ *
20
+ * The returned function:
21
+ * - is strongly typed to the GraphQL context
22
+ * - accepts `TypedDocumentNode` subscriptions to provide strong operation typing
23
+ * - forwards any additional arguments to the supplied context creation function
24
+ * - returns the subscription's `AsyncIterableIterator<ExecutionResult<TData>>` so tests can consume events via `for await` or manual `next()` calls
25
+ * - throws if the subscription fails to produce an iterator (e.g. a validation error), surfacing the underlying errors
26
+ *
27
+ * @param schema The executable `GraphQLSchema`, or a factory that resolves one (useful when schema construction is async).
28
+ * @param createContext A context creation function. Any args supplied after the request are forwarded to it,
29
+ * so test fixtures can drive auth/user state per-call (e.g. `(jwtPayload?) => Promise<GraphQLContext>`).
30
+ */
31
+ export declare function buildSubscribeOperation<TContext extends AnyGraphqlContext, TContextFunction extends (...args: any) => Promise<TContext>>(schema: GraphQLSchema | (() => Promise<GraphQLSchema>), createContext: TContextFunction): <TData = Record<string, unknown>, TVariables extends VariableValues = VariableValues>({ subscription, variables, operationName }: TypedSubscribeRequest<TData, TVariables>, ...createContextArgs: Parameters<TContextFunction>) => Promise<AsyncIterableIterator<ExecutionResult<TData>>>;
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ var graphql = require('graphql');
4
+
5
+ /**
6
+ * Returns a `subscribeOperation` test helper for the provided schema and context creation function.
7
+ *
8
+ * The returned function runs a subscription operation directly against the schema using `graphql.subscribe`,
9
+ * bypassing the websocket / `graphql-ws` transport. This gives tests complete control over JWT payloads
10
+ * and other context inputs, the same way `buildExecuteOperation` does for queries and mutations in
11
+ * `@makerx/graphql-apollo-server/testing`.
12
+ *
13
+ * The returned function:
14
+ * - is strongly typed to the GraphQL context
15
+ * - accepts `TypedDocumentNode` subscriptions to provide strong operation typing
16
+ * - forwards any additional arguments to the supplied context creation function
17
+ * - returns the subscription's `AsyncIterableIterator<ExecutionResult<TData>>` so tests can consume events via `for await` or manual `next()` calls
18
+ * - throws if the subscription fails to produce an iterator (e.g. a validation error), surfacing the underlying errors
19
+ *
20
+ * @param schema The executable `GraphQLSchema`, or a factory that resolves one (useful when schema construction is async).
21
+ * @param createContext A context creation function. Any args supplied after the request are forwarded to it,
22
+ * so test fixtures can drive auth/user state per-call (e.g. `(jwtPayload?) => Promise<GraphQLContext>`).
23
+ */
24
+ function buildSubscribeOperation(schema, createContext) {
25
+ return async function subscribeOperation({ subscription, variables, operationName }, ...createContextArgs) {
26
+ const resolvedSchema = typeof schema === 'function' ? await schema() : schema;
27
+ const document = typeof subscription === 'string' ? graphql.parse(subscription) : subscription;
28
+ const contextValue = await createContext(...createContextArgs);
29
+ const result = await graphql.subscribe({
30
+ schema: resolvedSchema,
31
+ document,
32
+ contextValue,
33
+ variableValues: variables,
34
+ operationName,
35
+ });
36
+ if (Symbol.asyncIterator in result)
37
+ return result;
38
+ throw new Error(`Subscription did not produce an iterator: ${JSON.stringify(result.errors)}`);
39
+ };
40
+ }
41
+
42
+ exports.buildSubscribeOperation = buildSubscribeOperation;
43
+ //# sourceMappingURL=subscribe-operation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscribe-operation.js","sources":["../../src/testing/subscribe-operation.ts"],"sourcesContent":["import type { TypedDocumentNode } from '@graphql-typed-document-node/core'\nimport { parse, subscribe, type DocumentNode, type ExecutionResult, type GraphQLSchema } from 'graphql'\nimport type { AnyGraphqlContext } from '../context'\n\nexport type VariableValues = { [key: string]: any }\n\nexport type TypedSubscribeRequest<TData = Record<string, unknown>, TVariables extends VariableValues = VariableValues> = {\n subscription: string | DocumentNode | TypedDocumentNode<TData, TVariables>\n variables?: TVariables\n operationName?: string\n}\n\n/**\n * Returns a `subscribeOperation` test helper for the provided schema and context creation function.\n *\n * The returned function runs a subscription operation directly against the schema using `graphql.subscribe`,\n * bypassing the websocket / `graphql-ws` transport. This gives tests complete control over JWT payloads\n * and other context inputs, the same way `buildExecuteOperation` does for queries and mutations in\n * `@makerx/graphql-apollo-server/testing`.\n *\n * The returned function:\n * - is strongly typed to the GraphQL context\n * - accepts `TypedDocumentNode` subscriptions to provide strong operation typing\n * - forwards any additional arguments to the supplied context creation function\n * - returns the subscription's `AsyncIterableIterator<ExecutionResult<TData>>` so tests can consume events via `for await` or manual `next()` calls\n * - throws if the subscription fails to produce an iterator (e.g. a validation error), surfacing the underlying errors\n *\n * @param schema The executable `GraphQLSchema`, or a factory that resolves one (useful when schema construction is async).\n * @param createContext A context creation function. Any args supplied after the request are forwarded to it,\n * so test fixtures can drive auth/user state per-call (e.g. `(jwtPayload?) => Promise<GraphQLContext>`).\n */\nexport function buildSubscribeOperation<TContext extends AnyGraphqlContext, TContextFunction extends (...args: any) => Promise<TContext>>(\n schema: GraphQLSchema | (() => Promise<GraphQLSchema>),\n createContext: TContextFunction,\n) {\n return async function subscribeOperation<TData = Record<string, unknown>, TVariables extends VariableValues = VariableValues>(\n { subscription, variables, operationName }: TypedSubscribeRequest<TData, TVariables>,\n ...createContextArgs: Parameters<TContextFunction>\n ): Promise<AsyncIterableIterator<ExecutionResult<TData>>> {\n const resolvedSchema = typeof schema === 'function' ? await schema() : schema\n const document = typeof subscription === 'string' ? parse(subscription) : subscription\n const contextValue = await createContext(...createContextArgs)\n const result = await subscribe({\n schema: resolvedSchema,\n document,\n contextValue,\n variableValues: variables as VariableValues | undefined,\n operationName,\n })\n if (Symbol.asyncIterator in result) return result as AsyncIterableIterator<ExecutionResult<TData>>\n throw new Error(`Subscription did not produce an iterator: ${JSON.stringify((result as ExecutionResult).errors)}`)\n }\n}\n"],"names":["parse","subscribe"],"mappings":";;;;AAYA;;;;;;;;;;;;;;;;;;AAkBG;AACG,SAAU,uBAAuB,CACrC,MAAsD,EACtD,aAA+B,EAAA;AAE/B,IAAA,OAAO,eAAe,kBAAkB,CACtC,EAAE,YAAY,EAAE,SAAS,EAAE,aAAa,EAA4C,EACpF,GAAG,iBAA+C,EAAA;AAElD,QAAA,MAAM,cAAc,GAAG,OAAO,MAAM,KAAK,UAAU,GAAG,MAAM,MAAM,EAAE,GAAG,MAAM;AAC7E,QAAA,MAAM,QAAQ,GAAG,OAAO,YAAY,KAAK,QAAQ,GAAGA,aAAK,CAAC,YAAY,CAAC,GAAG,YAAY;QACtF,MAAM,YAAY,GAAG,MAAM,aAAa,CAAC,GAAG,iBAAiB,CAAC;AAC9D,QAAA,MAAM,MAAM,GAAG,MAAMC,iBAAS,CAAC;AAC7B,YAAA,MAAM,EAAE,cAAc;YACtB,QAAQ;YACR,YAAY;AACZ,YAAA,cAAc,EAAE,SAAuC;YACvD,aAAa;AACd,SAAA,CAAC;AACF,QAAA,IAAI,MAAM,CAAC,aAAa,IAAI,MAAM;AAAE,YAAA,OAAO,MAAuD;AAClG,QAAA,MAAM,IAAI,KAAK,CAAC,CAAA,0CAAA,EAA6C,IAAI,CAAC,SAAS,CAAE,MAA0B,CAAC,MAAM,CAAC,CAAA,CAAE,CAAC;AACpH,IAAA,CAAC;AACH;;;;"}
@@ -0,0 +1,41 @@
1
+ import { parse, subscribe } from 'graphql';
2
+
3
+ /**
4
+ * Returns a `subscribeOperation` test helper for the provided schema and context creation function.
5
+ *
6
+ * The returned function runs a subscription operation directly against the schema using `graphql.subscribe`,
7
+ * bypassing the websocket / `graphql-ws` transport. This gives tests complete control over JWT payloads
8
+ * and other context inputs, the same way `buildExecuteOperation` does for queries and mutations in
9
+ * `@makerx/graphql-apollo-server/testing`.
10
+ *
11
+ * The returned function:
12
+ * - is strongly typed to the GraphQL context
13
+ * - accepts `TypedDocumentNode` subscriptions to provide strong operation typing
14
+ * - forwards any additional arguments to the supplied context creation function
15
+ * - returns the subscription's `AsyncIterableIterator<ExecutionResult<TData>>` so tests can consume events via `for await` or manual `next()` calls
16
+ * - throws if the subscription fails to produce an iterator (e.g. a validation error), surfacing the underlying errors
17
+ *
18
+ * @param schema The executable `GraphQLSchema`, or a factory that resolves one (useful when schema construction is async).
19
+ * @param createContext A context creation function. Any args supplied after the request are forwarded to it,
20
+ * so test fixtures can drive auth/user state per-call (e.g. `(jwtPayload?) => Promise<GraphQLContext>`).
21
+ */
22
+ function buildSubscribeOperation(schema, createContext) {
23
+ return async function subscribeOperation({ subscription, variables, operationName }, ...createContextArgs) {
24
+ const resolvedSchema = typeof schema === 'function' ? await schema() : schema;
25
+ const document = typeof subscription === 'string' ? parse(subscription) : subscription;
26
+ const contextValue = await createContext(...createContextArgs);
27
+ const result = await subscribe({
28
+ schema: resolvedSchema,
29
+ document,
30
+ contextValue,
31
+ variableValues: variables,
32
+ operationName,
33
+ });
34
+ if (Symbol.asyncIterator in result)
35
+ return result;
36
+ throw new Error(`Subscription did not produce an iterator: ${JSON.stringify(result.errors)}`);
37
+ };
38
+ }
39
+
40
+ export { buildSubscribeOperation };
41
+ //# sourceMappingURL=subscribe-operation.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscribe-operation.mjs","sources":["../../src/testing/subscribe-operation.ts"],"sourcesContent":["import type { TypedDocumentNode } from '@graphql-typed-document-node/core'\nimport { parse, subscribe, type DocumentNode, type ExecutionResult, type GraphQLSchema } from 'graphql'\nimport type { AnyGraphqlContext } from '../context'\n\nexport type VariableValues = { [key: string]: any }\n\nexport type TypedSubscribeRequest<TData = Record<string, unknown>, TVariables extends VariableValues = VariableValues> = {\n subscription: string | DocumentNode | TypedDocumentNode<TData, TVariables>\n variables?: TVariables\n operationName?: string\n}\n\n/**\n * Returns a `subscribeOperation` test helper for the provided schema and context creation function.\n *\n * The returned function runs a subscription operation directly against the schema using `graphql.subscribe`,\n * bypassing the websocket / `graphql-ws` transport. This gives tests complete control over JWT payloads\n * and other context inputs, the same way `buildExecuteOperation` does for queries and mutations in\n * `@makerx/graphql-apollo-server/testing`.\n *\n * The returned function:\n * - is strongly typed to the GraphQL context\n * - accepts `TypedDocumentNode` subscriptions to provide strong operation typing\n * - forwards any additional arguments to the supplied context creation function\n * - returns the subscription's `AsyncIterableIterator<ExecutionResult<TData>>` so tests can consume events via `for await` or manual `next()` calls\n * - throws if the subscription fails to produce an iterator (e.g. a validation error), surfacing the underlying errors\n *\n * @param schema The executable `GraphQLSchema`, or a factory that resolves one (useful when schema construction is async).\n * @param createContext A context creation function. Any args supplied after the request are forwarded to it,\n * so test fixtures can drive auth/user state per-call (e.g. `(jwtPayload?) => Promise<GraphQLContext>`).\n */\nexport function buildSubscribeOperation<TContext extends AnyGraphqlContext, TContextFunction extends (...args: any) => Promise<TContext>>(\n schema: GraphQLSchema | (() => Promise<GraphQLSchema>),\n createContext: TContextFunction,\n) {\n return async function subscribeOperation<TData = Record<string, unknown>, TVariables extends VariableValues = VariableValues>(\n { subscription, variables, operationName }: TypedSubscribeRequest<TData, TVariables>,\n ...createContextArgs: Parameters<TContextFunction>\n ): Promise<AsyncIterableIterator<ExecutionResult<TData>>> {\n const resolvedSchema = typeof schema === 'function' ? await schema() : schema\n const document = typeof subscription === 'string' ? parse(subscription) : subscription\n const contextValue = await createContext(...createContextArgs)\n const result = await subscribe({\n schema: resolvedSchema,\n document,\n contextValue,\n variableValues: variables as VariableValues | undefined,\n operationName,\n })\n if (Symbol.asyncIterator in result) return result as AsyncIterableIterator<ExecutionResult<TData>>\n throw new Error(`Subscription did not produce an iterator: ${JSON.stringify((result as ExecutionResult).errors)}`)\n }\n}\n"],"names":[],"mappings":";;AAYA;;;;;;;;;;;;;;;;;;AAkBG;AACG,SAAU,uBAAuB,CACrC,MAAsD,EACtD,aAA+B,EAAA;AAE/B,IAAA,OAAO,eAAe,kBAAkB,CACtC,EAAE,YAAY,EAAE,SAAS,EAAE,aAAa,EAA4C,EACpF,GAAG,iBAA+C,EAAA;AAElD,QAAA,MAAM,cAAc,GAAG,OAAO,MAAM,KAAK,UAAU,GAAG,MAAM,MAAM,EAAE,GAAG,MAAM;AAC7E,QAAA,MAAM,QAAQ,GAAG,OAAO,YAAY,KAAK,QAAQ,GAAG,KAAK,CAAC,YAAY,CAAC,GAAG,YAAY;QACtF,MAAM,YAAY,GAAG,MAAM,aAAa,CAAC,GAAG,iBAAiB,CAAC;AAC9D,QAAA,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC;AAC7B,YAAA,MAAM,EAAE,cAAc;YACtB,QAAQ;YACR,YAAY;AACZ,YAAA,cAAc,EAAE,SAAuC;YACvD,aAAa;AACd,SAAA,CAAC;AACF,QAAA,IAAI,MAAM,CAAC,aAAa,IAAI,MAAM;AAAE,YAAA,OAAO,MAAuD;AAClG,QAAA,MAAM,IAAI,KAAK,CAAC,CAAA,0CAAA,EAA6C,IAAI,CAAC,SAAS,CAAE,MAA0B,CAAC,MAAM,CAAC,CAAA,CAAE,CAAC;AACpH,IAAA,CAAC;AACH;;;;"}