@hasura/ndc-sdk-typescript
Version:
This SDK is mostly analogous to the Rust SDK, except where necessary.
288 lines • 11.7 kB
JavaScript
;
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