From e03ff9a5582750dde63c3a6eec06da29ec90f404 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Fri, 18 Sep 2026 14:15:14 +0200 Subject: [PATCH 01/14] database migration --- .../src/actions/2026.09.18T00-00-00.graphs.ts | 43 +++++++++++++++++++ ...8T00-00-01.graph-schema-version-indexes.ts | 36 ++++++++++++++++ packages/migrations/src/run-pg-migrations.ts | 2 + 3 files changed, 81 insertions(+) create mode 100644 packages/migrations/src/actions/2026.09.18T00-00-00.graphs.ts create mode 100644 packages/migrations/src/actions/2026.09.18T00-00-01.graph-schema-version-indexes.ts diff --git a/packages/migrations/src/actions/2026.09.18T00-00-00.graphs.ts b/packages/migrations/src/actions/2026.09.18T00-00-00.graphs.ts new file mode 100644 index 0000000000..1ea50b5178 --- /dev/null +++ b/packages/migrations/src/actions/2026.09.18T00-00-00.graphs.ts @@ -0,0 +1,43 @@ +import { type MigrationExecutor } from '../pg-migrator'; + +// type graphs__config = { +// type: 'contract'; +// includeTags: Array; +// excludeTargs: Array; +// removeUnreachableTypesFromPublicApiSchema: boolean; +// isDisabled: boolean; +// }; + +// type schema_versions__graph_metadata = { +// type: 'contract'; +// graphId: string; +// graphName: string; +// }; + +export default { + name: '2026.09.18T00-00-00.graphs.ts', + run: ({ psql }) => psql` + CREATE TABLE "graphs" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4() + , "organization_id" uuid NOT NULL REFERENCES "organizations"("id") ON DELETE CASCADE + , "project_id" uuid NOT NULL REFERENCES "projects"("id") ON DELETE CASCADE + , "target_id" uuid NOT NULL REFERENCES "targets"("id") ON DELETE CASCADE + , "name" text NOT NULL + , "config" jsonb NOT NULL + , "source_graph_id" uuid REFERENCES "graphs"("id") ON DELETE CASCADE + , "created_at" timestamptz NOT NULL DEFAULT now() + , PRIMARY KEY ("id") + ); + + CREATE INDEX "graphs_organization_id" ON "graphs" ("organization_id"); + CREATE INDEX "graphs_project_id" ON "graphs" ("project_id"); + CREATE INDEX "graphs_target_id" ON "graphs" ("target_id"); + CREATE INDEX "graphs_source_graph_id" ON "graphs" ("source_graph_id"); + + ALTER TABLE "schema_versions" + ADD COLUMN "graph_id" uuid REFERENCES "graphs"("id") ON DELETE SET NULL + , ADD COLUMN "graph_metadata" jsonb + , ADD COLUMN "source_schema_version_id" uuid REFERENCES "schema_versions"."id" + ; + `, +} satisfies MigrationExecutor; diff --git a/packages/migrations/src/actions/2026.09.18T00-00-01.graph-schema-version-indexes.ts b/packages/migrations/src/actions/2026.09.18T00-00-01.graph-schema-version-indexes.ts new file mode 100644 index 0000000000..7f178d17eb --- /dev/null +++ b/packages/migrations/src/actions/2026.09.18T00-00-01.graph-schema-version-indexes.ts @@ -0,0 +1,36 @@ +import { type MigrationExecutor } from '../pg-migrator'; + +export default { + name: '2026.09.18T00-00-01.graph-schema-version-indexes.ts', + noTransaction: true, + run: ({ psql }) => [ + { + name: 'create schema_versions_graph_pagination index', + query: psql` + CREATE INDEX CONCURRENTLY IF NOT EXISTS "schema_versions_graph_pagination" + ON "schema_versions" ( + "graph_id" ASC, + "created_at" DESC, + "id" DESC + ) + WHERE "graph_id" IS NOT NULL + `, + }, + { + name: 'create schema_versions_source_schema_version_id index', + query: psql` + CREATE INDEX CONCURRENTLY IF NOT EXISTS "schema_versions_source_schema_version_id" + ON "schema_versions" ("source_schema_version_id") + WHERE "source_schema_version_id" IS NOT NULL + `, + }, + { + name: 'create schema_versions_graph_id index', + query: psql` + CREATE INDEX CONCURRENTLY IF NOT EXISTS "schema_versions_graph_id" + ON "schema_versions" ("graph_id") + WHERE "graph_id" IS NOT NULL + `, + }, + ], +} satisfies MigrationExecutor; diff --git a/packages/migrations/src/run-pg-migrations.ts b/packages/migrations/src/run-pg-migrations.ts index c01dafcaa4..5517dc0db3 100644 --- a/packages/migrations/src/run-pg-migrations.ts +++ b/packages/migrations/src/run-pg-migrations.ts @@ -133,5 +133,7 @@ export const runPGMigrations = async (args: { slonik: PostgresDatabasePool; runT import('./actions/2026.08.11T00-00-00.schema-check-baseline-sdl'), import('./actions/2026.08.11T00-00-01.schema-check-baseline-sdl-indexes'), import('./actions/2026.08.27T00-00-00.schema-push'), + import('./actions/2026.09.18T00-00-00.graphs'), + import('./actions/2026.09.18T00-00-01.graph-schema-version-indexes'), ]), }); From 6d6694279c4dfcb9bd84315dc80c2614afd18907 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Fri, 18 Sep 2026 16:28:11 +0200 Subject: [PATCH 02/14] create default graph when creating a target --- .../services/api/src/modules/graph/index.ts | 12 ++ .../api/src/modules/graph/module.graphql.ts | 5 + .../modules/graph/providers/graph-store.ts | 145 ++++++++++++++++++ .../modules/target/providers/target-store.ts | 19 ++- 4 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 packages/services/api/src/modules/graph/index.ts create mode 100644 packages/services/api/src/modules/graph/module.graphql.ts create mode 100644 packages/services/api/src/modules/graph/providers/graph-store.ts diff --git a/packages/services/api/src/modules/graph/index.ts b/packages/services/api/src/modules/graph/index.ts new file mode 100644 index 0000000000..f38178cca2 --- /dev/null +++ b/packages/services/api/src/modules/graph/index.ts @@ -0,0 +1,12 @@ +import { createModule } from 'graphql-modules'; +import { GraphStore } from './providers/graph-store'; +import { resolvers } from './resolvers.generated'; +import typeDefs from './module.graphql'; + +export const graphModule = createModule({ + id: 'graph', + dirname: __dirname, + typeDefs, + resolvers, + providers: [GraphStore], +}); diff --git a/packages/services/api/src/modules/graph/module.graphql.ts b/packages/services/api/src/modules/graph/module.graphql.ts new file mode 100644 index 0000000000..fdc9f6ffc7 --- /dev/null +++ b/packages/services/api/src/modules/graph/module.graphql.ts @@ -0,0 +1,5 @@ +import { gql } from 'graphql-modules'; + +export default gql` + extend schema +`; diff --git a/packages/services/api/src/modules/graph/providers/graph-store.ts b/packages/services/api/src/modules/graph/providers/graph-store.ts new file mode 100644 index 0000000000..a57b492dcf --- /dev/null +++ b/packages/services/api/src/modules/graph/providers/graph-store.ts @@ -0,0 +1,145 @@ +import { Injectable, Scope } from 'graphql-modules'; +import { z } from 'zod'; +import { PostgresDatabasePool, psql, type CommonQueryMethods } from '@hive/postgres'; +import { Logger } from '../../shared/providers/logger'; + +const ContractGraphConfigModel = z.object({ + type: z.literal('contract'), + includeTags: z.array(z.string()).nullable(), + excludeTags: z.array(z.string()).nullable(), + removeUnreachableTypesFromPublicApiSchema: z.boolean(), + isDisabled: z.boolean(), +}); + +const GraphConfigModel = z.discriminatedUnion('type', [ContractGraphConfigModel]); + +const GraphModel = z.object({ + id: z.string(), + organizationId: z.string(), + projectId: z.string(), + targetId: z.string(), + name: z.string(), + config: GraphConfigModel.nullable(), + sourceGraphId: z.string().nullable(), + createdAt: z.string(), +}); + +export type Graph = z.infer; +export type GraphConfig = z.infer; + +@Injectable({ + scope: Scope.Operation, + global: true, +}) +export class GraphStore { + private logger: Logger; + + constructor( + logger: Logger, + private pg: PostgresDatabasePool, + ) { + this.logger = logger.child({ + source: 'GraphStore', + }); + } + + async createGraph( + args: { + organizationId: string; + projectId: string; + targetId: string; + name: string; + sourceGraphId: string | null; + config: GraphConfig | null; + }, + trx: CommonQueryMethods = this.pg, + ): Promise { + this.logger.debug( + 'create graph (organizationId=%s, projectId=%s, targetId=%s, name=%s)', + args.organizationId, + args.projectId, + args.targetId, + args.name, + ); + + return await trx + .one( + psql`/* createGraph */ + INSERT INTO "graphs" ( + "organization_id" + , "project_id" + , "target_id" + , "name" + , "config" + , "source_graph_id" + ) + VALUES ( + ${args.organizationId} + , ${args.projectId} + , ${args.targetId} + , ${args.name} + , ${psql.jsonb(args.config)} + , ${args.sourceGraphId} + ) + RETURNING + ${graphFields} + `, + ) + .then(GraphModel.parse); + } + + async findGraphForTargetIdByName(targetId: string, graphName: string): Promise { + this.logger.debug( + 'find graph by target id and name (targetId=%s, graphName=%s)', + targetId, + graphName, + ); + + const query = psql` + SELECT + ${graphFields} + FROM + "graphs" + WHERE + "target_id" = ${targetId} + AND "name" = ${graphName} + `; + + return this.pg.maybeOne(query).then(GraphModel.nullable().parse); + } + + async deleteGraphByTargetIdAndName( + targetId: string, + graphName: string, + trx: CommonQueryMethods = this.pg, + ): Promise { + this.logger.debug( + 'delete graph by target id and name (targetId=%s, graphName=%s)', + targetId, + graphName, + ); + + const query = psql` + DELETE + FROM + "graphs" + WHERE + "target_id" = ${targetId} + AND "name" = ${graphName} + `; + + await trx.query(query); + } +} + +const graphFields = psql` + "id" + , "organization_id" AS "organizationId" + , "project_id" AS "projectId" + , "target_id" AS "targetId" + , "name" + , "type" + , "config" + , "source_graph_id" AS "sourceGraphId" + , to_json("created_at") AS "createdAt" +`; diff --git a/packages/services/api/src/modules/target/providers/target-store.ts b/packages/services/api/src/modules/target/providers/target-store.ts index dd9dccca0d..158fbbc179 100644 --- a/packages/services/api/src/modules/target/providers/target-store.ts +++ b/packages/services/api/src/modules/target/providers/target-store.ts @@ -4,6 +4,7 @@ import { PostgresDatabasePool, psql, TaggedTemplateLiteralInvocation } from '@hi import { FeatureFlagsModel, TargetBreadcrumbModel } from '@hive/storage'; import type { Target } from '../../../shared/entities'; import { batch, batchBy } from '../../../shared/helpers'; +import { GraphStore } from '../../graph/providers/graph-store'; import { Logger } from '../../shared/providers/logger'; @Injectable({ @@ -15,6 +16,7 @@ export class TargetStore { constructor( logger: Logger, + private graphStore: GraphStore, private pg: PostgresDatabasePool, ) { this.logger = logger.child({ source: 'TargetStore' }); @@ -60,9 +62,24 @@ export class TargetStore { RETURNING ${targetFields} `); + const target = { ...TargetModel.parse(result), orgId: args.organizationId }; + + const defaultGraph = await this.graphStore.createGraph( + { + name: 'default', + config: null, + organizationId: args.organizationId, + projectId: args.projectId, + targetId: target.id, + sourceGraphId: null, + }, + trx, + ); + return { ok: true, - target: { ...TargetModel.parse(result), orgId: args.organizationId }, + target, + defaultGraph, } as const; }); } From 3f6ec0db859501f99007ccca0e15b043435ab077 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Fri, 18 Sep 2026 16:28:38 +0200 Subject: [PATCH 03/14] create and delete a contract graph when creating/disabling a contract --- .../schema/providers/contracts-manager.ts | 7 + .../src/modules/schema/providers/contracts.ts | 120 ++++++++++++------ 2 files changed, 88 insertions(+), 39 deletions(-) diff --git a/packages/services/api/src/modules/schema/providers/contracts-manager.ts b/packages/services/api/src/modules/schema/providers/contracts-manager.ts index 516f8d207b..47ec79ae5c 100644 --- a/packages/services/api/src/modules/schema/providers/contracts-manager.ts +++ b/packages/services/api/src/modules/schema/providers/contracts-manager.ts @@ -4,6 +4,7 @@ import * as GraphQLSchema from '../../../__generated__/types'; import type { Target } from '../../../shared/entities'; import { cache } from '../../../shared/helpers'; import { Session } from '../../auth/lib/authz'; +import { GraphStore } from '../../graph/providers/graph-store'; import { IdTranslator } from '../../shared/providers/id-translator'; import { Logger } from '../../shared/providers/logger'; import { TargetStore } from '../../target/providers/target-store'; @@ -26,6 +27,7 @@ export class ContractsManager { logger: Logger, private contracts: Contracts, private targetStore: TargetStore, + private graphStore: GraphStore, private session: Session, private idTranslator: IdTranslator, private breakingSchemaChangeUsageHelper: BreakingSchemaChangeUsageHelper, @@ -62,7 +64,12 @@ export class ContractsManager { }, }); + const sourceGraph = await this.graphStore.findGraphForTargetIdByName(targetId, 'default'); + return await this.contracts.createContract({ + organizationId, + projectId, + sourceGraphId: sourceGraph?.id ?? null, contract: { ...args.contract, targetId, diff --git a/packages/services/api/src/modules/schema/providers/contracts.ts b/packages/services/api/src/modules/schema/providers/contracts.ts index 55ea0bc898..3fed9cb586 100644 --- a/packages/services/api/src/modules/schema/providers/contracts.ts +++ b/packages/services/api/src/modules/schema/providers/contracts.ts @@ -16,6 +16,7 @@ import { type SchemaCheckApprovalMetadata, } from '@hive/storage'; import { isUUID } from '../../../shared/is-uuid'; +import { GraphStore } from '../../graph/providers/graph-store'; import { Logger } from '../../shared/providers/logger'; import { ArtifactStorageWriter } from './artifact-storage-writer'; import { SchemaVersion } from './schema-version-store'; @@ -30,11 +31,17 @@ export class Contracts { logger: Logger, private pool: PostgresDatabasePool, private artifactStorageWriter: ArtifactStorageWriter, + private graphStore: GraphStore, ) { this.logger = logger.child({ source: 'Contracts' }); } - async createContract(args: { contract: CreateContractInput }) { + async createContract(args: { + contract: CreateContractInput; + organizationId: string; + projectId: string; + sourceGraphId: string | null; + }) { this.logger.debug( 'Create contract (targetId=%s, contractName=%s)', args.contract.targetId, @@ -63,23 +70,44 @@ export class Contracts { let result: unknown; try { - result = await this.pool.maybeOne(psql` - INSERT INTO "contracts" ( - "target_id" - , "contract_name" - , "include_tags" - , "exclude_tags" - , "remove_unreachable_types_from_public_api_schema" - ) VALUES ( - ${validatedContract.data.targetId} - , ${validatedContract.data.contractName} - , ${toNullableTextArray(validatedContract.data.includeTags)} - , ${toNullableTextArray(validatedContract.data.excludeTags)} - , ${validatedContract.data.removeUnreachableTypesFromPublicApiSchema} - ) - RETURNING - ${contractFields} - `); + await this.pool.transaction('create contract', async trx => { + result = await trx.maybeOne(psql` + INSERT INTO "contracts" ( + "target_id" + , "contract_name" + , "include_tags" + , "exclude_tags" + , "remove_unreachable_types_from_public_api_schema" + ) VALUES ( + ${validatedContract.data.targetId} + , ${validatedContract.data.contractName} + , ${toNullableTextArray(validatedContract.data.includeTags)} + , ${toNullableTextArray(validatedContract.data.excludeTags)} + , ${validatedContract.data.removeUnreachableTypesFromPublicApiSchema} + ) + RETURNING + ${contractFields} + `); + + // Only create the graph record if the source graph id already exists + if (args.sourceGraphId) { + await this.graphStore.createGraph({ + name: `default/${validatedContract.data.contractName}`, + organizationId: args.organizationId, + projectId: args.projectId, + targetId: validatedContract.data.targetId, + config: { + type: 'contract', + includeTags: validatedContract.data.includeTags, + excludeTags: validatedContract.data.excludeTags, + isDisabled: false, + removeUnreachableTypesFromPublicApiSchema: + validatedContract.data.removeUnreachableTypesFromPublicApiSchema, + }, + sourceGraphId: args.sourceGraphId, + }); + } + }); } catch (err: unknown) { if ( err instanceof UniqueIntegrityConstraintViolationError && @@ -148,26 +176,43 @@ export class Contracts { }; } - const record = await this.pool.maybeOne(psql` - UPDATE - "contracts" - SET - "is_disabled" = true - WHERE - "id" = ${args.contract.id} - RETURNING - ${contractFields} - `); + const result = await this.pool.transaction('disable contract', async trx => { + const record = await trx + .maybeOne( + psql` + UPDATE + "contracts" + SET + "is_disabled" = true + WHERE + "id" = ${args.contract.id} + RETURNING + ${contractFields} + `, + ) + .then(ContractModel.nullable().parse); + + if (!record) { + this.logger.debug( + 'Contract can not be disabled as it was not found. (contractId=%s)', + args.contract.id, + ); + return { + type: 'error' as const, + message: 'Contract not found.', + }; + } + + await this.graphStore.deleteGraphByTargetIdAndName(record.targetId, record.contractName, trx); - if (!record) { - this.logger.debug( - 'Contract can not be disabled as it was not found. (contractId=%s)', - args.contract.id, - ); return { - type: 'error' as const, - message: 'Contract not found.', + type: 'success' as const, + contract: record, }; + }); + + if (result.type === 'error') { + return result; } this.logger.debug('Updated contract. (contractId=%s)', args.contract.id); @@ -190,10 +235,7 @@ export class Contracts { }), ]); - return { - type: 'success' as const, - contract: ContractModel.parse(record), - }; + return result; } public async getActiveContractsByTargetId(args: { From a6bedf2a3194eb1641aefef6b2b13021ccbc8733 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Fri, 18 Sep 2026 17:24:17 +0200 Subject: [PATCH 04/14] associate new schema versions with the default graph (if it exists) --- .../schema/providers/schema-manager.ts | 98 +-------------- .../schema/providers/schema-publisher.ts | 21 +++- .../schema/providers/schema-version-store.ts | 116 +++++++++++++++--- 3 files changed, 118 insertions(+), 117 deletions(-) diff --git a/packages/services/api/src/modules/schema/providers/schema-manager.ts b/packages/services/api/src/modules/schema/providers/schema-manager.ts index 41bcefcf32..473cd41f67 100644 --- a/packages/services/api/src/modules/schema/providers/schema-manager.ts +++ b/packages/services/api/src/modules/schema/providers/schema-manager.ts @@ -5,12 +5,7 @@ import { Inject, Injectable, Scope } from 'graphql-modules'; import lodash from 'lodash'; import { z } from 'zod'; import { Encryptor, trace, traceFn } from '@hive/service-common'; -import type { - ConditionalBreakingChangeMetadata, - SchemaChangeType, - SchemaCheck, - SchemaCompositionError, -} from '@hive/storage'; +import type { SchemaCheck } from '@hive/storage'; import { sortSDL } from '@theguild/federation-composition'; import { SchemaChecksFilter } from '../../../__generated__/types'; import * as GraphQLSchema from '../../../__generated__/types'; @@ -467,97 +462,6 @@ export class SchemaManager { return this.schemaVersions.getSchemaLogById(schemaLogId); } - @traceFn('SchemaManager.createVersion', { - initAttributes: input => ({ - 'hive.target.id': input.targetId, - 'hive.organization.id': input.organizationId, - 'hive.project.id': input.projectId, - 'hive.version.commit': input.commit, - 'hive.version.valid': input.valid, - 'hive.version.service': input.service?.name || '', - }), - }) - async createPublishVersion( - input: ({ - service: { - name: string; - url: string; - } | null; - serviceChanges: Array | null; - previousSchemaLogId: string | null; - commit: string; - schema: string; - author: string; - valid: boolean; - existingSchemaLogs: Array<{ id: string; serviceName: string | null }>; - base_schema: string | null; - metadata: string | null; - schemaRevisionId: string | null; - revision: string | null; - actionFn(versionId: string): Promise; - changes: Array; - previousSchemaVersion: string | null; - diffSchemaVersionId: string | null; - github: null | { - repository: string; - sha: string; - }; - contracts: null | Array<{ - contractId: string; - contractName: string; - compositeSchemaSDL: string | null; - supergraphSDL: string | null; - schemaCompositionErrors: Array | null; - changes: null | Array; - }>; - conditionalBreakingChangeMetadata: null | ConditionalBreakingChangeMetadata; - } & TargetSelector) & - ( - | { - compositeSchemaSDL: null; - supergraphSDL: null; - supergraphChanges: null; - schemaCompositionErrors: Array; - tags: null; - schemaMetadata: null; - metadataAttributes: null; - } - | { - compositeSchemaSDL: string; - supergraphSDL: string | null; - supergraphChanges: Array | null; - schemaCompositionErrors: null; - tags: Array | null; - schemaMetadata: null | Record< - string, - Array<{ name: string; content: string; source: string | null }> - >; - metadataAttributes: null | Record; - } - ), - ) { - this.logger.info( - 'Creating a new version (input=%o)', - lodash.pick(input, [ - 'commit', - 'author', - 'valid', - 'service', - 'logIds', - 'url', - 'previousSchemaVersion', - 'diffSchemaVersionId', - 'github', - 'conditionalBreakingChangeMetadata', - ]), - ); - - return this.schemaVersions.createPublishSchemaVersion({ - ...input, - existingSchemaLogs: input.existingSchemaLogs, - }); - } - async testExternalSchemaComposition(selector: { projectId: string; organizationId: string }) { await this.session.assertPerformAction({ organizationId: selector.organizationId, 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 70eb8ffc64..fc7f34c393 100644 --- a/packages/services/api/src/modules/schema/providers/schema-publisher.ts +++ b/packages/services/api/src/modules/schema/providers/schema-publisher.ts @@ -23,6 +23,7 @@ import { AlertsManager } from '../../alerts/providers/alerts-manager'; import { AppDeployments } from '../../app-deployments/providers/app-deployments'; import { Session } from '../../auth/lib/authz'; import { RateLimitProvider } from '../../commerce/providers/rate-limit.provider'; +import { GraphStore } from '../../graph/providers/graph-store'; import { GitHubIntegrationManager, type GitHubCheckRun, @@ -186,6 +187,7 @@ export class SchemaPublisher { private registryChecks: RegistryChecks, private appDeployments: AppDeployments, private schemaRevisions: SchemaRevisionStore, + private graphStore: GraphStore, @Inject(SCHEMA_MODULE_CONFIG) private schemaModuleConfig: SchemaModuleConfig, singleModel: SingleModel, compositeModel: CompositeModel, @@ -1584,7 +1586,7 @@ export class SchemaPublisher { signal, }, async () => { - const [organization, project, target] = await Promise.all([ + const [organization, project, target, defaultGraph] = await Promise.all([ this.storage.getOrganization({ organizationId: selector.organizationId, }), @@ -1597,6 +1599,7 @@ export class SchemaPublisher { projectId: selector.projectId, targetId: selector.targetId, }), + this.graphStore.findGraphForTargetIdByName(selector.targetId, 'default'), ]); schemaDeleteCount.inc({ model: 'modern', projectType: project.type }); @@ -1709,6 +1712,7 @@ export class SchemaPublisher { name: affectedService.service_name, versionId: affectedService.id, }, + graph: defaultGraph, composable: deleteResult.state.composable, diffSchemaVersionId: latestComposableVersion?.version.id ?? null, changes: deleteResult.state.changes, @@ -1883,7 +1887,7 @@ export class SchemaPublisher { metadata: !!input.metadata, }); - const [organization, project, target, baseSchema] = await Promise.all([ + const [organization, project, target, baseSchema, defaultGraph] = await Promise.all([ this.storage.getOrganization({ organizationId: organizationId, }), @@ -1896,12 +1900,12 @@ export class SchemaPublisher { projectId: projectId, targetId: targetId, }), - this.storage.getBaseSchema({ organizationId: organizationId, projectId: projectId, targetId: targetId, }), + this.graphStore.findGraphForTargetIdByName(targetId, 'default'), ]); const [latestVersion, latestComposable] = await Promise.all([ @@ -2310,11 +2314,12 @@ export class SchemaPublisher { serviceUrl = pushedSchema.serviceUrl; } - const schemaVersion = await this.schemaManager.createPublishVersion({ + const schemaVersion = await this.schemaVersions.createPublishSchemaVersion({ valid: composable, organizationId: organizationId, projectId: project.id, targetId: target.id, + graph: defaultGraph, commit: input.commit, existingSchemaLogs: previousSchemaLogs, schema: input.sdl, @@ -3151,6 +3156,8 @@ export class SchemaPublisher { const [ targetLatestSchemaVersion, targetLatestValidSchemaVersion, + targetDefaultGraph, + originDefaultGraph, originPublicSchemaSdl, originSupergraphSdl, originLogEdges, @@ -3161,6 +3168,10 @@ export class SchemaPublisher { // The latest versions within the target we promote to this.schemaManager.getMaybeLatestVersion(target), this.schemaManager.getMaybeLatestValidVersion(target), + // the default graph in the target we promote to + this.graphStore.findGraphForTargetIdByName(target.id, 'default'), + // the default graph in the target we promote from + this.graphStore.findGraphForTargetIdByName(originTarget.id, 'default'), // We have some old schema versions that do not store the SDLs on the record // we need to use the helpers to ensure the SDL is produced for these this.schemaVersionHelper.getCompositeSchemaSdl(originSchemaVersion), @@ -3290,11 +3301,13 @@ export class SchemaPublisher { const schemaVersion = await this.schemaVersions.createPromotionSchemaVersion({ target: { target, + graph: targetDefaultGraph, latestVersion: targetLatestSchemaVersion, latestValidVersion: targetLatestValidSchemaVersion, }, origin: { target: originTarget, + graph: originDefaultGraph, version: originSchemaVersion, publicSchemaSdl: originPublicSchemaSdl, supergraphSdl: originSupergraphSdl, diff --git a/packages/services/api/src/modules/schema/providers/schema-version-store.ts b/packages/services/api/src/modules/schema/providers/schema-version-store.ts index 1d6ddfcd64..c31b836d5b 100644 --- a/packages/services/api/src/modules/schema/providers/schema-version-store.ts +++ b/packages/services/api/src/modules/schema/providers/schema-version-store.ts @@ -1,7 +1,8 @@ import { Injectable, Scope } from 'graphql-modules'; +import lodash from 'lodash'; import { z } from 'zod'; import { CommonQueryMethods, PostgresDatabasePool, psql } from '@hive/postgres'; -import { invariant } from '@hive/service-common'; +import { invariant, traceFn } from '@hive/service-common'; import { ConditionalBreakingChangeMetadata, ConditionalBreakingChangeMetadataModel, @@ -16,9 +17,29 @@ import { } from '@hive/storage'; import type { Project, Target } from '../../../shared/entities'; import { batch, cache } from '../../../shared/helpers'; +import { Graph } from '../../graph/providers/graph-store'; import { Logger, NoopLogger } from '../../shared/providers/logger'; import { SchemaRevisionStore } from './schema-revision-store'; +const DefaultGraphMetadataModel = z.object({ + type: z.literal('default'), + id: z.string(), + name: z.string(), +}); + +const ContractGraphMetadataModel = z.object({ + type: z.literal('contract'), + id: z.string(), + name: z.string(), +}); + +const GraphMetadataModel = z.discriminatedUnion('type', [ + ContractGraphMetadataModel, + DefaultGraphMetadataModel, +]); + +type GraphMetadata = z.TypeOf; + @Injectable({ scope: Scope.Operation, global: true, @@ -59,12 +80,15 @@ export class SchemaVersionStore { }; meta: SchemaVersionMeta | null; conditionalBreakingChangeMetadata: ConditionalBreakingChangeMetadata | null; + /** The UUID of the graph this schema version belongs to */ + graphId: string | null; /** - * The action ID that caused this version. - * This column is a leftover, so we can easily rollback the introduced changes. - * In the future we should delete this column fully and instead sorely use the `origin` column. - **/ - actionId: string; + * Additional metadata about the graph + * Since graphs can be deleted but its version could still be referenced somewhere else, we store that information here. + */ + graphMetadata: GraphMetadata | null; + /** Contracts have a direct relationship to the parent schema version that caused it. */ + sourceSchemaVersionId: string | null; }, ) { const query = psql`/* insertSchemaVersion */ @@ -90,7 +114,9 @@ export class SchemaVersionStore { "metadata_attributes", "origin", "meta", - "action_id" + "graph_id", + "graph_metadata", + "source_schema_version_id" ) VALUES ( @@ -114,7 +140,9 @@ export class SchemaVersionStore { ${psql.jsonbOrNull(args.metadataAttributes)}, ${psql.jsonb(SchemaVersionOriginModel.parse(args.origin))}, ${psql.jsonbOrNull(SchemaVersionMetaModel.nullable().parse(args.meta))}, - ${args.actionId} + ${args.graphId}, + ${psql.jsonbOrNull(GraphMetadataModel.nullable().parse(args.graphMetadata))}, + ${args.sourceSchemaVersionId} ) RETURNING ${schemaVersionSQLFields()} @@ -274,6 +302,16 @@ export class SchemaVersionStore { `); } + @traceFn('SchemaVersionsStore.createPublishSchemaVersion', { + initAttributes: args => ({ + 'hive.target.id': args.targetId, + 'hive.organization.id': args.organizationId, + 'hive.project.id': args.projectId, + 'hive.version.commit': args.commit, + 'hive.version.valid': args.valid, + 'hive.version.service': args.service?.name || '', + }), + }) async createPublishSchemaVersion( args: { schema: string; @@ -305,6 +343,7 @@ export class SchemaVersionStore { organizationId: string; schemaRevisionId: string | null; revision: string | null; + graph: Graph | null; } & ( | { compositeSchemaSDL: null; @@ -329,6 +368,22 @@ export class SchemaVersionStore { } ), ): Promise { + this.logger.info( + 'Creating a new version (input=%o)', + lodash.pick(args, [ + 'commit', + 'author', + 'valid', + 'service', + 'logIds', + 'url', + 'previousSchemaVersion', + 'diffSchemaVersionId', + 'github', + 'conditionalBreakingChangeMetadata', + ]), + ); + const output = await this.pg.transaction('createSchemaVersion', async trx => { const newLog = await this.insertPushSchemaLog(trx, { author: args.author, @@ -349,6 +404,15 @@ export class SchemaVersionStore { const version = await this.insertSchemaVersion(trx, { isComposable: args.valid, targetId: args.targetId, + graphId: args.graph?.id ?? null, + graphMetadata: args.graph + ? { + id: args.graph.id, + name: args.graph.name, + type: 'default', + } + : null, + sourceSchemaVersionId: null, origin: { type: 'publish', revision: args.service ? null : args.revision, @@ -380,7 +444,6 @@ export class SchemaVersionStore { hasContractCompositionErrors: args.contracts?.some(c => c.schemaCompositionErrors != null) ?? false, conditionalBreakingChangeMetadata: args.conditionalBreakingChangeMetadata, - actionId: newLog.id, }); await trx.query(psql`/* insertSchemaVersionToLog */ @@ -457,6 +520,7 @@ export class SchemaVersionStore { diffSchemaVersionId: string | null; conditionalBreakingChangeMetadata: null | ConditionalBreakingChangeMetadata; contracts: null | Array; + graph: Graph | null; } & ( | { compositeSchemaSDL: null; @@ -559,7 +623,11 @@ export class SchemaVersionStore { hasContractCompositionErrors: args.contracts?.some(c => c.schemaCompositionErrors != null) ?? false, conditionalBreakingChangeMetadata: args.conditionalBreakingChangeMetadata, - actionId: deleteActionResult.id, + graphId: args.graph?.id ?? null, + graphMetadata: args.graph + ? { id: args.graph.id, name: args.graph.name, type: 'default' } + : null, + sourceSchemaVersionId: null, }); // Move all the schema_version_to_log entries of the previous version to the new version @@ -1310,6 +1378,7 @@ export class SchemaVersionStore { origin: { version: SchemaVersion; target: Target; + graph: Graph | null; /** Because of legacy schema versions we cannot rely on the value on the version itself. */ publicSchemaSdl: string | null; /** Because of legacy schema versions we cannot rely on the value on the version itself. */ @@ -1317,6 +1386,7 @@ export class SchemaVersionStore { }; target: { target: Target; + graph: Graph | null; latestVersion: SchemaVersion | null; latestValidVersion: SchemaVersion | null; }; @@ -1346,6 +1416,9 @@ export class SchemaVersionStore { source: { schemaVersion: { id: args.origin.version.id }, target: { id: args.origin.target.id, name: args.origin.target.name }, + graph: args.origin.graph + ? { id: args.origin.graph.id, name: args.origin.graph.name } + : undefined, }, }, baseSchema: args.origin.version.baseSchema, @@ -1363,12 +1436,11 @@ export class SchemaVersionStore { hasContractCompositionErrors: args.contracts?.some(c => c.schemaCompositionErrors != null) ?? false, conditionalBreakingChangeMetadata: args.conditionalBreakingChangeMetadata, - // Note: we re-use the original version action id here to allow rolling back the introduced changes easily. - // In the future we will make the actionId column nullable and remove it from being inserted here. - // In case we would rollback the schema promotion feature, the users would still see the promoted schema versions - // even though the action would be misleading. This is a trade-off to make sure we can quickly rollback the schema promotion feature - // in case it causes unexpected issues. - actionId: args.origin.version.actionId, + graphId: args.target.graph?.id ?? null, + graphMetadata: args.target.graph + ? { id: args.target.graph.id, name: args.target.graph.name, type: 'default' } + : null, + sourceSchemaVersionId: null, }); if (args.publicSchemaChanges?.length) { @@ -1514,6 +1586,9 @@ const schemaVersionSQLFields = (t = psql``) => psql` , ${t}"origin" , ${t}"meta" , ${t}"supergraph_changes" as "supergraphChanges" + , ${t}"graph_id" as "graphId" + , ${t}"graph_metadata" as "graphMetadata" + , ${t}"source_schema_version_id" as "sourceSchemaVersionId" `; const schemaLogFields = (prefix = psql``) => psql` @@ -1612,6 +1687,12 @@ const SchemaVersionOriginPromotionModel = z.object({ id: z.string(), name: z.string(), }), + graph: z + .object({ + id: z.string(), + name: z.string(), + }) + .optional(), }), }); @@ -1686,6 +1767,9 @@ const SchemaVersionModel = z meta: SchemaVersionMetaModel.nullable(), actionId: z.string(), origin: SchemaVersionOriginModel.nullable(), + graphId: z.string(), + graphMetadata: GraphMetadataModel.nullable(), + sourceSchemaVersionId: z.string().nullable(), }) .and( z From 0c411d0e44b379158f4b5a63889071503072e302 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Wed, 23 Sep 2026 12:06:09 +0200 Subject: [PATCH 05/14] fix failing graphql:generate command --- packages/services/api/src/modules/graph/module.graphql.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/services/api/src/modules/graph/module.graphql.ts b/packages/services/api/src/modules/graph/module.graphql.ts index fdc9f6ffc7..d7f4204837 100644 --- a/packages/services/api/src/modules/graph/module.graphql.ts +++ b/packages/services/api/src/modules/graph/module.graphql.ts @@ -1,5 +1,5 @@ import { gql } from 'graphql-modules'; export default gql` - extend schema + extend schema @link(url: "https://specs.apollo.dev/link/v1.0") `; From 5e5754025ad738529dcf15a782d2d38f1394aecb Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Wed, 23 Sep 2026 12:09:46 +0200 Subject: [PATCH 06/14] cascade delete on "schema_versions"."source_schema_version_id" --- packages/migrations/src/actions/2026.09.18T00-00-00.graphs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/migrations/src/actions/2026.09.18T00-00-00.graphs.ts b/packages/migrations/src/actions/2026.09.18T00-00-00.graphs.ts index 1ea50b5178..94ba756865 100644 --- a/packages/migrations/src/actions/2026.09.18T00-00-00.graphs.ts +++ b/packages/migrations/src/actions/2026.09.18T00-00-00.graphs.ts @@ -37,7 +37,7 @@ export default { ALTER TABLE "schema_versions" ADD COLUMN "graph_id" uuid REFERENCES "graphs"("id") ON DELETE SET NULL , ADD COLUMN "graph_metadata" jsonb - , ADD COLUMN "source_schema_version_id" uuid REFERENCES "schema_versions"."id" + , ADD COLUMN "source_schema_version_id" uuid REFERENCES "schema_versions"("id") ON DELETE CASCADE ; `, } satisfies MigrationExecutor; From 1269680bfbba1444a504a5d8ae62d0820cf418d6 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Wed, 23 Sep 2026 12:21:37 +0200 Subject: [PATCH 07/14] pass graph store --- packages/services/usage/src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/services/usage/src/index.ts b/packages/services/usage/src/index.ts index 75fbd2fc8c..b730c1dee2 100644 --- a/packages/services/usage/src/index.ts +++ b/packages/services/usage/src/index.ts @@ -1,5 +1,6 @@ #!/usr/bin/env node import 'reflect-metadata'; +import { GraphStore } from '@hive/api/modules/graph/providers/graph-store'; import { PrometheusConfig } from '@hive/api/modules/shared/providers/prometheus-config'; import { TargetStore } from '@hive/api/modules/target/providers/target-store'; import { TargetsByIdCache } from '@hive/api/modules/target/providers/targets-by-id-cache'; @@ -95,7 +96,8 @@ async function main() { }); const prometheusConfig = new PrometheusConfig(!!env.prometheus); - const targetStore = new TargetStore(server.log, pgPool); + const graphStore = new GraphStore(server.log, pgPool); + const targetStore = new TargetStore(server.log, graphStore, pgPool); const targetsByIdCache = new TargetsByIdCache(redis, targetStore, prometheusConfig); const targetsBySlugCache = new TargetsBySlugCache(redis, targetStore, prometheusConfig); const targetTokenCache = new TargetTokenCache(redis, pgPool, prometheusConfig); From 043f90cfb3f48effad08625b8373e2c90f0ba184 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Wed, 23 Sep 2026 12:28:20 +0200 Subject: [PATCH 08/14] sync db types --- packages/services/storage/src/db/types.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/services/storage/src/db/types.ts b/packages/services/storage/src/db/types.ts index c99870d14b..5453c06f16 100644 --- a/packages/services/storage/src/db/types.ts +++ b/packages/services/storage/src/db/types.ts @@ -163,6 +163,17 @@ export interface graphile_worker_deduplication { task_name: string; } +export interface graphs { + config: any; + created_at: Date; + id: string; + name: string; + organization_id: string; + project_id: string; + source_graph_id: string | null; + target_id: string; +} + export interface group_members { created_at: Date | null; group_id: string | null; @@ -550,6 +561,8 @@ export interface schema_versions { diff_schema_version_id: string | null; github_repository: string | null; github_sha: string | null; + graph_id: string | null; + graph_metadata: any | null; has_contract_composition_errors: boolean | null; has_persisted_schema_changes: boolean | null; id: string; @@ -561,6 +574,7 @@ export interface schema_versions { record_version: string | null; schema_composition_errors: any | null; schema_metadata: any | null; + source_schema_version_id: string | null; supergraph_changes: any | null; supergraph_sdl: string | null; tags: Array | null; @@ -678,6 +692,7 @@ export interface DBTables { document_preflight_scripts: document_preflight_scripts; email_verifications: email_verifications; graphile_worker_deduplication: graphile_worker_deduplication; + graphs: graphs; group_members: group_members; group_role_assignments: group_role_assignments; groups: groups; From b9874b3ab7128ff6199940a9858e310aee976dc2 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Wed, 23 Sep 2026 12:32:22 +0200 Subject: [PATCH 09/14] add constraint for unique graph name target combination --- packages/migrations/src/actions/2026.09.18T00-00-00.graphs.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/migrations/src/actions/2026.09.18T00-00-00.graphs.ts b/packages/migrations/src/actions/2026.09.18T00-00-00.graphs.ts index 94ba756865..e2faa76276 100644 --- a/packages/migrations/src/actions/2026.09.18T00-00-00.graphs.ts +++ b/packages/migrations/src/actions/2026.09.18T00-00-00.graphs.ts @@ -27,6 +27,7 @@ export default { , "source_graph_id" uuid REFERENCES "graphs"("id") ON DELETE CASCADE , "created_at" timestamptz NOT NULL DEFAULT now() , PRIMARY KEY ("id") + , CONSTRAINT "graphs_target_id_name_key" UNIQUE ("target_id", "name") ); CREATE INDEX "graphs_organization_id" ON "graphs" ("organization_id"); From 6b5141ec2cd95246126ac15d7bfd083921b796f6 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Wed, 23 Sep 2026 13:08:18 +0200 Subject: [PATCH 10/14] make graph store global (mr worldwide) --- .../services/api/src/modules/graph/providers/graph-store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/services/api/src/modules/graph/providers/graph-store.ts b/packages/services/api/src/modules/graph/providers/graph-store.ts index a57b492dcf..4f75baeea1 100644 --- a/packages/services/api/src/modules/graph/providers/graph-store.ts +++ b/packages/services/api/src/modules/graph/providers/graph-store.ts @@ -28,7 +28,7 @@ export type Graph = z.infer; export type GraphConfig = z.infer; @Injectable({ - scope: Scope.Operation, + scope: Scope.Singleton, global: true, }) export class GraphStore { From e1c56b265f7b16980f351b95c404d66c4ae460cc Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Wed, 23 Sep 2026 13:12:40 +0200 Subject: [PATCH 11/14] fix type checks on tests --- integration-tests/tests/api/schema/check.spec.ts | 1 + integration-tests/tests/api/schema/publish.spec.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/integration-tests/tests/api/schema/check.spec.ts b/integration-tests/tests/api/schema/check.spec.ts index 85405c5695..436436c6c5 100644 --- a/integration-tests/tests/api/schema/check.spec.ts +++ b/integration-tests/tests/api/schema/check.spec.ts @@ -2626,6 +2626,7 @@ test.concurrent( supergraphChanges: null, schemaMetadata: null, metadataAttributes: null, + graph: null, }); await storage.destroy(); diff --git a/integration-tests/tests/api/schema/publish.spec.ts b/integration-tests/tests/api/schema/publish.spec.ts index 91f8a31745..bf316696ad 100644 --- a/integration-tests/tests/api/schema/publish.spec.ts +++ b/integration-tests/tests/api/schema/publish.spec.ts @@ -4641,6 +4641,7 @@ test.concurrent( previousSchemaLogId: null, serviceChanges: null, supergraphChanges: null, + graph: null, }); await storage.destroy(); From 66fe54c971fc996a9e700edca0bb57c6ecaf3afd Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Wed, 23 Sep 2026 13:29:14 +0200 Subject: [PATCH 12/14] add "is_backfilled" column to differenciate between "legacy" and new "graphs" where `"graph_id" = null` needs to be used to find all schema versions belonging to that graph --- packages/migrations/src/actions/2026.09.18T00-00-00.graphs.ts | 1 + .../services/api/src/modules/graph/providers/graph-store.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/packages/migrations/src/actions/2026.09.18T00-00-00.graphs.ts b/packages/migrations/src/actions/2026.09.18T00-00-00.graphs.ts index e2faa76276..05ed58a2ae 100644 --- a/packages/migrations/src/actions/2026.09.18T00-00-00.graphs.ts +++ b/packages/migrations/src/actions/2026.09.18T00-00-00.graphs.ts @@ -25,6 +25,7 @@ export default { , "name" text NOT NULL , "config" jsonb NOT NULL , "source_graph_id" uuid REFERENCES "graphs"("id") ON DELETE CASCADE + , "is_backfilled" boolean NOT NULL DEFAULT false , "created_at" timestamptz NOT NULL DEFAULT now() , PRIMARY KEY ("id") , CONSTRAINT "graphs_target_id_name_key" UNIQUE ("target_id", "name") diff --git a/packages/services/api/src/modules/graph/providers/graph-store.ts b/packages/services/api/src/modules/graph/providers/graph-store.ts index 4f75baeea1..4df0b571e7 100644 --- a/packages/services/api/src/modules/graph/providers/graph-store.ts +++ b/packages/services/api/src/modules/graph/providers/graph-store.ts @@ -21,6 +21,7 @@ const GraphModel = z.object({ name: z.string(), config: GraphConfigModel.nullable(), sourceGraphId: z.string().nullable(), + isBackfilled: z.boolean(), createdAt: z.string(), }); @@ -141,5 +142,6 @@ const graphFields = psql` , "type" , "config" , "source_graph_id" AS "sourceGraphId" + , "is_backfilled" AS "isBackfilled" , to_json("created_at") AS "createdAt" `; From a59f869facfad33a6527442ee2a82252e016364b Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Wed, 23 Sep 2026 13:56:03 +0200 Subject: [PATCH 13/14] register module lol --- packages/services/api/src/create.ts | 2 ++ packages/services/api/src/modules/graph/index.ts | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/services/api/src/create.ts b/packages/services/api/src/create.ts index f356358ef3..2032ae2b46 100644 --- a/packages/services/api/src/create.ts +++ b/packages/services/api/src/create.ts @@ -22,6 +22,7 @@ import { CommerceConfig, provideCommerceConfig, } from './modules/commerce/providers/commerce-client'; +import { graphModule } from './modules/graph'; import { integrationsModule } from './modules/integrations'; import { GITHUB_APP_CONFIG, @@ -96,6 +97,7 @@ const modules = [ auditLogsModule, proposalsModule, supportModule, + graphModule, ]; export function createRegistry({ diff --git a/packages/services/api/src/modules/graph/index.ts b/packages/services/api/src/modules/graph/index.ts index f38178cca2..316a6304f1 100644 --- a/packages/services/api/src/modules/graph/index.ts +++ b/packages/services/api/src/modules/graph/index.ts @@ -1,12 +1,10 @@ import { createModule } from 'graphql-modules'; import { GraphStore } from './providers/graph-store'; -import { resolvers } from './resolvers.generated'; import typeDefs from './module.graphql'; export const graphModule = createModule({ id: 'graph', dirname: __dirname, typeDefs, - resolvers, providers: [GraphStore], }); From b14e188de126178f8f6cb4429a4f34cbebf15e5d Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Wed, 23 Sep 2026 14:27:44 +0200 Subject: [PATCH 14/14] graph id must be nullable --- .../api/src/modules/schema/providers/schema-version-store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/services/api/src/modules/schema/providers/schema-version-store.ts b/packages/services/api/src/modules/schema/providers/schema-version-store.ts index c31b836d5b..995cd24bc3 100644 --- a/packages/services/api/src/modules/schema/providers/schema-version-store.ts +++ b/packages/services/api/src/modules/schema/providers/schema-version-store.ts @@ -1767,7 +1767,7 @@ const SchemaVersionModel = z meta: SchemaVersionMetaModel.nullable(), actionId: z.string(), origin: SchemaVersionOriginModel.nullable(), - graphId: z.string(), + graphId: z.string().nullable(), graphMetadata: GraphMetadataModel.nullable(), sourceSchemaVersionId: z.string().nullable(), })