diff --git a/deployment/index.ts b/deployment/index.ts index 90128423c51..8e968ec5435 100644 --- a/deployment/index.ts +++ b/deployment/index.ts @@ -136,17 +136,6 @@ const tokens = deployTokens({ observability, }); -deployWorkflows({ - image: docker.factory.getImageId('workflows', imagesTag), - docker, - environment, - postgres, - postmarkSecret, - observability, - sentry, - heartbeat: heartbeatsConfig.get('webhooks'), -}); - const commerce = deployCommerce({ image: docker.factory.getImageId('commerce', imagesTag), docker, @@ -201,6 +190,18 @@ const schemaPolicy = deploySchemaPolicy({ observability, }); +deployWorkflows({ + image: docker.factory.getImageId('workflows', imagesTag), + docker, + environment, + postgres, + postmarkSecret, + observability, + sentry, + heartbeat: heartbeatsConfig.get('webhooks'), + schema, +}); + const supertokens = deploySuperTokens(postgres, { dependencies: [dbMigrations] }, environment); const zendesk = configureZendesk({ environment }); const githubApp = configureGithubApp(); diff --git a/deployment/services/workflows.ts b/deployment/services/workflows.ts index ff394ce5b6e..4d4be3f824c 100644 --- a/deployment/services/workflows.ts +++ b/deployment/services/workflows.ts @@ -1,10 +1,12 @@ import * as pulumi from '@pulumi/pulumi'; +import { serviceLocalEndpoint } from '../utils/local-endpoint'; import { ServiceSecret } from '../utils/secrets'; import { ServiceDeployment } from '../utils/service-deployment'; import { Docker } from './docker'; import { Environment } from './environment'; import { Observability } from './observability'; import { Postgres } from './postgres'; +import { Schema } from './schema'; import { Sentry } from './sentry'; export class PostmarkSecret extends ServiceSecret<{ @@ -22,6 +24,7 @@ export function deployWorkflows({ postgres, observability, postmarkSecret, + schema, }: { postgres: Postgres; observability: Observability; @@ -31,6 +34,7 @@ export function deployWorkflows({ heartbeat?: string; sentry: Sentry; postmarkSecret: PostmarkSecret; + schema: Schema; }) { return ( new ServiceDeployment( @@ -47,6 +51,7 @@ export function deployWorkflows({ ? observability.tracingEndpoint : '', LOG_JSON: '1', + SCHEMA_ENDPOINT: serviceLocalEndpoint(schema.service), }, readinessProbe: '/_readiness', livenessProbe: '/_health', diff --git a/docker/docker-compose.community.yml b/docker/docker-compose.community.yml index eeafc15ea28..ed35cf42b7a 100644 --- a/docker/docker-compose.community.yml +++ b/docker/docker-compose.community.yml @@ -322,6 +322,10 @@ services: SENTRY_DSN: '${SENTRY_DSN:-}' PROMETHEUS_METRICS: '${PROMETHEUS_METRICS:-}' LOG_JSON: '1' + SCHEMA_ENDPOINT: http://schema:3002 + REDIS_HOST: redis + REDIS_PORT: 6379 + REDIS_PASSWORD: '${REDIS_PASSWORD}' usage: image: '${DOCKER_REGISTRY}usage${DOCKER_TAG}' diff --git a/integration-tests/docker-compose.integration.yaml b/integration-tests/docker-compose.integration.yaml index 70ff54d9a8a..4e6ad78b977 100644 --- a/integration-tests/docker-compose.integration.yaml +++ b/integration-tests/docker-compose.integration.yaml @@ -239,6 +239,7 @@ services: workflows: environment: EMAIL_PROVIDER: '${EMAIL_PROVIDER}' + SCHEMA_ENDPOINT: http://schema:3002 LOG_LEVEL: debug ports: - '3014:3014' diff --git a/integration-tests/package.json b/integration-tests/package.json index 308c020171e..abb4db0671f 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -34,6 +34,7 @@ "dockerode": "4.0.8", "dotenv": "16.4.7", "graphql": "16.9.0", + "graphql-sse": "2.6.0", "human-id": "4.1.1", "ioredis": "5.8.2", "slonik": "30.4.4", diff --git a/integration-tests/testkit/graphql.ts b/integration-tests/testkit/graphql.ts index ea960875984..f8ce222120a 100644 --- a/integration-tests/testkit/graphql.ts +++ b/integration-tests/testkit/graphql.ts @@ -1,4 +1,5 @@ import { ExecutionResult, parse, print } from 'graphql'; +import { createClient } from 'graphql-sse'; import { TypedDocumentNode } from '@graphql-typed-document-node/core'; import { sortSDL } from '@theguild/federation-composition'; import { getServiceHost } from './utils'; @@ -87,3 +88,42 @@ export async function execute( }, }; } + +export async function subscribe( + params: { + document: TypedDocumentNode; + operationName?: string; + authToken?: string; + token?: string; + legacyAuthorizationMode?: boolean; + } & (TVariables extends Record + ? { variables?: never } + : { variables: TVariables }), +) { + const registryAddress = await getServiceHost('server', 8082); + const client = createClient({ + url: `http://${registryAddress}/graphql`, + headers: { + ...(params.authToken + ? { + authorization: `Bearer ${params.authToken}`, + } + : {}), + ...(params.token + ? params.legacyAuthorizationMode + ? { + 'x-api-token': params.token, + } + : { + authorization: `Bearer ${params.token}`, + } + : {}), + }, + }); + + return client.iterate({ + operationName: params.operationName, + query: print(params.document), + variables: params.variables ?? {}, + }); +} diff --git a/integration-tests/testkit/seed.ts b/integration-tests/testkit/seed.ts index 76742afc2ae..5e76888e002 100644 --- a/integration-tests/testkit/seed.ts +++ b/integration-tests/testkit/seed.ts @@ -766,6 +766,7 @@ export function initSeed() { commit: string; }, contextId?: string, + schemaProposalId?: string, ) { return await checkSchema( { @@ -773,6 +774,7 @@ export function initSeed() { service, meta, contextId, + schemaProposalId, }, secret, ); diff --git a/integration-tests/tests/api/proposals/create.spec.ts b/integration-tests/tests/api/proposals/create.spec.ts new file mode 100644 index 00000000000..c15b1f1c624 --- /dev/null +++ b/integration-tests/tests/api/proposals/create.spec.ts @@ -0,0 +1,83 @@ +import { graphql } from 'testkit/gql'; +import { ProjectType, ResourceAssignmentModeType } from 'testkit/gql/graphql'; +import { execute } from 'testkit/graphql'; +import { initSeed } from 'testkit/seed'; + +const CreateProposalMutation = graphql(` + mutation CreateProposalMutation($input: CreateSchemaProposalInput!) { + createSchemaProposal(input: $input) { + ok { + schemaProposal { + id + } + } + error { + message + } + } + } +`); + +describe('Schema Proposals', () => { + test.concurrent( + 'cannot be proposed without "schemaProposal:modify" permission', + async ({ expect }) => { + const { createOrg, ownerToken } = await initSeed().createOwner(); + const { createProject, createOrganizationAccessToken, setFeatureFlag } = await createOrg(); + await setFeatureFlag('schemaProposals', true); + const { target } = await createProject(ProjectType.Federation); + const { privateAccessKey: accessKey } = await createOrganizationAccessToken( + { + resources: { + mode: ResourceAssignmentModeType.All, + }, + permissions: ['schemaProposal:describe'], + }, + ownerToken, + ); + + const result = await execute({ + document: CreateProposalMutation, + variables: { + input: { + target: { byId: target.id }, + author: 'Jeff', + title: 'Proposed changes to the schema...', + }, + }, + authToken: accessKey, + }).then(r => r.expectGraphQLErrors()); + }, + ); + + test.concurrent( + 'can be proposed successfully with "schemaProposal:modify" permission', + async ({ expect }) => { + const { createOrg, ownerToken } = await initSeed().createOwner(); + const { createProject, createOrganizationAccessToken, setFeatureFlag } = await createOrg(); + await setFeatureFlag('schemaProposals', true); + const { target } = await createProject(ProjectType.Federation); + + const { privateAccessKey: accessKey } = await createOrganizationAccessToken({ + resources: { + mode: ResourceAssignmentModeType.All, + }, + permissions: ['schemaProposal:modify'], + }); + + const result = await execute({ + document: CreateProposalMutation, + variables: { + input: { + target: { byId: target.id }, + author: 'Jeff', + title: 'Proposed changes to the schema...', + }, + }, + authToken: accessKey, + }).then(r => r.expectNoGraphQLErrors()); + + expect(result.createSchemaProposal.ok?.schemaProposal).toHaveProperty('id'); + }, + ); +}); diff --git a/integration-tests/tests/api/proposals/read.spec.ts b/integration-tests/tests/api/proposals/read.spec.ts new file mode 100644 index 00000000000..20721d7b5c3 --- /dev/null +++ b/integration-tests/tests/api/proposals/read.spec.ts @@ -0,0 +1,115 @@ +import { graphql } from 'testkit/gql'; +import { ProjectType, ResourceAssignmentModeType } from 'testkit/gql/graphql'; +import { execute } from 'testkit/graphql'; +import { initSeed } from 'testkit/seed'; + +const CreateProposalMutation = graphql(` + mutation CreateProposalMutation($input: CreateSchemaProposalInput!) { + createSchemaProposal(input: $input) { + ok { + schemaProposal { + id + } + } + error { + message + } + } + } +`); + +const ReadProposalQuery = graphql(` + query ReadProposalQuery($input: SchemaProposalInput!) { + schemaProposal(input: $input) { + title + description + checks(input: { latestPerService: true }) { + edges { + node { + id + } + } + } + } + } +`); + +/** + * Creates a proposal and returns a token with specified permissions + **/ +async function setup(input: { + tokenPermissions: string[]; +}): Promise<{ accessKey: string; proposalId: string }> { + const { createOrg, ownerToken } = await initSeed().createOwner(); + const { createProject, createOrganizationAccessToken, setFeatureFlag } = await createOrg(); + await setFeatureFlag('schemaProposals', true); + const { target } = await createProject(ProjectType.Federation); + + // create as owner + const result = await execute({ + document: CreateProposalMutation, + variables: { + input: { + target: { byId: target.id }, + author: 'Jeff', + title: 'Proposed changes to the schema...', + }, + }, + token: ownerToken, + }).then(r => r.expectNoGraphQLErrors()); + + const { privateAccessKey: accessKey } = await createOrganizationAccessToken( + { + resources: { + mode: ResourceAssignmentModeType.All, + }, + permissions: input.tokenPermissions, + }, + ownerToken, + ); + const proposalId = result.createSchemaProposal.ok?.schemaProposal.id!; + return { accessKey, proposalId }; +} + +describe('Schema Proposals', () => { + test.concurrent( + 'can read proposal with "schemaProposal:describe" permission', + async ({ expect }) => { + const { accessKey, proposalId } = await setup({ + tokenPermissions: ['schemaProposal:describe'], + }); + + { + const proposal = await execute({ + document: ReadProposalQuery, + variables: { + input: { + id: proposalId, + }, + }, + token: accessKey, + }).then(r => r.expectNoGraphQLErrors()); + + expect(proposal.schemaProposal?.title).toMatchInlineSnapshot( + `Proposed changes to the schema...`, + ); + } + }, + ); + + test.concurrent('cannot read proposal without "schemaProposal:describe" permission', async () => { + const { accessKey, proposalId } = await setup({ tokenPermissions: [] }); + + { + await execute({ + document: ReadProposalQuery, + variables: { + input: { + id: proposalId, + }, + }, + token: accessKey, + }).then(r => r.expectGraphQLErrors()); + } + }); +}); diff --git a/integration-tests/tests/api/proposals/subscribe.spec.ts b/integration-tests/tests/api/proposals/subscribe.spec.ts new file mode 100644 index 00000000000..060ae3cd601 --- /dev/null +++ b/integration-tests/tests/api/proposals/subscribe.spec.ts @@ -0,0 +1,116 @@ +import { graphql } from 'testkit/gql'; +import { ProjectType, ResourceAssignmentModeType } from 'testkit/gql/graphql'; +import { execute, subscribe } from 'testkit/graphql'; +import { initSeed } from 'testkit/seed'; + +const CreateProposalMutation = graphql(` + mutation CreateProposalMutation($input: CreateSchemaProposalInput!) { + createSchemaProposal(input: $input) { + ok { + schemaProposal { + id + } + } + error { + message + } + } + } +`); + +const ProposalCompositionSubscription = graphql(` + subscription ProposalCompositionSubscription( + $input: SchemaProposalCompositionSubscriptionInput! + ) { + schemaProposalComposition(input: $input) { + status + timestamp + } + } +`); + +/** + * Creates a proposal and returns a token with specified permissions + **/ +async function setup(input: { tokenPermissions: string[] }) { + const { createOrg, ownerToken } = await initSeed().createOwner(); + const { createProject, createOrganizationAccessToken, setFeatureFlag } = await createOrg(); + await setFeatureFlag('schemaProposals', true); + const project = await createProject(ProjectType.Federation); + + // create as owner + const result = await execute({ + document: CreateProposalMutation, + variables: { + input: { + target: { byId: project.target.id }, + author: 'Jeff', + title: 'Proposed changes to the schema...', + }, + }, + token: ownerToken, + }).then(r => r.expectNoGraphQLErrors()); + + const { privateAccessKey: accessKey } = await createOrganizationAccessToken( + { + resources: { + mode: ResourceAssignmentModeType.All, + }, + permissions: input.tokenPermissions, + }, + ownerToken, + ); + const proposalId = result.createSchemaProposal.ok?.schemaProposal.id!; + return { accessKey, proposalId, project }; +} + +describe('Schema Proposals', () => { + test.concurrent( + 'can subscribe for proposal events with "schemaProposal:describe" permission', + async ({ expect }) => { + const { accessKey, proposalId, project } = await setup({ + tokenPermissions: ['schemaProposal:describe'], + }); + + const query = await subscribe({ + document: ProposalCompositionSubscription, + variables: { + input: { + proposalId, + }, + }, + token: accessKey, + }); + + // create the schema check to trigger the composition and subscription event + const token = await project.createTargetAccessToken({ mode: 'readWrite' }); + await token.publishSchema({ + sdl: /* GraphQL */ ` + type Query { + ping: String + } + `, + service: 'example', + url: 'http://localhost:4001', + }); + const checkResultErrors = await token + .checkSchema( + /* GraphQL */ ` + type Query { + ping: String + pong: String + } + `, + 'example', + undefined, + undefined, + proposalId, + ) + .then(r => r.expectNoGraphQLErrors()); + expect(checkResultErrors.schemaCheck.__typename).toBe(`SchemaCheckSuccess`); + const { value } = await query.next(); + expect(value.data.schemaProposalComposition.status).toBe(`success`); + await expect(query.return?.()).resolves.toMatchObject({ done: true }); + }, + ); +}); diff --git a/package.json b/package.json index c611df236a4..bcf131651e3 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@graphql-eslint/eslint-plugin": "3.20.1", "@graphql-inspector/cli": "6.0.6", "@graphql-inspector/core": "7.1.2", - "@graphql-inspector/patch": "0.1.2", + "@graphql-inspector/patch": "0.1.3-alpha-20260225183305-b1e68e3f363401fe16845d7f731ff8ed3467f5a7", "@graphql-tools/load": "8.1.2", "@manypkg/get-packages": "2.2.2", "@next/eslint-plugin-next": "14.2.23", diff --git a/packages/libraries/pubsub/LICENSE b/packages/libraries/pubsub/LICENSE new file mode 100644 index 00000000000..1cf5b9c7d23 --- /dev/null +++ b/packages/libraries/pubsub/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 The Guild + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/libraries/pubsub/package.json b/packages/libraries/pubsub/package.json new file mode 100644 index 00000000000..7eae513c6ee --- /dev/null +++ b/packages/libraries/pubsub/package.json @@ -0,0 +1,62 @@ +{ + "name": "@hive/pubsub", + "version": "0.0.1", + "type": "module", + "repository": { + "type": "git", + "url": "graphql-hive/console", + "directory": "packages/libraries/pubsub" + }, + "homepage": "https://the-guild.dev/graphql/hive", + "author": { + "email": "contact@the-guild.dev", + "name": "The Guild", + "url": "https://the-guild.dev" + }, + "license": "MIT", + "private": true, + "engines": { + "node": ">=16.0.0" + }, + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "exports": { + ".": { + "require": { + "types": "./dist/typings/index.d.cts", + "default": "./dist/cjs/index.js" + }, + "import": { + "types": "./dist/typings/index.d.ts", + "default": "./dist/esm/index.js" + }, + "default": { + "types": "./dist/typings/index.d.ts", + "default": "./dist/esm/index.js" + } + }, + "./package.json": "./package.json" + }, + "typings": "dist/typings/index.d.ts", + "scripts": { + "build": "node ../../../scripts/generate-version.mjs && bob build", + "check:build": "bob check", + "typecheck": "tsc --noEmit" + }, + "peerDependencies": { + "ioredis": "^5.0.0" + }, + "dependencies": { + "@graphql-yoga/redis-event-target": "3.0.3", + "graphile-worker": "^0.16.0", + "graphql-yoga": "5.13.3" + }, + "devDependencies": { + "tslib": "2.8.1", + "vitest": "4.0.9" + }, + "sideEffects": false, + "typescript": { + "definition": "dist/typings/index.d.ts" + } +} diff --git a/packages/libraries/pubsub/src/index.ts b/packages/libraries/pubsub/src/index.ts new file mode 100644 index 00000000000..f6c7a6b06b6 --- /dev/null +++ b/packages/libraries/pubsub/src/index.ts @@ -0,0 +1,16 @@ +import { createPubSub } from 'graphql-yoga'; +import { Redis } from 'ioredis'; +import { createRedisEventTarget } from '@graphql-yoga/redis-event-target'; +import { bridgeGraphileLogger, type Logger } from './logger.js'; +import type { HivePubSub } from './pub-sub.js'; + +export function createHivePubSub(args: { publisher: Redis; subscriber: Redis }) { + return createPubSub({ + eventTarget: createRedisEventTarget({ + publishClient: args.publisher, + subscribeClient: args.subscriber, + }), + }) as HivePubSub; +} + +export { type HivePubSub, type Logger, bridgeGraphileLogger }; diff --git a/packages/libraries/pubsub/src/logger.ts b/packages/libraries/pubsub/src/logger.ts new file mode 100644 index 00000000000..0e9d2cf679d --- /dev/null +++ b/packages/libraries/pubsub/src/logger.ts @@ -0,0 +1,32 @@ +import { Logger as GraphileLogger, type LogLevel as GraphileLogLevel } from 'graphile-worker'; + +export type Logger = { + error(msg: string, ...interpol: unknown[]): void; + warn(msg: string, ...interpol: unknown[]): void; + info(msg: string, ...interpol: unknown[]): void; + debug(msg: string, ...interpol: unknown[]): void; +}; + +function logLevel(level: GraphileLogLevel) { + switch (level) { + case 'warning': + return 'warn' as const; + case 'info': { + return 'info' as const; + } + case 'debug': { + return 'debug' as const; + } + case 'error': { + return 'error' as const; + } + } + + return 'info'; +} + +export function bridgeGraphileLogger(logger: Logger) { + return new GraphileLogger(_scope => (level, message, _meta) => { + logger[logLevel(level)](message); + }); +} diff --git a/packages/libraries/pubsub/src/pub-sub.ts b/packages/libraries/pubsub/src/pub-sub.ts new file mode 100644 index 00000000000..0cba7270c3b --- /dev/null +++ b/packages/libraries/pubsub/src/pub-sub.ts @@ -0,0 +1,9 @@ +import type { PubSub } from 'graphql-yoga'; + +export type HivePubSub = PubSub<{ + oidcIntegrationLogs: [oidcIntegrationId: string, payload: { timestamp: string; message: string }]; + schemaProposalComposition: [ + proposalId: string, + payload: { timestamp: string; status: 'success' | 'fail' | 'error'; reason?: string | null }, + ]; +}>; diff --git a/packages/libraries/pubsub/src/version.ts b/packages/libraries/pubsub/src/version.ts new file mode 100644 index 00000000000..266e19f220a --- /dev/null +++ b/packages/libraries/pubsub/src/version.ts @@ -0,0 +1 @@ +export const version = '0.0.1'; diff --git a/packages/libraries/pubsub/tsconfig.json b/packages/libraries/pubsub/tsconfig.json new file mode 100644 index 00000000000..31d8323b929 --- /dev/null +++ b/packages/libraries/pubsub/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["src"], + "compilerOptions": { + "types": ["node"], + "baseUrl": ".", + "outDir": "dist", + "rootDir": "src", + "target": "es2017", + "module": "esnext", + "skipLibCheck": true, + "declaration": true, + "declarationMap": true + } +} diff --git a/packages/migrations/src/actions/2026.02.24T00-00-00.proposal-composition.ts b/packages/migrations/src/actions/2026.02.24T00-00-00.proposal-composition.ts new file mode 100644 index 00000000000..c0d83243b62 --- /dev/null +++ b/packages/migrations/src/actions/2026.02.24T00-00-00.proposal-composition.ts @@ -0,0 +1,17 @@ +import { type MigrationExecutor } from '../pg-migrator'; + +export default { + name: '2026.02.24T00-00-00.proposal-composition.ts', + run: ({ sql }) => [ + { + name: 'add schema proposal composition state', + query: sql` + ALTER TABLE IF EXISTS "schema_proposals" + ADD COLUMN IF NOT EXISTS "composition_status" TEXT + , ADD COLUMN IF NOT EXISTS "composition_timestamp" TIMESTAMPTZ + , ADD COLUMN IF NOT EXISTS "composition_status_reason" TEXT + ; + `, + }, + ], +} satisfies MigrationExecutor; diff --git a/packages/migrations/src/run-pg-migrations.ts b/packages/migrations/src/run-pg-migrations.ts index f888f29542d..1b213dcfb5f 100644 --- a/packages/migrations/src/run-pg-migrations.ts +++ b/packages/migrations/src/run-pg-migrations.ts @@ -189,5 +189,6 @@ export const runPGMigrations = async (args: { slonik: DatabasePool; runTo?: stri ...(env.useSupertokensAtHome ? [await import('./actions/2026.02.18T00-00-00.ensure-supertokens-tables')] : []), + await import('./actions/2026.02.24T00-00-00.proposal-composition'), ], }); diff --git a/packages/services/api/package.json b/packages/services/api/package.json index 1f4db07de7c..a03311a91bd 100644 --- a/packages/services/api/package.json +++ b/packages/services/api/package.json @@ -25,6 +25,7 @@ "@graphql-inspector/core": "7.1.2", "@graphql-tools/merge": "9.1.1", "@hive/cdn-script": "workspace:*", + "@hive/pubsub": "workspace:*", "@hive/schema": "workspace:*", "@hive/service-common": "workspace:*", "@hive/storage": "workspace:*", @@ -55,6 +56,7 @@ "date-fns": "4.1.0", "fast-json-stable-stringify": "2.1.0", "got": "14.4.7", + "graphile-worker": "0.16.6", "graphql": "16.9.0", "graphql-modules": "3.1.1", "graphql-parse-resolve-info": "4.13.0", diff --git a/packages/services/api/src/modules/organization/lib/organization-access-token-permissions.ts b/packages/services/api/src/modules/organization/lib/organization-access-token-permissions.ts index eedc6144864..d80bd59d7b8 100644 --- a/packages/services/api/src/modules/organization/lib/organization-access-token-permissions.ts +++ b/packages/services/api/src/modules/organization/lib/organization-access-token-permissions.ts @@ -94,6 +94,16 @@ export const permissionGroups: Array = [ title: 'Manage CDN access tokens', description: 'Allow managing access tokens for the CDN.', }, + { + id: 'schemaProposal:modify', + title: 'Create and edit schema proposals', + description: 'Allow managing schema proposals.', + }, + { + id: 'schemaProposal:describe', + title: 'View schema proposals', + description: 'Allow viewing schema proposals and adding comments.', + }, ], }, { diff --git a/packages/services/api/src/modules/proposals/module.graphql.ts b/packages/services/api/src/modules/proposals/module.graphql.ts index 546927c4ad0..3117ce2b782 100644 --- a/packages/services/api/src/modules/proposals/module.graphql.ts +++ b/packages/services/api/src/modules/proposals/module.graphql.ts @@ -122,6 +122,21 @@ export default gql` body: String! } + extend type Subscription { + schemaProposalComposition( + input: SchemaProposalCompositionSubscriptionInput! + ): SchemaProposalCompositionEvent! + } + + type SchemaProposalCompositionEvent { + status: String! + timestamp: String! + } + + input SchemaProposalCompositionSubscriptionInput { + proposalId: ID! + } + extend type Query { schemaProposals( after: String @@ -240,6 +255,10 @@ export default gql` """ fromCursor: String ): String + + compositionStatus: String + compositionTimestamp: DateTime + compositionStatusReason: String } input SchemaProposalChecksInput { diff --git a/packages/services/api/src/modules/proposals/providers/schema-proposal-manager.ts b/packages/services/api/src/modules/proposals/providers/schema-proposal-manager.ts index 86dab5b5469..34de9ac96d3 100644 --- a/packages/services/api/src/modules/proposals/providers/schema-proposal-manager.ts +++ b/packages/services/api/src/modules/proposals/providers/schema-proposal-manager.ts @@ -1,13 +1,14 @@ /** * This wraps the higher level logic with schema proposals. */ -import { Injectable, Scope } from 'graphql-modules'; +import { Inject, Injectable, Scope } from 'graphql-modules'; import { TargetReferenceInput } from 'packages/libraries/core/src/client/__generated__/types'; import type { SchemaProposalStage } from '../../../__generated__/types'; import { HiveError } from '../../../shared/errors'; import { Session } from '../../auth/lib/authz'; import { IdTranslator } from '../../shared/providers/id-translator'; import { Logger } from '../../shared/providers/logger'; +import { PUB_SUB_CONFIG, type HivePubSub } from '../../shared/providers/pub-sub'; import { Storage } from '../../shared/providers/storage'; import { SchemaProposalStorage } from './schema-proposal-storage'; @@ -23,10 +24,41 @@ export class SchemaProposalManager { private storage: Storage, private session: Session, private idTranslator: IdTranslator, + @Inject(PUB_SUB_CONFIG) private pubSub: HivePubSub, ) { this.logger = logger.child({ source: 'SchemaProposalsManager' }); } + async subscribeToSchemaProposalCompositions(args: { proposalId: string }) { + const proposal = await this.proposalStorage.getProposalTargetId({ + id: args.proposalId, + }); + + if (!proposal) { + this.session.raise('schemaProposal:describe'); + } + + const selector = await this.idTranslator.resolveTargetReference({ + reference: { + byId: proposal.targetId, + }, + }); + + if (!selector) { + this.session.raise('schemaProposal:describe'); + } + + await this.session.assertPerformAction({ + organizationId: selector.organizationId, + action: 'schemaProposal:describe', + params: selector, + }); + + this.logger.info(`Subscribed to "schemaProposalComposition" (id=${args.proposalId})`); + + return this.pubSub.subscribe('schemaProposalComposition', args.proposalId); + } + async proposeSchema(args: { target: TargetReferenceInput; title: string; @@ -39,6 +71,12 @@ export class SchemaProposalManager { this.session.raise('schemaProposal:modify'); } + await this.session.assertPerformAction({ + action: 'schemaProposal:modify', + organizationId: selector.organizationId, + params: selector, + }); + const createProposalResult = await this.proposalStorage.createProposal({ organizationId: selector.organizationId, author: args.author ?? null, @@ -79,7 +117,24 @@ export class SchemaProposalManager { } async getProposal(args: { id: string }) { - return this.proposalStorage.getProposal(args); + const proposal = await this.proposalStorage.getProposal(args); + + if (proposal) { + const selector = await this.idTranslator.resolveTargetReference({ + reference: { + byId: proposal.targetId, + }, + }); + if (selector === null) { + this.session.raise('schemaProposal:describe'); + } + await this.session.assertPerformAction({ + action: 'schemaProposal:describe', + organizationId: selector.organizationId, + params: selector, + }); + } + return proposal; } async getPaginatedReviews(args: { @@ -90,6 +145,23 @@ export class SchemaProposalManager { authors: string[]; }) { this.logger.debug('Get paginated reviews (target=%s, after=%s)', args.proposalId, args.after); + const proposal = await this.proposalStorage.getProposalTargetId({ id: args.proposalId }); + + if (proposal) { + const selector = await this.idTranslator.resolveTargetReference({ + reference: { + byId: proposal.targetId, + }, + }); + if (selector === null) { + this.session.raise('schemaProposal:describe'); + } + await this.session.assertPerformAction({ + action: 'schemaProposal:describe', + organizationId: selector.organizationId, + params: selector, + }); + } return this.proposalStorage.getPaginatedReviews(args); } @@ -110,9 +182,15 @@ export class SchemaProposalManager { reference: args.target, }); if (selector === null) { - this.session.raise('schemaProposal:modify'); + this.session.raise('schemaProposal:describe'); } + await this.session.assertPerformAction({ + action: 'schemaProposal:describe', + organizationId: selector.organizationId, + params: selector, + }); + return this.proposalStorage.getPaginatedProposals({ targetId: selector.targetId, after: args.after, @@ -129,8 +207,12 @@ export class SchemaProposalManager { }) { this.logger.debug(`Reviewing proposal (proposal=%s, stage=%s)`, args.proposalId, args.stage); - // @todo check permissions for user - const proposal = await this.proposalStorage.getProposal({ id: args.proposalId }); + const proposal = await this.proposalStorage.getProposalTargetId({ id: args.proposalId }); + + if (!proposal) { + throw new HiveError('Proposal target lookup failed.'); + } + const user = await this.session.getViewer(); const target = await this.storage.getTargetById(proposal.targetId); @@ -138,6 +220,16 @@ export class SchemaProposalManager { throw new HiveError('Proposal target lookup failed.'); } + await this.session.assertPerformAction({ + action: 'schemaProposal:describe', + organizationId: target.orgId, + params: { + organizationId: target.orgId, + projectId: target.projectId, + targetId: proposal.targetId, + }, + }); + if (args.stage) { const review = await this.proposalStorage.manuallyTransitionProposal({ organizationId: target.orgId, diff --git a/packages/services/api/src/modules/proposals/providers/schema-proposal-storage.ts b/packages/services/api/src/modules/proposals/providers/schema-proposal-storage.ts index f811de927c5..225e307275e 100644 --- a/packages/services/api/src/modules/proposals/providers/schema-proposal-storage.ts +++ b/packages/services/api/src/modules/proposals/providers/schema-proposal-storage.ts @@ -1,9 +1,11 @@ /** * This wraps the database calls for schema proposals and required validation */ +import { makeWorkerUtils, WorkerUtils } from 'graphile-worker'; import { Inject, Injectable, Scope } from 'graphql-modules'; import { sql, type DatabasePool } from 'slonik'; import { z } from 'zod'; +import { bridgeGraphileLogger } from '@hive/pubsub'; import { decodeCreatedAtAndUUIDIdBasedCursor, encodeCreatedAtAndUUIDIdBasedCursor, @@ -43,6 +45,23 @@ export class SchemaProposalStorage { this.logger = logger.child({ source: 'SchemaProposalStorage' }); } + async runBackgroundComposition(input: { + proposalId: string; + targetId: string; + externalComposition: { + enabled: boolean; + endpoint?: string | null; + encryptedSecret?: string | null; + }; + native: boolean; + }) { + const tools: WorkerUtils = await makeWorkerUtils({ + pgPool: this.pool.pool, + logger: bridgeGraphileLogger(this.logger), + }); + await tools.addJob('schemaProposalComposition', { input }); + } + private async assertSchemaProposalsEnabled(args: { organizationId: string; targetId: string; @@ -189,6 +208,29 @@ export class SchemaProposalStorage { }; } + /** + * A stripped down version of getProposal that only returns the ID. This is intended + * to be used + */ + async getProposalTargetId(args: { id: string }) { + this.logger.debug('Get proposal target ID (proposal=%s)', args.id); + const result = await this.pool + .maybeOne( + sql` + SELECT + id + , target_id as "targetId" + FROM + "schema_proposals" + WHERE + id=${args.id} + `, + ) + .then(row => SchemaProposalTargetIdModel.safeParse(row)); + + return result.data ?? null; + } + async getProposal(args: { id: string }) { this.logger.debug('Get proposal (proposal=%s)', args.id); const result = await this.pool @@ -202,9 +244,9 @@ export class SchemaProposalStorage { "sp"."id" = ${args.id} `, ) - .then(row => SchemaProposalModel.parse(row)); + .then(row => SchemaProposalModel.safeParse(row)); - return result; + return result.data ?? null; } async getPaginatedProposals(args: { @@ -344,7 +386,7 @@ export class SchemaProposalStorage { } const schemaProposalFields = sql` - sp."id" + sp."id" , to_json(sp."created_at") as "createdAt" , to_json(sp."updated_at") as "updatedAt" , sp."title" @@ -352,6 +394,9 @@ const schemaProposalFields = sql` , sp."stage" , sp."target_id" as "targetId" , sp."author" + , sp."composition_status" as "compositionStatus" + , to_json(sp."composition_timestamp") as "compositionTimestamp" + , sp."composition_status_reason" as "compositionStatusReason" `; const schemaProposalReviewFields = sql` @@ -388,6 +433,17 @@ const SchemaProposalModel = z.object({ stage: StageModel, targetId: z.string(), author: z.string(), + compositionStatus: z.string().nullable(), + compositionStatusReason: z.string().nullable(), + compositionTimestamp: z.string().nullable(), +}); + +/** + * Minimal model for extracting just the target Id for permission checks. + */ +const SchemaProposalTargetIdModel = z.object({ + id: z.string(), + targetId: z.string(), }); export type SchemaProposalRecord = z.infer; diff --git a/packages/services/api/src/modules/proposals/resolvers/Subscription/schemaProposalComposition.ts b/packages/services/api/src/modules/proposals/resolvers/Subscription/schemaProposalComposition.ts new file mode 100644 index 00000000000..8f5a1c4b0a5 --- /dev/null +++ b/packages/services/api/src/modules/proposals/resolvers/Subscription/schemaProposalComposition.ts @@ -0,0 +1,12 @@ +import type { SubscriptionResolvers } from '../../../../__generated__/types'; +import { SchemaProposalManager } from '../../providers/schema-proposal-manager'; + +export const schemaProposalComposition: NonNullable< + SubscriptionResolvers['schemaProposalComposition'] +> = { + subscribe: (_, args, { injector }) => + injector + .get(SchemaProposalManager) + .subscribeToSchemaProposalCompositions({ proposalId: args.input.proposalId }), + resolve: (payload: { status: string; timestamp: string; reason?: string | null }) => payload, +}; diff --git a/packages/services/api/src/modules/schema/providers/schema-publisher.ts b/packages/services/api/src/modules/schema/providers/schema-publisher.ts index b308369c9ed..eb0bbd874ac 100644 --- a/packages/services/api/src/modules/schema/providers/schema-publisher.ts +++ b/packages/services/api/src/modules/schema/providers/schema-publisher.ts @@ -728,6 +728,15 @@ export class SchemaPublisher { const retention = await this.rateLimit.getRetention({ targetId: target.id }); const expiresAt = retention ? new Date(Date.now() + retention * millisecondsPerDay) : null; + if (input.schemaProposalId) { + await this.schemaProposals.runBackgroundComposition({ + externalComposition: project.externalComposition, + native: project.nativeFederation, + proposalId: input.schemaProposalId, + targetId: target.id, + }); + } + if (checkResult.conclusion === SchemaCheckConclusion.Failure) { schemaCheck = await this.storage.createSchemaCheck({ schemaSDL: sdl, diff --git a/packages/services/api/src/modules/shared/providers/pub-sub.ts b/packages/services/api/src/modules/shared/providers/pub-sub.ts index 7854479bb3e..e7546fe2969 100644 --- a/packages/services/api/src/modules/shared/providers/pub-sub.ts +++ b/packages/services/api/src/modules/shared/providers/pub-sub.ts @@ -1,8 +1,5 @@ import { InjectionToken } from 'graphql-modules'; -import type { PubSub } from 'graphql-yoga'; - -export type HivePubSub = PubSub<{ - oidcIntegrationLogs: [oidcIntegrationId: string, payload: { timestamp: string; message: string }]; -}>; +import type { HivePubSub } from '@hive/pubsub'; export const PUB_SUB_CONFIG = new InjectionToken('PUB_SUB'); +export type { HivePubSub }; diff --git a/packages/services/api/src/modules/shared/providers/storage.ts b/packages/services/api/src/modules/shared/providers/storage.ts index f8b8c85a81c..bb561ac8839 100644 --- a/packages/services/api/src/modules/shared/providers/storage.ts +++ b/packages/services/api/src/modules/shared/providers/storage.ts @@ -413,7 +413,6 @@ export interface Storage { first: number | null; cursor: null | string; }): Promise; - // @todo consider moving to proposals provider getPaginatedSchemaChecksForSchemaProposal< TransformedSchemaCheck extends SchemaCheck = SchemaCheck, >(_: { diff --git a/packages/services/server/package.json b/packages/services/server/package.json index 87c26f13ccc..915662b7ff3 100644 --- a/packages/services/server/package.json +++ b/packages/services/server/package.json @@ -31,6 +31,7 @@ "@graphql-yoga/redis-event-target": "3.0.3", "@hive/api": "workspace:*", "@hive/cdn-script": "workspace:*", + "@hive/pubsub": "workspace:*", "@hive/schema": "workspace:*", "@hive/service-common": "workspace:*", "@hive/storage": "workspace:*", diff --git a/packages/services/server/src/index.ts b/packages/services/server/src/index.ts index f68e605abc6..58f11771814 100644 --- a/packages/services/server/src/index.ts +++ b/packages/services/server/src/index.ts @@ -8,9 +8,7 @@ import { } from 'supertokens-node/framework/fastify/index.js'; import cors from '@fastify/cors'; import type { FastifyCorsOptionsDelegateCallback } from '@fastify/cors'; -import { createRedisEventTarget } from '@graphql-yoga/redis-event-target'; import 'reflect-metadata'; -import { createPubSub } from 'graphql-yoga'; import { z } from 'zod'; import formDataPlugin from '@fastify/formbody'; import { @@ -24,7 +22,6 @@ import { import { AccessTokenKeyContainer } from '@hive/api/modules/auth/lib/supertokens-at-home/crypto'; import { EmailVerification } from '@hive/api/modules/auth/providers/email-verification'; import { OAuthCache } from '@hive/api/modules/auth/providers/oauth-cache'; -import { HivePubSub } from '@hive/api/modules/shared/providers/pub-sub'; import { createRedisClient } from '@hive/api/modules/shared/providers/redis'; import { RedisRateLimiter } from '@hive/api/modules/shared/providers/redis-rate-limiter'; import { TargetsByIdCache } from '@hive/api/modules/target/providers/targets-by-id-cache'; @@ -34,6 +31,7 @@ import { ArtifactStorageReader } from '@hive/cdn-script/artifact-storage-reader' import { AwsClient } from '@hive/cdn-script/aws'; import { createIsAppDeploymentActive } from '@hive/cdn-script/is-app-deployment-active'; import { createIsKeyValid } from '@hive/cdn-script/key-validation'; +import { createHivePubSub } from '@hive/pubsub'; import { configureTracing, createServer, @@ -184,16 +182,14 @@ export async function main() { const redis = createRedisClient('Redis', env.redis, server.log.child({ source: 'Redis' })); - const pubSub = createPubSub({ - eventTarget: createRedisEventTarget({ - publishClient: redis, - subscribeClient: createRedisClient( - 'subscriber', - env.redis, - server.log.child({ source: 'RedisSubscribe' }), - ), - }), - }) as HivePubSub; + const pubSub = createHivePubSub({ + publisher: redis, + subscriber: createRedisClient( + 'subscriber', + env.redis, + server.log.child({ source: 'RedisSubscribe' }), + ), + }); registerShutdown({ logger: server.log, diff --git a/packages/services/storage/src/db/types.ts b/packages/services/storage/src/db/types.ts index ae52d5ead18..abfa1888132 100644 --- a/packages/services/storage/src/db/types.ts +++ b/packages/services/storage/src/db/types.ts @@ -383,6 +383,9 @@ export interface schema_proposal_reviews { export interface schema_proposals { author: string; comments_count: number; + composition_status: string | null; + composition_status_reason: string | null; + composition_timestamp: Date | null; created_at: Date; description: string; id: string; diff --git a/packages/services/workflows/.env.template b/packages/services/workflows/.env.template index 8ac28c8c7ff..4ab7cd696d0 100644 --- a/packages/services/workflows/.env.template +++ b/packages/services/workflows/.env.template @@ -5,3 +5,7 @@ POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres EMAIL_FROM=mock@graphql-hive.com EMAIL_PROVIDER=mock +SCHEMA_ENDPOINT=http://localhost:6500 +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= \ No newline at end of file diff --git a/packages/services/workflows/package.json b/packages/services/workflows/package.json index e757dd3031d..719b31f8c26 100644 --- a/packages/services/workflows/package.json +++ b/packages/services/workflows/package.json @@ -10,15 +10,23 @@ }, "devDependencies": { "@graphql-hive/logger": "1.0.9", + "@graphql-inspector/core": "7.1.2", + "@graphql-inspector/patch": "0.1.3-alpha-20260225183305-b1e68e3f363401fe16845d7f731ff8ed3467f5a7", + "@graphql-yoga/redis-event-target": "3.0.3", + "@hive/pubsub": "workspace:*", "@hive/service-common": "workspace:*", "@hive/storage": "workspace:*", "@sentry/node": "7.120.2", + "@trpc/client": "10.45.3", "@types/mjml": "4.7.1", "@types/nodemailer": "7.0.4", "@types/sendmail": "1.4.7", "bentocache": "1.1.0", "dotenv": "16.4.7", "graphile-worker": "0.16.6", + "graphql": "16.9.0", + "graphql-yoga": "5.13.3", + "ioredis": "5.8.2", "mjml": "4.14.0", "nodemailer": "7.0.11", "sendmail": "1.6.1", diff --git a/packages/services/workflows/src/context.ts b/packages/services/workflows/src/context.ts index 884702dbf0d..944ea2b87c4 100644 --- a/packages/services/workflows/src/context.ts +++ b/packages/services/workflows/src/context.ts @@ -1,11 +1,15 @@ import type { DatabasePool } from 'slonik'; import type { Logger } from '@graphql-hive/logger'; +import type { HivePubSub } from '@hive/pubsub'; import type { EmailProvider } from './lib/emails/providers.js'; -import { RequestBroker } from './lib/webhooks/send-webhook.js'; +import type { SchemaProvider } from './lib/schema/provider.js'; +import type { RequestBroker } from './lib/webhooks/send-webhook.js'; export type Context = { logger: Logger; email: EmailProvider; + schema: SchemaProvider; pg: DatabasePool; requestBroker: RequestBroker | null; + pubSub: HivePubSub; }; diff --git a/packages/services/workflows/src/environment.ts b/packages/services/workflows/src/environment.ts index c8b1904fdbc..44e9f4cb7da 100644 --- a/packages/services/workflows/src/environment.ts +++ b/packages/services/workflows/src/environment.ts @@ -26,6 +26,7 @@ const EnvironmentModel = zod.object({ RELEASE: emptyString(zod.string().optional()), HEARTBEAT_ENDPOINT: emptyString(zod.string().url().optional()), EMAIL_FROM: zod.string().email(), + SCHEMA_ENDPOINT: zod.string().url(), }); const SentryModel = zod.union([ @@ -82,6 +83,13 @@ const EmailProviderModel = zod.union([ SendmailEmailModel, ]); +const RedisModel = zod.object({ + REDIS_HOST: zod.string(), + REDIS_PORT: NumberFromString, + REDIS_PASSWORD: emptyString(zod.string().optional()), + REDIS_TLS_ENABLED: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()), +}); + const RequestBrokerModel = zod.union([ zod.object({ REQUEST_BROKER: emptyString(zod.literal('0').optional()), @@ -127,6 +135,7 @@ const configs = { log: LogModel.safeParse(process.env), tracing: OpenTelemetryConfigurationModel.safeParse(process.env), requestBroker: RequestBrokerModel.safeParse(process.env), + redis: RedisModel.safeParse(process.env), }; const environmentErrors: Array = []; @@ -158,6 +167,7 @@ const prometheus = extractConfig(configs.prometheus); const log = extractConfig(configs.log); const tracing = extractConfig(configs.tracing); const requestBroker = extractConfig(configs.requestBroker); +const redis = extractConfig(configs.redis); const emailProviderConfig = email.EMAIL_PROVIDER === 'postmark' @@ -204,6 +214,9 @@ export const env = { provider: emailProviderConfig, emailFrom: base.EMAIL_FROM, }, + schema: { + serviceUrl: base.SCHEMA_ENDPOINT, + }, sentry: sentry.SENTRY === '1' ? { dsn: sentry.SENTRY_DSN } : null, log: { level: log.LOG_LEVEL ?? 'info', @@ -235,4 +248,10 @@ export const env = { } satisfies RequestBroker) : null, httpHeartbeat: base.HEARTBEAT_ENDPOINT ? { endpoint: base.HEARTBEAT_ENDPOINT } : null, + redis: { + host: redis.REDIS_HOST, + port: redis.REDIS_PORT, + password: redis.REDIS_PASSWORD ?? '', + tlsEnabled: redis.REDIS_TLS_ENABLED === '1', + }, } as const; diff --git a/packages/services/workflows/src/index.ts b/packages/services/workflows/src/index.ts index 625b7170de3..db80dd74ef3 100644 --- a/packages/services/workflows/src/index.ts +++ b/packages/services/workflows/src/index.ts @@ -1,6 +1,7 @@ import { run } from 'graphile-worker'; import { createPool } from 'slonik'; import { Logger } from '@graphql-hive/logger'; +import { bridgeGraphileLogger, createHivePubSub } from '@hive/pubsub'; import { createServer, registerShutdown, @@ -12,7 +13,9 @@ import { import { Context } from './context.js'; import { env } from './environment.js'; import { createEmailProvider } from './lib/emails/providers.js'; -import { bridgeFastifyLogger, bridgeGraphileLogger } from './logger.js'; +import { schemaProvider } from './lib/schema/provider.js'; +import { bridgeFastifyLogger } from './logger.js'; +import { createRedisClient } from './redis'; import { createTaskEventEmitter } from './task-events.js'; if (env.sentry) { @@ -39,6 +42,7 @@ const modules = await Promise.all([ import('./tasks/schema-change-notification.js'), import('./tasks/usage-rate-limit-exceeded.js'), import('./tasks/usage-rate-limit-warning.js'), + import('./tasks/schema-proposal-composition.js'), ]); const crontab = ` @@ -63,19 +67,35 @@ const stopHttpHeartbeat = env.httpHeartbeat }) : null; +const server = await createServer({ + sentryErrorHandler: !!env.sentry, + name: 'workflows', + log: logger, +}); + +const redis = createRedisClient('Redis', env.redis, server.log.child({ source: 'Redis' })); + +const pubSub = createHivePubSub({ + publisher: redis, + subscriber: createRedisClient( + 'subscriber', + env.redis, + server.log.child({ source: 'RedisSubscribe' }), + ), +}); + const context: Context = { logger, email: createEmailProvider(env.email.provider, env.email.emailFrom), pg, requestBroker: env.requestBroker, + schema: schemaProvider({ + logger, + schemaServiceUrl: env.schema.serviceUrl, + }), + pubSub, }; -const server = await createServer({ - sentryErrorHandler: !!env.sentry, - name: 'workflows', - log: logger, -}); - server.route({ method: ['GET', 'HEAD'], url: '/_health', @@ -132,6 +152,8 @@ registerShutdown({ logger.info('Shutdown postgres connection.'); await pg.end(); logger.info('Shutdown postgres connection successful.'); + logger.info('Shutdown redis connection.'); + redis.disconnect(false); if (shutdownMetrics) { logger.info('Stopping prometheus endpoint'); await shutdownMetrics(); diff --git a/packages/services/workflows/src/kit.ts b/packages/services/workflows/src/kit.ts index 3f9c6255898..987ba2e3754 100644 --- a/packages/services/workflows/src/kit.ts +++ b/packages/services/workflows/src/kit.ts @@ -4,8 +4,8 @@ import { makeWorkerUtils, WorkerUtils, type JobHelpers, type Task } from 'graphi import type { Pool } from 'pg'; import { z } from 'zod'; import { Logger } from '@graphql-hive/logger'; +import { bridgeGraphileLogger } from '@hive/pubsub'; import type { Context } from './context'; -import { bridgeGraphileLogger } from './logger'; export type TaskDefinition = { name: TName; diff --git a/packages/services/workflows/src/lib/schema/provider.ts b/packages/services/workflows/src/lib/schema/provider.ts new file mode 100644 index 00000000000..1fde013ac12 --- /dev/null +++ b/packages/services/workflows/src/lib/schema/provider.ts @@ -0,0 +1,394 @@ +import { DocumentNode, GraphQLError, parse, print, SourceLocation } from 'graphql'; +import { DatabasePool, sql } from 'slonik'; +import { z } from 'zod'; +import type { Logger } from '@graphql-hive/logger'; +import type { Change } from '@graphql-inspector/core'; +import { errors, patch } from '@graphql-inspector/patch'; +import type { Project, SchemaObject } from '@hive/api'; +import type { ComposeAndValidateResult } from '@hive/api/shared/entities'; +import type { ContractsInputType, SchemaBuilderApi } from '@hive/schema'; +import { decodeCreatedAtAndUUIDIdBasedCursor, HiveSchemaChangeModel } from '@hive/storage'; +import { createTRPCProxyClient, httpLink } from '@trpc/client'; + +type SchemaProviderConfig = { + /** URL of the Schema Service */ + schemaServiceUrl: string; + logger: Logger; +}; + +const SchemaModel = z.object({ + id: z.string().uuid(), + sdl: z.string(), + serviceName: z.string().nullable(), + serviceUrl: z.string().nullable(), + type: z.string(), +}); + +const SchemaProposalChangesModel = z.object({ + id: z.string().uuid(), + serviceName: z.string().nullable(), + serviceUrl: z.string().nullable(), + schemaProposalChanges: z.array(HiveSchemaChangeModel).default([]), + createdAt: z.string(), +}); + +function createExternalConfig(config: Project['externalComposition']) { + // : ExternalCompositionConfig { + if (config && config.enabled) { + if (!config.endpoint) { + throw new Error('External composition error: endpoint is missing'); + } + + if (!config.encryptedSecret) { + throw new Error('External composition error: encryptedSecret is missing'); + } + + return { + endpoint: config.endpoint, + encryptedSecret: config.encryptedSecret, + }; + } + + return null; +} + +export class GraphQLDocumentStringInvalidError extends Error { + constructor(message: string, location?: SourceLocation) { + const locationString = location ? ` at line ${location.line}, column ${location.column}` : ''; + super(`The provided SDL is not valid${locationString}\n: ${message}`); + } +} + +export type CreateSchemaObjectInput = { + sdl: string; + serviceName?: string | null; + serviceUrl?: string | null; +}; + +export const emptySource = '*'; + +export function createSchemaObject(schema: CreateSchemaObjectInput): SchemaObject { + let document: DocumentNode; + + try { + document = parse(schema.sdl, { + noLocation: true, + }); + } catch (err) { + if (err instanceof GraphQLError) { + throw new GraphQLDocumentStringInvalidError(err.message, err.locations?.[0]); + } + throw err; + } + + return { + document, + raw: schema.sdl, + source: schema.serviceName ?? emptySource, + url: schema.serviceUrl ?? null, + }; +} + +export type SchemaProvider = ReturnType; + +export function convertProjectType(t: string) { + const result = ( + { + ['FEDERATION']: 'federation', + ['SINGLE']: 'single', + ['STITCHING']: 'stitching', + } as const + )[t]; + + if (!result) { + throw new Error('Invalid project type.'); + } + return result; +} + +export function schemaProvider(providerConfig: SchemaProviderConfig) { + const schemaService = createTRPCProxyClient({ + links: [ + httpLink({ + url: `${providerConfig.schemaServiceUrl}/trpc`, + fetch, + }), + ], + }); + + return { + id: 'schema' as const, + /** + * Compose and validate schemas via the schema service. + * - Requests time out after 30 seconds and result in a human readable error response + * - In case the incoming request is canceled, the call to the schema service is aborted + */ + async composeAndValidate( + projectType: string, + schemas: SchemaObject[], + config: { + /** Whether external composition should be used (only Federation) */ + external: Project['externalComposition']; + /** Whether native composition should be used (only Federation) */ + native: boolean; + /** Specified contracts (only Federation) */ + contracts: ContractsInputType | null; + }, + ) { + const compositionType = convertProjectType(projectType); + providerConfig.logger.debug( + 'Composing and validating schemas (type=%s, method=%s)', + compositionType, + compositionType === 'federation' + ? config.native + ? 'native' + : config.external.enabled + ? 'external' + : 'v1' + : 'none', + ); + + const timeoutAbortSignal = AbortSignal.timeout(30_000); + + const onTimeout = () => { + providerConfig.logger.debug( + 'Composition HTTP request aborted due to timeout of 30 seconds.', + ); + }; + timeoutAbortSignal.addEventListener('abort', onTimeout); + + try { + const result = await schemaService.composeAndValidate.mutate( + { + type: compositionType, + schemas: schemas.map(s => ({ + raw: s.raw, + source: s.source, + url: s.url ?? null, + })), + external: createExternalConfig(config.external), + native: config.native, + contracts: config.contracts, + }, + { + // Limit the maximum time allowed for composition requests to 30 seconds to avoid a dead-lock + signal: timeoutAbortSignal, + }, + ); + return result; + } catch (err) { + // In case of a timeout error we return something the user can process + if (timeoutAbortSignal.reason) { + return { + contracts: null, + metadataAttributes: null, + schemaMetadata: null, + sdl: null, + supergraph: null, + tags: null, + includesNetworkError: true, + includesException: false, + errors: [ + { + message: 'The schema composition timed out. Please try again.', + source: 'composition', + }, + ], + } satisfies ComposeAndValidateResult; + } + + throw err; + } finally { + timeoutAbortSignal.removeEventListener('abort', onTimeout); + } + }, + + async updateSchemaProposalComposition(args: { + proposalId: string; + timestamp: string; + reason: string | null; + status: 'error' | 'success' | 'fail'; + pool: DatabasePool; + }) { + const { pool, ...state } = args; + await pool.query( + sql`/* updateSchemaProposalComposition */ + UPDATE schema_proposals + SET + composition_status = ${state.status} + , composition_timestamp = ${state.timestamp} + , composition_status_reason = ${state.reason} + WHERE id=${state.proposalId}`, + ); + }, + + async latestComposableSchemas(args: { targetId: string; pool: DatabasePool }) { + const latestVersion = await args.pool.maybeOne<{ id: string }>( + sql`/* findLatestComposableSchemaVersion */ + SELECT sv.id + FROM schema_versions as sv + WHERE sv.target_id = ${args.targetId} AND sv.is_composable IS TRUE + ORDER BY sv.created_at DESC + LIMIT 1 + `, + ); + + if (!latestVersion) { + return []; + } + + const version = latestVersion.id; + const result = await args.pool.query( + sql`/* getSchemasOfVersion */ + SELECT + sl.id + , sl.sdl + , lower(sl.service_name) as "serviceName" + , sl.service_url as "serviceUrl" + , p.type + FROM schema_version_to_log AS svl + LEFT JOIN schema_log AS sl ON (sl.id = svl.action_id) + LEFT JOIN projects as p ON (p.id = sl.project_id) + WHERE + svl.version_id = ${version} + AND sl.action = 'PUSH' + AND p.type != 'CUSTOM' + ORDER BY + sl.created_at DESC + `, + ); + return result.rows.map(row => SchemaModel.parse(row)); + }, + + async getBaseSchema(args: { targetId: string; pool: DatabasePool }) { + const data = await args.pool.maybeOne>( + sql`/* getBaseSchema */ SELECT base_schema FROM targets WHERE id=${args.targetId}`, + ); + return data?.base_schema ?? null; + }, + + async proposedSchemas(args: { + targetId: string; + proposalId: string; + cursor?: string | null; + pool: DatabasePool; + }) { + const now = new Date().toISOString(); + let cursor: { + createdAt: string; + id: string; + } | null = null; + + if (args.cursor) { + cursor = decodeCreatedAtAndUUIDIdBasedCursor(args.cursor); + } + + // fetch all latest schemas. Support up to 2_000 subgraphs. + const maxLoops = 100; + const services = await this.latestComposableSchemas({ + targetId: args.targetId, + pool: args.pool, + }); + + let nextCursor = cursor; + // collect changes in paginated requests to avoid stalling the db + let i = 0; + do { + const result = await args.pool.query(sql` + SELECT + c."id" + , c."service_name" as "serviceName" + , c."service_url" as "serviceUrl" + , c."schema_proposal_changes" as "schemaProposalChanges" + , to_json(c."created_at") as "createdAt" + FROM + "schema_checks" as c + INNER JOIN ( + SELECT COALESCE("service_name", '') as "service", "schema_proposal_id", max("created_at") as "maxdate" + FROM schema_checks + ${ + cursor + ? sql` + WHERE "schema_proposal_id" = ${args.proposalId} + AND ( + ( + "created_at" = ${cursor.createdAt} + AND "id" < ${cursor.id} + ) + OR "created_at" < ${cursor.createdAt} + ) + ` + : sql`` + } + GROUP BY "service", "schema_proposal_id" + ) as cc + ON c."schema_proposal_id" = cc."schema_proposal_id" + AND COALESCE(c."service_name", '') = cc."service" + AND c."created_at" = cc."maxdate" + WHERE + c."target_id" = ${args.targetId} + AND c."schema_proposal_id" = ${args.proposalId} + ${ + cursor + ? sql` + AND ( + ( + c."created_at" = ${cursor.createdAt} + AND c."id" < ${cursor.id} + ) + OR c."created_at" < ${cursor.createdAt} + ) + ` + : sql`` + } + ORDER BY + c."created_at" DESC + , c."id" DESC + LIMIT 20 + `); + + const changes = result.rows.map(row => { + const value = SchemaProposalChangesModel.parse(row); + return { + ...value, + schemaProposalChanges: value.schemaProposalChanges.map(c => { + const change: Change = { + ...c, + path: c.path ?? undefined, + criticality: { + level: c.criticality, + }, + }; + return change; + }), + }; + }); + + if (changes.length === 20) { + nextCursor = { + // Keep the created at because we want the same set of checks when joining on the "latest". + createdAt: nextCursor?.createdAt ?? changes[0]?.createdAt ?? now, + id: changes[changes.length - 1].id, + }; + } else { + nextCursor = null; + } + + for (const change of changes) { + const service = services.find(s => change.serviceName === s.serviceName); + if (service) { + const ast = parse(service.sdl, { noLocation: true }); + service.sdl = print( + patch(ast, change.schemaProposalChanges, { onError: errors.looseErrorHandler }), + ); + if (change.serviceUrl) { + service.serviceUrl = change.serviceUrl; + } + } + } + } while (nextCursor && ++i < maxLoops); + + return services; + }, + }; +} diff --git a/packages/services/workflows/src/logger.ts b/packages/services/workflows/src/logger.ts index 094c3f782c4..4a66dee026d 100644 --- a/packages/services/workflows/src/logger.ts +++ b/packages/services/workflows/src/logger.ts @@ -1,33 +1,5 @@ -import { Logger as GraphileLogger, type LogLevel as GraphileLogLevel } from 'graphile-worker'; -import type { Logger } from '@graphql-hive/logger'; -import { ServiceLogger } from '@hive/service-common'; - -function logLevel(level: GraphileLogLevel) { - switch (level) { - case 'warning': - return 'warn' as const; - case 'info': { - return 'info' as const; - } - case 'debug': { - return 'debug' as const; - } - case 'error': { - return 'error' as const; - } - } - - return 'info'; -} - -/** - * Bridges Hive Logger to Graphile Logger - */ -export function bridgeGraphileLogger(logger: Logger) { - return new GraphileLogger(_scope => (level, message, _meta) => { - logger[logLevel(level)](message); - }); -} +import type { Logger } from '@hive/pubsub'; +import type { ServiceLogger } from '@hive/service-common'; export function bridgeFastifyLogger(logger: Logger): ServiceLogger { return logger as unknown as ServiceLogger; diff --git a/packages/services/workflows/src/redis.ts b/packages/services/workflows/src/redis.ts new file mode 100644 index 00000000000..cb1a86034e3 --- /dev/null +++ b/packages/services/workflows/src/redis.ts @@ -0,0 +1,54 @@ +/** + * Nearly identical to @hive/api's redis definition. + * This is duplicated in case there are additional requirements for workflows. + **/ +import type { Redis as RedisInstance, RedisOptions } from 'ioredis'; +import Redis from 'ioredis'; +import type { Logger } from '@hive/pubsub'; + +export type { RedisInstance as Redis }; + +export type RedisConfig = Required> & { + tlsEnabled: boolean; +}; + +export function createRedisClient(label: string, config: RedisConfig, logger: Logger) { + const redis = new Redis({ + host: config.host, + port: config.port, + password: config.password, + retryStrategy(times) { + return Math.min(times * 500, 2000); + }, + reconnectOnError(error) { + logger.warn('Redis reconnectOnError (error=%s)', error); + return 1; + }, + db: 0, + maxRetriesPerRequest: null, + enableReadyCheck: false, + tls: config.tlsEnabled ? {} : undefined, + }); + + redis.on('error', err => { + logger.error('Redis connection error (error=%s,label=%s)', err, label); + }); + + redis.on('connect', () => { + logger.debug('Redis connection established (label=%s)', label); + }); + + redis.on('ready', () => { + logger.info('Redis connection ready (label=%s)', label); + }); + + redis.on('close', () => { + logger.info('Redis connection closed (label=%s)', label); + }); + + redis.on('reconnecting', (timeToReconnect?: number) => { + logger.info('Redis reconnecting in %s (label=%s)', timeToReconnect, label); + }); + + return redis; +} diff --git a/packages/services/workflows/src/tasks/schema-proposal-composition.ts b/packages/services/workflows/src/tasks/schema-proposal-composition.ts new file mode 100644 index 00000000000..99fb78c0f89 --- /dev/null +++ b/packages/services/workflows/src/tasks/schema-proposal-composition.ts @@ -0,0 +1,85 @@ +/** + * Runs schema composition as a job and then notifies the server when completed. + * + * There's a delay between when this is added and when it's ran on the off chance + * that more requests come in requesting composition for the proposal + */ + +import { z } from 'zod'; +import { defineTask, implementTask } from '../kit.js'; +import { createSchemaObject } from '../lib/schema/provider'; + +function extendWithBase(schemas: Array<{ sdl: string }>, baseSchema: string | null) { + if (!baseSchema) { + return schemas; + } + + return schemas.map((schema, index) => { + if (index === 0) { + return { + ...schema, + sdl: baseSchema + ' ' + schema.sdl, + }; + } + + return schema; + }); +} + +export const SchemaProposalCompositionTask = defineTask({ + name: 'schemaProposalComposition', + schema: z.object({ + proposalId: z.string(), + targetId: z.string(), + externalComposition: z.object({ + enabled: z.boolean(), + endpoint: z.string().nullable().optional(), + encryptedSecret: z.string().nullable().optional(), + }), + native: z.boolean(), + }), +}); + +export const task = implementTask(SchemaProposalCompositionTask, async args => { + const schemas = await args.context.schema.proposedSchemas({ + pool: args.context.pg, + proposalId: args.input.proposalId, + targetId: args.input.targetId, + }); + + const baseSchema = await args.context.schema.getBaseSchema({ + pool: args.context.pg, + targetId: args.input.targetId, + }); + + try { + const result = await args.context.schema.composeAndValidate( + schemas[0].type, + extendWithBase(schemas, baseSchema).map(s => createSchemaObject(s)), + { + /** Whether external composition should be used (only Federation) */ + external: args.input.externalComposition, + /** Whether native composition should be used (only Federation) */ + native: args.input.native, + /** Proposals currently ignore contracts. */ + contracts: null, + }, + ); + + const payload: { timestamp: string; status: 'error' | 'success'; reason: string | null } = { + timestamp: new Date().toISOString(), + status: result.errors.length ? 'error' : 'success', + reason: result.errors.length ? result.errors.map(e => e.message).join('\n') : null, + }; + await args.context.schema.updateSchemaProposalComposition({ + ...payload, + proposalId: args.input.proposalId, + pool: args.context.pg, + }); + args.context.pubSub.publish('schemaProposalComposition', args.input.proposalId, payload); + } catch (e) { + // if the internal error persists, then this will be ran multiple times. + args.logger.error('Something went wrong. %s', (e as Error).message); + throw e; + } +}); diff --git a/packages/web/app/package.json b/packages/web/app/package.json index 07861247080..8fe6de952cc 100644 --- a/packages/web/app/package.json +++ b/packages/web/app/package.json @@ -25,7 +25,7 @@ "@graphiql/toolkit": "0.9.1", "@graphql-codegen/client-preset-swc-plugin": "0.2.0", "@graphql-inspector/core": "7.1.2", - "@graphql-inspector/patch": "0.1.2", + "@graphql-inspector/patch": "0.1.3-alpha-20260225183305-b1e68e3f363401fe16845d7f731ff8ed3467f5a7", "@graphql-tools/mock": "9.0.25", "@graphql-typed-document-node/core": "3.2.0", "@headlessui/react": "2.2.0", diff --git a/packages/web/app/src/components/target/proposals/service-heading.tsx b/packages/web/app/src/components/target/proposals/service-heading.tsx index 7e8b5f996da..dd64829a146 100644 --- a/packages/web/app/src/components/target/proposals/service-heading.tsx +++ b/packages/web/app/src/components/target/proposals/service-heading.tsx @@ -1,6 +1,6 @@ -import type { MouseEventHandler } from 'react'; +import { useState, type MouseEventHandler } from 'react'; import { cn } from '@/lib/utils'; -import { CubeIcon } from '@radix-ui/react-icons'; +import { ChevronDownIcon, CubeIcon } from '@radix-ui/react-icons'; export enum ServiceHeadingType { NEW, @@ -11,26 +11,40 @@ export function ServiceHeading(props: { serviceName: string; type?: ServiceHeadingType; onClick?: MouseEventHandler; + showToggleIcon?: boolean; }) { + const [isOpen, setIsOpen] = useState(true); if (props.serviceName.length === 0) { return null; } + const showToggleIcon = props.showToggleIcon ?? props.onClick !== undefined; return (
{ + props.onClick?.(e); + setIsOpen(!isOpen); + }} > - - {props.serviceName} - {props.type === ServiceHeadingType.NEW ? ( - *NEW* - ) : null} - {props.type === ServiceHeadingType.DELETED ? ( - *DELETED* - ) : null} +
+ + {props.serviceName} + {props.type === ServiceHeadingType.NEW ? ( + *NEW* + ) : null} + {props.type === ServiceHeadingType.DELETED ? ( + *DELETED* + ) : null} +
+ {showToggleIcon && ( +
+ +
+ )}
); } diff --git a/packages/web/app/src/components/target/proposals/stage-transition-select.tsx b/packages/web/app/src/components/target/proposals/stage-transition-select.tsx index 9f08d468fcc..61e4edef902 100644 --- a/packages/web/app/src/components/target/proposals/stage-transition-select.tsx +++ b/packages/web/app/src/components/target/proposals/stage-transition-select.tsx @@ -57,18 +57,19 @@ const STAGE_TITLES = { export function StageTransitionSelect(props: { stage: SchemaProposalStage; onSelect: (stage: SchemaProposalStage) => void | Promise; + className?: string; }) { const [open, setOpen] = useState(false); return ( diff --git a/packages/web/app/src/pages/target-proposal-checks.tsx b/packages/web/app/src/pages/target-proposal-checks.tsx index d6f76decd47..a692ba23ac3 100644 --- a/packages/web/app/src/pages/target-proposal-checks.tsx +++ b/packages/web/app/src/pages/target-proposal-checks.tsx @@ -38,7 +38,7 @@ export function TargetProposalChecksPage(props: { }) { const checks = useFragment(ProposalOverview_ChecksFragment, props.checks); return ( -
+
{checks?.edges?.map(({ node }, index) => { return ( [0] services={services ?? []} reviews={proposal.reviews ?? {}} checks={proposal.checks ?? null} - versions={proposal.versions ?? null} isDistributedGraph={isDistributedGraph} proposal={proposal} target={query.data.target} @@ -432,10 +428,42 @@ const ProposalsContent = (props: Parameters[0] ) : ( proposal && ( <> -
- -
+
+ {/* */} + + <div className="truncate">{proposal.title}</div> + <TooltipProvider delayDuration={0}> + <Tooltip> + <TooltipTrigger> + {proposal?.compositionStatus === 'error' ? ( + <XIcon className="text-red-600" /> + ) : null} + {proposal?.compositionStatus === 'success' ? ( + <CheckIcon className="text-emerald-500" /> + ) : null} + </TooltipTrigger> + <TooltipContent align="start"> + {proposal?.compositionStatus === 'error' ? ( + <> + Composition Error{' '} + {proposal.compositionTimestamp ? ( + <> + (<TimeAgo date={proposal.compositionTimestamp} />) + </> + ) : null} + {proposal.compositionStatusReason + ?.split('\n') + .map((e, i) => <div key={i}>- {e}</div>) ?? 'Unknown cause.'}{' '} + </> + ) : null} + {proposal?.compositionStatus === 'success' ? 'Composes Successfully' : null} + </TooltipContent> + </Tooltip> + </TooltipProvider> + +
{ const _review = await reviewSchemaProposal({ @@ -452,12 +480,11 @@ const ProposalsContent = (props: Parameters[0] />
-
- {proposal.title} -
+
+
{proposal.description}
+
proposed by {proposal.author}
-
{proposal.description}
) @@ -478,7 +505,6 @@ function TabbedContent(props: { services: ServiceProposalDetails[]; reviews: FragmentType; checks: FragmentType | null; - versions: FragmentType | null; proposal: FragmentType; target: FragmentType; me: FragmentType | null; @@ -486,7 +512,7 @@ function TabbedContent(props: { }) { return ( - + { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 63901d65d4f..60bb6be85fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -141,8 +141,8 @@ importers: specifier: 7.1.2 version: 7.1.2(graphql@16.9.0) '@graphql-inspector/patch': - specifier: 0.1.2 - version: 0.1.2(graphql@16.9.0) + specifier: 0.1.3-alpha-20260225183305-b1e68e3f363401fe16845d7f731ff8ed3467f5a7 + version: 0.1.3-alpha-20260225183305-b1e68e3f363401fe16845d7f731ff8ed3467f5a7(graphql@16.9.0) '@graphql-tools/load': specifier: 8.1.2 version: 8.1.2(graphql@16.9.0) @@ -384,6 +384,9 @@ importers: graphql: specifier: 16.9.0 version: 16.9.0 + graphql-sse: + specifier: 2.6.0 + version: 2.6.0(graphql@16.9.0) human-id: specifier: 4.1.1 version: 4.1.1 @@ -627,6 +630,28 @@ importers: version: 16.9.0 publishDirectory: dist + packages/libraries/pubsub: + dependencies: + '@graphql-yoga/redis-event-target': + specifier: 3.0.3 + version: 3.0.3(ioredis@5.8.2) + graphile-worker: + specifier: ^0.16.0 + version: 0.16.6(typescript@5.7.3) + graphql-yoga: + specifier: 5.13.3 + version: 5.13.3(graphql@16.12.0) + ioredis: + specifier: ^5.0.0 + version: 5.8.2 + devDependencies: + tslib: + specifier: 2.8.1 + version: 2.8.1 + vitest: + specifier: 4.0.9 + version: 4.0.9(@types/debug@4.1.12)(@types/node@25.0.2)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(msw@2.12.7(@types/node@25.0.2)(typescript@5.7.3))(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) + packages/libraries/router: {} packages/libraries/yoga: @@ -769,6 +794,9 @@ importers: '@hive/cdn-script': specifier: workspace:* version: link:../cdn-worker + '@hive/pubsub': + specifier: workspace:* + version: link:../../libraries/pubsub '@hive/schema': specifier: workspace:* version: link:../schema @@ -859,6 +887,9 @@ importers: got: specifier: 14.4.7 version: 14.4.7(patch_hash=f7660444905ddadee251ff98241119fb54f5fec1e673a428192da361d5636299) + graphile-worker: + specifier: 0.16.6 + version: 0.16.6(typescript@5.7.3) graphql: specifier: 16.9.0 version: 16.9.0 @@ -1268,6 +1299,9 @@ importers: '@hive/cdn-script': specifier: workspace:* version: link:../cdn-worker + '@hive/pubsub': + specifier: workspace:* + version: link:../../libraries/pubsub '@hive/schema': specifier: workspace:* version: link:../schema @@ -1650,6 +1684,18 @@ importers: '@graphql-hive/logger': specifier: 1.0.9 version: 1.0.9(pino@10.3.0) + '@graphql-inspector/core': + specifier: 7.1.2 + version: 7.1.2(graphql@16.9.0) + '@graphql-inspector/patch': + specifier: 0.1.3-alpha-20260225183305-b1e68e3f363401fe16845d7f731ff8ed3467f5a7 + version: 0.1.3-alpha-20260225183305-b1e68e3f363401fe16845d7f731ff8ed3467f5a7(graphql@16.9.0) + '@graphql-yoga/redis-event-target': + specifier: 3.0.3 + version: 3.0.3(ioredis@5.8.2) + '@hive/pubsub': + specifier: workspace:* + version: link:../../libraries/pubsub '@hive/service-common': specifier: workspace:* version: link:../service-common @@ -1659,6 +1705,9 @@ importers: '@sentry/node': specifier: 7.120.2 version: 7.120.2 + '@trpc/client': + specifier: 10.45.3 + version: 10.45.3(@trpc/server@10.45.3) '@types/mjml': specifier: 4.7.1 version: 4.7.1 @@ -1677,6 +1726,15 @@ importers: graphile-worker: specifier: 0.16.6 version: 0.16.6(typescript@5.7.3) + graphql: + specifier: 16.9.0 + version: 16.9.0 + graphql-yoga: + specifier: 5.13.3 + version: 5.13.3(graphql@16.9.0) + ioredis: + specifier: 5.8.2 + version: 5.8.2 mjml: specifier: 4.14.0 version: 4.14.0(encoding@0.1.13) @@ -1738,8 +1796,8 @@ importers: specifier: 7.1.2 version: 7.1.2(graphql@16.9.0) '@graphql-inspector/patch': - specifier: 0.1.2 - version: 0.1.2(graphql@16.9.0) + specifier: 0.1.3-alpha-20260225183305-b1e68e3f363401fe16845d7f731ff8ed3467f5a7 + version: 0.1.3-alpha-20260225183305-b1e68e3f363401fe16845d7f731ff8ed3467f5a7(graphql@16.9.0) '@graphql-tools/mock': specifier: 9.0.25 version: 9.0.25(graphql@16.9.0) @@ -4390,8 +4448,8 @@ packages: resolution: {integrity: sha512-rEo+HoQt+qjdayy7p5vcR9GeGTdKXmN0LbIm3W+jKKoXeAMlV4zHxnOW6jEhO6E0eVQxf8Sc1TlcH78i2P2a9w==} engines: {node: '>=18.0.0'} - '@graphql-inspector/patch@0.1.2': - resolution: {integrity: sha512-iSuRozeQmWm4cX3iNA19+1ADDLdN+qyptkfedKcm1WbGaT7UDDdbh7/RyfAeomVagnhbBKFLLsf3S7I9jV5Iew==} + '@graphql-inspector/patch@0.1.3-alpha-20260225183305-b1e68e3f363401fe16845d7f731ff8ed3467f5a7': + resolution: {integrity: sha512-r+sW5w1Gu8njCKZd0sR4Z4oXXJ2mvoeJuufZM78iAdu68JxGSBvAuKE9wIlZxp+1Q2GQcAQ9EpB/ofgj/vnojg==} engines: {node: '>=18.0.0'} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 @@ -4780,12 +4838,6 @@ packages: peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - '@graphql-tools/executor@1.4.7': - resolution: {integrity: sha512-U0nK9jzJRP9/9Izf1+0Gggd6K6RNRsheFo1gC/VWzfnsr0qjcOSS9qTjY0OTC5iTPt4tQ+W5Zpw/uc7mebI6aA==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - '@graphql-tools/executor@1.4.9': resolution: {integrity: sha512-SAUlDT70JAvXeqV87gGzvDzUGofn39nvaVcVhNf12Dt+GfWHtNNO/RCn/Ea4VJaSLGzraUd41ObnN3i80EBU7w==} engines: {node: '>=16.0.0'} @@ -5263,10 +5315,6 @@ packages: peerDependencies: graphql-yoga: ^5.16.2 - '@graphql-yoga/subscription@5.0.4': - resolution: {integrity: sha512-Bcj1LYVQyQmFN/vsl73TRSNistd8lBJJcPxqFVCT8diUuH/gTv2be2OYZsirpeO0l5tFjJWJv9RPgIlCYv3Khw==} - engines: {node: '>=18.0.0'} - '@graphql-yoga/subscription@5.0.5': resolution: {integrity: sha512-oCMWOqFs6QV96/NZRt/ZhTQvzjkGB4YohBOpKM4jH/lDT4qb7Lex/aGCxpi/JD9njw3zBBtMqxbaC22+tFHVvw==} engines: {node: '>=18.0.0'} @@ -9646,9 +9694,6 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} - '@types/debug@4.1.7': - resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} - '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -10170,10 +10215,6 @@ packages: resolution: {integrity: sha512-QxI+HQfJeI/UscFNCTcSri6nrHP25mtyAMbhEri7W2ctdb3EsorPuJz7IovSgNjvKVs73dg9Fmayewx1O2xOxA==} engines: {node: '>=18.0.0'} - '@whatwg-node/server@0.10.3': - resolution: {integrity: sha512-2Dnfey57vWR+hDUMjPhNzJQc9z116BBzSQwR9eD0vhnzYmN2rJXuY0QuMaHDCCqEZRmFHg2bo8iJ+/1uHOlxpg==} - engines: {node: '>=18.0.0'} - '@whatwg-node/server@0.9.65': resolution: {integrity: sha512-CnYTFEUJkbbAcuBXnXirVIgKBfs2YA6sSGjxeq07AUiyXuoQ0fbvTIQoteMglmn09QeGzcH/l0B7nIml83xvVw==} engines: {node: '>=18.0.0'} @@ -13131,6 +13172,12 @@ packages: peerDependencies: graphql: '>=0.11 <=16' + graphql-sse@2.6.0: + resolution: {integrity: sha512-BXT5Rjv9UFunjQsmN9WWEIq+TFNhgYibgwo1xkXLxzguQVyOd6paJ4v5DlL9K5QplS0w74bhF+aUiqaGXZBaug==} + engines: {node: '>=12'} + peerDependencies: + graphql: '>=0.11 <=16' + graphql-tag@2.12.6: resolution: {integrity: sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==} engines: {node: '>=10'} @@ -18718,46 +18765,6 @@ packages: terser: optional: true - vite@7.1.11: - resolution: {integrity: sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - jiti: '>=1.21.0' - less: ^4.0.0 - lightningcss: ^1.21.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - vite@7.3.1: resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -21813,7 +21820,7 @@ snapshots: dependencies: '@envelop/core': 5.5.1 '@envelop/extended-validation': 7.0.0(@envelop/core@5.5.1)(graphql@16.12.0) - '@graphql-tools/executor': 1.4.9(graphql@16.12.0) + '@graphql-tools/executor': 1.5.0(graphql@16.12.0) '@graphql-tools/utils': 10.9.1(graphql@16.12.0) '@whatwg-node/promise-helpers': 1.3.2 graphql: 16.12.0 @@ -21823,7 +21830,7 @@ snapshots: dependencies: '@envelop/core': 5.5.1 '@envelop/extended-validation': 7.0.0(@envelop/core@5.5.1)(graphql@16.9.0) - '@graphql-tools/executor': 1.4.9(graphql@16.9.0) + '@graphql-tools/executor': 1.5.0(graphql@16.9.0) '@graphql-tools/utils': 10.9.1(graphql@16.9.0) '@whatwg-node/promise-helpers': 1.3.2 graphql: 16.9.0 @@ -22049,7 +22056,7 @@ snapshots: '@escape.tech/graphql-armor-block-field-suggestions@3.0.1': dependencies: - graphql: 16.11.0 + graphql: 16.12.0 optionalDependencies: '@envelop/core': 5.5.1 @@ -22670,7 +22677,7 @@ snapshots: '@graphql-hive/logger': 1.0.9(pino@10.3.0) '@graphql-hive/pubsub': 2.1.1(ioredis@5.8.2) '@graphql-hive/signal': 2.0.0 - '@graphql-hive/yoga': 0.42.5(graphql-yoga@5.16.2(graphql@16.12.0))(graphql@16.12.0) + '@graphql-hive/yoga': 0.42.5(graphql-yoga@5.17.1(graphql@16.12.0))(graphql@16.12.0) '@graphql-mesh/cross-helpers': 0.4.10(graphql@16.12.0) '@graphql-mesh/fusion-runtime': 1.5.1(@types/node@24.10.1)(graphql@16.12.0)(ioredis@5.8.2)(pino@10.3.0) '@graphql-mesh/hmac-upstream-signature': 2.0.8(graphql@16.12.0)(ioredis@5.8.2) @@ -22686,10 +22693,10 @@ snapshots: '@graphql-tools/stitch': 10.1.3(graphql@16.12.0) '@graphql-tools/utils': 10.10.3(graphql@16.12.0) '@graphql-tools/wrap': 11.0.5(graphql@16.12.0) - '@graphql-yoga/plugin-apollo-usage-report': 0.11.2(@envelop/core@5.5.1)(graphql-yoga@5.16.2(graphql@16.12.0))(graphql@16.12.0) - '@graphql-yoga/plugin-csrf-prevention': 3.16.2(graphql-yoga@5.16.2(graphql@16.12.0)) - '@graphql-yoga/plugin-defer-stream': 3.16.2(graphql-yoga@5.16.2(graphql@16.12.0))(graphql@16.12.0) - '@graphql-yoga/plugin-persisted-operations': 3.16.2(graphql-yoga@5.16.2(graphql@16.12.0))(graphql@16.12.0) + '@graphql-yoga/plugin-apollo-usage-report': 0.11.2(@envelop/core@5.5.1)(graphql-yoga@5.17.1(graphql@16.12.0))(graphql@16.12.0) + '@graphql-yoga/plugin-csrf-prevention': 3.16.2(graphql-yoga@5.17.1(graphql@16.12.0)) + '@graphql-yoga/plugin-defer-stream': 3.16.2(graphql-yoga@5.17.1(graphql@16.12.0))(graphql@16.12.0) + '@graphql-yoga/plugin-persisted-operations': 3.16.2(graphql-yoga@5.17.1(graphql@16.12.0))(graphql@16.12.0) '@types/node': 24.10.1 '@whatwg-node/disposablestack': 0.0.6 '@whatwg-node/promise-helpers': 1.3.2 @@ -22697,7 +22704,7 @@ snapshots: '@whatwg-node/server-plugin-cookies': 1.0.5 graphql: 16.12.0 graphql-ws: 6.0.6(graphql@16.12.0)(ws@8.18.0) - graphql-yoga: 5.16.2(graphql@16.12.0) + graphql-yoga: 5.17.1(graphql@16.12.0) tslib: 2.8.1 transitivePeerDependencies: - '@fastify/websocket' @@ -23085,12 +23092,12 @@ snapshots: '@graphql-hive/signal@2.0.0': {} - '@graphql-hive/yoga@0.42.5(graphql-yoga@5.16.2(graphql@16.12.0))(graphql@16.12.0)': + '@graphql-hive/yoga@0.42.5(graphql-yoga@5.17.1(graphql@16.12.0))(graphql@16.12.0)': dependencies: '@graphql-hive/core': 0.14.0(graphql@16.12.0) - '@graphql-yoga/plugin-persisted-operations': 3.16.2(graphql-yoga@5.16.2(graphql@16.12.0))(graphql@16.12.0) + '@graphql-yoga/plugin-persisted-operations': 3.16.2(graphql-yoga@5.17.1(graphql@16.12.0))(graphql@16.12.0) graphql: 16.12.0 - graphql-yoga: 5.16.2(graphql@16.12.0) + graphql-yoga: 5.17.1(graphql@16.12.0) '@graphql-hive/yoga@0.46.0(graphql-yoga@5.17.1(graphql@16.12.0))(graphql@16.12.0)(pino@10.3.0)': dependencies: @@ -23292,7 +23299,7 @@ snapshots: std-env: 3.7.0 tslib: 2.6.2 - '@graphql-inspector/patch@0.1.2(graphql@16.9.0)': + '@graphql-inspector/patch@0.1.3-alpha-20260225183305-b1e68e3f363401fe16845d7f731ff8ed3467f5a7(graphql@16.9.0)': dependencies: '@graphql-tools/utils': 10.9.1(graphql@16.9.0) graphql: 16.9.0 @@ -23432,7 +23439,7 @@ snapshots: '@graphql-mesh/utils': 0.104.16(graphql@16.12.0)(ioredis@5.8.2) '@graphql-tools/batch-execute': 10.0.4(graphql@16.12.0) '@graphql-tools/delegate': 11.1.3(graphql@16.12.0) - '@graphql-tools/executor': 1.4.13(graphql@16.12.0) + '@graphql-tools/executor': 1.5.0(graphql@16.12.0) '@graphql-tools/federation': 4.2.3(@types/node@22.10.5)(graphql@16.12.0) '@graphql-tools/merge': 9.1.5(graphql@16.12.0) '@graphql-tools/stitch': 10.1.3(graphql@16.12.0) @@ -23442,7 +23449,7 @@ snapshots: '@whatwg-node/disposablestack': 0.0.6 '@whatwg-node/promise-helpers': 1.3.2 graphql: 16.12.0 - graphql-yoga: 5.16.2(graphql@16.12.0) + graphql-yoga: 5.17.1(graphql@16.12.0) tslib: 2.8.1 transitivePeerDependencies: - '@logtape/logtape' @@ -23463,7 +23470,7 @@ snapshots: '@graphql-mesh/utils': 0.104.16(graphql@16.12.0)(ioredis@5.8.2) '@graphql-tools/batch-execute': 10.0.4(graphql@16.12.0) '@graphql-tools/delegate': 11.1.3(graphql@16.12.0) - '@graphql-tools/executor': 1.4.13(graphql@16.12.0) + '@graphql-tools/executor': 1.5.0(graphql@16.12.0) '@graphql-tools/federation': 4.2.3(@types/node@24.10.1)(graphql@16.12.0) '@graphql-tools/merge': 9.1.5(graphql@16.12.0) '@graphql-tools/stitch': 10.1.3(graphql@16.12.0) @@ -23473,7 +23480,7 @@ snapshots: '@whatwg-node/disposablestack': 0.0.6 '@whatwg-node/promise-helpers': 1.3.2 graphql: 16.12.0 - graphql-yoga: 5.16.2(graphql@16.12.0) + graphql-yoga: 5.17.1(graphql@16.12.0) tslib: 2.8.1 transitivePeerDependencies: - '@logtape/logtape' @@ -23494,7 +23501,7 @@ snapshots: '@graphql-mesh/utils': 0.104.16(graphql@16.12.0)(ioredis@5.8.2) '@graphql-tools/batch-execute': 10.0.4(graphql@16.12.0) '@graphql-tools/delegate': 12.0.2(graphql@16.12.0) - '@graphql-tools/executor': 1.4.13(graphql@16.12.0) + '@graphql-tools/executor': 1.5.0(graphql@16.12.0) '@graphql-tools/federation': 4.2.6(@types/node@25.0.2)(graphql@16.12.0) '@graphql-tools/merge': 9.1.5(graphql@16.12.0) '@graphql-tools/stitch': 10.1.6(graphql@16.12.0) @@ -23525,7 +23532,7 @@ snapshots: '@graphql-mesh/utils': 0.104.16(graphql@16.12.0) '@graphql-tools/batch-execute': 10.0.4(graphql@16.12.0) '@graphql-tools/delegate': 12.0.2(graphql@16.12.0) - '@graphql-tools/executor': 1.4.13(graphql@16.12.0) + '@graphql-tools/executor': 1.5.0(graphql@16.12.0) '@graphql-tools/federation': 4.2.6(@types/node@25.0.2)(graphql@16.12.0) '@graphql-tools/merge': 9.1.5(graphql@16.12.0) '@graphql-tools/stitch': 10.1.6(graphql@16.12.0) @@ -23556,7 +23563,7 @@ snapshots: '@graphql-mesh/utils': 0.104.16(graphql@16.9.0)(ioredis@5.8.2) '@graphql-tools/batch-execute': 10.0.4(graphql@16.9.0) '@graphql-tools/delegate': 12.0.2(graphql@16.9.0) - '@graphql-tools/executor': 1.4.13(graphql@16.9.0) + '@graphql-tools/executor': 1.5.0(graphql@16.9.0) '@graphql-tools/federation': 4.2.6(@types/node@25.0.2)(graphql@16.9.0) '@graphql-tools/merge': 9.1.5(graphql@16.9.0) '@graphql-tools/stitch': 10.1.6(graphql@16.9.0) @@ -23709,11 +23716,11 @@ snapshots: '@graphql-mesh/types': 0.104.16(graphql@16.12.0)(ioredis@5.8.2) '@graphql-mesh/utils': 0.104.16(graphql@16.12.0) '@graphql-tools/utils': 10.9.1(graphql@16.12.0) - '@graphql-yoga/plugin-response-cache': 3.15.4(graphql-yoga@5.16.2(graphql@16.12.0))(graphql@16.12.0) + '@graphql-yoga/plugin-response-cache': 3.15.4(graphql-yoga@5.17.1(graphql@16.12.0))(graphql@16.12.0) '@whatwg-node/promise-helpers': 1.3.2 cache-control-parser: 2.0.6 graphql: 16.12.0 - graphql-yoga: 5.16.2(graphql@16.12.0) + graphql-yoga: 5.17.1(graphql@16.12.0) tslib: 2.8.1 transitivePeerDependencies: - '@nats-io/nats-core' @@ -23728,11 +23735,11 @@ snapshots: '@graphql-mesh/types': 0.104.16(graphql@16.12.0)(ioredis@5.8.2) '@graphql-mesh/utils': 0.104.16(graphql@16.12.0)(ioredis@5.8.2) '@graphql-tools/utils': 10.9.1(graphql@16.12.0) - '@graphql-yoga/plugin-response-cache': 3.15.4(graphql-yoga@5.16.2(graphql@16.12.0))(graphql@16.12.0) + '@graphql-yoga/plugin-response-cache': 3.15.4(graphql-yoga@5.17.1(graphql@16.12.0))(graphql@16.12.0) '@whatwg-node/promise-helpers': 1.3.2 cache-control-parser: 2.0.6 graphql: 16.12.0 - graphql-yoga: 5.16.2(graphql@16.12.0) + graphql-yoga: 5.17.1(graphql@16.12.0) tslib: 2.8.1 transitivePeerDependencies: - '@nats-io/nats-core' @@ -23747,11 +23754,11 @@ snapshots: '@graphql-mesh/types': 0.104.16(graphql@16.9.0)(ioredis@5.8.2) '@graphql-mesh/utils': 0.104.16(graphql@16.9.0)(ioredis@5.8.2) '@graphql-tools/utils': 10.9.1(graphql@16.9.0) - '@graphql-yoga/plugin-response-cache': 3.15.4(graphql-yoga@5.16.2(graphql@16.9.0))(graphql@16.9.0) + '@graphql-yoga/plugin-response-cache': 3.15.4(graphql-yoga@5.17.1(graphql@16.9.0))(graphql@16.9.0) '@whatwg-node/promise-helpers': 1.3.2 cache-control-parser: 2.0.6 graphql: 16.9.0 - graphql-yoga: 5.16.2(graphql@16.9.0) + graphql-yoga: 5.17.1(graphql@16.9.0) tslib: 2.8.1 transitivePeerDependencies: - '@nats-io/nats-core' @@ -23796,7 +23803,7 @@ snapshots: '@graphql-mesh/types': 0.104.16(graphql@16.12.0)(ioredis@5.8.2) '@graphql-tools/executor': 1.4.13(graphql@16.12.0) '@graphql-tools/executor-common': 1.0.5(graphql@16.12.0) - '@graphql-tools/utils': 10.10.3(graphql@16.12.0) + '@graphql-tools/utils': 10.11.0(graphql@16.12.0) graphql: 16.12.0 tslib: 2.8.1 transitivePeerDependencies: @@ -23815,7 +23822,7 @@ snapshots: '@graphql-mesh/types': 0.104.16(graphql@16.12.0)(ioredis@5.8.2) '@graphql-tools/executor': 1.4.13(graphql@16.12.0) '@graphql-tools/executor-common': 1.0.5(graphql@16.12.0) - '@graphql-tools/utils': 10.10.3(graphql@16.12.0) + '@graphql-tools/utils': 10.11.0(graphql@16.12.0) graphql: 16.12.0 tslib: 2.8.1 transitivePeerDependencies: @@ -23834,7 +23841,7 @@ snapshots: '@graphql-mesh/types': 0.104.16(graphql@16.9.0)(ioredis@5.8.2) '@graphql-tools/executor': 1.4.13(graphql@16.9.0) '@graphql-tools/executor-common': 1.0.5(graphql@16.9.0) - '@graphql-tools/utils': 10.10.3(graphql@16.9.0) + '@graphql-tools/utils': 10.11.0(graphql@16.9.0) graphql: 16.9.0 tslib: 2.8.1 transitivePeerDependencies: @@ -24190,7 +24197,7 @@ snapshots: '@graphql-tools/delegate@11.1.3(graphql@16.12.0)': dependencies: '@graphql-tools/batch-execute': 10.0.4(graphql@16.12.0) - '@graphql-tools/executor': 1.4.13(graphql@16.12.0) + '@graphql-tools/executor': 1.5.0(graphql@16.12.0) '@graphql-tools/schema': 10.0.29(graphql@16.12.0) '@graphql-tools/utils': 10.11.0(graphql@16.12.0) '@repeaterjs/repeater': 3.0.6 @@ -24202,7 +24209,7 @@ snapshots: '@graphql-tools/delegate@11.1.3(graphql@16.9.0)': dependencies: '@graphql-tools/batch-execute': 10.0.4(graphql@16.9.0) - '@graphql-tools/executor': 1.4.13(graphql@16.9.0) + '@graphql-tools/executor': 1.5.0(graphql@16.9.0) '@graphql-tools/schema': 10.0.29(graphql@16.9.0) '@graphql-tools/utils': 10.11.0(graphql@16.9.0) '@repeaterjs/repeater': 3.0.6 @@ -24214,7 +24221,7 @@ snapshots: '@graphql-tools/delegate@12.0.2(graphql@16.12.0)': dependencies: '@graphql-tools/batch-execute': 10.0.4(graphql@16.12.0) - '@graphql-tools/executor': 1.4.13(graphql@16.12.0) + '@graphql-tools/executor': 1.5.0(graphql@16.12.0) '@graphql-tools/schema': 10.0.29(graphql@16.12.0) '@graphql-tools/utils': 10.11.0(graphql@16.12.0) '@repeaterjs/repeater': 3.0.6 @@ -24226,7 +24233,7 @@ snapshots: '@graphql-tools/delegate@12.0.2(graphql@16.9.0)': dependencies: '@graphql-tools/batch-execute': 10.0.4(graphql@16.9.0) - '@graphql-tools/executor': 1.4.13(graphql@16.9.0) + '@graphql-tools/executor': 1.5.0(graphql@16.9.0) '@graphql-tools/schema': 10.0.29(graphql@16.9.0) '@graphql-tools/utils': 10.11.0(graphql@16.9.0) '@repeaterjs/repeater': 3.0.6 @@ -24550,16 +24557,6 @@ snapshots: graphql: 16.9.0 tslib: 2.8.1 - '@graphql-tools/executor@1.4.7(graphql@16.9.0)': - dependencies: - '@graphql-tools/utils': 10.9.1(graphql@16.9.0) - '@graphql-typed-document-node/core': 3.2.0(graphql@16.9.0) - '@repeaterjs/repeater': 3.0.6 - '@whatwg-node/disposablestack': 0.0.6 - '@whatwg-node/promise-helpers': 1.3.2 - graphql: 16.9.0 - tslib: 2.8.1 - '@graphql-tools/executor@1.4.9(graphql@16.12.0)': dependencies: '@graphql-tools/utils': 10.9.1(graphql@16.12.0) @@ -24603,7 +24600,7 @@ snapshots: '@graphql-tools/federation@4.2.3(@types/node@22.10.5)(graphql@16.12.0)': dependencies: '@graphql-tools/delegate': 11.1.3(graphql@16.12.0) - '@graphql-tools/executor': 1.4.13(graphql@16.12.0) + '@graphql-tools/executor': 1.5.0(graphql@16.12.0) '@graphql-tools/executor-http': 3.0.7(@types/node@22.10.5)(graphql@16.12.0) '@graphql-tools/merge': 9.1.5(graphql@16.12.0) '@graphql-tools/schema': 10.0.29(graphql@16.12.0) @@ -24623,7 +24620,7 @@ snapshots: '@graphql-tools/federation@4.2.3(@types/node@24.10.1)(graphql@16.12.0)': dependencies: '@graphql-tools/delegate': 11.1.3(graphql@16.12.0) - '@graphql-tools/executor': 1.4.13(graphql@16.12.0) + '@graphql-tools/executor': 1.5.0(graphql@16.12.0) '@graphql-tools/executor-http': 3.0.7(@types/node@24.10.1)(graphql@16.12.0) '@graphql-tools/merge': 9.1.5(graphql@16.12.0) '@graphql-tools/schema': 10.0.29(graphql@16.12.0) @@ -24643,7 +24640,7 @@ snapshots: '@graphql-tools/federation@4.2.6(@types/node@25.0.2)(graphql@16.12.0)': dependencies: '@graphql-tools/delegate': 12.0.2(graphql@16.12.0) - '@graphql-tools/executor': 1.4.13(graphql@16.12.0) + '@graphql-tools/executor': 1.5.0(graphql@16.12.0) '@graphql-tools/executor-http': 3.0.7(@types/node@25.0.2)(graphql@16.12.0) '@graphql-tools/merge': 9.1.5(graphql@16.12.0) '@graphql-tools/schema': 10.0.29(graphql@16.12.0) @@ -24663,7 +24660,7 @@ snapshots: '@graphql-tools/federation@4.2.6(@types/node@25.0.2)(graphql@16.9.0)': dependencies: '@graphql-tools/delegate': 12.0.2(graphql@16.9.0) - '@graphql-tools/executor': 1.4.13(graphql@16.9.0) + '@graphql-tools/executor': 1.5.0(graphql@16.9.0) '@graphql-tools/executor-http': 3.0.7(@types/node@25.0.2)(graphql@16.9.0) '@graphql-tools/merge': 9.1.5(graphql@16.9.0) '@graphql-tools/schema': 10.0.29(graphql@16.9.0) @@ -25056,7 +25053,7 @@ snapshots: dependencies: '@graphql-tools/batch-delegate': 10.0.5(graphql@16.12.0) '@graphql-tools/delegate': 11.1.3(graphql@16.12.0) - '@graphql-tools/executor': 1.4.13(graphql@16.12.0) + '@graphql-tools/executor': 1.5.0(graphql@16.12.0) '@graphql-tools/merge': 9.1.5(graphql@16.12.0) '@graphql-tools/schema': 10.0.29(graphql@16.12.0) '@graphql-tools/utils': 10.11.0(graphql@16.12.0) @@ -25069,7 +25066,7 @@ snapshots: dependencies: '@graphql-tools/batch-delegate': 10.0.8(graphql@16.12.0) '@graphql-tools/delegate': 12.0.2(graphql@16.12.0) - '@graphql-tools/executor': 1.4.13(graphql@16.12.0) + '@graphql-tools/executor': 1.5.0(graphql@16.12.0) '@graphql-tools/merge': 9.1.5(graphql@16.12.0) '@graphql-tools/schema': 10.0.29(graphql@16.12.0) '@graphql-tools/utils': 10.11.0(graphql@16.12.0) @@ -25082,7 +25079,7 @@ snapshots: dependencies: '@graphql-tools/batch-delegate': 10.0.8(graphql@16.9.0) '@graphql-tools/delegate': 12.0.2(graphql@16.9.0) - '@graphql-tools/executor': 1.4.13(graphql@16.9.0) + '@graphql-tools/executor': 1.5.0(graphql@16.9.0) '@graphql-tools/merge': 9.1.5(graphql@16.9.0) '@graphql-tools/schema': 10.0.29(graphql@16.9.0) '@graphql-tools/utils': 10.11.0(graphql@16.9.0) @@ -25391,12 +25388,12 @@ snapshots: dependencies: tslib: 2.8.1 - '@graphql-yoga/plugin-apollo-inline-trace@3.16.2(@envelop/core@5.5.1)(graphql-yoga@5.16.2(graphql@16.12.0))(graphql@16.12.0)': + '@graphql-yoga/plugin-apollo-inline-trace@3.16.2(@envelop/core@5.5.1)(graphql-yoga@5.17.1(graphql@16.12.0))(graphql@16.12.0)': dependencies: '@apollo/usage-reporting-protobuf': 4.1.1 '@envelop/on-resolve': 7.0.0(@envelop/core@5.5.1)(graphql@16.12.0) graphql: 16.12.0 - graphql-yoga: 5.16.2(graphql@16.12.0) + graphql-yoga: 5.17.1(graphql@16.12.0) tslib: 2.8.1 transitivePeerDependencies: - '@envelop/core' @@ -25421,16 +25418,16 @@ snapshots: transitivePeerDependencies: - '@envelop/core' - '@graphql-yoga/plugin-apollo-usage-report@0.11.2(@envelop/core@5.5.1)(graphql-yoga@5.16.2(graphql@16.12.0))(graphql@16.12.0)': + '@graphql-yoga/plugin-apollo-usage-report@0.11.2(@envelop/core@5.5.1)(graphql-yoga@5.17.1(graphql@16.12.0))(graphql@16.12.0)': dependencies: '@apollo/server-gateway-interface': 2.0.0(graphql@16.12.0) '@apollo/usage-reporting-protobuf': 4.1.1 '@apollo/utils.usagereporting': 2.1.0(graphql@16.12.0) '@graphql-tools/utils': 10.9.1(graphql@16.12.0) - '@graphql-yoga/plugin-apollo-inline-trace': 3.16.2(@envelop/core@5.5.1)(graphql-yoga@5.16.2(graphql@16.12.0))(graphql@16.12.0) + '@graphql-yoga/plugin-apollo-inline-trace': 3.16.2(@envelop/core@5.5.1)(graphql-yoga@5.17.1(graphql@16.12.0))(graphql@16.12.0) '@whatwg-node/promise-helpers': 1.3.2 graphql: 16.12.0 - graphql-yoga: 5.16.2(graphql@16.12.0) + graphql-yoga: 5.17.1(graphql@16.12.0) tslib: 2.8.1 transitivePeerDependencies: - '@envelop/core' @@ -25463,10 +25460,6 @@ snapshots: transitivePeerDependencies: - '@envelop/core' - '@graphql-yoga/plugin-csrf-prevention@3.16.2(graphql-yoga@5.16.2(graphql@16.12.0))': - dependencies: - graphql-yoga: 5.16.2(graphql@16.12.0) - '@graphql-yoga/plugin-csrf-prevention@3.16.2(graphql-yoga@5.17.1(graphql@16.12.0))': dependencies: graphql-yoga: 5.17.1(graphql@16.12.0) @@ -25475,12 +25468,6 @@ snapshots: dependencies: graphql-yoga: 5.17.1(graphql@16.9.0) - '@graphql-yoga/plugin-defer-stream@3.16.2(graphql-yoga@5.16.2(graphql@16.12.0))(graphql@16.12.0)': - dependencies: - '@graphql-tools/utils': 10.9.1(graphql@16.12.0) - graphql: 16.12.0 - graphql-yoga: 5.16.2(graphql@16.12.0) - '@graphql-yoga/plugin-defer-stream@3.16.2(graphql-yoga@5.17.1(graphql@16.12.0))(graphql@16.12.0)': dependencies: '@graphql-tools/utils': 10.9.1(graphql@16.12.0) @@ -25507,7 +25494,7 @@ snapshots: '@graphql-yoga/plugin-graphql-sse@3.7.0(graphql-yoga@5.13.3(graphql@16.9.0))(graphql@16.9.0)': dependencies: graphql: 16.9.0 - graphql-sse: 2.5.3(graphql@16.9.0) + graphql-sse: 2.6.0(graphql@16.9.0) graphql-yoga: 5.13.3(graphql@16.9.0) '@graphql-yoga/plugin-jwt@3.10.2(graphql-yoga@5.16.2(graphql@16.12.0))(graphql@16.12.0)': @@ -25522,12 +25509,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@graphql-yoga/plugin-persisted-operations@3.16.2(graphql-yoga@5.16.2(graphql@16.12.0))(graphql@16.12.0)': - dependencies: - '@whatwg-node/promise-helpers': 1.3.2 - graphql: 16.12.0 - graphql-yoga: 5.16.2(graphql@16.12.0) - '@graphql-yoga/plugin-persisted-operations@3.16.2(graphql-yoga@5.17.1(graphql@16.12.0))(graphql@16.12.0)': dependencies: '@whatwg-node/promise-helpers': 1.3.2 @@ -25563,21 +25544,21 @@ snapshots: graphql: 16.9.0 graphql-yoga: 5.13.3(graphql@16.9.0) - '@graphql-yoga/plugin-response-cache@3.15.4(graphql-yoga@5.16.2(graphql@16.12.0))(graphql@16.12.0)': + '@graphql-yoga/plugin-response-cache@3.15.4(graphql-yoga@5.17.1(graphql@16.12.0))(graphql@16.12.0)': dependencies: '@envelop/core': 5.5.1 '@envelop/response-cache': 7.1.3(@envelop/core@5.5.1)(graphql@16.12.0) '@whatwg-node/promise-helpers': 1.3.0 graphql: 16.12.0 - graphql-yoga: 5.16.2(graphql@16.12.0) + graphql-yoga: 5.17.1(graphql@16.12.0) - '@graphql-yoga/plugin-response-cache@3.15.4(graphql-yoga@5.16.2(graphql@16.9.0))(graphql@16.9.0)': + '@graphql-yoga/plugin-response-cache@3.15.4(graphql-yoga@5.17.1(graphql@16.9.0))(graphql@16.9.0)': dependencies: '@envelop/core': 5.5.1 '@envelop/response-cache': 7.1.3(@envelop/core@5.5.1)(graphql@16.9.0) '@whatwg-node/promise-helpers': 1.3.0 graphql: 16.9.0 - graphql-yoga: 5.16.2(graphql@16.9.0) + graphql-yoga: 5.17.1(graphql@16.9.0) '@graphql-yoga/plugin-response-cache@3.9.0(@envelop/core@5.5.1)(graphql-yoga@5.13.3(graphql@16.9.0))(graphql@16.9.0)': dependencies: @@ -25590,20 +25571,13 @@ snapshots: '@graphql-yoga/redis-event-target@3.0.3(ioredis@5.8.2)': dependencies: '@graphql-yoga/typed-event-target': 3.0.2 - '@whatwg-node/events': 0.1.1 + '@whatwg-node/events': 0.1.2 ioredis: 5.8.2 '@graphql-yoga/render-graphiql@5.16.2(graphql-yoga@5.16.2(graphql@16.12.0))': dependencies: graphql-yoga: 5.16.2(graphql@16.12.0) - '@graphql-yoga/subscription@5.0.4': - dependencies: - '@graphql-yoga/typed-event-target': 3.0.2 - '@repeaterjs/repeater': 3.0.6 - '@whatwg-node/events': 0.1.1 - tslib: 2.8.1 - '@graphql-yoga/subscription@5.0.5': dependencies: '@graphql-yoga/typed-event-target': 3.0.2 @@ -31415,10 +31389,6 @@ snapshots: dependencies: '@types/ms': 0.7.34 - '@types/debug@4.1.7': - dependencies: - '@types/ms': 0.7.34 - '@types/deep-eql@4.0.2': {} '@types/docker-modem@3.0.2': @@ -31901,23 +31871,23 @@ snapshots: chai: 6.2.0 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.9(msw@2.12.7(@types/node@22.10.5)(typescript@5.7.3))(vite@7.1.11(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0))': + '@vitest/mocker@4.0.9(msw@2.12.7(@types/node@22.10.5)(typescript@5.7.3))(vite@7.3.1(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0))': dependencies: '@vitest/spy': 4.0.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.7(@types/node@22.10.5)(typescript@5.7.3) - vite: 7.1.11(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) + vite: 7.3.1(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) - '@vitest/mocker@4.0.9(msw@2.12.7(@types/node@25.0.2)(typescript@5.7.3))(vite@7.1.11(@types/node@25.0.2)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0))': + '@vitest/mocker@4.0.9(msw@2.12.7(@types/node@25.0.2)(typescript@5.7.3))(vite@7.3.1(@types/node@25.0.2)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0))': dependencies: '@vitest/spy': 4.0.9 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.7(@types/node@25.0.2)(typescript@5.7.3) - vite: 7.1.11(@types/node@25.0.2)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) + vite: 7.3.1(@types/node@25.0.2)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) '@vitest/pretty-format@4.0.9': dependencies: @@ -32048,14 +32018,6 @@ snapshots: '@whatwg-node/promise-helpers': 1.3.2 tslib: 2.8.1 - '@whatwg-node/server@0.10.3': - dependencies: - '@envelop/instrumentation': 1.0.0 - '@whatwg-node/disposablestack': 0.0.6 - '@whatwg-node/fetch': 0.10.13 - '@whatwg-node/promise-helpers': 1.3.2 - tslib: 2.8.1 - '@whatwg-node/server@0.9.65': dependencies: '@whatwg-node/disposablestack': 0.0.5 @@ -35641,6 +35603,10 @@ snapshots: dependencies: graphql: 16.9.0 + graphql-sse@2.6.0(graphql@16.9.0): + dependencies: + graphql: 16.9.0 + graphql-tag@2.12.6(graphql@16.9.0): dependencies: graphql: 16.9.0 @@ -35670,52 +35636,53 @@ snapshots: optionalDependencies: ws: 8.18.0 - graphql-yoga@5.13.3(graphql@16.9.0): + graphql-yoga@5.13.3(graphql@16.12.0): dependencies: '@envelop/core': 5.5.1 '@envelop/instrumentation': 1.0.0 - '@graphql-tools/executor': 1.4.7(graphql@16.9.0) - '@graphql-tools/schema': 10.0.25(graphql@16.9.0) - '@graphql-tools/utils': 10.9.1(graphql@16.9.0) + '@graphql-tools/executor': 1.5.0(graphql@16.12.0) + '@graphql-tools/schema': 10.0.25(graphql@16.12.0) + '@graphql-tools/utils': 10.9.1(graphql@16.12.0) '@graphql-yoga/logger': 2.0.1 - '@graphql-yoga/subscription': 5.0.4 + '@graphql-yoga/subscription': 5.0.5 '@whatwg-node/fetch': 0.10.13 - '@whatwg-node/promise-helpers': 1.3.0 - '@whatwg-node/server': 0.10.3 + '@whatwg-node/promise-helpers': 1.3.2 + '@whatwg-node/server': 0.10.17 dset: 3.1.4 - graphql: 16.9.0 + graphql: 16.12.0 lru-cache: 10.2.0 tslib: 2.8.1 - graphql-yoga@5.16.2(graphql@16.12.0): + graphql-yoga@5.13.3(graphql@16.9.0): dependencies: '@envelop/core': 5.5.1 '@envelop/instrumentation': 1.0.0 - '@graphql-tools/executor': 1.4.9(graphql@16.12.0) - '@graphql-tools/schema': 10.0.25(graphql@16.12.0) - '@graphql-tools/utils': 10.9.1(graphql@16.12.0) + '@graphql-tools/executor': 1.5.0(graphql@16.9.0) + '@graphql-tools/schema': 10.0.25(graphql@16.9.0) + '@graphql-tools/utils': 10.9.1(graphql@16.9.0) '@graphql-yoga/logger': 2.0.1 '@graphql-yoga/subscription': 5.0.5 '@whatwg-node/fetch': 0.10.13 - '@whatwg-node/promise-helpers': 1.3.1 + '@whatwg-node/promise-helpers': 1.3.2 '@whatwg-node/server': 0.10.17 - graphql: 16.12.0 + dset: 3.1.4 + graphql: 16.9.0 lru-cache: 10.2.0 tslib: 2.8.1 - graphql-yoga@5.16.2(graphql@16.9.0): + graphql-yoga@5.16.2(graphql@16.12.0): dependencies: '@envelop/core': 5.5.1 '@envelop/instrumentation': 1.0.0 - '@graphql-tools/executor': 1.4.9(graphql@16.9.0) - '@graphql-tools/schema': 10.0.25(graphql@16.9.0) - '@graphql-tools/utils': 10.9.1(graphql@16.9.0) + '@graphql-tools/executor': 1.4.9(graphql@16.12.0) + '@graphql-tools/schema': 10.0.25(graphql@16.12.0) + '@graphql-tools/utils': 10.9.1(graphql@16.12.0) '@graphql-yoga/logger': 2.0.1 '@graphql-yoga/subscription': 5.0.5 '@whatwg-node/fetch': 0.10.13 '@whatwg-node/promise-helpers': 1.3.1 '@whatwg-node/server': 0.10.17 - graphql: 16.9.0 + graphql: 16.12.0 lru-cache: 10.2.0 tslib: 2.8.1 @@ -37949,7 +37916,7 @@ snapshots: micromark@4.0.0: dependencies: - '@types/debug': 4.1.7 + '@types/debug': 4.1.12 debug: 4.4.3(supports-color@8.1.1) decode-named-character-reference: 1.0.2 devlop: 1.1.0 @@ -41990,7 +41957,7 @@ snapshots: unified-engine@11.2.0: dependencies: '@types/concat-stream': 2.0.0 - '@types/debug': 4.1.7 + '@types/debug': 4.1.12 '@types/is-empty': 1.2.1 '@types/node': 20.17.1 '@types/unist': 3.0.0 @@ -42410,42 +42377,6 @@ snapshots: lightningcss: 1.31.1 terser: 5.37.0 - vite@7.1.11(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0): - dependencies: - esbuild: 0.25.9 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.59.0 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 22.10.5 - fsevents: 2.3.3 - jiti: 2.6.1 - less: 4.2.0 - lightningcss: 1.31.1 - terser: 5.37.0 - tsx: 4.19.2 - yaml: 2.5.0 - - vite@7.1.11(@types/node@25.0.2)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0): - dependencies: - esbuild: 0.25.9 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.59.0 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 25.0.2 - fsevents: 2.3.3 - jiti: 2.6.1 - less: 4.2.0 - lightningcss: 1.31.1 - terser: 5.37.0 - tsx: 4.19.2 - yaml: 2.5.0 - vite@7.3.1(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0): dependencies: esbuild: 0.25.9 @@ -42463,7 +42394,6 @@ snapshots: terser: 5.37.0 tsx: 4.19.2 yaml: 2.5.0 - optional: true vite@7.3.1(@types/node@25.0.2)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0): dependencies: @@ -42486,7 +42416,7 @@ snapshots: vitest@4.0.9(@types/debug@4.1.12)(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(msw@2.12.7(@types/node@22.10.5)(typescript@5.7.3))(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0): dependencies: '@vitest/expect': 4.0.9 - '@vitest/mocker': 4.0.9(msw@2.12.7(@types/node@22.10.5)(typescript@5.7.3))(vite@7.1.11(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0)) + '@vitest/mocker': 4.0.9(msw@2.12.7(@types/node@22.10.5)(typescript@5.7.3))(vite@7.3.1(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0)) '@vitest/pretty-format': 4.0.9 '@vitest/runner': 4.0.9 '@vitest/snapshot': 4.0.9 @@ -42503,7 +42433,7 @@ snapshots: tinyexec: 0.3.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.1.11(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) + vite: 7.3.1(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -42525,7 +42455,7 @@ snapshots: vitest@4.0.9(@types/debug@4.1.12)(@types/node@25.0.2)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(msw@2.12.7(@types/node@25.0.2)(typescript@5.7.3))(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0): dependencies: '@vitest/expect': 4.0.9 - '@vitest/mocker': 4.0.9(msw@2.12.7(@types/node@25.0.2)(typescript@5.7.3))(vite@7.1.11(@types/node@25.0.2)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0)) + '@vitest/mocker': 4.0.9(msw@2.12.7(@types/node@25.0.2)(typescript@5.7.3))(vite@7.3.1(@types/node@25.0.2)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0)) '@vitest/pretty-format': 4.0.9 '@vitest/runner': 4.0.9 '@vitest/snapshot': 4.0.9 @@ -42542,7 +42472,7 @@ snapshots: tinyexec: 0.3.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.1.11(@types/node@25.0.2)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) + vite: 7.3.1(@types/node@25.0.2)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.31.1)(terser@5.37.0)(tsx@4.19.2)(yaml@2.5.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 diff --git a/tsconfig.json b/tsconfig.json index 64eecc305ec..f9d907c4977 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -74,7 +74,8 @@ ], "@graphql-hive/plugin-opentelemetry/setup": [ "./node_modules/@graphql-hive/plugin-opentelemetry/dist/setup.d.ts" - ] + ], + "@hive/pubsub": ["./packages/libraries/pubsub/src/index.ts"] } }, "include": ["packages", "tsup.config.node.ts"],