sofa-api
Version:
Create REST APIs with GraphQL
400 lines (399 loc) • 14.4 kB
JavaScript
;
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];
}