UNPKG

@hasura/ndc-sdk-typescript

Version:

This SDK is mostly analogous to the Rust SDK, except where necessary.

288 lines 11.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.startServer = startServer; const fastify_1 = __importDefault(require("fastify")); const compress_1 = __importDefault(require("@fastify/compress")); const api_1 = __importStar(require("@opentelemetry/api")); const error_1 = require("./error"); const logging_1 = require("./logging"); const schema_1 = require("./schema"); const instrumentation_1 = require("./instrumentation"); const prom_client_1 = require("prom-client"); const semver_1 = __importDefault(require("semver")); // Create custom Ajv options to handle Rust's uint types which are formats used in the JSON schemas, so this converts that to a number const customAjvOptions = { allErrors: true, removeAdditional: true, formats: { uint: { validate: (data) => { return (typeof data === "number" && data >= 0 && data <= 4294967295 && Number.isInteger(data)); }, type: "number", }, uint32: { validate: (data) => { return (typeof data === "number" && data >= 0 && data <= 4294967295 && Number.isInteger(data)); }, type: "number", }, }, }; const errorResponses = { 400: schema_1.ErrorResponseSchema, 403: schema_1.ErrorResponseSchema, 409: schema_1.ErrorResponseSchema, 422: schema_1.ErrorResponseSchema, 500: schema_1.ErrorResponseSchema, 501: schema_1.ErrorResponseSchema, 502: schema_1.ErrorResponseSchema, }; const tracer = api_1.default.trace.getTracer("ndc-sdk-typescript.server"); async function startServer(connector, options) { const configuration = await connector.parseConfiguration(options.configuration); const metrics = new prom_client_1.Registry(); (0, prom_client_1.collectDefaultMetrics)({ register: metrics }); const state = await connector.tryInitState(configuration, metrics); const server = (0, fastify_1.default)({ logger: (0, logging_1.configureFastifyLogging)(options), bodyLimit: 1048576 * 30, // 30mb body limit ajv: { customOptions: customAjvOptions, }, }); // Register compression plugin await server.register(compress_1.default, { global: true, // TODO add zstd when we upgrade to Node.js 22.15+/23.8+ encodings: ['gzip', 'deflate'], }); // temporary: use JSON.stringify instead of https://github.com/fastify/fast-json-stringify // todo: remove this once issue is addressed https://github.com/fastify/fastify/issues/5073 server.setSerializerCompiler(({ schema, method, url, httpStatus, contentType }) => { return (data) => JSON.stringify(data); }); // Authorization handler server.addHook("preHandler", async (request, reply) => { // Don't apply authorization to the healthcheck endpoint if (request.routeOptions.method === "GET" && request.routeOptions.url === "/health") { return; } const expectedAuthHeader = options.serviceTokenSecret === undefined ? undefined : `Bearer ${options.serviceTokenSecret}`; if (request.headers.authorization === expectedAuthHeader) { return; } else { reply.code(401).send({ message: "Internal Error", details: { cause: "Bearer token does not match.", }, }); return reply; } }); // NDC Version header handler const lowercaseVersionHeaderName = schema_1.VERSION_HEADER_NAME.toLowerCase(); const connectorSemVer = new semver_1.default.SemVer(schema_1.VERSION); server.addHook("preHandler", async (request, reply) => { const versionHeader = request.headers[lowercaseVersionHeaderName]; if (versionHeader === undefined) { return; } if (Array.isArray(versionHeader)) { reply.code(400).send({ message: `Multiple ${schema_1.VERSION_HEADER_NAME} headers received. Only one is supported.`, }); return reply; } let wantedSemVer; try { wantedSemVer = new semver_1.default.SemVer(versionHeader); } catch (e) { reply.code(400).send({ message: `Invalid semver in ${schema_1.VERSION_HEADER_NAME}s header`, details: e instanceof Error ? { error: e.message } : {} }); return reply; } const wantedSemVerRange = new semver_1.default.Range(`^${wantedSemVer.toString()}`); if (!semver_1.default.satisfies(connectorSemVer, wantedSemVerRange)) { reply.code(400).send({ message: `The connector does not support the requested NDC version`, details: { connectorVersion: connectorSemVer.toString(), requestedVersionRange: wantedSemVerRange.toString(), } }); return reply; } }); server.get("/capabilities", { schema: { response: { 200: schema_1.CapabilitiesResponseSchema, ...errorResponses, }, }, }, (_request) => { return (0, instrumentation_1.withActiveSpan)(tracer, "getCapabilities", () => ({ version: schema_1.VERSION, capabilities: connector.getCapabilities(configuration), })); }); server.get("/health", async (_request) => { return connector.getHealthReadiness ? await connector.getHealthReadiness(configuration, state) : undefined; }); server.get("/metrics", (_request) => { connector.fetchMetrics(configuration, state); return metrics.metrics(); }); server.get("/schema", { schema: { response: { 200: schema_1.SchemaResponseSchema, ...errorResponses, }, }, }, (_request) => { return (0, instrumentation_1.withActiveSpan)(tracer, "getSchema", () => connector.getSchema(configuration)); }); server.post("/query", { schema: { body: schema_1.QueryRequestSchema, response: { 200: schema_1.QueryResponseSchema, ...errorResponses, }, }, }, async (request) => { request.log.debug({ requestHeaders: request.headers, requestBody: request.body }, "Query Request"); const queryResponse = await (0, instrumentation_1.withActiveSpan)(tracer, "handleQuery", () => connector.query(configuration, state, request.body)); request.log.debug({ responseBody: queryResponse }, "Query Response"); return queryResponse; }); server.post("/query/explain", { schema: { body: schema_1.QueryRequestSchema, response: { 200: schema_1.ExplainResponseSchema, ...errorResponses, }, }, }, async (request) => { request.log.debug({ requestHeaders: request.headers, requestBody: request.body }, "Explain Request"); const explainResponse = await (0, instrumentation_1.withActiveSpan)(tracer, "handleQueryExplain", () => connector.queryExplain(configuration, state, request.body)); request.log.debug({ responseBody: explainResponse }, "Query Explain Response"); return explainResponse; }); server.post("/mutation", { schema: { body: schema_1.MutationRequestSchema, response: { 200: schema_1.MutationResponseSchema, ...errorResponses, }, }, }, async (request) => { request.log.debug({ requestHeaders: request.headers, requestBody: request.body }, "Mutation Request"); const mutationResponse = await (0, instrumentation_1.withActiveSpan)(tracer, "handleMutation", () => connector.mutation(configuration, state, request.body)); request.log.debug({ responseBody: mutationResponse }, "Mutation Response"); return mutationResponse; }); server.post("/mutation/explain", { schema: { body: schema_1.MutationRequestSchema, response: { 200: schema_1.ExplainResponseSchema, ...errorResponses, }, }, }, async (request) => { request.log.debug({ requestHeaders: request.headers, requestBody: request.body }, "Mutation Explain Request"); const explainResponse = await (0, instrumentation_1.withActiveSpan)(tracer, "handleMutationExplain", () => connector.mutationExplain(configuration, state, request.body)); request.log.debug({ responseBody: explainResponse }, "Mutation Explain Response"); return explainResponse; }); server.setErrorHandler(function (error, _request, reply) { // pino trace instrumentation will add trace information to log output this.log.error(error); if (error.validation) { reply.status(400).send({ message: "Validation Error - https://fastify.dev/docs/latest/Reference/Validation-and-Serialization#error-handling", details: error.validation, }); } else if (error instanceof error_1.ConnectorError) { // Send error response reply.status(error.statusCode).send({ message: error.message, details: error.details ?? {}, }); } else { const span = api_1.default.trace.getActiveSpan(); span?.recordException(error); span?.setStatus({ code: api_1.SpanStatusCode.ERROR }); reply.status(500).send({ message: error.message, details: {}, }); } }); try { await server.listen({ port: options.port, host: options.host }); } catch (error) { server.log.error(error); process.exitCode = 1; } } //# sourceMappingURL=server.js.map