diff --git a/controlplane/package.json b/controlplane/package.json index 51dac24c33..a8e4acb6cc 100644 --- a/controlplane/package.json +++ b/controlplane/package.json @@ -94,6 +94,7 @@ "stream-json": "^1.8.0", "stripe": "^14.19.0", "tiny-lru": "^11.2.11", + "tinypool": "^2.1.0", "uid": "^2.0.2", "uuid": "^10.0.0", "zod": "^3.25.0", diff --git a/controlplane/src/core/bufservices/federated-graph/checkFederatedGraph.ts b/controlplane/src/core/bufservices/federated-graph/checkFederatedGraph.ts index c94ea1f5b9..ce529145e6 100644 --- a/controlplane/src/core/bufservices/federated-graph/checkFederatedGraph.ts +++ b/controlplane/src/core/bufservices/federated-graph/checkFederatedGraph.ts @@ -8,9 +8,8 @@ import { CompositionWarning, Subgraph, } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; -import { parse } from 'graphql'; import { COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID } from '../../../types/index.js'; -import { composeSubgraphs } from '../../composition/composition.js'; +import { composeGraphsInWorker } from '../../composition/composeGraphs.pool.js'; import { FederatedGraphRepository } from '../../repositories/FederatedGraphRepository.js'; import { DefaultNamespace } from '../../repositories/NamespaceRepository.js'; import { OrganizationRepository } from '../../repositories/OrganizationRepository.js'; @@ -108,19 +107,25 @@ export function checkFederatedGraph( featureId: COMPOSITION_IGNORE_EXTERNAL_KEYS_FEATURE_ID, }); - const result = composeSubgraphs( - subgraphsUsedForComposition.map((s) => ({ - id: s.id, - name: s.name, - url: s.routingUrl, - definitions: parse(s.schemaSDL), - })), - federatedGraph.routerCompatibilityVersion, - { + const { results } = await composeGraphsInWorker({ + federatedGraph, + subgraphsToCompose: [ + { + subgraphs: subgraphsUsedForComposition, + isFeatureFlagComposition: false, + featureFlagName: '', + featureFlagId: '', + }, + ], + tagOptionsByContractName: [], + compositionOptions: { disableResolvabilityValidation: req.disableResolvabilityValidation, ignoreExternalKeys: ignoreExternalKeysFeature?.enabled ?? false, }, - ); + skipRouterConfig: true, + }); + + const compositionResult = results[0].base; // If req.limit is not provided, we return all rows const returnLimit = req.limit === undefined ? null : clamp(req.limit, 1, maxRowLimitForChecks); @@ -138,9 +143,9 @@ export function checkFederatedGraph( }; const compositionWarnings: PlainMessage[] = []; - counts.compositionWarnings = result.warnings.length; + counts.compositionWarnings = compositionResult.warnings.length; - const clampedWarnings = returnLimit ? result.warnings.slice(0, returnLimit) : result.warnings; + const clampedWarnings = returnLimit ? compositionResult.warnings.slice(0, returnLimit) : compositionResult.warnings; for (const warning of clampedWarnings) { compositionWarnings.push({ message: warning.message, @@ -150,14 +155,14 @@ export function checkFederatedGraph( }); } - if (!result.success) { + if (!compositionResult.success) { const compositionErrors: PlainMessage[] = []; - counts.compositionErrors = result.errors.length; + counts.compositionErrors = compositionResult.errors.length; - const clampedErrors = returnLimit ? result.errors.slice(0, returnLimit) : result.errors; + const clampedErrors = returnLimit ? compositionResult.errors.slice(0, returnLimit) : compositionResult.errors; for (const error of clampedErrors) { compositionErrors.push({ - message: error.message, + message: error, federatedGraphName: req.name, namespace: federatedGraph.namespace, featureFlag: '', diff --git a/controlplane/src/core/build-server.ts b/controlplane/src/core/build-server.ts index 0c454da204..2b18c007dc 100644 --- a/controlplane/src/core/build-server.ts +++ b/controlplane/src/core/build-server.ts @@ -53,9 +53,13 @@ import { createReactivateOrganizationWorker, ReactivateOrganizationQueue, } from './workers/ReactivateOrganizationWorker.js'; +import { configureComposeGraphsPool, destroyComposeGraphsPool } from './composition/composeGraphs.pool.js'; export interface BuildConfig { logger: LoggerOptions; + composition?: { + maxThreads: number; + }; database: { url: string; tls?: { @@ -157,6 +161,10 @@ const developmentLoggerOpts: LoggerOptions = { }; export default async function build(opts: BuildConfig) { + configureComposeGraphsPool({ + maxThreads: opts.composition?.maxThreads ?? 0, + }); + opts.logger = { timestamp: stdTimeFunctions.isoTime, formatters: { @@ -535,6 +543,12 @@ export default async function build(opts: BuildConfig) { await Promise.all(bullWorkers.map((worker) => worker.close())); fastify.log.debug('Bull workers shut down'); + + fastify.log.debug('Shutting down composition worker pool'); + + await destroyComposeGraphsPool(); + + fastify.log.debug('Composition worker pool shut down'); }); return fastify; diff --git a/controlplane/src/core/composition/composeGraphs.pool.ts b/controlplane/src/core/composition/composeGraphs.pool.ts new file mode 100644 index 0000000000..5a9d5ba6a8 --- /dev/null +++ b/controlplane/src/core/composition/composeGraphs.pool.ts @@ -0,0 +1,132 @@ +/** + * Main-thread bridge for composition worker execution. + * + * The worker only exchanges plain `Serialized*` payloads so we do not rely on + * structured cloning of rich runtime objects across the Tinypool boundary. + * Node 22 loads the source `.ts` worker natively in development, and the built + * `.js` worker in production. + */ +import { existsSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { availableParallelism } from 'node:os'; +import { Warning } from '@wundergraph/composition'; +import { RouterConfig } from '@wundergraph/cosmo-connect/dist/node/v1/node_pb'; +import WorkerPool from 'tinypool'; +import { FederatedGraphDTO } from '../../types/index.js'; +import { validateRouterCompatibilityVersion } from './composition.js'; +import { ComposedFederatedGraph, CompositionSubgraphRecord } from './composer.js'; +import { + ComposeGraphsTaskInput, + ComposeGraphsTaskResult, + SerializedComposedGraphArtifact, +} from './composeGraphs.types.js'; + +let composeGraphsPool: WorkerPool | undefined; +const composeGraphsPoolConfig = { + maxThreads: 0, +}; + +export interface ConfigureComposeGraphsPoolOptions { + maxThreads: number; +} + +function getWorkerFilename() { + const sourceWorker = new URL('composeGraphs.worker.ts', import.meta.url); + if (existsSync(fileURLToPath(sourceWorker))) { + return { + filename: sourceWorker.href, + }; + } + + return { + filename: new URL('composeGraphs.worker.js', import.meta.url).href, + }; +} + +function getMaxThreads() { + if (composeGraphsPoolConfig.maxThreads > 0) { + return composeGraphsPoolConfig.maxThreads; + } + + return Math.max(1, availableParallelism()); +} + +function getComposeGraphsPool() { + if (composeGraphsPool) { + return composeGraphsPool; + } + + const { filename } = getWorkerFilename(); + + composeGraphsPool = new WorkerPool({ + filename, + minThreads: 1, + maxThreads: getMaxThreads(), + concurrentTasksPerWorker: 2, + }); + + return composeGraphsPool; +} + +function deserializeWarning(message: string, subgraphName?: string) { + return new Warning({ + message, + subgraph: { + name: subgraphName || '', + }, + }); +} + +export type DeserializedComposedGraph = Omit & { + subgraphs: CompositionSubgraphRecord[]; +}; + +export function deserializeComposedGraphArtifact( + federatedGraph: Pick, + artifact: SerializedComposedGraphArtifact, +): DeserializedComposedGraph { + return { + id: federatedGraph.id, + targetID: federatedGraph.targetId, + name: federatedGraph.name, + namespace: federatedGraph.namespace, + namespaceId: federatedGraph.namespaceId, + composedSchema: artifact.composedSchema, + federatedClientSchema: artifact.federatedClientSchema, + shouldIncludeClientSchema: artifact.shouldIncludeClientSchema, + errors: artifact.errors.map((message) => new Error(message)), + fieldConfigurations: artifact.fieldConfigurations, + subgraphs: artifact.subgraphs, + warnings: artifact.warnings.map((warning) => deserializeWarning(warning.message, warning.subgraphName)), + }; +} + +export function deserializeRouterExecutionConfig(routerExecutionConfigJson?: ReturnType) { + if (!routerExecutionConfigJson) { + return; + } + + return RouterConfig.fromJson(routerExecutionConfigJson); +} + +export function composeGraphsInWorker(task: Omit) { + const fullTask: ComposeGraphsTaskInput = { + ...task, + routerCompatibilityVersion: validateRouterCompatibilityVersion(task.federatedGraph.routerCompatibilityVersion), + }; + return getComposeGraphsPool().run(fullTask) as Promise; +} + +export function configureComposeGraphsPool(options: ConfigureComposeGraphsPoolOptions) { + composeGraphsPoolConfig.maxThreads = options.maxThreads; +} + +export async function destroyComposeGraphsPool() { + if (!composeGraphsPool) { + return; + } + + const pool = composeGraphsPool; + composeGraphsPool = undefined; + await pool.destroy(); +} diff --git a/controlplane/src/core/composition/composeGraphs.types.ts b/controlplane/src/core/composition/composeGraphs.types.ts new file mode 100644 index 0000000000..3f909f46c7 --- /dev/null +++ b/controlplane/src/core/composition/composeGraphs.types.ts @@ -0,0 +1,80 @@ +/** + * These types define the thread boundary for compose-and-deploy work. + * + * The `Serialized*` prefix is intentional: these payloads are flattened to + * structured-clone-safe data before crossing the Tinypool worker boundary. + * Rich runtime objects such as GraphQL schema instances, Maps, protobuf + * classes, and custom Error/Warning instances are reconstructed outside the + * worker when needed. + */ +import type { + CompositionOptions, + FieldConfiguration, + SupportedRouterCompatibilityVersion, +} from '@wundergraph/composition'; +import { RouterConfig } from '@wundergraph/cosmo-connect/dist/node/v1/node_pb'; +import { FederatedGraphDTO, SubgraphDTO } from '../../types/index.js'; + +export interface SerializedContractTagOptions { + contractName: string; + excludeTags: string[]; + includeTags: string[]; +} + +export interface SerializedCompositionWarning { + message: string; + subgraphName?: string; +} + +export interface SerializedComposedSubgraph { + id: string; + isFeatureSubgraph: boolean; + name: string; + sdl: string; + schemaVersionId: string; + targetId: string; +} + +export interface SerializedComposedGraphArtifact { + success: boolean; + errors: string[]; + warnings: SerializedCompositionWarning[]; + composedSchema?: string; + federatedClientSchema?: string; + shouldIncludeClientSchema: boolean; + fieldConfigurations: FieldConfiguration[]; + subgraphs: SerializedComposedSubgraph[]; + routerExecutionConfigJson?: ReturnType; +} + +export interface SerializedContractCompositionArtifact { + contractName: string; + artifact: SerializedComposedGraphArtifact; +} + +export interface ComposeGraphsTaskInput { + federatedGraph: FederatedGraphDTO; + /** Pre-validated on the main thread before dispatching to the worker. */ + routerCompatibilityVersion: SupportedRouterCompatibilityVersion; + subgraphsToCompose: { + subgraphs: SubgraphDTO[]; + isFeatureFlagComposition: boolean; + featureFlagName: string; + featureFlagId: string; + }[]; + tagOptionsByContractName: SerializedContractTagOptions[]; + compositionOptions?: CompositionOptions; + skipRouterConfig?: boolean; +} + +export interface ComposeGraphsTaskResultItem { + isFeatureFlagComposition: boolean; + featureFlagName: string; + featureFlagId: string; + base: SerializedComposedGraphArtifact; + contracts: SerializedContractCompositionArtifact[]; +} + +export interface ComposeGraphsTaskResult { + results: ComposeGraphsTaskResultItem[]; +} diff --git a/controlplane/src/core/composition/composeGraphs.worker.ts b/controlplane/src/core/composition/composeGraphs.worker.ts new file mode 100644 index 0000000000..381d84efdb --- /dev/null +++ b/controlplane/src/core/composition/composeGraphs.worker.ts @@ -0,0 +1,246 @@ +/** + * Self-contained worker entry for federated graph composition. + * + * Keep this file independent from local controlplane runtime helpers where + * possible. The worker returns plain `Serialized*` payloads so the thread + * boundary stays stable even when richer in-process models change. + * + * IMPORTANT: Avoid adding value imports from local `.ts` files (e.g. + * `./composition.js`). Tinypool worker threads cannot resolve `.js` imports + * to `.ts` source files, so only `import type` (which is erased at runtime) + * is safe for local modules. Value imports from npm packages are fine. + */ +import { randomUUID } from 'node:crypto'; +import { printSchemaWithDirectives } from '@graphql-tools/utils'; +import { + federateSubgraphsContract, + federateSubgraphsWithContracts, + newContractTagOptionsFromArrays, +} from '@wundergraph/composition'; +import { buildRouterConfig, SubgraphKind } from '@wundergraph/cosmo-shared'; +import { GRPCMapping, ImageReference, RouterConfig } from '@wundergraph/cosmo-connect/dist/node/v1/node_pb'; +import { parse } from 'graphql'; +import type { FederationResult, FederationResultWithContracts } from '@wundergraph/composition'; +import type { RouterSubgraph } from '@wundergraph/cosmo-shared'; +import type { SubgraphDTO } from '../../types/index.js'; +import type { + ComposeGraphsTaskInput, + ComposeGraphsTaskResult, + SerializedContractCompositionArtifact, + SerializedComposedGraphArtifact, +} from './composeGraphs.types.js'; + +function parseGRPCMapping(mappings: string): GRPCMapping { + try { + return GRPCMapping.fromJson(JSON.parse(mappings)); + } catch (error) { + throw new Error(`Failed to parse gRPC mappings: ${error instanceof Error ? error.message : 'Unknown error'}`); + } +} + +/** + * Build rich RouterSubgraph objects from SubgraphDTOs and federation result. + * Only needed when building router execution config. + */ +function subgraphDTOsToRouterSubgraphs( + organizationId: string, + subgraphs: SubgraphDTO[], + result: FederationResult, +): RouterSubgraph[] { + return subgraphs.map((subgraph) => { + const subgraphConfig = result.success ? result.subgraphConfigBySubgraphName.get(subgraph.name) : undefined; + const schema = subgraphConfig?.schema; + const configurationDataByTypeName = subgraphConfig?.configurationDataByTypeName; + + if (subgraph.type === 'grpc_plugin') { + if (!subgraph.proto?.pluginData) { + throw new Error(`Subgraph ${subgraph.name} is a plugin but does not have a plugin data`); + } + + return { + kind: SubgraphKind.Plugin, + id: subgraph.id, + version: subgraph.proto.pluginData.version, + name: subgraph.name, + sdl: subgraph.schemaSDL, + url: subgraph.routingUrl, + configurationDataByTypeName, + schema, + protoSchema: subgraph.proto.schema, + mapping: parseGRPCMapping(subgraph.proto.mappings), + imageReference: new ImageReference({ + repository: `${organizationId}/${subgraph.id}`, + reference: subgraph.proto.pluginData.version, + }), + }; + } + + if (subgraph.type === 'grpc_service') { + if (!subgraph.proto) { + throw new Error(`Subgraph ${subgraph.name} is a GRPC service but does not have a proto`); + } + + return { + kind: SubgraphKind.GRPC, + id: subgraph.id, + name: subgraph.name, + sdl: subgraph.schemaSDL, + url: subgraph.routingUrl, + configurationDataByTypeName, + schema, + protoSchema: subgraph.proto.schema, + mapping: parseGRPCMapping(subgraph.proto.mappings), + }; + } + + return { + kind: SubgraphKind.Standard, + id: subgraph.id, + name: subgraph.name, + url: subgraph.routingUrl, + sdl: subgraph.schemaSDL, + subscriptionUrl: subgraph.subscriptionUrl, + subscriptionProtocol: subgraph.subscriptionProtocol, + websocketSubprotocol: + subgraph.subscriptionProtocol === 'ws' ? subgraph.websocketSubprotocol || 'auto' : undefined, + configurationDataByTypeName, + schema, + }; + }); +} + +// Serialize the worker-side composition result into a structured-clone-safe +// artifact for the main thread. This keeps the boundary limited to plain data, +// and the main thread is responsible for rebuilding richer runtime objects +// such as RouterConfig instances before persistence and upload. +function serializeComposedGraphArtifact( + organizationId: string, + routerCompatibilityVersion: string, + subgraphs: SubgraphDTO[], + result: FederationResult, + includeRouterExecutionConfig: boolean, +): SerializedComposedGraphArtifact { + const composedSchema = result.success ? printSchemaWithDirectives(result.federatedGraphSchema) : undefined; + const federatedClientSchema = result.success + ? printSchemaWithDirectives(result.federatedGraphClientSchema) + : undefined; + const shouldIncludeClientSchema = result.success ? (result.shouldIncludeClientSchema ?? false) : false; + const fieldConfigurations = result.success ? result.fieldConfigurations : []; + + let routerExecutionConfigJson: ReturnType | undefined; + if (includeRouterExecutionConfig && result.success && composedSchema) { + const routerSubgraphs = subgraphDTOsToRouterSubgraphs(organizationId, subgraphs, result); + const routerExecutionConfig = buildRouterConfig({ + federatedClientSDL: shouldIncludeClientSchema ? federatedClientSchema || '' : '', + federatedSDL: composedSchema, + fieldConfigurations, + routerCompatibilityVersion, + subgraphs: routerSubgraphs, + schemaVersionId: randomUUID(), + }); + routerExecutionConfigJson = routerExecutionConfig.toJson(); + } + + return { + success: result.success, + errors: result.success ? [] : result.errors.map((error) => error.message), + warnings: result.warnings.map((warning) => ({ + message: warning.message, + subgraphName: warning.subgraph?.name, + })), + composedSchema, + federatedClientSchema, + shouldIncludeClientSchema, + fieldConfigurations, + subgraphs: subgraphs.map((subgraph) => ({ + id: subgraph.id, + isFeatureSubgraph: subgraph.isFeatureSubgraph, + name: subgraph.name, + sdl: subgraph.schemaSDL, + schemaVersionId: subgraph.schemaVersionId, + targetId: subgraph.targetId, + })), + routerExecutionConfigJson, + }; +} + +function toCompositionSubgraphs(subgraphs: SubgraphDTO[]) { + return subgraphs + .filter((s) => s.schemaSDL !== '') + .map((subgraph) => ({ + name: subgraph.name, + url: subgraph.routingUrl, + definitions: parse(subgraph.schemaSDL), + })); +} + +export default function composeGraphsInWorker(task: ComposeGraphsTaskInput): ComposeGraphsTaskResult { + const tagOptionsByContractName = new Map( + task.tagOptionsByContractName.map((tagOptions) => [ + tagOptions.contractName, + newContractTagOptionsFromArrays(tagOptions.excludeTags, tagOptions.includeTags), + ]), + ); + + return { + results: task.subgraphsToCompose.map((subgraphsToCompose) => { + const compositionSubgraphs = toCompositionSubgraphs(subgraphsToCompose.subgraphs); + + // Version is validated on the main thread before dispatching to the worker. + const version = task.routerCompatibilityVersion; + + const result: FederationResult | FederationResultWithContracts = task.federatedGraph.contract + ? federateSubgraphsContract({ + contractTagOptions: newContractTagOptionsFromArrays( + task.federatedGraph.contract.excludeTags, + task.federatedGraph.contract.includeTags, + ), + options: task.compositionOptions, + subgraphs: compositionSubgraphs, + version, + }) + : federateSubgraphsWithContracts({ + options: task.compositionOptions, + subgraphs: compositionSubgraphs, + tagOptionsByContractName, + version, + }); + + const includeRouterConfig = !task.skipRouterConfig; + const base = serializeComposedGraphArtifact( + task.federatedGraph.organizationId, + task.federatedGraph.routerCompatibilityVersion, + subgraphsToCompose.subgraphs, + result, + includeRouterConfig, + ); + + const contracts: SerializedContractCompositionArtifact[] = []; + if ('federationResultByContractName' in result && result.success) { + for (const [contractName, contractResult] of result.federationResultByContractName as Map< + string, + FederationResult + >) { + contracts.push({ + contractName, + artifact: serializeComposedGraphArtifact( + task.federatedGraph.organizationId, + task.federatedGraph.routerCompatibilityVersion, + subgraphsToCompose.subgraphs, + contractResult, + includeRouterConfig, + ), + }); + } + } + + return { + isFeatureFlagComposition: subgraphsToCompose.isFeatureFlagComposition, + featureFlagName: subgraphsToCompose.featureFlagName, + featureFlagId: subgraphsToCompose.featureFlagId, + base, + contracts, + }; + }), + }; +} diff --git a/controlplane/src/core/composition/composer.ts b/controlplane/src/core/composition/composer.ts index e64af7f624..5c592cc021 100644 --- a/controlplane/src/core/composition/composer.ts +++ b/controlplane/src/core/composition/composer.ts @@ -1,31 +1,17 @@ import type { UUID } from 'node:crypto'; -import { printSchemaWithDirectives } from '@graphql-tools/utils'; import { CompositionOptions, - ContractTagOptions, - FederationResult, FieldConfiguration, - newContractTagOptionsFromArrays, ROUTER_COMPATIBILITY_VERSION_ONE, ROUTER_COMPATIBILITY_VERSIONS, - Subgraph, SupportedRouterCompatibilityVersion, Warning, } from '@wundergraph/composition'; -import { - buildRouterConfig, - ComposedSubgraph as IComposedSubgraph, - ComposedSubgraphGRPC, - ComposedSubgraphPlugin, - SubgraphKind, -} from '@wundergraph/cosmo-shared'; import { FastifyBaseLogger } from 'fastify'; -import { DocumentNode, GraphQLSchema, parse } from 'graphql'; +import { GraphQLSchema } from 'graphql'; import { FeatureFlagRouterExecutionConfig, FeatureFlagRouterExecutionConfigs, - GRPCMapping, - ImageReference, RouterConfig, } from '@wundergraph/cosmo-connect/dist/node/v1/node_pb'; import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; @@ -47,7 +33,11 @@ import { CacheWarmerRepository } from '../repositories/CacheWarmerRepository.js' import { NamespaceRepository } from '../repositories/NamespaceRepository.js'; import { InspectorSchemaChange } from '../services/SchemaUsageTrafficInspector.js'; import { SchemaCheckChangeAction } from '../../db/models.js'; -import { composeFederatedGraphWithPotentialContracts, composeSubgraphs } from './composition.js'; +import { + composeGraphsInWorker, + DeserializedComposedGraph, + deserializeComposedGraphArtifact, +} from './composeGraphs.pool.js'; import { getDiffBetweenGraphs, GetDiffBetweenGraphsResult, GetDiffBetweenGraphsSuccess } from './schemaCheck.js'; export function getRouterCompatibilityVersionPath(routerCompatibilityVersion: string): string { @@ -61,7 +51,7 @@ export function getRouterCompatibilityVersionPath(routerCompatibilityVersion: st } } export type CompositionResult = { - compositions: ComposedFederatedGraph[]; + compositions: DeserializedComposedGraph[]; }; export interface S3RouterConfigMetadata extends Record { @@ -92,142 +82,18 @@ export function routerConfigToFeatureFlagExecutionConfig(routerConfig: RouterCon }); } -export function buildRouterExecutionConfig( - composedGraph: ComposedFederatedGraph, - federatedSchemaVersionId: UUID, - routerCompatibilityVersion: string, -): RouterConfig | undefined { - if (composedGraph.errors.length > 0 || !composedGraph.composedSchema) { - return; - } - const federatedClientSDL = composedGraph.shouldIncludeClientSchema ? composedGraph.federatedClientSchema || '' : ''; - return buildRouterConfig({ - federatedClientSDL, - federatedSDL: composedGraph.composedSchema, - fieldConfigurations: composedGraph.fieldConfigurations, - routerCompatibilityVersion, - subgraphs: composedGraph.subgraphs, - schemaVersionId: federatedSchemaVersionId, - }); -} - -export type ComposedSubgraph = (IComposedSubgraph | ComposedSubgraphPlugin | ComposedSubgraphGRPC) & { +/** + * The minimal subgraph fields required for composition persistence (changelog, composition records). + * The full ComposedSubgraph carries additional runtime data (url, sdl, schema, gRPC metadata, etc.) + * that is only needed for building router execution configs. + */ +export interface CompositionSubgraphRecord { + id: string; + name: string; + sdl: string; targetId: string; - isFeatureSubgraph: boolean; schemaVersionId: string; -}; - -const parseGRPCMapping = (mappings: string): GRPCMapping => { - try { - const mappingsJson = JSON.parse(mappings); - return GRPCMapping.fromJson(mappingsJson); - } catch (error) { - throw new Error(`Failed to parse gRPC mappings: ${error instanceof Error ? error.message : 'Unknown error'}`); - } -}; - -export function subgraphDTOsToComposedSubgraphs( - organizationId: string, - subgraphs: SubgraphDTO[], - result: FederationResult, -): ComposedSubgraph[] { - return subgraphs.map((subgraph) => { - /* batchNormalize returns an intermediate representation of the engine configuration - * and a normalized schema per subgraph. - * Batch normalization is necessary because validation of certain things such as the @override directive requires - * knowledge of the other subgraphs. - * Each normalized schema and engine configuration is mapped by subgraph name to a SubgraphConfig object wrapper. - * This is passed to the FederationFactory and is returned by federateSubgraphs if federation is successful. - * The normalized schema and engine configuration is used by buildRouterConfig. - * */ - const subgraphConfig = result.success ? result.subgraphConfigBySubgraphName.get(subgraph.name) : undefined; - const schema = subgraphConfig?.schema; - const configurationDataByTypeName = subgraphConfig?.configurationDataByTypeName; - - if (subgraph.type === 'grpc_plugin') { - if (!subgraph.proto || !subgraph.proto.pluginData) { - throw new Error(`Subgraph ${subgraph.name} is a plugin but does not have a plugin data`); - } - - return { - kind: SubgraphKind.Plugin, - id: subgraph.id, - version: subgraph.proto.pluginData.version, - name: subgraph.name, - sdl: subgraph.schemaSDL, - url: subgraph.routingUrl, - schemaVersionId: subgraph.schemaVersionId, - targetId: subgraph.targetId, - isFeatureSubgraph: subgraph.isFeatureSubgraph, - configurationDataByTypeName, - schema, - protoSchema: subgraph.proto.schema, - mapping: parseGRPCMapping(subgraph.proto.mappings), - imageReference: new ImageReference({ - repository: `${organizationId}/${subgraph.id}`, - reference: subgraph.proto.pluginData.version, - }), - }; - } - if (subgraph.type === 'grpc_service') { - if (!subgraph.proto) { - throw new Error(`Subgraph ${subgraph.name} is a GRPC service but does not have a proto`); - } - - return { - kind: SubgraphKind.GRPC, - id: subgraph.id, - name: subgraph.name, - sdl: subgraph.schemaSDL, - url: subgraph.routingUrl, - schemaVersionId: subgraph.schemaVersionId, - targetId: subgraph.targetId, - isFeatureSubgraph: subgraph.isFeatureSubgraph, - configurationDataByTypeName, - schema, - protoSchema: subgraph.proto.schema, - mapping: parseGRPCMapping(subgraph.proto.mappings), - }; - } - - return { - kind: SubgraphKind.Standard, - id: subgraph.id, - name: subgraph.name, - targetId: subgraph.targetId, - isFeatureSubgraph: subgraph.isFeatureSubgraph, - url: subgraph.routingUrl, - sdl: subgraph.schemaSDL, - schemaVersionId: subgraph.schemaVersionId, - subscriptionUrl: subgraph.subscriptionUrl, - subscriptionProtocol: subgraph.subscriptionProtocol, - websocketSubprotocol: - subgraph.subscriptionProtocol === 'ws' ? subgraph.websocketSubprotocol || 'auto' : undefined, - configurationDataByTypeName, - schema, - }; - }); -} - -export function mapResultToComposedGraph( - federatedGraph: FederatedGraphDTO, - subgraphs: SubgraphDTO[], - result: FederationResult, -): ComposedFederatedGraph { - return { - id: federatedGraph.id, - targetID: federatedGraph.targetId, - name: federatedGraph.name, - namespace: federatedGraph.namespace, - namespaceId: federatedGraph.namespaceId, - composedSchema: result.success ? printSchemaWithDirectives(result.federatedGraphSchema) : undefined, - federatedClientSchema: result.success ? printSchemaWithDirectives(result.federatedGraphClientSchema) : undefined, - shouldIncludeClientSchema: result.success ? result.shouldIncludeClientSchema : false, - errors: result.success ? [] : result.errors, - subgraphs: subgraphDTOsToComposedSubgraphs(federatedGraph.organizationId, subgraphs, result), - fieldConfigurations: result.success ? result.fieldConfigurations : [], - warnings: result.warnings, - }; + isFeatureSubgraph: boolean; } export interface ComposedFederatedGraph { @@ -238,7 +104,7 @@ export interface ComposedFederatedGraph { namespaceId: string; composedSchema?: string; errors: Error[]; - subgraphs: ComposedSubgraph[]; + subgraphs: CompositionSubgraphRecord[]; fieldConfigurations: FieldConfiguration[]; federatedClientSchema?: string; shouldIncludeClientSchema?: boolean; @@ -625,12 +491,10 @@ export class Composer { protected async composeWithLabels( subgraphLabels: Label[], namespaceId: string, - mapSubgraphs: ( - subgraphs: SubgraphDTO[], - ) => [SubgraphDTO[], { name: string; url: string; definitions: DocumentNode }[]], + mapSubgraphs: (subgraphs: SubgraphDTO[]) => SubgraphDTO[], compositionOptions?: CompositionOptions, ): Promise { - const composedGraphs: ComposedFederatedGraph[] = []; + const composedGraphs: DeserializedComposedGraph[] = []; const graphs = await this.federatedGraphRepo.bySubgraphLabels({ labels: subgraphLabels, @@ -640,49 +504,42 @@ export class Composer { for await (const graph of graphs) { try { - const [subgraphs, subgraphsToBeComposed] = mapSubgraphs( - await this.subgraphRepo.listByFederatedGraph({ federatedGraphTargetId: graph.targetId }), - ); + const allSubgraphs = await this.subgraphRepo.listByFederatedGraph({ + federatedGraphTargetId: graph.targetId, + }); + const subgraphsToSend = mapSubgraphs(allSubgraphs); const contracts = await this.contractRepo.bySourceFederatedGraphId(graph.id); - - if (contracts.length === 0) { - const federationResult = composeSubgraphs( - subgraphsToBeComposed, - graph.routerCompatibilityVersion, - compositionOptions, - ); - composedGraphs.push(mapResultToComposedGraph(graph, subgraphs, federationResult)); - continue; - } - - const tagOptionsByContractName = new Map(); - - for (const contract of contracts) { - tagOptionsByContractName.set( - contract.downstreamFederatedGraph.target.name, - newContractTagOptionsFromArrays(contract.excludeTags, contract.includeTags), - ); - } - - const federationResult = composeFederatedGraphWithPotentialContracts( - subgraphsToBeComposed, + const tagOptionsByContractName = contracts.map((c) => ({ + contractName: c.downstreamFederatedGraph.target.name, + excludeTags: c.excludeTags, + includeTags: c.includeTags, + })); + + const { results } = await composeGraphsInWorker({ + federatedGraph: graph, + subgraphsToCompose: [ + { + subgraphs: subgraphsToSend, + isFeatureFlagComposition: false, + featureFlagName: '', + featureFlagId: '', + }, + ], tagOptionsByContractName, - graph.routerCompatibilityVersion, compositionOptions, - ); - composedGraphs.push(mapResultToComposedGraph(graph, subgraphs, federationResult)); + skipRouterConfig: true, + }); - if (!federationResult.success) { - continue; - } + const base = results[0]; + composedGraphs.push(deserializeComposedGraphArtifact(graph, base.base)); - for (const [contractName, contractResult] of federationResult.federationResultByContractName) { - const contractGraph = await this.federatedGraphRepo.byName(contractName, graph.namespace); + for (const contractArtifact of base.contracts) { + const contractGraph = await this.federatedGraphRepo.byName(contractArtifact.contractName, graph.namespace); if (!contractGraph) { - throw new Error(`Contract graph ${contractName} not found`); + throw new Error(`Contract graph ${contractArtifact.contractName} not found`); } - composedGraphs.push(mapResultToComposedGraph(contractGraph, subgraphs, contractResult)); + composedGraphs.push(deserializeComposedGraphArtifact(contractGraph, contractArtifact.artifact)); } } catch (e: any) { composedGraphs.push({ @@ -718,25 +575,9 @@ export class Composer { subgraphLabels, namespaceId, (subgraphs) => { - const subgraphsToBeComposed: Array = []; - - for (const subgraph of subgraphs) { - if (subgraph.name === subgraphName) { - subgraphsToBeComposed.push({ - name: subgraph.name, - url: subgraph.routingUrl, - definitions: parse(subgraphSchemaSDL), - }); - } else if (subgraph.schemaSDL !== '') { - subgraphsToBeComposed.push({ - name: subgraph.name, - url: subgraph.routingUrl, - definitions: parse(subgraph.schemaSDL), - }); - } - } - - return [subgraphs, subgraphsToBeComposed]; + return subgraphs + .filter((s) => s.name === subgraphName || s.schemaSDL !== '') + .map((s) => (s.name === subgraphName ? { ...s, schemaSDL: subgraphSchemaSDL } : s)); }, compositionOptions, ); @@ -751,7 +592,7 @@ export class Composer { inputSubgraphs: Map; compositionOptions?: CompositionOptions; }) { - const composedGraphs: ComposedFederatedGraph[] = []; + const composedGraphs: DeserializedComposedGraph[] = []; // the key is the federated graph id and the value is the list of check subgraph ids which are part of the composition for that federated graph const checkSubgraphsByFedGraph = new Map(); for (const graph of graphs) { @@ -760,7 +601,7 @@ export class Composer { federatedGraphTargetId: graph.targetId, }); - const subgraphsToBeComposed: Subgraph[] = []; + const subgraphsToSend: SubgraphDTO[] = []; for (const subgraph of subgraphsOfFedGraph) { const inputSubgraph = inputSubgraphs.get(subgraph.name); if (inputSubgraph) { @@ -771,17 +612,9 @@ export class Composer { if (inputSubgraph.newSchemaSDL === '') { continue; } - subgraphsToBeComposed.push({ - name: subgraph.name, - url: subgraph.routingUrl, - definitions: parse(inputSubgraph.newSchemaSDL), - }); + subgraphsToSend.push({ ...subgraph, schemaSDL: inputSubgraph.newSchemaSDL }); } else if (subgraph.schemaSDL !== '') { - subgraphsToBeComposed.push({ - name: subgraph.name, - url: subgraph.routingUrl, - definitions: parse(subgraph.schemaSDL), - }); + subgraphsToSend.push(subgraph); } } @@ -806,52 +639,56 @@ export class Composer { ...(checkSubgraphsByFedGraph.get(graph.id) || []), subgraph.checkSubgraphId, ]); - subgraphsToBeComposed.push({ + subgraphsToSend.push({ + id: '', name: subgraphName, - url: '', - definitions: parse(subgraph.newSchemaSDL), - }); + targetId: '', + routingUrl: '', + schemaSDL: subgraph.newSchemaSDL, + schemaVersionId: '', + isFeatureSubgraph: false, + subscriptionUrl: '', + subscriptionProtocol: 'ws', + namespace: graph.namespace, + namespaceId: graph.namespaceId, + type: 'standard', + labels: subgraph.labels || [], + lastUpdatedAt: '', + isEventDrivenGraph: false, + } as SubgraphDTO); } const contracts = await this.contractRepo.bySourceFederatedGraphId(graph.id); - - if (contracts.length === 0) { - const federationResult = composeSubgraphs( - subgraphsToBeComposed, - graph.routerCompatibilityVersion, - compositionOptions, - ); - composedGraphs.push(mapResultToComposedGraph(graph, subgraphsOfFedGraph, federationResult)); - continue; - } - - const tagOptionsByContractName = new Map(); - - for (const contract of contracts) { - tagOptionsByContractName.set( - contract.downstreamFederatedGraph.target.name, - newContractTagOptionsFromArrays(contract.excludeTags, contract.includeTags), - ); - } - - const federationResult = composeFederatedGraphWithPotentialContracts( - subgraphsToBeComposed, + const tagOptionsByContractName = contracts.map((c) => ({ + contractName: c.downstreamFederatedGraph.target.name, + excludeTags: c.excludeTags, + includeTags: c.includeTags, + })); + + const { results } = await composeGraphsInWorker({ + federatedGraph: graph, + subgraphsToCompose: [ + { + subgraphs: subgraphsToSend, + isFeatureFlagComposition: false, + featureFlagName: '', + featureFlagId: '', + }, + ], tagOptionsByContractName, - graph.routerCompatibilityVersion, compositionOptions, - ); - composedGraphs.push(mapResultToComposedGraph(graph, subgraphsOfFedGraph, federationResult)); + skipRouterConfig: true, + }); - if (!federationResult.success) { - continue; - } + const base = results[0]; + composedGraphs.push(deserializeComposedGraphArtifact(graph, base.base)); - for (const [contractName, contractResult] of federationResult.federationResultByContractName) { - const contractGraph = await this.federatedGraphRepo.byName(contractName, graph.namespace); + for (const contractArtifact of base.contracts) { + const contractGraph = await this.federatedGraphRepo.byName(contractArtifact.contractName, graph.namespace); if (!contractGraph) { - throw new Error(`Contract graph ${contractName} not found`); + throw new Error(`Contract graph ${contractArtifact.contractName} not found`); } - composedGraphs.push(mapResultToComposedGraph(contractGraph, subgraphsOfFedGraph, contractResult)); + composedGraphs.push(deserializeComposedGraphArtifact(contractGraph, contractArtifact.artifact)); } } catch (e: any) { composedGraphs.push({ diff --git a/controlplane/src/core/composition/composition.ts b/controlplane/src/core/composition/composition.ts index 5eb508f041..729b89b320 100644 --- a/controlplane/src/core/composition/composition.ts +++ b/controlplane/src/core/composition/composition.ts @@ -1,66 +1,11 @@ import { CompositionOptions, - ContractTagOptions, - federateSubgraphs, - federateSubgraphsContract, - federateSubgraphsWithContracts, - FederationResult, NormalizationResult, normalizeSubgraphFromString, ROUTER_COMPATIBILITY_VERSIONS, - Subgraph, SupportedRouterCompatibilityVersion, } from '@wundergraph/composition'; -/** - * Composes a list of subgraphs and returns the result for the base graph and its contract graphs - */ -export function composeFederatedGraphWithPotentialContracts( - subgraphs: Subgraph[], - tagOptionsByContractName: Map, - version: string, - options?: CompositionOptions, -) { - return federateSubgraphsWithContracts({ - options, - subgraphs, - tagOptionsByContractName, - version: validateRouterCompatibilityVersion(version), - }); -} - -/** - * Composes a list of subgraphs for a contract using a set of exclusion tags - */ -export function composeFederatedContract( - subgraphs: Subgraph[], - contractTagOptions: ContractTagOptions, - version: string, - options?: CompositionOptions, -) { - return federateSubgraphsContract({ - contractTagOptions, - options, - subgraphs, - version: validateRouterCompatibilityVersion(version), - }); -} - -/** - * Composes a list of subgraphs into a single schema. - */ -export function composeSubgraphs( - subgraphs: Subgraph[], - version: string, - options?: CompositionOptions, -): FederationResult { - return federateSubgraphs({ - options, - subgraphs, - version: validateRouterCompatibilityVersion(version), - }); -} - /** * Normalizes and builds a GraphQLSchema from a string. It is not the same as buildSchema from graphql-js. */ @@ -78,7 +23,7 @@ export function buildSchema( }); } -function validateRouterCompatibilityVersion(version: string): SupportedRouterCompatibilityVersion { +export function validateRouterCompatibilityVersion(version: string): SupportedRouterCompatibilityVersion { const castVersion = version as SupportedRouterCompatibilityVersion; if (!ROUTER_COMPATIBILITY_VERSIONS.has(castVersion)) { throw new Error( diff --git a/controlplane/src/core/env.schema.ts b/controlplane/src/core/env.schema.ts index 8a5b5c2071..d244e9f95f 100644 --- a/controlplane/src/core/env.schema.ts +++ b/controlplane/src/core/env.schema.ts @@ -53,6 +53,10 @@ export const envVariables = z * OPEN AI */ OPENAI_API_KEY: z.string().optional(), + /** + * Composition workers + */ + COMPOSITION_MAX_THREADS: z.coerce.number().int().min(0).default(0), /** * Auth */ diff --git a/controlplane/src/core/repositories/FederatedGraphRepository.ts b/controlplane/src/core/repositories/FederatedGraphRepository.ts index bd9fe6f123..12ba78de71 100644 --- a/controlplane/src/core/repositories/FederatedGraphRepository.ts +++ b/controlplane/src/core/repositories/FederatedGraphRepository.ts @@ -31,14 +31,7 @@ import { FastifyBaseLogger } from 'fastify'; import { parse } from 'graphql'; import { generateKeyPair, importPKCS8, SignJWT } from 'jose'; import { uid } from 'uid/secure'; -import { - CompositionOptions, - ContractTagOptions, - FederationResult, - FederationResultWithContracts, - newContractTagOptionsFromArrays, - Warning, -} from '@wundergraph/composition'; +import { CompositionOptions, Warning } from '@wundergraph/composition'; import * as schema from '../../db/schema.js'; import { federatedGraphs, @@ -64,22 +57,20 @@ import { import { BlobStorage } from '../blobstorage/index.js'; import { BaseCompositionData, - buildRouterExecutionConfig, - ComposedSubgraph, + CompositionSubgraphRecord, Composer, ContractBaseCompositionData, - mapResultToComposedGraph, routerConfigToFeatureFlagExecutionConfig, RouterConfigUploadError, } from '../composition/composer.js'; +import { + composeGraphsInWorker, + deserializeComposedGraphArtifact, + deserializeRouterExecutionConfig, +} from '../composition/composeGraphs.pool.js'; import { SchemaDiff } from '../composition/schemaCheck.js'; import { AdmissionError } from '../services/AdmissionWebhookController.js'; -import { - checkIfLabelMatchersChanged, - getFederationResultWithPotentialContracts, - normalizeLabelMatchers, - normalizeLabels, -} from '../util.js'; +import { checkIfLabelMatchersChanged, normalizeLabelMatchers, normalizeLabels } from '../util.js'; import { unsuccessfulBaseCompositionError } from '../errors/errors.js'; import { ClickHouseClient } from '../clickhouse/index.js'; import { RBACEvaluator } from '../services/RBACEvaluator.js'; @@ -805,7 +796,7 @@ export class FederatedGraphRepository { clientSchema?: string; compositionErrors?: Error[]; compositionWarnings?: Warning[]; - composedSubgraphs: ComposedSubgraph[]; + composedSubgraphs: CompositionSubgraphRecord[]; composedById: string; isFeatureFlagComposition: boolean; featureFlagId: string; @@ -1551,13 +1542,11 @@ export class FederatedGraphRepository { }); const contracts = await contractRepo.bySourceFederatedGraphId(federatedGraph.id); - const tagOptionsByContractName = new Map(); - for (const contract of contracts) { - tagOptionsByContractName.set( - contract.downstreamFederatedGraph.target.name, - newContractTagOptionsFromArrays(contract.excludeTags, contract.includeTags), - ); - } + const tagOptionsByContractName = contracts.map((contract) => ({ + contractName: contract.downstreamFederatedGraph.target.name, + excludeTags: contract.excludeTags, + includeTags: contract.includeTags, + })); const baseCompositionSubgraphs = subgraphs.map((s) => ({ name: s.name, @@ -1572,6 +1561,18 @@ export class FederatedGraphRepository { fedGraphLabelMatchers: federatedGraph.labelMatchers, }); + const { results } = await composeGraphsInWorker({ + federatedGraph, + subgraphsToCompose: allSubgraphsToCompose.map((subgraphsToCompose) => ({ + subgraphs: subgraphsToCompose.subgraphs, + isFeatureFlagComposition: subgraphsToCompose.isFeatureFlagComposition, + featureFlagName: subgraphsToCompose.featureFlagName, + featureFlagId: subgraphsToCompose.featureFlagId, + })), + tagOptionsByContractName, + compositionOptions, + }); + /* baseCompositionData contains the router execution config and the schema version ID for the source graph * base composition (not a contract or feature flag composition) * */ @@ -1583,142 +1584,163 @@ export class FederatedGraphRepository { * and any feature flag schema version IDs by contract ID */ const contractBaseCompositionDataByContractId = new Map(); - for (const subgraphsToCompose of allSubgraphsToCompose) { - const result: FederationResult | FederationResultWithContracts = getFederationResultWithPotentialContracts( - federatedGraph, - subgraphsToCompose, - tagOptionsByContractName, - compositionOptions, - ); - - if (!result.success) { + for (const compositionResult of results) { + if (!compositionResult.base.success) { // Collect all composition errors allCompositionErrors.push( - ...result.errors.map((e) => ({ + ...compositionResult.base.errors.map((message) => ({ federatedGraphName: federatedGraph.name, namespace: federatedGraph.namespace, - message: e.message, - featureFlag: subgraphsToCompose.featureFlagName || '', + message, + featureFlag: compositionResult.featureFlagName || '', })), ); } // Collect all composition warnings allCompositionWarnings.push( - ...result.warnings.map((w) => ({ + ...compositionResult.base.warnings.map((warning) => ({ federatedGraphName: federatedGraph.name, namespace: federatedGraph.namespace, - message: w.message, - featureFlag: subgraphsToCompose.featureFlagName || '', + message: warning.message, + featureFlag: compositionResult.featureFlagName || '', })), ); - if (!subgraphsToCompose.isFeatureFlagComposition && !result.success && !federatedGraph.contract) { + if ( + !compositionResult.isFeatureFlagComposition && + !compositionResult.base.success && + !federatedGraph.contract + ) { allCompositionErrors.push(unsuccessfulBaseCompositionError(federatedGraph.name, federatedGraph.namespace)); } - const composedGraph = mapResultToComposedGraph(federatedGraph, subgraphsToCompose.subgraphs, result); - const federatedSchemaVersionId = randomUUID(); + const baseComposedGraph = deserializeComposedGraphArtifact(federatedGraph, compositionResult.base); + let routerExecutionConfig; + if (compositionResult.base.success) { + if (!compositionResult.base.routerExecutionConfigJson) { + throw new Error( + `Successful composition for federated graph "${federatedGraph.name}" does not contain a router execution config.`, + ); + } - // Build the router execution config if the composed schema is valid - const routerExecutionConfig = buildRouterExecutionConfig( - composedGraph, - federatedSchemaVersionId, - federatedGraph.routerCompatibilityVersion, - ); + routerExecutionConfig = deserializeRouterExecutionConfig(compositionResult.base.routerExecutionConfigJson); + } + + if (routerExecutionConfig) { + routerExecutionConfig.version = federatedSchemaVersionId; + } const baseComposition = await composer.saveComposition({ - composedGraph, + composedGraph: baseComposedGraph, composedById: actorId, - isFeatureFlagComposition: subgraphsToCompose.isFeatureFlagComposition, + isFeatureFlagComposition: compositionResult.isFeatureFlagComposition, federatedSchemaVersionId, routerExecutionConfig, - featureFlagId: subgraphsToCompose.featureFlagId, + featureFlagId: compositionResult.featureFlagId, }); - if (!result.success || !baseComposition.schemaVersionId || !routerExecutionConfig) { + if (!compositionResult.base.success || !baseComposition.schemaVersionId) { /* If the base composition failed to compose or deploy, return to the parent loop, because * contracts are not composed if the base composition fails. */ - if (!subgraphsToCompose.isFeatureFlagComposition) { + if (!compositionResult.isFeatureFlagComposition) { continue parentLoop; } // Record the feature flag composition to upload (if there are no errors) - } else if (subgraphsToCompose.isFeatureFlagComposition) { + } else if (compositionResult.isFeatureFlagComposition) { + if (!routerExecutionConfig) { + throw new Error( + `Successful feature flag composition for federated graph "${federatedGraph.name}" does not contain a router execution config.`, + ); + } baseCompositionData.featureFlagRouterExecutionConfigByFeatureFlagName.set( - subgraphsToCompose.featureFlagName, + compositionResult.featureFlagName, routerConfigToFeatureFlagExecutionConfig(routerExecutionConfig), ); // Otherwise, this is the base composition, so store the schema version id } else { + if (!routerExecutionConfig) { + throw new Error( + `Successful composition for federated graph "${federatedGraph.name}" does not contain a router execution config.`, + ); + } baseCompositionData.schemaVersionId = baseComposition.schemaVersionId; baseCompositionData.routerExecutionConfig = routerExecutionConfig; } // If there are no contracts, there is nothing further to do - if (!('federationResultByContractName' in result)) { + if (compositionResult.contracts.length === 0) { continue; } - for (const [contractName, contractResult] of result.federationResultByContractName) { + for (const { contractName, artifact } of compositionResult.contracts) { const contractGraph = await fedGraphRepo.byName(contractName, federatedGraph.namespace); if (!contractGraph) { throw new Error(`The contract graph "${contractName}" was not found.`); } - if (!contractResult.success) { + if (!artifact.success) { allCompositionErrors.push( - ...contractResult.errors.map((e) => ({ + ...artifact.errors.map((message) => ({ federatedGraphName: contractGraph.name, namespace: contractGraph.namespace, - message: e.message, - featureFlag: subgraphsToCompose.featureFlagName, + message, + featureFlag: compositionResult.featureFlagName, })), ); } allCompositionWarnings.push( - ...contractResult.warnings.map((w) => ({ + ...artifact.warnings.map((warning) => ({ federatedGraphName: contractGraph.name, namespace: contractGraph.namespace, - message: w.message, - featureFlag: subgraphsToCompose.featureFlagName, + message: warning.message, + featureFlag: compositionResult.featureFlagName, })), ); - const composedContract = mapResultToComposedGraph( - contractGraph, - subgraphsToCompose.subgraphs, - contractResult, - ); - const contractSchemaVersionId = randomUUID(); - - // Build the router execution config if the composed schema is valid - const contractRouterExecutionConfig = buildRouterExecutionConfig( - composedContract, - contractSchemaVersionId, - federatedGraph.routerCompatibilityVersion, - ); + const contractComposedGraph = deserializeComposedGraphArtifact(contractGraph, artifact); + let contractRouterExecutionConfig; + if (artifact.success) { + if (!artifact.routerExecutionConfigJson) { + throw new Error( + `Successful contract composition for federated graph "${contractGraph.name}" does not contain a router execution config.`, + ); + } + contractRouterExecutionConfig = deserializeRouterExecutionConfig(artifact.routerExecutionConfigJson); + if (!contractRouterExecutionConfig) { + throw new Error( + `Successful contract composition for federated graph "${contractGraph.name}" did not produce a router execution config.`, + ); + } + contractRouterExecutionConfig.version = contractSchemaVersionId; + } const contractComposition = await composer.saveComposition({ - composedGraph: composedContract, + composedGraph: contractComposedGraph, composedById: actorId, - isFeatureFlagComposition: subgraphsToCompose.isFeatureFlagComposition, + isFeatureFlagComposition: compositionResult.isFeatureFlagComposition, federatedSchemaVersionId: contractSchemaVersionId, routerExecutionConfig: contractRouterExecutionConfig, - featureFlagId: subgraphsToCompose.featureFlagId, + featureFlagId: compositionResult.featureFlagId, }); - if (!contractResult.success || !contractComposition.schemaVersionId || !contractRouterExecutionConfig) { + if (!artifact.success || !contractComposition.schemaVersionId) { continue; } + if (!contractRouterExecutionConfig) { + throw new Error( + `Successful contract composition for federated graph "${contractGraph.name}" did not produce a router execution config.`, + ); + } /* If the base composition for which this contract has been made is NOT a feature flag composition, * it must be the contract base composition, which must always be uploaded. * The base composition is always the first item in the subgraphsToCompose array. * */ - if (!subgraphsToCompose.isFeatureFlagComposition) { + if (!compositionResult.isFeatureFlagComposition) { contractBaseCompositionDataByContractId.set(contractGraph.id, { schemaVersionId: contractComposition.schemaVersionId, routerExecutionConfig: contractRouterExecutionConfig, @@ -1739,7 +1761,7 @@ export class FederatedGraphRepository { continue; } existingContractBaseCompositionData.featureFlagRouterExecutionConfigByFeatureFlagName.set( - subgraphsToCompose.featureFlagName, + compositionResult.featureFlagName, routerConfigToFeatureFlagExecutionConfig(contractRouterExecutionConfig), ); } diff --git a/controlplane/src/core/repositories/GraphCompositionRepository.ts b/controlplane/src/core/repositories/GraphCompositionRepository.ts index 898e117fe8..7651a79968 100644 --- a/controlplane/src/core/repositories/GraphCompositionRepository.ts +++ b/controlplane/src/core/repositories/GraphCompositionRepository.ts @@ -11,7 +11,7 @@ import { users, } from '../../db/schema.js'; import { DateRange, GraphCompositionDTO } from '../../types/index.js'; -import { ComposedSubgraph } from '../composition/composer.js'; +import { CompositionSubgraphRecord } from '../composition/composer.js'; import { FederatedGraphRepository } from './FederatedGraphRepository.js'; export class GraphCompositionRepository { @@ -38,7 +38,7 @@ export class GraphCompositionRepository { compositionErrorString: string; compositionWarningString: string; routerConfigSignature?: string; - composedSubgraphs: ComposedSubgraph[]; + composedSubgraphs: CompositionSubgraphRecord[]; composedById: string; admissionErrorString?: string; deploymentErrorString?: string; diff --git a/controlplane/src/core/repositories/SchemaCheckRepository.ts b/controlplane/src/core/repositories/SchemaCheckRepository.ts index 933d578683..e78e134573 100644 --- a/controlplane/src/core/repositories/SchemaCheckRepository.ts +++ b/controlplane/src/core/repositories/SchemaCheckRepository.ts @@ -36,7 +36,8 @@ import { SchemaLintIssues, } from '../../types/index.js'; import { ClickHouseClient } from '../clickhouse/index.js'; -import { CheckSubgraph, ComposedFederatedGraph, Composer } from '../composition/composer.js'; +import { CheckSubgraph, Composer } from '../composition/composer.js'; +import { DeserializedComposedGraph } from '../composition/composeGraphs.pool.js'; import { buildSchema } from '../composition/composition.js'; import { getDiffBetweenGraphs, SchemaDiff } from '../composition/schemaCheck.js'; import { @@ -531,7 +532,7 @@ export class SchemaCheckRepository { return 0; } - public createSchemaCheckCompositions(data: { schemaCheckID: string; compositions: ComposedFederatedGraph[] }) { + public createSchemaCheckCompositions(data: { schemaCheckID: string; compositions: DeserializedComposedGraph[] }) { if (data.compositions.length === 0) { return; } diff --git a/controlplane/src/core/util.ts b/controlplane/src/core/util.ts index e45182361a..12d8ea2010 100644 --- a/controlplane/src/core/util.ts +++ b/controlplane/src/core/util.ts @@ -14,21 +14,12 @@ import { FastifyBaseLogger } from 'fastify'; import { parse, visit } from 'graphql'; import { uid } from 'uid/secure'; import DOMPurify from 'isomorphic-dompurify'; -import { - CompositionOptions, - ContractTagOptions, - FederationResult, - FederationResultWithContracts, - LATEST_ROUTER_COMPATIBILITY_VERSION, - newContractTagOptionsFromArrays, -} from '@wundergraph/composition'; +import { LATEST_ROUTER_COMPATIBILITY_VERSION } from '@wundergraph/composition'; import { ProposalOrigin, SubgraphType } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb'; import { MemberRole, ProposalOrigin as ProposalOriginEnum, WebsocketSubprotocol } from '../db/models.js'; import { AuthContext, DateRange, FederatedGraphDTO, Label, ResponseMessage, S3StorageOptions } from '../types/index.js'; import { isAuthenticationError, isAuthorizationError, isPublicError } from './errors/errors.js'; import { GraphKeyAuthContext } from './services/GraphApiTokenAuthenticator.js'; -import { composeFederatedContract, composeFederatedGraphWithPotentialContracts } from './composition/composition.js'; -import { SubgraphsToCompose } from './repositories/FeatureFlagRepository.js'; const labelRegex = /^[\dA-Za-z](?:[\w.-]{0,61}[\dA-Za-z])?$/; const namespaceRegex = /^[\da-z]+(?:[_-][\da-z]+)*$/; @@ -543,29 +534,6 @@ export const checkIfLabelMatchersChanged = (data: { return false; }; -export function getFederationResultWithPotentialContracts( - federatedGraph: FederatedGraphDTO, - subgraphsToCompose: SubgraphsToCompose, - tagOptionsByContractName: Map, - compositionOptions?: CompositionOptions, -): FederationResult | FederationResultWithContracts { - // This condition is only true when entering the method to specifically create/update a contract - if (federatedGraph.contract) { - return composeFederatedContract( - subgraphsToCompose.compositionSubgraphs, - newContractTagOptionsFromArrays(federatedGraph.contract.excludeTags, federatedGraph.contract.includeTags), - federatedGraph.routerCompatibilityVersion, - compositionOptions, - ); - } - return composeFederatedGraphWithPotentialContracts( - subgraphsToCompose.compositionSubgraphs, - tagOptionsByContractName, - federatedGraph.routerCompatibilityVersion, - compositionOptions, - ); -} - export function getFederatedGraphRouterCompatibilityVersion(federatedGraphDTOs: Array): string { if (federatedGraphDTOs.length === 0) { return LATEST_ROUTER_COMPATIBILITY_VERSION; diff --git a/controlplane/src/core/webhooks/OrganizationWebhookService.ts b/controlplane/src/core/webhooks/OrganizationWebhookService.ts index af05215ad9..2ae1bd0143 100644 --- a/controlplane/src/core/webhooks/OrganizationWebhookService.ts +++ b/controlplane/src/core/webhooks/OrganizationWebhookService.ts @@ -21,7 +21,7 @@ import { SchemaGraphPruningIssues, SchemaLintIssues, } from '../../types/index.js'; -import { ComposedFederatedGraph } from '../composition/composer.js'; +import { DeserializedComposedGraph } from '../composition/composeGraphs.pool.js'; import { GetDiffBetweenGraphsSuccess } from '../composition/schemaCheck.js'; import { SubgraphCheckExtensionsRepository } from '../repositories/SubgraphCheckExtensionsRepository.js'; import { BlobStorage } from '../blobstorage/index.js'; @@ -599,7 +599,7 @@ export class OrganizationWebhookService { isDeleted: boolean; }[]; affectedGraphs: FederatedGraphDTO[]; - composedGraphs: ComposedFederatedGraph[]; + composedGraphs: DeserializedComposedGraph[]; inspectedOperations: InspectorOperationResult[]; }): Promise< | { diff --git a/controlplane/src/index.ts b/controlplane/src/index.ts index a34d7577cb..5ac8cb6728 100644 --- a/controlplane/src/index.ts +++ b/controlplane/src/index.ts @@ -60,6 +60,7 @@ const { STRIPE_WEBHOOK_SECRET, DEFAULT_PLAN, OPENAI_API_KEY, + COMPOSITION_MAX_THREADS, REDIS_HOST, REDIS_PORT, REDIS_TLS_CA, @@ -90,6 +91,9 @@ const options: BuildConfig = { enabled: true, level: LOG_LEVEL as pino.LevelWithSilent, }, + composition: { + maxThreads: COMPOSITION_MAX_THREADS, + }, openaiAPIKey: OPENAI_API_KEY, keycloak: { realm: KC_REALM, diff --git a/controlplane/test/composition-errors.test.ts b/controlplane/test/composition-errors.test.ts index ebecb89edb..ec69d124f2 100644 --- a/controlplane/test/composition-errors.test.ts +++ b/controlplane/test/composition-errors.test.ts @@ -21,9 +21,9 @@ import { OBJECT, ObjectDefinitionData, STRING_SCALAR, + federateSubgraphs, } from '@wundergraph/composition'; import { SubgraphName } from '@wundergraph/composition/dist/types/types.js'; -import { composeSubgraphs } from '../src/core/composition/composition.js'; import { afterAllSetup, beforeAllSetup, genID, genUniqueLabel } from '../src/core/test-util.js'; import { ClickHouseClient } from '../src/core/clickhouse/index.js'; import { SetupTest } from './test-util.js'; @@ -140,7 +140,10 @@ describe('Composition error tests', (ctx) => { `), }; - const result = composeSubgraphs([subgraph1, subgraph2], LATEST_ROUTER_COMPATIBILITY_VERSION) as FederationFailure; + const result = federateSubgraphs({ + subgraphs: [subgraph1, subgraph2], + version: LATEST_ROUTER_COMPATIBILITY_VERSION, + }) as FederationFailure; expect(result.success).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0]).toStrictEqual( @@ -177,7 +180,10 @@ describe('Composition error tests', (ctx) => { `), }; - const result = composeSubgraphs([subgraph1, subgraph2], LATEST_ROUTER_COMPATIBILITY_VERSION) as FederationFailure; + const result = federateSubgraphs({ + subgraphs: [subgraph1, subgraph2], + version: LATEST_ROUTER_COMPATIBILITY_VERSION, + }) as FederationFailure; expect(result.success).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0]).toStrictEqual( @@ -216,7 +222,10 @@ describe('Composition error tests', (ctx) => { `), }; - const result = composeSubgraphs([subgraph1, subgraph2], LATEST_ROUTER_COMPATIBILITY_VERSION) as FederationFailure; + const result = federateSubgraphs({ + subgraphs: [subgraph1, subgraph2], + version: LATEST_ROUTER_COMPATIBILITY_VERSION, + }) as FederationFailure; expect(result.success).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0]).toStrictEqual( @@ -252,7 +261,10 @@ describe('Composition error tests', (ctx) => { name: 'subgraph2', }; - const result = composeSubgraphs([subgraph1, subgraph2], LATEST_ROUTER_COMPATIBILITY_VERSION) as FederationFailure; + const result = federateSubgraphs({ + subgraphs: [subgraph1, subgraph2], + version: LATEST_ROUTER_COMPATIBILITY_VERSION, + }) as FederationFailure; expect(result.success).toBe(false); expect(result.errors).toHaveLength(1); @@ -282,7 +294,10 @@ describe('Composition error tests', (ctx) => { name: 'subgraph2', }; - const result = composeSubgraphs([subgraph1, subgraph2], LATEST_ROUTER_COMPATIBILITY_VERSION) as FederationFailure; + const result = federateSubgraphs({ + subgraphs: [subgraph1, subgraph2], + version: LATEST_ROUTER_COMPATIBILITY_VERSION, + }) as FederationFailure; expect(result.success).toBe(false); expect(result.errors).toHaveLength(1); @@ -312,7 +327,10 @@ describe('Composition error tests', (ctx) => { name: 'subgraph2', }; - const result = composeSubgraphs([subgraph1, subgraph2], LATEST_ROUTER_COMPATIBILITY_VERSION) as FederationFailure; + const result = federateSubgraphs({ + subgraphs: [subgraph1, subgraph2], + version: LATEST_ROUTER_COMPATIBILITY_VERSION, + }) as FederationFailure; expect(result.success).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors?.[0]).toStrictEqual(noQueryRootTypeError()); @@ -343,7 +361,10 @@ describe('Composition error tests', (ctx) => { name: 'subgraph2', }; - const result = composeSubgraphs([subgraph1, subgraph2], LATEST_ROUTER_COMPATIBILITY_VERSION) as FederationFailure; + const result = federateSubgraphs({ + subgraphs: [subgraph1, subgraph2], + version: LATEST_ROUTER_COMPATIBILITY_VERSION, + }) as FederationFailure; expect(result.success).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors).toStrictEqual([ @@ -393,7 +414,10 @@ describe('Composition error tests', (ctx) => { name: 'subgraph2', }; - const result = composeSubgraphs([subgraph1, subgraph2], LATEST_ROUTER_COMPATIBILITY_VERSION) as FederationFailure; + const result = federateSubgraphs({ + subgraphs: [subgraph1, subgraph2], + version: LATEST_ROUTER_COMPATIBILITY_VERSION, + }) as FederationFailure; expect(result.success).toBe(false); expect(result.errors).toHaveLength(1); @@ -439,7 +463,10 @@ describe('Composition error tests', (ctx) => { url: '', }; - const result = composeSubgraphs([subgraph1, subgraph2], LATEST_ROUTER_COMPATIBILITY_VERSION) as FederationFailure; + const result = federateSubgraphs({ + subgraphs: [subgraph1, subgraph2], + version: LATEST_ROUTER_COMPATIBILITY_VERSION, + }) as FederationFailure; expect(result.success).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors?.[0].message).toBe( @@ -477,7 +504,10 @@ describe('Composition error tests', (ctx) => { name: 'subgraph2', }; - const result = composeSubgraphs([subgraph1, subgraph2], LATEST_ROUTER_COMPATIBILITY_VERSION) as FederationFailure; + const result = federateSubgraphs({ + subgraphs: [subgraph1, subgraph2], + version: LATEST_ROUTER_COMPATIBILITY_VERSION, + }) as FederationFailure; expect(result.success).toBe(false); expect(result.errors[0]).toStrictEqual( invalidRequiredInputValueError( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 93a29eb523..16e152f5e3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -616,6 +616,9 @@ importers: tiny-lru: specifier: ^11.2.11 version: 11.2.11 + tinypool: + specifier: ^2.1.0 + version: 2.1.0 uid: specifier: ^2.0.2 version: 2.0.2 @@ -8348,11 +8351,6 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - browserslist@4.25.2: - resolution: {integrity: sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - browserslist@4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -9459,9 +9457,6 @@ packages: electron-to-chromium@1.4.780: resolution: {integrity: sha512-NPtACGFe7vunRYzvYqVRhQvsDrTevxpgDKxG/Vcbe0BTNOY+5+/2mOXSw2ls7ToNbE5Bf/+uQbjTxcmwMozpCw==} - electron-to-chromium@1.5.200: - resolution: {integrity: sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==} - electron-to-chromium@1.5.267: resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} @@ -11914,9 +11909,6 @@ packages: node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} - node-releases@2.0.19: - resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} - node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} @@ -13867,6 +13859,10 @@ packages: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} + tinypool@2.1.0: + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} + engines: {node: ^20.0.0 || >=22.0.0} + tinyrainbow@2.0.0: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} @@ -14249,12 +14245,6 @@ packages: peerDependencies: browserslist: '>= 4.21.0' - update-browserslist-db@1.1.3: - resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - update-browserslist-db@1.2.2: resolution: {integrity: sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==} hasBin: true @@ -15462,7 +15452,7 @@ snapshots: '@babel/traverse': 7.28.0 '@babel/types': 7.28.2 convert-source-map: 2.0.0 - debug: 4.3.7 + debug: 4.4.1 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -15488,7 +15478,7 @@ snapshots: dependencies: '@babel/compat-data': 7.28.0 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.25.2 + browserslist: 4.28.1 lru-cache: 5.1.1 semver: 6.3.1 @@ -15647,7 +15637,7 @@ snapshots: '@babel/parser': 7.28.0 '@babel/template': 7.27.2 '@babel/types': 7.28.2 - debug: 4.3.7 + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -23238,13 +23228,6 @@ snapshots: node-releases: 2.0.14 update-browserslist-db: 1.0.16(browserslist@4.23.0) - browserslist@4.25.2: - dependencies: - caniuse-lite: 1.0.30001735 - electron-to-chromium: 1.5.200 - node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.25.2) - browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.9.7 @@ -23296,7 +23279,7 @@ snapshots: bun-types@1.2.12: dependencies: - '@types/node': 20.12.12 + '@types/node': 18.19.21 optional: true bun-types@1.2.3: @@ -24289,8 +24272,6 @@ snapshots: electron-to-chromium@1.4.780: {} - electron-to-chromium@1.5.200: {} - electron-to-chromium@1.5.267: {} emoji-regex@10.3.0: {} @@ -24687,10 +24668,10 @@ snapshots: dependencies: eslint: 8.57.1 - eslint-config-standard@17.1.0(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1))(eslint-plugin-n@16.6.2(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1): + eslint-config-standard@17.1.0(eslint-plugin-import@2.27.5)(eslint-plugin-n@16.6.2(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1): dependencies: eslint: 8.57.1 - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1) eslint-plugin-n: 16.6.2(eslint@8.57.1) eslint-plugin-promise: 6.6.0(eslint@8.57.1) @@ -24700,9 +24681,9 @@ snapshots: '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.5.2) eslint: 8.57.1 eslint-config-prettier: 8.10.0(eslint@8.57.1) - eslint-config-standard: 17.1.0(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1))(eslint-plugin-n@16.6.2(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1) - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-config-standard: 17.1.0(eslint-plugin-import@2.27.5)(eslint-plugin-n@16.6.2(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.27.5)(eslint@8.57.1) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1) eslint-plugin-n: 16.6.2(eslint@8.57.1) eslint-plugin-node: 11.1.0(eslint@8.57.1) eslint-plugin-promise: 6.6.0(eslint@8.57.1) @@ -24739,13 +24720,13 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1))(eslint@8.57.1): + eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.27.5)(eslint@8.57.1): dependencies: debug: 4.3.7 enhanced-resolve: 5.15.0 eslint: 8.57.1 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-typescript@3.5.5)(eslint@8.57.1) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1) get-tsconfig: 4.7.2 globby: 13.2.2 is-core-module: 2.12.1 @@ -24757,17 +24738,6 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): - dependencies: - debug: 4.3.7 - optionalDependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.5.2) - eslint: 8.57.1 - eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1))(eslint@8.57.1) - transitivePeerDependencies: - - supports-color - eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.57.1): dependencies: debug: 4.3.7 @@ -24775,17 +24745,17 @@ snapshots: '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.5.2) eslint: 8.57.1 eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.27.5(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.27.5)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-typescript@3.5.5)(eslint@8.57.1): dependencies: debug: 4.3.7 optionalDependencies: '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.5.2) eslint: 8.57.1 - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.27.5)(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -24802,7 +24772,7 @@ snapshots: eslint-utils: 2.1.0 regexpp: 3.2.0 - eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-typescript@3.5.5)(eslint@8.57.1): dependencies: array-includes: 3.1.6 array.prototype.flat: 1.3.1 @@ -24811,7 +24781,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.57.1) has: 1.0.3 is-core-module: 2.12.1 is-glob: 4.0.3 @@ -24827,7 +24797,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint-import-resolver-typescript@3.5.5)(eslint@8.57.1): + eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.5.2))(eslint@8.57.1): dependencies: array-includes: 3.1.6 array.prototype.flat: 1.3.1 @@ -27499,8 +27469,6 @@ snapshots: node-releases@2.0.14: {} - node-releases@2.0.19: {} - node-releases@2.0.27: {} nodemailer@7.0.10: {} @@ -29813,6 +29781,8 @@ snapshots: tinypool@1.1.1: {} + tinypool@2.1.0: {} + tinyrainbow@2.0.0: {} tinyspy@4.0.3: {} @@ -30234,12 +30204,6 @@ snapshots: escalade: 3.1.2 picocolors: 1.1.1 - update-browserslist-db@1.1.3(browserslist@4.25.2): - dependencies: - browserslist: 4.25.2 - escalade: 3.2.0 - picocolors: 1.1.1 - update-browserslist-db@1.2.2(browserslist@4.28.1): dependencies: browserslist: 4.28.1