diff --git a/examples/typescript-node/package.json b/examples/typescript-node/package.json index 1fd5f76..677afa6 100644 --- a/examples/typescript-node/package.json +++ b/examples/typescript-node/package.json @@ -9,8 +9,8 @@ "just-run": "node --loader ts-node/esm src/index.ts", "start": "npm run generate-typescript-node && node --import=tsx src/index.ts", "generate-typescript-node-config": "node ../../packages/graphql-zeus/lib/index.js --n -g ./zeus.graphql --td", - "generate-typescript-node": "node ../../packages/graphql-zeus/lib/index.js https://faker.prod.graphqleditor.com/a-team/olympus/graphql ./src --n -g ./zeus.graphql --td", - "generate-typescript-node-get": "node ../../packages/graphql-zeus/lib/index.js https://faker.prod.graphqleditor.com/a-team/olympus/graphql ./src --n -g ./zeus.graphql --method=GET --td" + "generate-typescript-node": "node ../../packages/graphql-zeus/lib/index.js http://localhost:4003/graphql ./src --n -g ./zeus.graphql --td", + "generate-typescript-node-get": "node ../../packages/graphql-zeus/lib/index.js http://localhost:4003/graphql ./src --n -g ./zeus.graphql --method=GET --td" }, "author": "Aexol (http://aexol.com)", "license": "ISC", diff --git a/examples/typescript-node/src/index.ts b/examples/typescript-node/src/index.ts index 6d10bbc..cdcf66d 100644 --- a/examples/typescript-node/src/index.ts +++ b/examples/typescript-node/src/index.ts @@ -1,466 +1,28 @@ -import chalk from 'chalk'; -import fetch from 'node-fetch'; -import { - Gql, - SpecialSkills, - Thunder, - Zeus, - InputType, - Selector, - GraphQLTypes, - ZeusScalars, - ValueTypes, - $, - FromSelector, - fields, - ComposableSelector, -} from './zeus/index.js'; -import { typedGql } from './zeus/typedDocumentNode.js'; - -export const testApollo = async () => { - const { ApolloClient, InMemoryCache, useMutation } = await import('@apollo/client'); - - const client = new ApolloClient({ - cache: new InMemoryCache(), - }); - - const useMyMutation = () => { - return ({ card }: { card: ValueTypes['createCard'] }) => - client.mutate({ - mutation: typedGql('mutation')({ - addCard: [{ card }, { id: true }], - }), - }); - }; - - const testMutate = () => { - const [mutate] = useMutation( - typedGql('mutation')({ - addCard: [ - { card: { Attack: $('attt', 'Int'), Defense: 2, name: $('name', 'String!'), description: 'Stronk' } }, - { id: true }, - ], - }), - ); - - mutate({ - variables: { - name: 'DDD', - attt: 1, - }, - }); - }; - return { - useMyMutation, - testMutate, - }; -}; -const sel = Selector('Query')({ - drawCard: { - Children: true, - Attack: true, - info: true, - ids: true, - '...on Card': { - id: true, - }, - attack: [{ cardID: ['sss'] }, { Attack: true }], - }, - cardById: [{ cardId: '' }, { Attack: true }], -}); - -const decoders = ZeusScalars({ - JSON: { - encode: (e: unknown) => JSON.stringify(e), - decode: (e: unknown) => { - if (!e) return; - return e as { power: number }; - }, - }, - ID: { - decode: (e: unknown) => e as number, - }, -}); - -export type IRT = InputType; - -const printQueryResult = (name: string, result: unknown) => - console.log(`${chalk.greenBright(name)} result:\n${chalk.cyan(JSON.stringify(result, null, 4))}\n\n`); -const printGQLString = (name: string, result: string) => - console.log(`${chalk.blue(name)} query:\n${chalk.magenta(result)}\n\n`); -const run = async () => { - const { addCard: ZeusCard } = await Gql('mutation')( - { - addCard: [ - { - card: { - Attack: 1, - Defense: 1, - description: 'lorem """ \' ipsum \n lorem ipsum', - name: 'SADSD', - skills: [SpecialSkills.FIRE], - }, - }, - { - info: true, - ids: true, - '...on Card': { - Attack: true, - }, - cardImage: { - bucket: true, - region: true, - key: true, - }, - }, - ], - }, - { operationName: 'ZausCard' }, - ); - printQueryResult('ZeusCard', ZeusCard); - const withComposable = , Z extends T>(id: string, rest: Z | T) => - Gql('query')({ - cardById: [{ cardId: id }, rest], - }); - const c1result = await withComposable('12', { - id: true, - }); - const c2result = await withComposable('12', { - Defense: true, - Attack: true, - }); - printQueryResult('composables', `1. ${c1result.cardById?.id} 2. ${c2result.cardById?.Attack}`); - const bbb = await Gql('query')({ - drawCard: { - ...fields('Card'), - }, - }); - printQueryResult('scalarsSelector', bbb.drawCard); - - const blalba = await Gql('query')({ - drawChangeCard: { - __typename: true, - '...on EffectCard': { - effectSize: true, - name: true, - }, - }, - }); - printQueryResult('drawChangeCard', blalba.drawChangeCard); - const blalbaScalars = await Gql('query', { scalars: decoders })({ - drawCard: { - info: true, - id: true, - }, - }); - console.log({ blalbaScalars }); - if (typeof blalbaScalars.drawCard.info?.power !== 'number') { - throw new Error('Invalid scalar decoder'); - } - printQueryResult('blalbaScalars', blalbaScalars.drawCard.info.power); - - // Thunder example - const thunder = Thunder(async (query) => { - const response = await fetch('https://faker.prod.graphqleditor.com/a-team/olympus/graphql', { - body: JSON.stringify({ query }), - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - }); - if (!response.ok) { - return new Promise((resolve, reject) => { - response - .text() - .then((text) => { - try { - reject(JSON.parse(text)); - } catch (err) { - reject(text); - } - }) - .catch(reject); - }); +import { SubscriptionSSE } from './zeus/index.js'; + +const main = async () => { + const subscriptionSSE = SubscriptionSSE('http://localhost:4003/graphql'); + const sub = subscriptionSSE('subscription'); + const { on, error, open, off, close } = sub({ + countdown: [{ from: 10 }, true], + }); + open(() => { + console.log('āœ“ SSE connection established'); + }); + on((data) => { + console.log('šŸ“Š Countdown:', data.countdown); + if (data.countdown === 0) { + console.log('\nāœ“ Countdown complete, closing connection...'); + close(); } - const json = await response.json(); - return json.data; - }); - const blalbaThunder = await thunder('query', { - scalars: decoders, - })({ - drawCard: { - Attack: true, - info: true, - }, - drawChangeCard: { - __typename: true, - '...on EffectCard': { - effectSize: true, - name: true, - }, - }, }); - printQueryResult('drawChangeCard thunder', blalbaThunder.drawChangeCard); - - const resPowerups = await Gql('query')( - { - public: { - powerups: [{ filter: 'dupa' }, { name: true }], - }, - }, - { operationName: 'Powerups' }, - ); - printQueryResult('Powerups', resPowerups?.public?.powerups); - - const { - listCards: stack, - drawCard: newCard, - drawChangeCard, - } = await Gql('query')({ - listCards: { - name: true, - cardImage: { - bucket: true, - }, - }, - drawCard: { - Attack: true, - name: `@skip(if:true)`, - }, - drawChangeCard: { - '...on SpecialCard': { - name: true, - }, - '...on EffectCard': { - effectSize: true, - }, - }, - }); - - printQueryResult('stack', stack); - printQueryResult('newCard', newCard); - printQueryResult('changeCard', drawChangeCard); - - const aliasedQuery = Zeus('query', { - __alias: { - myCards: { - listCards: { - name: true, - }, - }, - }, - listCards: { - __alias: { - atak: { - attack: [ - { cardID: ['aaa'] }, - { - name: true, - description: true, - __alias: { - bbb: { - Defense: true, - }, - }, - }, - ], - }, - }, - }, - }); - printGQLString('aliasedQuery', aliasedQuery); - const operationName = Zeus( - 'query', - { - listCards: { - Attack: true, - '...on Card': { - __directives: '@defer', - id: true, - }, - }, - }, - { - operationOptions: { - operationName: 'ListCards', - }, - }, - ); - printGQLString('operationName ListCards', operationName); - const aliasedQueryExecute = await Gql('query')( - { - listCards: { - __alias: { - namy: { - name: true, - }, - atak: { - attack: [ - { cardID: $('cardIds', '[String!]!') }, - { - name: true, - __alias: { - bbb: { - Defense: true, - }, - ccc: { - Children: true, - }, - }, - }, - ], - }, - }, - id: true, - }, - }, - { - variables: { - cardIds: ['aaa'], - }, - }, - ); - printQueryResult('aliasedQuery', aliasedQueryExecute); - const Children = undefined; - const emptyTestMutation = Zeus('mutation', { - addCard: [ - { - card: { - Attack: 1, - Defense: 2, - description: 'lorem ipsum \n lorem ipsum', - name: 'SADSD', - Children, - skills: [SpecialSkills.FIRE], - }, - }, - { - id: true, - description: true, - name: true, - Attack: true, - // // skills: true, - Children, - Defense: true, - cardImage: { - bucket: true, - region: true, - key: true, - }, - }, - ], - }); - printQueryResult('emptyTestMutation', emptyTestMutation); - - const interfaceTest = await Gql('query')({ - nameables: { - __typename: true, - name: true, - '...on Card': { - Attack: true, - }, - '...on SpecialCard': { - effect: true, - }, - '...on CardStack': { - cards: { - name: true, - }, - }, - }, - }); - - printQueryResult('interfaceTest', interfaceTest); - // Variable test - - const test = await Gql('mutation')( - { - addCard: [ - { - card: { - Attack: $('Attack', 'Int!'), - Defense: $('Defense', 'Int!'), - name: 'aa', - description: 'aa', - }, - }, - { - id: true, - description: true, - name: true, - Attack: true, - // skills: true, - Children: true, - Defense: true, - cardImage: { - bucket: true, - region: true, - key: true, - }, - }, - ], - }, - { - variables: { Attack: 1, Defense: 1 }, - }, - ); - printQueryResult('variable Test', test); - - const selectorTDD = Selector('Query')({ - drawCard: { - id: true, - Attack: true, - Defense: true, - attacks: true, - }, - cardById: [{ cardId: $('cardId', 'String!') }, { id: true }], + error((err) => { + console.error('āŒ SSE error:', err); }); - - //interface selector - const inSelector = Selector('Nameable')({ - name: true, + off((event) => { + console.log('šŸ”Œ SSE connection closed:', event.reason || 'Unknown reason'); + process.exit(0); }); - - type aa = FromSelector; - const ab: aa = {} as any; - if (ab.__typename === 'Card') { - ab.name; - } else if (ab.__typename === 'EffectCard') { - ab.name; - } else if (ab.__typename === 'CardStack') { - ab.name; - } - //interface selector - const inSelector2 = Selector('Nameable')({ - __typename: true, - name: true, - '...on Card': { - description: true, - }, - '...on EffectCard': { - effectSize: true, - }, - '...on CardStack': { - cards: { - info: true, - }, - }, - }); - - type bb = FromSelector; - const bc: bb = {} as any; - if (bc.__typename === 'Card') { - bc.name; - bc.description; - } else if (bc.__typename === 'EffectCard') { - bc.name; - bc.effectSize; - } else if (bc.__typename === 'CardStack') { - bc.name; - bc.cards; - } - - const generatedTypedDocumentNode = typedGql('query')(selectorTDD); - printQueryResult('Generated TypedDocumentNode Test', generatedTypedDocumentNode); }; -run(); + +main().catch(console.error); diff --git a/examples/typescript-node/src/zeus/const.ts b/examples/typescript-node/src/zeus/const.ts index ab9578a..e0538a9 100644 --- a/examples/typescript-node/src/zeus/const.ts +++ b/examples/typescript-node/src/zeus/const.ts @@ -1,31 +1,32 @@ /* eslint-disable */ export const AllTypesProps: Record = { - SpecialSkills: "enum" as const, - Query:{ - cardById:{ + AuthorizedUserMutation:{ + createTodo:{ - } - }, - createCard:{ - skills:"SpecialSkills" - }, - Card:{ - attack:{ + }, + todoOps:{ }, - testFn:{ + changePassword:{ + + } + }, + AuthorizedUserQuery:{ + todo:{ } }, Mutation:{ - addCard:{ - card:"createCard" + login:{ + + }, + register:{ + } }, - JSON: `scalar.JSON` as const, - Public:{ - powerups:{ + Subscription:{ + countdown:{ } }, @@ -33,70 +34,38 @@ export const AllTypesProps: Record = { } export const ReturnTypes: Record = { - Nameable:{ - "...on CardStack": "CardStack", - "...on Card": "Card", - "...on SpecialCard": "SpecialCard", - "...on EffectCard": "EffectCard", - name:"String" + Todo:{ + _id:"String", + content:"String", + done:"Boolean" }, - Query:{ - cardById:"Card", - drawCard:"Card", - drawChangeCard:"ChangeCard", - listCards:"Card", - myStacks:"CardStack", - nameables:"Nameable", - public:"Public" + TodoOps:{ + markDone:"Boolean" }, - CardStack:{ - cards:"Card", - name:"String" + User:{ + _id:"String", + username:"String" }, - Card:{ - Attack:"Int", - Children:"Int", - Defense:"Int", - attack:"Card", - cardImage:"S3Object", - description:"String", - id:"ID", - image:"String", - info:"JSON", - name:"String", - skills:"SpecialSkills", - testFn:"String", - ids:"ID" + AuthorizedUserMutation:{ + createTodo:"String", + todoOps:"TodoOps", + changePassword:"Boolean" }, - SpecialCard:{ - effect:"String", - name:"String" - }, - EffectCard:{ - effectSize:"Float", - name:"String" + AuthorizedUserQuery:{ + todos:"Todo", + todo:"Todo", + me:"User" }, Mutation:{ - addCard:"Card" - }, - JSON: `scalar.JSON` as const, - S3Object:{ - bucket:"String", - key:"String", - region:"String" - }, - Public:{ - powerups:"Powerup" - }, - Powerup:{ - name:"String" - }, - ChangeCard:{ - "...on SpecialCard":"SpecialCard", - "...on EffectCard":"EffectCard" + user:"AuthorizedUserMutation", + login:"String", + register:"String" }, Subscription:{ - deck:"Card" + countdown:"Int" + }, + Query:{ + user:"AuthorizedUserQuery" }, ID: `scalar.ID` as const } diff --git a/examples/typescript-node/src/zeus/index.ts b/examples/typescript-node/src/zeus/index.ts index a3e6c41..79296d7 100644 --- a/examples/typescript-node/src/zeus/index.ts +++ b/examples/typescript-node/src/zeus/index.ts @@ -1,7 +1,7 @@ /* eslint-disable */ import { AllTypesProps, ReturnTypes, Ops } from './const.js'; -export const HOST = "https://faker.prod.graphqleditor.com/a-team/olympus/graphql" +export const HOST = "http://localhost:4003/graphql" export const HEADERS = {} @@ -37,6 +37,119 @@ export const apiSubscription = (options: chainOptions) => (query: string) => { throw new Error('No websockets implemented'); } }; +export const apiSubscriptionSSE = (options: chainOptions) => (query: string) => { + const url = options[0]; + const fetchOptions = options[1] || {}; + + let abortController: AbortController | null = null; + let reader: ReadableStreamDefaultReader | null = null; + let onCallback: ((args: unknown) => void) | null = null; + let errorCallback: ((args: unknown) => void) | null = null; + let openCallback: (() => void) | null = null; + let offCallback: ((args: unknown) => void) | null = null; + + const startStream = async () => { + try { + abortController = new AbortController(); + + const response = await fetch(url, { + method: 'POST', + headers: { + Accept: 'text/event-stream', + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache', + ...fetchOptions.headers, + }, + body: JSON.stringify({ query }), + signal: abortController.signal, + ...fetchOptions, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + if (openCallback) { + openCallback(); + } + + reader = response.body?.getReader() || null; + if (!reader) { + throw new Error('No response body'); + } + + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + + if (done) { + if (offCallback) { + offCallback({ data: null, code: 1000, reason: 'Stream completed' }); + } + break; + } + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + if (line.startsWith('data: ')) { + try { + const data = line.slice(6); + const parsed = JSON.parse(data); + + if (parsed.errors) { + if (errorCallback) { + errorCallback({ data: parsed.data, errors: parsed.errors }); + } + } else if (onCallback && parsed.data) { + onCallback(parsed.data); + } + } catch { + if (errorCallback) { + errorCallback({ errors: ['Failed to parse SSE data'] }); + } + } + } + } + } + } catch (err: unknown) { + const error = err as Error; + if (error.name !== 'AbortError' && errorCallback) { + errorCallback({ errors: [error.message || 'Unknown error'] }); + } + } + }; + + return { + on: (e: (args: unknown) => void) => { + onCallback = e; + }, + off: (e: (args: unknown) => void) => { + offCallback = e; + }, + error: (e: (args: unknown) => void) => { + errorCallback = e; + }, + open: (e?: () => void) => { + if (e) { + openCallback = e; + } + startStream(); + }, + close: () => { + if (abortController) { + abortController.abort(); + } + if (reader) { + reader.cancel(); + } + }, + }; +}; const handleFetchResponse = (response: Response): Promise => { if (!response.ok) { return new Promise((_, reject) => { @@ -243,6 +356,58 @@ export const SubscriptionThunder = }; export const Subscription = (...options: chainOptions) => SubscriptionThunder(apiSubscription(options)); +export type SubscriptionToGraphQLSSE = { + on: (fn: (args: InputType) => void) => void; + off: (fn: (e: { data?: InputType; code?: number; reason?: string; message?: string }) => void) => void; + error: (fn: (e: { data?: InputType; errors?: string[] }) => void) => void; + open: (fn?: () => void) => void; + close: () => void; +}; +export const SubscriptionThunderSSE = + (fn: SubscriptionFunction, thunderGraphQLOptions?: ThunderGraphQLOptions) => + >( + operation: O, + graphqlOptions?: ThunderGraphQLOptions, + ) => + ( + o: Z & { + [P in keyof Z]: P extends keyof ValueTypes[R] ? Z[P] : never; + }, + ops?: OperationOptions & { variables?: ExtractVariables }, + ) => { + const options = { + ...thunderGraphQLOptions, + ...graphqlOptions, + }; + type CombinedSCLR = UnionOverrideKeys; + const returnedFunction = fn( + Zeus(operation, o, { + operationOptions: ops, + scalars: options?.scalars, + }), + ) as SubscriptionToGraphQLSSE; + if (returnedFunction?.on && options?.scalars) { + const wrapped = returnedFunction.on; + returnedFunction.on = (fnToCall: (args: InputType) => void) => + wrapped((data: InputType) => { + if (options?.scalars) { + return fnToCall( + decodeScalarsInResponse({ + response: data, + initialOp: operation, + initialZeusQuery: o as VType, + returns: ReturnTypes, + scalars: options.scalars, + ops: Ops, + }), + ); + } + return fnToCall(data); + }); + } + return returnedFunction; + }; +export const SubscriptionSSE = (...options: chainOptions) => SubscriptionThunderSSE(apiSubscriptionSSE(options)); export const Zeus = < Z extends ValueTypes[R], O extends keyof typeof Ops, @@ -897,235 +1062,105 @@ export const GRAPHQL_TYPE_SEPARATOR = `__$GRAPHQL__`; export const $ = (name: Name, graphqlType: Type) => { return (START_VAR_NAME + name + GRAPHQL_TYPE_SEPARATOR + graphqlType) as unknown as Variable; }; -type ZEUS_INTERFACES = GraphQLTypes["Nameable"] +type ZEUS_INTERFACES = never export type ScalarCoders = { - JSON?: ScalarResolver; ID?: ScalarResolver; } -type ZEUS_UNIONS = GraphQLTypes["ChangeCard"] +type ZEUS_UNIONS = never export type ValueTypes = { - ["Nameable"]:AliasType<{ - name?:boolean | `@${string}`; - ['...on CardStack']?: Omit; - ['...on Card']?: Omit; - ['...on SpecialCard']?: Omit; - ['...on EffectCard']?: Omit; - __typename?: boolean | `@${string}` -}>; - ["SpecialSkills"]:SpecialSkills; - ["Query"]: AliasType<{ -cardById?: [{ cardId?: string | undefined | null | Variable},ValueTypes["Card"]], - /** Draw a card
*/ - drawCard?:ValueTypes["Card"], - drawChangeCard?:ValueTypes["ChangeCard"], - /** list All Cards availble
*/ - listCards?:ValueTypes["Card"], - myStacks?:ValueTypes["CardStack"], - nameables?:ValueTypes["Nameable"], - public?:ValueTypes["Public"], + ["Todo"]: AliasType<{ + _id?:boolean | `@${string}`, + content?:boolean | `@${string}`, + done?:boolean | `@${string}`, __typename?: boolean | `@${string}`, - ['...on Query']?: Omit + ['...on Todo']?: Omit }>; - /** create card inputs
*/ -["createCard"]: { - /** The name of a card
*/ - name: string | Variable, - /** Description of a card
*/ - description: string | Variable, - /**
How many children the greek god had
*/ - Children?: number | undefined | null | Variable, - /** The attack power
*/ - Attack: number | Variable, - /** The defense power
*/ - Defense: number | Variable, - /** input skills */ - skills?: Array | undefined | null | Variable -}; - /** Stack of cards */ -["CardStack"]: AliasType<{ - cards?:ValueTypes["Card"], - name?:boolean | `@${string}`, + ["TodoOps"]: AliasType<{ + markDone?:boolean | `@${string}`, __typename?: boolean | `@${string}`, - ['...on CardStack']?: Omit + ['...on TodoOps']?: Omit }>; - /** Card used in card game
*/ -["Card"]: AliasType<{ - /** The attack power
*/ - Attack?:boolean | `@${string}`, - /**
How many children the greek god had
*/ - Children?:boolean | `@${string}`, - /** The defense power
*/ - Defense?:boolean | `@${string}`, -attack?: [{ /** Attacked card/card ids
*/ - cardID: Array | Variable},ValueTypes["Card"]], - /** Put your description here */ - cardImage?:ValueTypes["S3Object"], - /** Description of a card
*/ - description?:boolean | `@${string}`, - id?:boolean | `@${string}`, - image?:boolean | `@${string}`, - info?:boolean | `@${string}`, - /** The name of a card
*/ - name?:boolean | `@${string}`, - skills?:boolean | `@${string}`, -testFn?: [{ test?: string | undefined | null | Variable},boolean | `@${string}`], - ids?:boolean | `@${string}`, + ["User"]: AliasType<{ + _id?:boolean | `@${string}`, + username?:boolean | `@${string}`, __typename?: boolean | `@${string}`, - ['...on Card']?: Omit + ['...on User']?: Omit }>; - ["SpecialCard"]: AliasType<{ - effect?:boolean | `@${string}`, - name?:boolean | `@${string}`, + ["AuthorizedUserMutation"]: AliasType<{ +createTodo?: [{ content: string | Variable},boolean | `@${string}`], +todoOps?: [{ _id: string | Variable},ValueTypes["TodoOps"]], +changePassword?: [{ newPassword: string | Variable},boolean | `@${string}`], __typename?: boolean | `@${string}`, - ['...on SpecialCard']?: Omit + ['...on AuthorizedUserMutation']?: Omit }>; - ["EffectCard"]: AliasType<{ - effectSize?:boolean | `@${string}`, - name?:boolean | `@${string}`, + ["AuthorizedUserQuery"]: AliasType<{ + todos?:ValueTypes["Todo"], +todo?: [{ _id: string | Variable},ValueTypes["Todo"]], + me?:ValueTypes["User"], __typename?: boolean | `@${string}`, - ['...on EffectCard']?: Omit + ['...on AuthorizedUserQuery']?: Omit }>; ["Mutation"]: AliasType<{ -addCard?: [{ card: ValueTypes["createCard"] | Variable},ValueTypes["Card"]], + user?:ValueTypes["AuthorizedUserMutation"], +login?: [{ username: string | Variable, password: string | Variable},boolean | `@${string}`], +register?: [{ username: string | Variable, password: string | Variable},boolean | `@${string}`], __typename?: boolean | `@${string}`, ['...on Mutation']?: Omit -}>; - ["JSON"]:unknown; - /** Aws S3 File */ -["S3Object"]: AliasType<{ - bucket?:boolean | `@${string}`, - key?:boolean | `@${string}`, - region?:boolean | `@${string}`, - __typename?: boolean | `@${string}`, - ['...on S3Object']?: Omit -}>; - ["Public"]: AliasType<{ -powerups?: [{ filter: string | Variable},ValueTypes["Powerup"]], - __typename?: boolean | `@${string}`, - ['...on Public']?: Omit -}>; - ["Powerup"]: AliasType<{ - name?:boolean | `@${string}`, - __typename?: boolean | `@${string}`, - ['...on Powerup']?: Omit -}>; - ["ChangeCard"]: AliasType<{ ["...on SpecialCard"]?: ValueTypes["SpecialCard"], - ["...on EffectCard"]?: ValueTypes["EffectCard"] - __typename?: boolean | `@${string}` }>; ["Subscription"]: AliasType<{ - deck?:ValueTypes["Card"], +countdown?: [{ from: number | Variable},boolean | `@${string}`], __typename?: boolean | `@${string}`, ['...on Subscription']?: Omit +}>; + ["Query"]: AliasType<{ + user?:ValueTypes["AuthorizedUserQuery"], + __typename?: boolean | `@${string}`, + ['...on Query']?: Omit }>; ["ID"]:unknown } export type ResolverInputTypes = { - ["Nameable"]:AliasType<{ - name?:boolean | `@${string}`; - ['...on CardStack']?: Omit; - ['...on Card']?: Omit; - ['...on SpecialCard']?: Omit; - ['...on EffectCard']?: Omit; - __typename?: boolean | `@${string}` -}>; - ["SpecialSkills"]:SpecialSkills; - ["Query"]: AliasType<{ -cardById?: [{ cardId?: string | undefined | null},ResolverInputTypes["Card"]], - /** Draw a card
*/ - drawCard?:ResolverInputTypes["Card"], - drawChangeCard?:ResolverInputTypes["ChangeCard"], - /** list All Cards availble
*/ - listCards?:ResolverInputTypes["Card"], - myStacks?:ResolverInputTypes["CardStack"], - nameables?:ResolverInputTypes["Nameable"], - public?:ResolverInputTypes["Public"], + ["Todo"]: AliasType<{ + _id?:boolean | `@${string}`, + content?:boolean | `@${string}`, + done?:boolean | `@${string}`, __typename?: boolean | `@${string}` }>; - /** create card inputs
*/ -["createCard"]: { - /** The name of a card
*/ - name: string, - /** Description of a card
*/ - description: string, - /**
How many children the greek god had
*/ - Children?: number | undefined | null, - /** The attack power
*/ - Attack: number, - /** The defense power
*/ - Defense: number, - /** input skills */ - skills?: Array | undefined | null -}; - /** Stack of cards */ -["CardStack"]: AliasType<{ - cards?:ResolverInputTypes["Card"], - name?:boolean | `@${string}`, + ["TodoOps"]: AliasType<{ + markDone?:boolean | `@${string}`, __typename?: boolean | `@${string}` }>; - /** Card used in card game
*/ -["Card"]: AliasType<{ - /** The attack power
*/ - Attack?:boolean | `@${string}`, - /**
How many children the greek god had
*/ - Children?:boolean | `@${string}`, - /** The defense power
*/ - Defense?:boolean | `@${string}`, -attack?: [{ /** Attacked card/card ids
*/ - cardID: Array},ResolverInputTypes["Card"]], - /** Put your description here */ - cardImage?:ResolverInputTypes["S3Object"], - /** Description of a card
*/ - description?:boolean | `@${string}`, - id?:boolean | `@${string}`, - image?:boolean | `@${string}`, - info?:boolean | `@${string}`, - /** The name of a card
*/ - name?:boolean | `@${string}`, - skills?:boolean | `@${string}`, -testFn?: [{ test?: string | undefined | null},boolean | `@${string}`], - ids?:boolean | `@${string}`, + ["User"]: AliasType<{ + _id?:boolean | `@${string}`, + username?:boolean | `@${string}`, __typename?: boolean | `@${string}` }>; - ["SpecialCard"]: AliasType<{ - effect?:boolean | `@${string}`, - name?:boolean | `@${string}`, + ["AuthorizedUserMutation"]: AliasType<{ +createTodo?: [{ content: string},boolean | `@${string}`], +todoOps?: [{ _id: string},ResolverInputTypes["TodoOps"]], +changePassword?: [{ newPassword: string},boolean | `@${string}`], __typename?: boolean | `@${string}` }>; - ["EffectCard"]: AliasType<{ - effectSize?:boolean | `@${string}`, - name?:boolean | `@${string}`, + ["AuthorizedUserQuery"]: AliasType<{ + todos?:ResolverInputTypes["Todo"], +todo?: [{ _id: string},ResolverInputTypes["Todo"]], + me?:ResolverInputTypes["User"], __typename?: boolean | `@${string}` }>; ["Mutation"]: AliasType<{ -addCard?: [{ card: ResolverInputTypes["createCard"]},ResolverInputTypes["Card"]], - __typename?: boolean | `@${string}` -}>; - ["JSON"]:unknown; - /** Aws S3 File */ -["S3Object"]: AliasType<{ - bucket?:boolean | `@${string}`, - key?:boolean | `@${string}`, - region?:boolean | `@${string}`, - __typename?: boolean | `@${string}` -}>; - ["Public"]: AliasType<{ -powerups?: [{ filter: string},ResolverInputTypes["Powerup"]], - __typename?: boolean | `@${string}` -}>; - ["Powerup"]: AliasType<{ - name?:boolean | `@${string}`, + user?:ResolverInputTypes["AuthorizedUserMutation"], +login?: [{ username: string, password: string},boolean | `@${string}`], +register?: [{ username: string, password: string},boolean | `@${string}`], __typename?: boolean | `@${string}` }>; - ["ChangeCard"]: AliasType<{ - SpecialCard?:ResolverInputTypes["SpecialCard"], - EffectCard?:ResolverInputTypes["EffectCard"], + ["Subscription"]: AliasType<{ +countdown?: [{ from: number},boolean | `@${string}`], __typename?: boolean | `@${string}` }>; - ["Subscription"]: AliasType<{ - deck?:ResolverInputTypes["Card"], + ["Query"]: AliasType<{ + user?:ResolverInputTypes["AuthorizedUserQuery"], __typename?: boolean | `@${string}` }>; ["schema"]: AliasType<{ @@ -1138,90 +1173,38 @@ powerups?: [{ filter: string},ResolverInputTypes["Powerup"]], } export type ModelTypes = { - ["Nameable"]: ModelTypes["CardStack"] | ModelTypes["Card"] | ModelTypes["SpecialCard"] | ModelTypes["EffectCard"]; - ["SpecialSkills"]:SpecialSkills; - ["Query"]: { - cardById?: ModelTypes["Card"] | undefined | null, - /** Draw a card
*/ - drawCard: ModelTypes["Card"], - drawChangeCard: ModelTypes["ChangeCard"], - /** list All Cards availble
*/ - listCards: Array, - myStacks?: Array | undefined | null, - nameables: Array, - public?: ModelTypes["Public"] | undefined | null -}; - /** create card inputs
*/ -["createCard"]: { - /** The name of a card
*/ - name: string, - /** Description of a card
*/ - description: string, - /**
How many children the greek god had
*/ - Children?: number | undefined | null, - /** The attack power
*/ - Attack: number, - /** The defense power
*/ - Defense: number, - /** input skills */ - skills?: Array | undefined | null + ["Todo"]: { + _id: string, + content: string, + done?: boolean | undefined | null }; - /** Stack of cards */ -["CardStack"]: { - cards?: Array | undefined | null, - name: string + ["TodoOps"]: { + markDone?: boolean | undefined | null }; - /** Card used in card game
*/ -["Card"]: { - /** The attack power
*/ - Attack: number, - /**
How many children the greek god had
*/ - Children?: number | undefined | null, - /** The defense power
*/ - Defense: number, - /** Attack other cards on the table , returns Cards after attack
*/ - attack?: Array | undefined | null, - /** Put your description here */ - cardImage?: ModelTypes["S3Object"] | undefined | null, - /** Description of a card
*/ - description: string, - id: ModelTypes["ID"], - image: string, - info: ModelTypes["JSON"], - /** The name of a card
*/ - name: string, - skills?: Array | undefined | null, - testFn?: string | undefined | null, - ids?: Array | undefined | null + ["User"]: { + _id: string, + username: string }; - ["SpecialCard"]: { - effect: string, - name: string + ["AuthorizedUserMutation"]: { + createTodo: string, + todoOps: ModelTypes["TodoOps"], + changePassword?: boolean | undefined | null }; - ["EffectCard"]: { - effectSize: number, - name: string + ["AuthorizedUserQuery"]: { + todos?: Array | undefined | null, + todo: ModelTypes["Todo"], + me: ModelTypes["User"] }; ["Mutation"]: { - /** add Card to Cards database
*/ - addCard: ModelTypes["Card"] + user: ModelTypes["AuthorizedUserMutation"], + login: string, + register: string }; - ["JSON"]:any; - /** Aws S3 File */ -["S3Object"]: { - bucket: string, - key: string, - region: string -}; - ["Public"]: { - powerups?: Array | undefined | null -}; - ["Powerup"]: { - name?: string | undefined | null -}; - ["ChangeCard"]:ModelTypes["SpecialCard"] | ModelTypes["EffectCard"]; ["Subscription"]: { - deck?: Array | undefined | null + countdown: number +}; + ["Query"]: { + user: ModelTypes["AuthorizedUserQuery"] }; ["schema"]: { query?: ModelTypes["Query"] | undefined | null, @@ -1232,133 +1215,59 @@ export type ModelTypes = { } export type GraphQLTypes = { - ["Nameable"]: { - __typename:"CardStack" | "Card" | "SpecialCard" | "EffectCard", - name: string - ['...on CardStack']: '__union' & GraphQLTypes["CardStack"]; - ['...on Card']: '__union' & GraphQLTypes["Card"]; - ['...on SpecialCard']: '__union' & GraphQLTypes["SpecialCard"]; - ['...on EffectCard']: '__union' & GraphQLTypes["EffectCard"]; -}; - ["SpecialSkills"]: SpecialSkills; - ["Query"]: { - __typename: "Query", - cardById?: GraphQLTypes["Card"] | undefined | null, - /** Draw a card
*/ - drawCard: GraphQLTypes["Card"], - drawChangeCard: GraphQLTypes["ChangeCard"], - /** list All Cards availble
*/ - listCards: Array, - myStacks?: Array | undefined | null, - nameables: Array, - public?: GraphQLTypes["Public"] | undefined | null, - ['...on Query']: Omit -}; - /** create card inputs
*/ -["createCard"]: { - /** The name of a card
*/ - name: string, - /** Description of a card
*/ - description: string, - /**
How many children the greek god had
*/ - Children?: number | undefined | null, - /** The attack power
*/ - Attack: number, - /** The defense power
*/ - Defense: number, - /** input skills */ - skills?: Array | undefined | null + ["Todo"]: { + __typename: "Todo", + _id: string, + content: string, + done?: boolean | undefined | null, + ['...on Todo']: Omit }; - /** Stack of cards */ -["CardStack"]: { - __typename: "CardStack", - cards?: Array | undefined | null, - name: string, - ['...on CardStack']: Omit + ["TodoOps"]: { + __typename: "TodoOps", + markDone?: boolean | undefined | null, + ['...on TodoOps']: Omit }; - /** Card used in card game
*/ -["Card"]: { - __typename: "Card", - /** The attack power
*/ - Attack: number, - /**
How many children the greek god had
*/ - Children?: number | undefined | null, - /** The defense power
*/ - Defense: number, - /** Attack other cards on the table , returns Cards after attack
*/ - attack?: Array | undefined | null, - /** Put your description here */ - cardImage?: GraphQLTypes["S3Object"] | undefined | null, - /** Description of a card
*/ - description: string, - id: GraphQLTypes["ID"], - image: string, - info: GraphQLTypes["JSON"], - /** The name of a card
*/ - name: string, - skills?: Array | undefined | null, - testFn?: string | undefined | null, - ids?: Array | undefined | null, - ['...on Card']: Omit + ["User"]: { + __typename: "User", + _id: string, + username: string, + ['...on User']: Omit }; - ["SpecialCard"]: { - __typename: "SpecialCard", - effect: string, - name: string, - ['...on SpecialCard']: Omit + ["AuthorizedUserMutation"]: { + __typename: "AuthorizedUserMutation", + createTodo: string, + todoOps: GraphQLTypes["TodoOps"], + changePassword?: boolean | undefined | null, + ['...on AuthorizedUserMutation']: Omit }; - ["EffectCard"]: { - __typename: "EffectCard", - effectSize: number, - name: string, - ['...on EffectCard']: Omit + ["AuthorizedUserQuery"]: { + __typename: "AuthorizedUserQuery", + todos?: Array | undefined | null, + todo: GraphQLTypes["Todo"], + me: GraphQLTypes["User"], + ['...on AuthorizedUserQuery']: Omit }; ["Mutation"]: { __typename: "Mutation", - /** add Card to Cards database
*/ - addCard: GraphQLTypes["Card"], + user: GraphQLTypes["AuthorizedUserMutation"], + login: string, + register: string, ['...on Mutation']: Omit -}; - ["JSON"]: "scalar" & { name: "JSON" }; - /** Aws S3 File */ -["S3Object"]: { - __typename: "S3Object", - bucket: string, - key: string, - region: string, - ['...on S3Object']: Omit -}; - ["Public"]: { - __typename: "Public", - powerups?: Array | undefined | null, - ['...on Public']: Omit -}; - ["Powerup"]: { - __typename: "Powerup", - name?: string | undefined | null, - ['...on Powerup']: Omit -}; - ["ChangeCard"]:{ - __typename:"SpecialCard" | "EffectCard" - ['...on SpecialCard']: '__union' & GraphQLTypes["SpecialCard"]; - ['...on EffectCard']: '__union' & GraphQLTypes["EffectCard"]; }; ["Subscription"]: { __typename: "Subscription", - deck?: Array | undefined | null, + countdown: number, ['...on Subscription']: Omit +}; + ["Query"]: { + __typename: "Query", + user: GraphQLTypes["AuthorizedUserQuery"], + ['...on Query']: Omit }; ["ID"]: "scalar" & { name: "ID" } } -export enum SpecialSkills { - THUNDER = "THUNDER", - RAIN = "RAIN", - FIRE = "FIRE" -} + type ZEUS_VARIABLES = { - ["SpecialSkills"]: ValueTypes["SpecialSkills"]; - ["createCard"]: ValueTypes["createCard"]; - ["JSON"]: ValueTypes["JSON"]; ["ID"]: ValueTypes["ID"]; } \ No newline at end of file diff --git a/examples/typescript-node/zeus.graphql b/examples/typescript-node/zeus.graphql index 9f169dd..a664572 100644 --- a/examples/typescript-node/zeus.graphql +++ b/examples/typescript-node/zeus.graphql @@ -1,128 +1,42 @@ -interface Nameable { - name: String! +type Todo { + _id: String! + content: String! + done: Boolean } -enum SpecialSkills { - """Lower enemy defense -5
""" - THUNDER - - """Attack multiple Cards at once
""" - RAIN - - """50% chance to avoid any attack
""" - FIRE +type TodoOps { + markDone: Boolean } -type Query { - cardById(cardId: String): Card - - """Draw a card
""" - drawCard: Card! - drawChangeCard: ChangeCard! - - """list All Cards availble
""" - listCards: [Card!]! - myStacks: [CardStack!] - nameables: [Nameable!]! - public: Public +type User { + _id: String! + username: String! } -"""create card inputs
""" -input createCard { - """The name of a card
""" - name: String! - - """Description of a card
""" - description: String! - - """
How many children the greek god had
""" - Children: Int - - """The attack power
""" - Attack: Int! - - """The defense power
""" - Defense: Int! - - """input skills""" - skills: [SpecialSkills!] +type AuthorizedUserMutation { + createTodo(content: String!): String! + todoOps(_id: String!): TodoOps! + changePassword(newPassword: String!): Boolean } -"""Stack of cards""" -type CardStack implements Nameable { - cards: [Card!] - name: String! -} - -"""Card used in card game
""" -type Card implements Nameable { - """The attack power
""" - Attack: Int! - - """
How many children the greek god had
""" - Children: Int - - """The defense power
""" - Defense: Int! - - """Attack other cards on the table , returns Cards after attack
""" - attack( - """Attacked card/card ids
""" - cardID: [String!]! - ): [Card!] - - """Put your description here""" - cardImage: S3Object - - """Description of a card
""" - description: String! - id: ID! - image: String! - info: JSON! - - """The name of a card
""" - name: String! - skills: [SpecialSkills!] - testFn(test: String): String - ids: [ID!] -} - -type SpecialCard implements Nameable { - effect: String! - name: String! -} - -type EffectCard implements Nameable { - effectSize: Float! - name: String! +type AuthorizedUserQuery { + todos: [Todo!] + todo(_id: String!): Todo! + me: User! } type Mutation { - """add Card to Cards database
""" - addCard(card: createCard!): Card! + user: AuthorizedUserMutation! + login(username: String!, password: String!): String! + register(username: String!, password: String!): String! } -scalar JSON - -"""Aws S3 File""" -type S3Object { - bucket: String! - key: String! - region: String! -} - -type Public { - powerups(filter: String!): [Powerup!] -} - -type Powerup { - name: String +type Subscription { + countdown(from: Int!): Int! } -union ChangeCard = SpecialCard | EffectCard - -type Subscription { - deck: [Card!] +type Query { + user: AuthorizedUserQuery! } schema{ query: Query, diff --git a/libBuilder.ts b/libBuilder.ts index 0ccb0ed..5b185a0 100644 --- a/libBuilder.ts +++ b/libBuilder.ts @@ -1,6 +1,6 @@ #!/bin/node import * as fs from 'fs'; -import path = require('path'); +import * as path from 'path'; const removeImports = (s: string) => s.replace(/import\s(\n|\w|{|}|\s|,)*.*;(?! \/\/ keep)/gm, ''); const toTemplateString = (s: string) => '`' + s.replace(/\$\{/gm, '\\${').replace(/`/gm, '\\`') + '`'; @@ -27,10 +27,22 @@ const bundleFunctions = () => { return [key, code]; }); + const sseDir = path.join(baseDirFunctions, 'apiSSESubscription'); + const sseFunctions = fs.readdirSync(sseDir).map((file) => { + // The key is the filename without its extension + const key = path.basename(file, '.ts'); + const content = fs.readFileSync(path.join(sseDir, file)).toString('utf-8'); + const code = removeImports(content).replace(/\\/gm, '\\\\').trim(); + return [key, code]; + }); + const content = ` export default ${toTemplateString(allFunctions.join('\n\n'))}; export const subscriptionFunctions = {${subscriptionFunctions + .map(([key, value]) => JSON.stringify(key) + ': ' + toTemplateString(value)) + .join(',\n')}}; + export const sseFunctions = {${sseFunctions .map(([key, value]) => JSON.stringify(key) + ': ' + toTemplateString(value)) .join(',\n')}}`; diff --git a/packages/graphql-zeus-core/TreeToTS/functions/apiSSESubscription/sse.ts b/packages/graphql-zeus-core/TreeToTS/functions/apiSSESubscription/sse.ts new file mode 100644 index 0000000..ae425a3 --- /dev/null +++ b/packages/graphql-zeus-core/TreeToTS/functions/apiSSESubscription/sse.ts @@ -0,0 +1,115 @@ +import { chainOptions } from '@/TreeToTS/functions/new/models'; + +export const apiSubscriptionSSE = (options: chainOptions) => (query: string) => { + const url = options[0]; + const fetchOptions = options[1] || {}; + + let abortController: AbortController | null = null; + let reader: ReadableStreamDefaultReader | null = null; + let onCallback: ((args: unknown) => void) | null = null; + let errorCallback: ((args: unknown) => void) | null = null; + let openCallback: (() => void) | null = null; + let offCallback: ((args: unknown) => void) | null = null; + + const startStream = async () => { + try { + abortController = new AbortController(); + + const response = await fetch(url, { + method: 'POST', + headers: { + Accept: 'text/event-stream', + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache', + ...fetchOptions.headers, + }, + body: JSON.stringify({ query }), + signal: abortController.signal, + ...fetchOptions, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + if (openCallback) { + openCallback(); + } + + reader = response.body?.getReader() || null; + if (!reader) { + throw new Error('No response body'); + } + + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + + if (done) { + if (offCallback) { + offCallback({ data: null, code: 1000, reason: 'Stream completed' }); + } + break; + } + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + if (line.startsWith('data: ')) { + try { + const data = line.slice(6); + const parsed = JSON.parse(data); + + if (parsed.errors) { + if (errorCallback) { + errorCallback({ data: parsed.data, errors: parsed.errors }); + } + } else if (onCallback && parsed.data) { + onCallback(parsed.data); + } + } catch { + if (errorCallback) { + errorCallback({ errors: ['Failed to parse SSE data'] }); + } + } + } + } + } + } catch (err: unknown) { + const error = err as Error; + if (error.name !== 'AbortError' && errorCallback) { + errorCallback({ errors: [error.message || 'Unknown error'] }); + } + } + }; + + return { + on: (e: (args: unknown) => void) => { + onCallback = e; + }, + off: (e: (args: unknown) => void) => { + offCallback = e; + }, + error: (e: (args: unknown) => void) => { + errorCallback = e; + }, + open: (e?: () => void) => { + if (e) { + openCallback = e; + } + startStream(); + }, + close: () => { + if (abortController) { + abortController.abort(); + } + if (reader) { + reader.cancel(); + } + }, + }; +}; diff --git a/packages/graphql-zeus-core/TreeToTS/functions/generated.ts b/packages/graphql-zeus-core/TreeToTS/functions/generated.ts index 53232ad..1db61a1 100644 --- a/packages/graphql-zeus-core/TreeToTS/functions/generated.ts +++ b/packages/graphql-zeus-core/TreeToTS/functions/generated.ts @@ -204,6 +204,58 @@ export const SubscriptionThunder = }; export const Subscription = (...options: chainOptions) => SubscriptionThunder(apiSubscription(options)); +export type SubscriptionToGraphQLSSE = { + on: (fn: (args: InputType) => void) => void; + off: (fn: (e: { data?: InputType; code?: number; reason?: string; message?: string }) => void) => void; + error: (fn: (e: { data?: InputType; errors?: string[] }) => void) => void; + open: (fn?: () => void) => void; + close: () => void; +}; +export const SubscriptionThunderSSE = + (fn: SubscriptionFunction, thunderGraphQLOptions?: ThunderGraphQLOptions) => + >( + operation: O, + graphqlOptions?: ThunderGraphQLOptions, + ) => + ( + o: Z & { + [P in keyof Z]: P extends keyof ValueTypes[R] ? Z[P] : never; + }, + ops?: OperationOptions & { variables?: ExtractVariables }, + ) => { + const options = { + ...thunderGraphQLOptions, + ...graphqlOptions, + }; + type CombinedSCLR = UnionOverrideKeys; + const returnedFunction = fn( + Zeus(operation, o, { + operationOptions: ops, + scalars: options?.scalars, + }), + ) as SubscriptionToGraphQLSSE; + if (returnedFunction?.on && options?.scalars) { + const wrapped = returnedFunction.on; + returnedFunction.on = (fnToCall: (args: InputType) => void) => + wrapped((data: InputType) => { + if (options?.scalars) { + return fnToCall( + decodeScalarsInResponse({ + response: data, + initialOp: operation, + initialZeusQuery: o as VType, + returns: ReturnTypes, + scalars: options.scalars, + ops: Ops, + }), + ); + } + return fnToCall(data); + }); + } + return returnedFunction; + }; +export const SubscriptionSSE = (...options: chainOptions) => SubscriptionThunderSSE(apiSubscriptionSSE(options)); export const Zeus = < Z extends ValueTypes[R], O extends keyof typeof Ops, @@ -950,3 +1002,118 @@ export const apiSubscription = (options: chainOptions) => { } };`, }; +export const sseFunctions = { + sse: `export const apiSubscriptionSSE = (options: chainOptions) => (query: string) => { + const url = options[0]; + const fetchOptions = options[1] || {}; + + let abortController: AbortController | null = null; + let reader: ReadableStreamDefaultReader | null = null; + let onCallback: ((args: unknown) => void) | null = null; + let errorCallback: ((args: unknown) => void) | null = null; + let openCallback: (() => void) | null = null; + let offCallback: ((args: unknown) => void) | null = null; + + const startStream = async () => { + try { + abortController = new AbortController(); + + const response = await fetch(url, { + method: 'POST', + headers: { + Accept: 'text/event-stream', + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache', + ...fetchOptions.headers, + }, + body: JSON.stringify({ query }), + signal: abortController.signal, + ...fetchOptions, + }); + + if (!response.ok) { + throw new Error(\`HTTP error! status: \${response.status}\`); + } + + if (openCallback) { + openCallback(); + } + + reader = response.body?.getReader() || null; + if (!reader) { + throw new Error('No response body'); + } + + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + + if (done) { + if (offCallback) { + offCallback({ data: null, code: 1000, reason: 'Stream completed' }); + } + break; + } + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + if (line.startsWith('data: ')) { + try { + const data = line.slice(6); + const parsed = JSON.parse(data); + + if (parsed.errors) { + if (errorCallback) { + errorCallback({ data: parsed.data, errors: parsed.errors }); + } + } else if (onCallback && parsed.data) { + onCallback(parsed.data); + } + } catch { + if (errorCallback) { + errorCallback({ errors: ['Failed to parse SSE data'] }); + } + } + } + } + } + } catch (err: unknown) { + const error = err as Error; + if (error.name !== 'AbortError' && errorCallback) { + errorCallback({ errors: [error.message || 'Unknown error'] }); + } + } + }; + + return { + on: (e: (args: unknown) => void) => { + onCallback = e; + }, + off: (e: (args: unknown) => void) => { + offCallback = e; + }, + error: (e: (args: unknown) => void) => { + errorCallback = e; + }, + open: (e?: () => void) => { + if (e) { + openCallback = e; + } + startStream(); + }, + close: () => { + if (abortController) { + abortController.abort(); + } + if (reader) { + reader.cancel(); + } + }, + }; +};`, +}; diff --git a/packages/graphql-zeus-core/TreeToTS/functions/new/clientFunctions.ts b/packages/graphql-zeus-core/TreeToTS/functions/new/clientFunctions.ts index 5830698..19b173b 100644 --- a/packages/graphql-zeus-core/TreeToTS/functions/new/clientFunctions.ts +++ b/packages/graphql-zeus-core/TreeToTS/functions/new/clientFunctions.ts @@ -8,6 +8,7 @@ import { ValueTypes, apiFetch, apiSubscription, + apiSubscriptionSSE, HOST, ScalarCoders, ModelTypes, @@ -112,6 +113,58 @@ export const SubscriptionThunder = }; export const Subscription = (...options: chainOptions) => SubscriptionThunder(apiSubscription(options)); +export type SubscriptionToGraphQLSSE = { + on: (fn: (args: InputType) => void) => void; + off: (fn: (e: { data?: InputType; code?: number; reason?: string; message?: string }) => void) => void; + error: (fn: (e: { data?: InputType; errors?: string[] }) => void) => void; + open: (fn?: () => void) => void; + close: () => void; +}; +export const SubscriptionThunderSSE = + (fn: SubscriptionFunction, thunderGraphQLOptions?: ThunderGraphQLOptions) => + >( + operation: O, + graphqlOptions?: ThunderGraphQLOptions, + ) => + ( + o: Z & { + [P in keyof Z]: P extends keyof ValueTypes[R] ? Z[P] : never; + }, + ops?: OperationOptions & { variables?: ExtractVariables }, + ) => { + const options = { + ...thunderGraphQLOptions, + ...graphqlOptions, + }; + type CombinedSCLR = UnionOverrideKeys; + const returnedFunction = fn( + Zeus(operation, o, { + operationOptions: ops, + scalars: options?.scalars, + }), + ) as SubscriptionToGraphQLSSE; + if (returnedFunction?.on && options?.scalars) { + const wrapped = returnedFunction.on; + returnedFunction.on = (fnToCall: (args: InputType) => void) => + wrapped((data: InputType) => { + if (options?.scalars) { + return fnToCall( + decodeScalarsInResponse({ + response: data, + initialOp: operation, + initialZeusQuery: o as VType, + returns: ReturnTypes, + scalars: options.scalars, + ops: Ops, + }), + ); + } + return fnToCall(data); + }); + } + return returnedFunction; + }; +export const SubscriptionSSE = (...options: chainOptions) => SubscriptionThunderSSE(apiSubscriptionSSE(options)); export const Zeus = < Z extends ValueTypes[R], O extends keyof typeof Ops, diff --git a/packages/graphql-zeus-core/TreeToTS/functions/new/mocks.ts b/packages/graphql-zeus-core/TreeToTS/functions/new/mocks.ts index b8c199f..50ea2e4 100644 --- a/packages/graphql-zeus-core/TreeToTS/functions/new/mocks.ts +++ b/packages/graphql-zeus-core/TreeToTS/functions/new/mocks.ts @@ -333,6 +333,8 @@ export type ScalarCoders = { JSON?: ScalarResolver; }; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const apiSubscriptionSSE = (opts: chainOptions) => ((q: string) => 1) as unknown as SubscriptionFunction; // eslint-disable-next-line @typescript-eslint/no-unused-vars export const apiSubscription = (opts: chainOptions) => ((q: string) => 1) as unknown as SubscriptionFunction; // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/packages/graphql-zeus-core/TreeToTS/index.ts b/packages/graphql-zeus-core/TreeToTS/index.ts index 225710f..8ecc987 100644 --- a/packages/graphql-zeus-core/TreeToTS/index.ts +++ b/packages/graphql-zeus-core/TreeToTS/index.ts @@ -8,7 +8,7 @@ import { resolveInputTypes } from '@/TreeToTS/templates/valueTypes/inputTypes'; import { resolveVariableTypes } from '@/TreeToTS/templates/variableTypes'; import { createParserField, Options, ParserTree, TypeDefinition } from 'graphql-js-tree'; import { Environment } from '../Models'; -import { default as typescriptFunctions, subscriptionFunctions } from './functions/generated'; +import { default as typescriptFunctions, subscriptionFunctions, sseFunctions } from './functions/generated'; import { resolvePropTypeFromRoot } from './templates/returnedPropTypes'; import { resolveReturnFromRoot } from './templates/returnedReturns'; import { resolveTypes } from './templates/returnedTypes'; @@ -123,6 +123,8 @@ export class TreeToTS { .concat('\n') .concat(subscriptionFunctions[subscriptions]) .concat('\n') + .concat(sseFunctions['sse']) + .concat('\n') .concat(typescriptFunctions) .concat('\n') .concat( diff --git a/packages/graphql-zeus-core/package.json b/packages/graphql-zeus-core/package.json index 35be84c..0651808 100644 --- a/packages/graphql-zeus-core/package.json +++ b/packages/graphql-zeus-core/package.json @@ -1,6 +1,6 @@ { "name": "graphql-zeus-core", - "version": "7.1.3", + "version": "7.1.4", "private": false, "main": "./lib/index.js", "author": "GraphQL Editor, Artur Czemiel", diff --git a/packages/graphql-zeus-jsonschema/package.json b/packages/graphql-zeus-jsonschema/package.json index 7fdeb24..efb235a 100644 --- a/packages/graphql-zeus-jsonschema/package.json +++ b/packages/graphql-zeus-jsonschema/package.json @@ -1,6 +1,6 @@ { "name": "graphql-zeus-jsonschema", - "version": "7.1.3", + "version": "7.1.4", "private": false, "main": "./lib/index.js", "author": "GraphQL Editor, Artur Czemiel", diff --git a/packages/graphql-zeus/package.json b/packages/graphql-zeus/package.json index 0013219..205d9f4 100644 --- a/packages/graphql-zeus/package.json +++ b/packages/graphql-zeus/package.json @@ -1,6 +1,6 @@ { "name": "graphql-zeus", - "version": "7.1.3", + "version": "7.1.4", "private": false, "scripts": { "start": "ttsc --watch", @@ -27,8 +27,8 @@ "config-maker": "^0.0.6", "cross-fetch": "^3.0.4", "graphql-js-tree": "^3.0.4", - "graphql-zeus-core": "^7.1.3", - "graphql-zeus-jsonschema": "^7.1.3", + "graphql-zeus-core": "^7.1.4", + "graphql-zeus-jsonschema": "^7.1.4", "yargs": "^16.1.1" } }