UNPKG

@awesomeniko/kafka-trail

Version:

A Node.js library for managing message queue with Kafka

334 lines 11 kB
import { GlueClient, GetSchemaVersionCommand } from "@aws-sdk/client-glue"; import { KTSchemaRegistryError } from "../schema-errors.js"; import { createAjvCodec } from "./ajv-adapter.js"; import { createZodCodec } from "./zod-adapter.js"; const DEFAULT_AWS_GLUE_SCHEMA_CACHE = new Map(); const asSchemaVersion = (value) => { if (typeof value === "string") { return value; } if (typeof value === "number" && Number.isFinite(value)) { return String(value); } return undefined; }; const asVersionNumber = (value) => { if (typeof value === "number" && Number.isFinite(value)) { return value; } if (typeof value === "string" && value.trim()) { const parsed = Number(value); if (Number.isFinite(parsed)) { return parsed; } } return undefined; }; const createSchemaCacheKey = (lookup) => { const registryName = lookup.registryName ?? "default"; const schemaArn = lookup.schemaArn ?? ""; const schemaVersionId = lookup.schemaVersionId ?? ""; const schemaVersionNumber = lookup.schemaVersionNumber ?? ""; return `${registryName}::${lookup.schemaName}::${schemaArn}::${schemaVersionId}::${schemaVersionNumber}`; }; const getCacheEntry = (params) => { const cacheEntry = params.store.get(params.key); if (!cacheEntry) { return undefined; } if (cacheEntry.expiresAt !== null && cacheEntry.expiresAt <= params.now) { params.store.delete(params.key); return undefined; } return cacheEntry; }; const parseSchemaDefinition = (schemaDefinition) => { if (typeof schemaDefinition !== "string") { return schemaDefinition; } try { const parsed = JSON.parse(schemaDefinition); if (!parsed || typeof parsed !== "object") { throw new Error("Schema definition JSON must resolve to an object"); } return parsed; } catch (error) { throw new KTSchemaRegistryError({ message: "Unable to parse AWS Glue schema definition as JSON object", details: error, }); } }; const deriveSchemaMeta = (params) => { const schemaName = params.fetchedSchema.schemaName ?? params.lookup.schemaName; const schemaVersion = asSchemaVersion(params.fetchedSchema.schemaVersionNumber) ?? asSchemaVersion(params.lookup.schemaVersionNumber); const schemaId = params.fetchedSchema.schemaVersionId ?? params.lookup.schemaVersionId ?? params.fetchedSchema.schemaArn ?? params.lookup.schemaArn; const schemaMeta = { provider: "aws-glue", }; if (schemaName) { schemaMeta.schemaName = schemaName; } if (schemaVersion) { schemaMeta.schemaVersion = schemaVersion; } if (schemaId) { schemaMeta.schemaId = schemaId; } return schemaMeta; }; const resolveSchemaFromGlue = (params) => { if (params.fetchedSchema.dataFormat && params.fetchedSchema.dataFormat !== "JSON") { throw new KTSchemaRegistryError({ message: `Unsupported AWS Glue schema format: ${params.fetchedSchema.dataFormat}. Only JSON is supported`, details: { schemaName: params.fetchedSchema.schemaName ?? params.schemaLookup.schemaName, dataFormat: params.fetchedSchema.dataFormat, }, }); } const parsedSchema = parseSchemaDefinition(params.fetchedSchema.schemaDefinition); const schemaMeta = deriveSchemaMeta({ lookup: params.schemaLookup, fetchedSchema: params.fetchedSchema, }); return { schema: parsedSchema, schemaMeta, }; }; const createGetSchemaVersionInput = (lookup) => { if (lookup.schemaVersionId) { return { SchemaVersionId: lookup.schemaVersionId, }; } const schemaVersionNumber = asVersionNumber(lookup.schemaVersionNumber); if (lookup.schemaVersionNumber !== undefined && schemaVersionNumber === undefined) { throw new KTSchemaRegistryError({ message: `Invalid schemaVersionNumber for AWS Glue lookup: ${String(lookup.schemaVersionNumber)}`, details: { schemaName: lookup.schemaName, schemaVersionNumber: lookup.schemaVersionNumber, }, }); } const schemaId = { SchemaArn: lookup.schemaArn, SchemaName: lookup.schemaArn ? undefined : lookup.schemaName, RegistryName: lookup.schemaArn ? undefined : lookup.registryName, }; const commandInput = { SchemaId: schemaId, }; if (schemaVersionNumber !== undefined) { commandInput.SchemaVersionNumber = { VersionNumber: schemaVersionNumber, }; } else { commandInput.SchemaVersionNumber = { LatestVersion: true, }; } return commandInput; }; const fetchSchemaFromAwsGlue = async (params) => { const commandInput = createGetSchemaVersionInput(params.lookup); const command = new GetSchemaVersionCommand(commandInput); const response = await params.client.send(command); if (!response.SchemaDefinition) { throw new KTSchemaRegistryError({ message: `AWS Glue returned empty schema definition for schemaName "${params.lookup.schemaName}"`, details: response, }); } const fetchedSchema = { schemaDefinition: response.SchemaDefinition, schemaName: params.lookup.schemaName, }; if (response.SchemaVersionId) { fetchedSchema.schemaVersionId = response.SchemaVersionId; } if (response.VersionNumber !== undefined) { fetchedSchema.schemaVersionNumber = response.VersionNumber; } if (response.SchemaArn) { fetchedSchema.schemaArn = response.SchemaArn; } if (response.DataFormat) { fetchedSchema.dataFormat = response.DataFormat; } return fetchedSchema; }; const resolveGlueSchema = async (params) => { const store = params.cache?.store ?? DEFAULT_AWS_GLUE_SCHEMA_CACHE; const cacheKey = createSchemaCacheKey(params.lookup); const now = Date.now(); const existingEntry = getCacheEntry({ store, key: cacheKey, now, }); if (existingEntry) { return { schema: existingEntry.schema, schemaMeta: existingEntry.schemaMeta, fetchedSchema: { schemaDefinition: existingEntry.schema, }, }; } let fetchedSchema; try { fetchedSchema = await params.glue.getSchema(params.lookup); } catch (error) { throw new KTSchemaRegistryError({ message: `Unable to fetch schema from AWS Glue registry for schemaName "${params.lookup.schemaName}"`, details: error, }); } const resolvedSchema = resolveSchemaFromGlue({ schemaLookup: params.lookup, fetchedSchema, }); const ttlMs = params.cache?.ttlMs; const expiresAt = typeof ttlMs === "number" && ttlMs > 0 ? now + ttlMs : null; const cacheEntry = { schema: resolvedSchema.schema, schemaMeta: resolvedSchema.schemaMeta, expiresAt, }; store.set(cacheKey, cacheEntry); return { schema: cacheEntry.schema, schemaMeta: cacheEntry.schemaMeta, fetchedSchema, }; }; export const clearAwsGlueSchemaCache = (store) => { const resolvedStore = store ?? DEFAULT_AWS_GLUE_SCHEMA_CACHE; resolvedStore.clear(); }; export const preloadAwsGlueSchemas = async (params) => { for (const schemaLookup of params.schemas) { const resolveParams = { glue: params.glue, lookup: schemaLookup, cache: params.cache, }; await resolveGlueSchema(resolveParams); } }; const createGlueClient = (params) => { if (params.client) { return params.client; } const clientConfig = { region: params.region, }; if (params.credentials) { clientConfig.credentials = params.credentials; } if (params.profile) { clientConfig.profile = params.profile; } if (params.endpoint) { clientConfig.endpoint = params.endpoint; } const glueClient = new GlueClient(clientConfig); return glueClient; }; export const createAwsGlueSchemaRegistryAdapter = async (params) => { const client = createGlueClient(params); const adapter = { async getSchema(lookup) { const fetchParams = { client, lookup, }; return fetchSchemaFromAwsGlue(fetchParams); }, async preloadSchemas(schemas, options) { const preloadParams = { glue: adapter, schemas, }; if (options?.cache) { preloadParams.cache = options.cache; } await preloadAwsGlueSchemas(preloadParams); }, destroy() { if (client.destroy) { client.destroy(); } }, }; if (params.preload?.schemas.length) { const preloadParams = { glue: adapter, schemas: params.preload.schemas, }; if (params.preload.cache) { preloadParams.cache = params.preload.cache; } await preloadAwsGlueSchemas(preloadParams); } return adapter; }; export const createAwsGlueCodec = async (params) => { const resolvedSchema = await resolveGlueSchema({ glue: params.glue, lookup: params.schema, cache: params.cache, }); const mergedSchemaMeta = { ...resolvedSchema.schemaMeta, ...params.schemaMeta, }; if (params.validator === "zod") { if (!params.zodSchemaFactory) { throw new KTSchemaRegistryError({ message: "zodSchemaFactory is required when validator is set to \"zod\"", }); } const zodSchemaFactoryParams = { schema: resolvedSchema.schema, lookup: params.schema, fetchedSchema: resolvedSchema.fetchedSchema, schemaMeta: mergedSchemaMeta, }; const zodSchema = params.zodSchemaFactory(zodSchemaFactoryParams); const codec = createZodCodec(zodSchema, { schemaMeta: mergedSchemaMeta, }); return codec; } if (!params.ajv) { throw new KTSchemaRegistryError({ message: "ajv compiler is required for AWS Glue codec with ajv validator", }); } const validate = params.ajv.compile(resolvedSchema.schema); const createCodecParams = { validate, schemaMeta: mergedSchemaMeta, }; if (params.serialize) { createCodecParams.serialize = params.serialize; } if (params.parse) { createCodecParams.parse = params.parse; } return createAjvCodec(createCodecParams); }; //# sourceMappingURL=aws-glue-adapter.js.map