UNPKG

sofa-api

Version:

Create REST APIs with GraphQL

400 lines (399 loc) 14.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createSofaRouter = createSofaRouter; const graphql_1 = require("graphql"); const utils_1 = require("@graphql-tools/utils"); const ast_js_1 = require("./ast.js"); const common_js_1 = require("./common.js"); const parse_js_1 = require("./parse.js"); const subscriptions_js_1 = require("./subscriptions.js"); const logger_js_1 = require("./logger.js"); const fets_1 = require("fets"); const operations_js_1 = require("./open-api/operations.js"); const types_js_1 = require("./open-api/types.js"); const qs_1 = require("qs"); const defaultErrorHandler = (errors) => { let status; const headers = { 'Content-Type': 'application/json; charset=utf-8', }; for (const error of errors) { if (typeof error === 'object' && error != null && error.extensions?.http) { if (error.extensions.http.status && (!status || error.extensions.http.status > status)) { status = error.extensions.http.status; } if (error.extensions.http.headers) { Object.assign(headers, error.extensions.http.headers); } delete error.extensions.http; } } if (!status) { status = 500; } return fets_1.Response.json({ errors }, { status, headers, }); }; function useRequestBody(method) { return method === 'POST' || method === 'PUT' || method === 'PATCH'; } function createSofaRouter(sofa) { logger_js_1.logger.debug('[Sofa] Creating router'); sofa.openAPI ||= {}; sofa.openAPI.info ||= {}; sofa.openAPI.info.title ||= 'SOFA API'; sofa.openAPI.info.description ||= 'Generated by SOFA'; sofa.openAPI.info.version ||= '0.0.0'; sofa.openAPI.components ||= {}; sofa.openAPI.components.schemas ||= {}; const types = sofa.schema.getTypeMap(); for (const typeName in types) { const type = types[typeName]; if (!(0, graphql_1.isIntrospectionType)(type)) { if ((0, graphql_1.isObjectType)(type) || (0, graphql_1.isInputObjectType)(type)) { sofa.openAPI.components.schemas[typeName] = (0, types_js_1.buildSchemaObjectFromType)(type, { customScalars: sofa.customScalars, }); } else if ((0, graphql_1.isEnumType)(type)) { sofa.openAPI.components.schemas[typeName] = { title: type.name, type: 'string', enum: type.getValues().map((value) => value.name), }; } } } const router = (0, fets_1.createRouter)({ base: sofa.basePath, openAPI: sofa.openAPI, swaggerUI: sofa.swaggerUI, landingPage: false, }); const queryType = sofa.schema.getQueryType(); const mutationType = sofa.schema.getMutationType(); const subscriptionManager = new subscriptions_js_1.SubscriptionManager(sofa); if (queryType) { Object.keys(queryType.getFields()).forEach((fieldName) => { createQueryRoute({ sofa, router, fieldName }); }); } if (mutationType) { Object.keys(mutationType.getFields()).forEach((fieldName) => { createMutationRoute({ sofa, router, fieldName }); }); } router.route({ path: '/webhook', method: 'POST', async handler(request, serverContext) { const { subscription, variables, url } = await request.json(); try { const sofaContext = Object.assign(serverContext, { request, }); const result = await subscriptionManager.start({ subscription, variables, url, }, sofaContext); return fets_1.Response.json(result); } catch (error) { return fets_1.Response.json(error, { status: 500, statusText: 'Subscription failed', }); } }, }); router.route({ path: '/webhook/:id', method: 'POST', async handler(request, serverContext) { const id = request.params?.id; const body = await request.json(); const variables = body.variables; try { const sofaContext = Object.assign(serverContext, { request, }); const contextValue = await sofa.contextFactory(sofaContext); const result = await subscriptionManager.update({ id, variables, }, contextValue); return fets_1.Response.json(result); } catch (error) { return fets_1.Response.json(error, { status: 500, statusText: 'Subscription failed to update', }); } }, }); router.route({ path: '/webhook/:id', method: 'DELETE', async handler(request) { const id = request.params?.id; try { const result = await subscriptionManager.stop(id); return fets_1.Response.json(result); } catch (error) { return fets_1.Response.json(error, { status: 500, statusText: 'Subscription failed to stop', }); } }, }); return router; } function createQueryRoute({ sofa, router, fieldName, }) { logger_js_1.logger.debug(`[Router] Creating ${fieldName} query`); const queryType = sofa.schema.getQueryType(); const operationNode = (0, utils_1.buildOperationNodeForField)({ kind: 'query', schema: sofa.schema, field: fieldName, models: sofa.models, ignore: sofa.ignore, circularReferenceDepth: sofa.depthLimit, }); const operation = { kind: graphql_1.Kind.DOCUMENT, definitions: [operationNode], }; const info = (0, ast_js_1.getOperationInfo)(operation); const field = queryType.getFields()[fieldName]; const fieldType = field.type; const isSingle = (0, graphql_1.isObjectType)(fieldType) || ((0, graphql_1.isNonNullType)(fieldType) && (0, graphql_1.isObjectType)(fieldType.ofType)); const hasIdArgument = field.args.some((arg) => arg.name === 'id'); const graphqlPath = `${queryType.name}.${fieldName}`; const routeConfig = sofa.routes?.[graphqlPath]; const route = { method: routeConfig?.method ?? 'GET', path: routeConfig?.path ?? getPath(fieldName, isSingle && hasIdArgument), responseStatus: routeConfig?.responseStatus ?? 200, tags: routeConfig?.tags ?? [], description: routeConfig?.description ?? field.description ?? '', }; router.route({ operationId: operationNode.name?.value, path: route.path, method: route.method, schemas: getRouteSchemas({ method: route.method, path: route.path, info, sofa, responseStatus: route.responseStatus, }), tags: route.tags, handler: useHandler({ info, route, fieldName, sofa, operation }), }); logger_js_1.logger.debug(`[Router] ${fieldName} query available at ${route.method} ${route.path}`); return { document: operation, path: route.path, method: route.method.toUpperCase(), }; } function getRouteSchemas({ method, path, info, sofa, responseStatus, }) { const params = { type: 'object', properties: {}, required: [], }; const query = { type: 'object', properties: {}, required: [], }; for (const variable of info.variables) { const varSchema = (0, operations_js_1.resolveParamSchema)(variable.type, { customScalars: sofa.customScalars, enumTypes: sofa.enumTypes, }); varSchema.description = (0, operations_js_1.resolveVariableDescription)(sofa.schema, info.operation, variable); const varName = variable.variable.name.value; const varObj = (0, operations_js_1.isInPath)(path, varName) ? params : query; varObj.properties[varName] = varSchema; if (variable.type.kind === graphql_1.Kind.NON_NULL_TYPE) { varObj.required.push(varName); } } return { request: { json: useRequestBody(method) ? (0, operations_js_1.resolveRequestBody)(info.variables, sofa.schema, info.operation, { customScalars: sofa.customScalars, enumTypes: sofa.enumTypes, }) : undefined, params, query, }, responses: { [responseStatus]: (0, operations_js_1.resolveResponse)({ schema: sofa.schema, operation: info.operation, opts: { customScalars: sofa.customScalars, enumTypes: sofa.enumTypes, }, }), }, }; } function createMutationRoute({ sofa, router, fieldName, }) { logger_js_1.logger.debug(`[Router] Creating ${fieldName} mutation`); const mutationType = sofa.schema.getMutationType(); const field = mutationType.getFields()[fieldName]; const operationNode = (0, utils_1.buildOperationNodeForField)({ kind: 'mutation', schema: sofa.schema, field: fieldName, models: sofa.models, ignore: sofa.ignore, circularReferenceDepth: sofa.depthLimit, }); const operation = { kind: graphql_1.Kind.DOCUMENT, definitions: [operationNode], }; const info = (0, ast_js_1.getOperationInfo)(operation); const graphqlPath = `${mutationType.name}.${fieldName}`; const routeConfig = sofa.routes?.[graphqlPath]; const method = routeConfig?.method ?? 'POST'; const path = routeConfig?.path ?? getPath(fieldName); const responseStatus = routeConfig?.responseStatus ?? 200; const tags = routeConfig?.tags ?? []; const description = routeConfig?.description ?? field.description ?? ''; const route = { method, path, responseStatus, tags, description, }; router.route({ method, path, schemas: getRouteSchemas({ method, path, info, responseStatus, sofa, }), tags: route.tags, description: route.description, handler: useHandler({ info, route, fieldName, sofa, operation }), }); logger_js_1.logger.debug(`[Router] ${fieldName} mutation available at ${method} ${path}`); return { document: operation, path, method, tags: routeConfig?.tags || [], description: routeConfig?.description ?? field.description ?? '', }; } function useHandler(config) { const { sofa, operation, fieldName } = config; const info = config.info; const errorHandler = sofa.errorHandler || defaultErrorHandler; return async (request, serverContext) => { try { let body = {}; if (request.body != null) { const strBody = await request.text(); if (strBody) { try { body = JSON.parse(strBody); } catch (error) { throw (0, utils_1.createGraphQLError)('POST body sent invalid JSON.', { extensions: { http: { status: 400, }, }, }); } } } let variableValues = {}; try { variableValues = info.variables.reduce((variables, variable) => { const name = variable.variable.name.value; const value = (0, parse_js_1.parseVariable)({ value: pickParam({ url: request.url, body, params: request.params || {}, name, }), variable, schema: sofa.schema, }); if (typeof value === 'undefined') { return variables; } return { ...variables, [name]: value, }; }, {}); } catch (error) { throw (0, utils_1.createGraphQLError)(error.message || error.toString?.() || error, { extensions: { http: { status: 400, }, }, }); } const sofaContext = Object.assign(serverContext, { request, }); const contextValue = await sofa.contextFactory(sofaContext); const result = await sofa.execute({ schema: sofa.schema, document: operation, contextValue, variableValues, operationName: info.operation.name && info.operation.name.value, }); if (result.errors) { return errorHandler(result.errors); } return fets_1.Response.json(result.data?.[fieldName], { status: config.route.responseStatus, }); } catch (error) { return errorHandler([error]); } }; } function getPath(fieldName, hasId = false) { return `/${(0, common_js_1.convertName)(fieldName)}${hasId ? '/:id' : ''}`; } function pickParam({ name, url, params, body, }) { if (name in params) { return params[name]; } const searchParams = (0, qs_1.parse)(url.split('?')[1]); return searchParams[name] || body[name]; }