Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions packages/migrations/src/actions/2026.09.18T00-00-00.graphs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { type MigrationExecutor } from '../pg-migrator';

// type graphs__config = {
// type: 'contract';
// includeTags: Array<string>;
// excludeTargs: Array<string>;
// 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;
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions packages/migrations/src/run-pg-migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
]),
});
12 changes: 12 additions & 0 deletions packages/services/api/src/modules/graph/index.ts
Original file line number Diff line number Diff line change
@@ -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],
});
5 changes: 5 additions & 0 deletions packages/services/api/src/modules/graph/module.graphql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { gql } from 'graphql-modules';

export default gql`
extend schema
`;
145 changes: 145 additions & 0 deletions packages/services/api/src/modules/graph/providers/graph-store.ts
Original file line number Diff line number Diff line change
@@ -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<typeof GraphModel>;
export type GraphConfig = z.infer<typeof GraphConfigModel>;

@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<Graph> {
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<Graph | null> {
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<void> {
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"
`;
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading