import type { AURItoFieldAlgebra, AURItoInputAlgebra, ExtendObjectTypeBuilder, FieldAURIS, GQLSubscriptionAlgebra, InputAURIS, InputObjectTypeBuilder, InterfaceTypeBuilder, MutationTypeBuilder, ObjectTypeBuilder, QueryTypeBuilder, ScalarTypeBuilder, ScalarTypeFromModelBuilder, SchemaGenerator, SchemaParts, SubscriptionTypeBuilder, UnionTypeBuilder } from '../schema' import type { Has } from '@principia/base/Has' import type { HttpConnection } from '@principia/http/HttpConnection' import type { Config } from 'apollo-server-core' import type { IResolvers } from 'graphql-tools' import type { DefaultState } from 'koa' import type { ConnectionContext } from 'subscriptions-transport-ws' import type WebSocket from 'ws' import { identity, pipe } from '@principia/base/function' import { tag } from '@principia/base/Has' import * as I from '@principia/base/IO' import * as L from '@principia/base/Layer' import { HttpConnectionTag } from '@principia/http/HttpConnection' import * as Koa from '@principia/koa' import { ApolloServer } from 'apollo-server-koa' import { formatError } from 'graphql' import { makeExecutableSchema } from 'graphql-tools' import { Context, makeExtendObjectTypeBuilder, makeInputObjectTypeBuilder, makeInterfaceTypeBuilder, makeMutationTypeBuilder, makeObjectTypeBuilder, makeQueryTypeBuilder, makeScalarTypeBuilder, makeScalarTypeFromModelBuilder, makeSchemaGenerator, makeSubscriptionTypeBuilder, makeUnionTypeBuilder } from '../schema' import { formatGraphQlException, GQLException } from '../schema/GQLException' import { transformResolvers, transformScalarResolvers } from './transform' export type GraphQlConfig = Omit & { subscriptions?: Partial<{ keepAlive?: number onConnect?: (connectionParams: unknown, websocket: WebSocket, context: ConnectionContext) => I.IO onDisconnect?: (websocket: WebSocket, context: ConnectionContext) => I.IO path: string }> } export type SubscriptionsEnv = C extends { subscriptions?: Partial<{ keepAlive?: number onConnect?: ( connectionParams: unknown, websocket: WebSocket, context: ConnectionContext ) => I.IO onDisconnect?: (websocket: WebSocket, context: ConnectionContext) => I.IO path: string }> } ? (R extends never ? unknown : R) & (R1 extends never ? unknown : R1) : unknown export interface GraphQlInstanceConfig { readonly additionalResolvers?: IResolvers readonly schemaParts: SchemaParts } export interface GraphQlDriver< FieldAURI extends FieldAURIS, InputAURI extends InputAURIS, Ctx, C extends GraphQlConfig, RE > { readonly askContext: I.URIO, HttpConnection> readonly extend: ExtendObjectTypeBuilder> readonly generateSchema: SchemaGenerator> readonly input: InputObjectTypeBuilder readonly object: () => ObjectTypeBuilder> readonly scalar: ScalarTypeBuilder readonly scalarFromModel: ScalarTypeFromModelBuilder readonly subscription: SubscriptionTypeBuilder> readonly query: QueryTypeBuilder> readonly mutation: MutationTypeBuilder> readonly union: UnionTypeBuilder> readonly interface: InterfaceTypeBuilder> readonly live: ( config: GraphQlInstanceConfig, R> ) => L.Layer & RE & Has, never, Has> } export interface GraphQlInstance { readonly server: ApolloServer } export const GraphQlInstance = tag() type KoaContextFn = (_: { connection?: Conf['subscriptions'] extends { onConnect: (...args: any[]) => I.IO } ? Omit & { context: A } : Koa.Context['connection'] ctx: Koa.Context }) => I.IO> export function makeGraphQl( interpreters: AURItoFieldAlgebra[FieldPURI] & AURItoInputAlgebra[InputPURI] & GQLSubscriptionAlgebra ) { return ( config: C, context: KoaContextFn ): GraphQlDriver => { const askContext: I.URIO, HttpConnection> = I.asksService(HttpConnectionTag)(identity) as any const gqlKoaInstance = (instanceConfig: GraphQlInstanceConfig, R>) => { const acquire = I.gen(function* (_) { const env = yield* _(I.ask & RE>()) const [app, httpServer] = yield* _(I.asksService(Koa.KoaAppTag)((koa) => [koa.app, koa.server] as const)) const scalars = transformScalarResolvers(instanceConfig.schemaParts.scalars ?? {}, env) const resolvers = transformResolvers>(instanceConfig.schemaParts.resolvers, env) const typeResolvers = instanceConfig.schemaParts.typeResolvers const apolloConfig = { ...config } as Omit if (config.subscriptions && config.subscriptions.onConnect) { const onConnect = config.subscriptions.onConnect const onDisconnect = config.subscriptions.onDisconnect apolloConfig.subscriptions = { keepAlive: config.subscriptions.keepAlive, onConnect: (connectionParams, websocket, context) => pipe(onConnect(connectionParams, websocket, context), I.give(env), I.runPromise), onDisconnect: onDisconnect ? (websocket, context) => pipe(onDisconnect(websocket, context), I.give(env), I.runPromise) : undefined, path: config.subscriptions.path } } const schema = yield* _( pipe( I.try(() => makeExecutableSchema({ resolvers: { ...resolvers, ...typeResolvers, ...(instanceConfig.additionalResolvers ?? {}), ...scalars }, typeDefs: instanceConfig.schemaParts.typeDefs }) ), I.orHalt ) ) return yield* _( pipe( I.try(() => { const server = new ApolloServer({ context: (ctx) => pipe(context(ctx), I.give(env), I.runPromise), schema, ...apolloConfig, formatError: (error) => { if (apolloConfig.formatError) { return apolloConfig.formatError(error) } return error.originalError instanceof GQLException ? formatGraphQlException(error as any) : formatError(error) } }) server.applyMiddleware({ app: app as any }) if (apolloConfig.subscriptions) { server.installSubscriptionHandlers(httpServer) } return server }), I.orHalt ) ) }) return L.prepare(GraphQlInstance)(I.map_(acquire, (server) => ({ server }))).release(({ server }) => pipe(I.fromPromiseHalt(server.stop)) ) } return { generateSchema: makeSchemaGenerator>(), askContext: askContext, extend: makeExtendObjectTypeBuilder>(interpreters), input: makeInputObjectTypeBuilder(interpreters), live: gqlKoaInstance, object: () => makeObjectTypeBuilder(interpreters)>(), subscription: makeSubscriptionTypeBuilder(interpreters)>(), scalar: makeScalarTypeBuilder, mutation: makeMutationTypeBuilder(interpreters)>(), scalarFromModel: makeScalarTypeFromModelBuilder, query: makeQueryTypeBuilder(interpreters)>(), union: makeUnionTypeBuilder>(), interface: makeInterfaceTypeBuilder(interpreters)>() } } }