mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
refactor schema publishing logic (#7535)
This commit is contained in:
parent
305477f415
commit
cb973b6b62
12 changed files with 518 additions and 613 deletions
|
|
@ -2190,17 +2190,6 @@ test.concurrent(
|
|||
});
|
||||
await storage.destroy();
|
||||
|
||||
const validSdl = /* GraphQL */ `
|
||||
type Query {
|
||||
a(b: B!): String
|
||||
}
|
||||
|
||||
input B {
|
||||
a: String @deprecated(reason: "This field is deprecated")
|
||||
b: String!
|
||||
}
|
||||
`;
|
||||
|
||||
const sdl = /* GraphQL */ `
|
||||
type Query {
|
||||
a(b: B!): String
|
||||
|
|
|
|||
|
|
@ -7,16 +7,8 @@ import {
|
|||
RegistryChecks,
|
||||
type ConditionalBreakingChangeDiffConfig,
|
||||
} from '../registry-checks';
|
||||
import { swapServices } from '../schema-helper';
|
||||
import { shouldUseLatestComposableVersion } from '../schema-manager';
|
||||
import type { PublishInput } from '../schema-publisher';
|
||||
import type {
|
||||
DeletedCompositeSchema,
|
||||
Organization,
|
||||
Project,
|
||||
PushedCompositeSchema,
|
||||
Target,
|
||||
} from './../../../../shared/entities';
|
||||
import { CompositeSchemaInput, swapServices } from '../schema-helper';
|
||||
import type { Organization, Project, Target } from './../../../../shared/entities';
|
||||
import { ProjectType } from './../../../../shared/entities';
|
||||
import { Logger } from './../../../shared/providers/logger';
|
||||
import {
|
||||
|
|
@ -32,7 +24,6 @@ import {
|
|||
SchemaDeleteConclusion,
|
||||
SchemaDeleteResult /* Publish */,
|
||||
SchemaPublishConclusion,
|
||||
SchemaPublishFailureReason,
|
||||
SchemaPublishResult,
|
||||
temp,
|
||||
} from './shared';
|
||||
|
|
@ -94,22 +85,14 @@ export class CompositeModel {
|
|||
}
|
||||
|
||||
@traceFn('Composite modern: diffSchema')
|
||||
async diffSchema({
|
||||
input,
|
||||
latest,
|
||||
}: {
|
||||
input: {
|
||||
sdl: string;
|
||||
serviceName: string;
|
||||
url: string | null;
|
||||
};
|
||||
latest: {
|
||||
schemas: Pick<PushedCompositeSchema, 'service_name' | 'sdl'>[];
|
||||
} | null;
|
||||
async diffSchema(args: {
|
||||
existing: Array<Pick<CompositeSchemaInput, 'sdl' | 'serviceName'>> | null;
|
||||
input: Pick<CompositeSchemaInput, 'sdl' | 'serviceName'>;
|
||||
}) {
|
||||
return this.checks.serviceDiff({
|
||||
existingSdl: latest?.schemas?.find(s => s.service_name === input.serviceName)?.sdl ?? null,
|
||||
incomingSdl: input.sdl,
|
||||
existing:
|
||||
args.existing?.find(schema => schema.serviceName === args.input.serviceName) ?? null,
|
||||
incoming: args.input,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -133,11 +116,11 @@ export class CompositeModel {
|
|||
contracts,
|
||||
failDiffOnDangerousChange,
|
||||
filterNestedChanges,
|
||||
compareToLatestComposableVersion,
|
||||
}: {
|
||||
input: {
|
||||
sdl: string;
|
||||
serviceName: string;
|
||||
url: string | null;
|
||||
input: Pick<CompositeSchemaInput, 'sdl' | 'serviceName'> & {
|
||||
// for a schema check the service url is optional
|
||||
serviceUrl: string | null;
|
||||
};
|
||||
selector: {
|
||||
organizationId: string;
|
||||
|
|
@ -147,13 +130,13 @@ export class CompositeModel {
|
|||
latest: {
|
||||
isComposable: boolean;
|
||||
sdl: string | null;
|
||||
schemas: PushedCompositeSchema[];
|
||||
schemas: CompositeSchemaInput[];
|
||||
contractNames: string[] | null;
|
||||
} | null;
|
||||
latestComposable: {
|
||||
isComposable: boolean;
|
||||
sdl: string | null;
|
||||
schemas: PushedCompositeSchema[];
|
||||
schemas: CompositeSchemaInput[];
|
||||
} | null;
|
||||
baseSchema: string | null;
|
||||
project: Project;
|
||||
|
|
@ -161,6 +144,7 @@ export class CompositeModel {
|
|||
approvedChanges: Map<string, SchemaChangeType>;
|
||||
conditionalBreakingChangeDiffConfig: null | ConditionalBreakingChangeDiffConfig;
|
||||
failDiffOnDangerousChange: null | boolean;
|
||||
compareToLatestComposableVersion: boolean;
|
||||
contracts: Array<
|
||||
ContractInput & {
|
||||
approvedChanges: Map<string, SchemaChangeType> | null;
|
||||
|
|
@ -168,33 +152,21 @@ export class CompositeModel {
|
|||
> | null;
|
||||
filterNestedChanges: boolean;
|
||||
}): Promise<SchemaCheckResult> {
|
||||
const incoming: PushedCompositeSchema = {
|
||||
kind: 'composite',
|
||||
const incoming: CompositeSchemaInput = {
|
||||
id: temp,
|
||||
author: temp,
|
||||
commit: temp,
|
||||
target: selector.targetId,
|
||||
date: Date.now(),
|
||||
sdl: input.sdl,
|
||||
service_name: input.serviceName,
|
||||
service_url:
|
||||
input.url ??
|
||||
latest?.schemas?.find(s => s.service_name === input.serviceName)?.service_url ??
|
||||
'temp',
|
||||
action: 'PUSH',
|
||||
serviceName: input.serviceName,
|
||||
serviceUrl:
|
||||
input.serviceUrl ??
|
||||
latest?.schemas?.find(s => s.serviceName === input.serviceName)?.serviceUrl ??
|
||||
temp,
|
||||
metadata: null,
|
||||
};
|
||||
|
||||
const schemaSwapResult = latest ? swapServices(latest.schemas, incoming) : null;
|
||||
const schemas = schemaSwapResult ? schemaSwapResult.schemas : [incoming];
|
||||
schemas.sort((a, b) => a.service_name.localeCompare(b.service_name));
|
||||
|
||||
const compareToPreviousComposableVersion = shouldUseLatestComposableVersion(
|
||||
selector.targetId,
|
||||
project,
|
||||
organization,
|
||||
);
|
||||
const comparedVersion = compareToPreviousComposableVersion ? latestComposable : latest;
|
||||
schemas.sort((a, b) => a.serviceName.localeCompare(b.serviceName));
|
||||
const comparedVersion = compareToLatestComposableVersion ? latestComposable : latest;
|
||||
|
||||
const checksumCheck = await this.checks.checksum({
|
||||
existing: schemaSwapResult?.existing
|
||||
|
|
@ -346,93 +318,73 @@ export class CompositeModel {
|
|||
contracts,
|
||||
conditionalBreakingChangeDiffConfig,
|
||||
failDiffOnDangerousChange,
|
||||
compareToLatestComposableVersion,
|
||||
}: {
|
||||
input: PublishInput;
|
||||
input: {
|
||||
sdl: string;
|
||||
service: string;
|
||||
url: string | null;
|
||||
metadata: string | null;
|
||||
};
|
||||
project: Project;
|
||||
organization: Organization;
|
||||
target: Target;
|
||||
latest: {
|
||||
isComposable: boolean;
|
||||
sdl: string | null;
|
||||
schemas: PushedCompositeSchema[];
|
||||
schemas: CompositeSchemaInput[];
|
||||
contractNames: string[] | null;
|
||||
} | null;
|
||||
latestComposable: {
|
||||
isComposable: boolean;
|
||||
sdl: string | null;
|
||||
schemas: PushedCompositeSchema[];
|
||||
schemas: CompositeSchemaInput[];
|
||||
} | null;
|
||||
baseSchema: string | null;
|
||||
contracts: Array<ContractInput> | null;
|
||||
conditionalBreakingChangeDiffConfig: null | ConditionalBreakingChangeDiffConfig;
|
||||
failDiffOnDangerousChange: null | boolean;
|
||||
compareToLatestComposableVersion: boolean;
|
||||
}): Promise<SchemaPublishResult> {
|
||||
const incoming: PushedCompositeSchema = {
|
||||
kind: 'composite',
|
||||
const latestSchemaVersion = latest;
|
||||
const latestServiceVersion = latest?.schemas?.find(
|
||||
schema => schema.serviceName === input.service,
|
||||
);
|
||||
|
||||
const serviceUrlCheck = await this.checks.serviceUrl(
|
||||
input.url ?? null,
|
||||
latestServiceVersion?.serviceUrl ?? null,
|
||||
);
|
||||
|
||||
if (serviceUrlCheck.status === 'failed') {
|
||||
return {
|
||||
conclusion: SchemaPublishConclusion.Reject,
|
||||
reasons: [
|
||||
{
|
||||
code: PublishFailureReasonCode.MissingServiceUrl,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const incoming: CompositeSchemaInput = {
|
||||
id: temp,
|
||||
author: input.author,
|
||||
sdl: input.sdl,
|
||||
commit: input.commit,
|
||||
target: target.id,
|
||||
date: Date.now(),
|
||||
service_name: input.service || '',
|
||||
service_url: input.url || null,
|
||||
action: 'PUSH',
|
||||
serviceName: input.service,
|
||||
serviceUrl: serviceUrlCheck.result.serviceUrl,
|
||||
metadata: input.metadata ?? null,
|
||||
};
|
||||
|
||||
const latestVersion = latest;
|
||||
const schemaSwapResult = latestVersion ? swapServices(latestVersion.schemas, incoming) : null;
|
||||
const schemaSwapResult = latestSchemaVersion
|
||||
? swapServices(latestSchemaVersion.schemas, incoming)
|
||||
: null;
|
||||
const previousService = schemaSwapResult?.existing;
|
||||
|
||||
// default to previous service url if not provided.
|
||||
incoming.service_url = incoming.service_url ?? previousService?.service_url ?? '';
|
||||
|
||||
const schemas = schemaSwapResult?.schemas ?? [incoming];
|
||||
schemas.sort((a, b) => a.service_name.localeCompare(b.service_name));
|
||||
const compareToLatestComposable = shouldUseLatestComposableVersion(
|
||||
target.id,
|
||||
project,
|
||||
organization,
|
||||
);
|
||||
const schemaVersionToCompareAgainst = compareToLatestComposable ? latestComposable : latest;
|
||||
|
||||
const [serviceNameCheck, serviceUrlCheck] = await Promise.all([
|
||||
this.checks.serviceName({
|
||||
name: incoming.service_name,
|
||||
}),
|
||||
this.checks.serviceUrl(
|
||||
{
|
||||
url: incoming.service_url,
|
||||
},
|
||||
previousService
|
||||
? {
|
||||
url: previousService.service_url,
|
||||
}
|
||||
: null,
|
||||
),
|
||||
]);
|
||||
|
||||
if (serviceNameCheck.status === 'failed' || serviceUrlCheck.status === 'failed') {
|
||||
const reasons: SchemaPublishFailureReason[] = [];
|
||||
|
||||
if (serviceNameCheck.status === 'failed') {
|
||||
reasons.push({
|
||||
code: PublishFailureReasonCode.MissingServiceName,
|
||||
});
|
||||
}
|
||||
|
||||
if (serviceUrlCheck.status === 'failed') {
|
||||
reasons.push({
|
||||
code: PublishFailureReasonCode.MissingServiceUrl,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
conclusion: SchemaPublishConclusion.Reject,
|
||||
reasons,
|
||||
};
|
||||
}
|
||||
schemas.sort((a, b) => a.serviceName.localeCompare(b.serviceName));
|
||||
const schemaVersionToCompareAgainst = compareToLatestComposableVersion
|
||||
? latestComposable
|
||||
: latest;
|
||||
|
||||
const checksumCheck = await this.checks.checksum({
|
||||
existing: schemaSwapResult?.existing
|
||||
|
|
@ -503,7 +455,7 @@ export class CompositeModel {
|
|||
if (
|
||||
compositionCheck.status === 'failed' &&
|
||||
compositionCheck.reason.errorsBySource.graphql.length > 0 &&
|
||||
!compareToLatestComposable
|
||||
!compareToLatestComposableVersion
|
||||
) {
|
||||
return {
|
||||
conclusion: SchemaPublishConclusion.Reject,
|
||||
|
|
@ -573,7 +525,7 @@ export class CompositeModel {
|
|||
conclusion: SchemaPublishConclusion.Publish,
|
||||
state: {
|
||||
composable: compositionCheck.status === 'completed',
|
||||
initial: latestVersion === null,
|
||||
initial: latestSchemaVersion === null,
|
||||
changes: diffCheck.result?.all ?? diffCheck.reason?.all ?? null,
|
||||
coordinatesDiff:
|
||||
diffCheck.result?.coordinatesDiff ??
|
||||
|
|
@ -619,6 +571,7 @@ export class CompositeModel {
|
|||
conditionalBreakingChangeDiffConfig,
|
||||
contracts,
|
||||
failDiffOnDangerousChange,
|
||||
compareToLatestComposableVersion,
|
||||
}: {
|
||||
input: {
|
||||
serviceName: string;
|
||||
|
|
@ -634,50 +587,22 @@ export class CompositeModel {
|
|||
latest: {
|
||||
isComposable: boolean;
|
||||
sdl: string | null;
|
||||
schemas: PushedCompositeSchema[];
|
||||
schemas: CompositeSchemaInput[];
|
||||
};
|
||||
latestComposable: {
|
||||
isComposable: boolean;
|
||||
sdl: string | null;
|
||||
schemas: PushedCompositeSchema[];
|
||||
schemas: CompositeSchemaInput[];
|
||||
} | null;
|
||||
contracts: Array<ContractInput> | null;
|
||||
conditionalBreakingChangeDiffConfig: null | ConditionalBreakingChangeDiffConfig;
|
||||
failDiffOnDangerousChange: null | boolean;
|
||||
compareToLatestComposableVersion: boolean;
|
||||
}): Promise<SchemaDeleteResult> {
|
||||
const incoming: DeletedCompositeSchema = {
|
||||
kind: 'composite',
|
||||
id: temp,
|
||||
target: selector.target,
|
||||
date: Date.now(),
|
||||
service_name: input.serviceName,
|
||||
action: 'DELETE',
|
||||
};
|
||||
|
||||
const latestVersion = latest;
|
||||
const compareToLatestComposable = shouldUseLatestComposableVersion(
|
||||
selector.target,
|
||||
project,
|
||||
organization,
|
||||
);
|
||||
|
||||
const serviceNameCheck = await this.checks.serviceName({
|
||||
name: incoming.service_name,
|
||||
});
|
||||
|
||||
if (serviceNameCheck.status === 'failed') {
|
||||
return {
|
||||
conclusion: SchemaDeleteConclusion.Reject,
|
||||
reasons: [
|
||||
{
|
||||
code: DeleteFailureReasonCode.MissingServiceName,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const schemas = latestVersion.schemas.filter(s => s.service_name !== input.serviceName);
|
||||
schemas.sort((a, b) => a.service_name.localeCompare(b.service_name));
|
||||
const schemas = latestVersion.schemas.filter(s => s.serviceName !== input.serviceName);
|
||||
schemas.sort((a, b) => a.serviceName.localeCompare(b.serviceName));
|
||||
|
||||
const compositionCheck = await this.checks.composition({
|
||||
targetId: selector.target,
|
||||
|
|
@ -698,7 +623,7 @@ export class CompositeModel {
|
|||
});
|
||||
|
||||
const previousVersionSdl = await this.checks.retrievePreviousVersionSdl({
|
||||
version: compareToLatestComposable ? latestComposable : latest,
|
||||
version: compareToLatestComposableVersion ? latestComposable : latest,
|
||||
organization,
|
||||
project,
|
||||
targetId: selector.target,
|
||||
|
|
@ -744,7 +669,7 @@ export class CompositeModel {
|
|||
compositionCheck.status === 'failed' &&
|
||||
compositionCheck.reason.errorsBySource.graphql.length > 0
|
||||
) {
|
||||
if (!compareToLatestComposable) {
|
||||
if (!compareToLatestComposableVersion) {
|
||||
return {
|
||||
conclusion: SchemaDeleteConclusion.Reject,
|
||||
reasons: [
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { PushedCompositeSchema, SingleSchema } from 'packages/services/api/src/shared/entities';
|
||||
import type { CheckPolicyResponse } from '@hive/policy';
|
||||
import { CompositionFailureError } from '@hive/schema';
|
||||
import type { SchemaChangeType, SchemaCompositionError } from '@hive/storage';
|
||||
|
|
@ -12,6 +11,7 @@ import type {
|
|||
SchemaDiffSkip,
|
||||
SchemaDiffSuccess,
|
||||
} from '../registry-checks';
|
||||
import type { CompositeSchemaInput, SingleSchemaInput } from '../schema-helper';
|
||||
|
||||
export const SchemaPublishConclusion = {
|
||||
/**
|
||||
|
|
@ -180,7 +180,6 @@ export const PublishIgnoreReasonCode = {
|
|||
|
||||
export const PublishFailureReasonCode = {
|
||||
MissingServiceUrl: 'MISSING_SERVICE_URL',
|
||||
MissingServiceName: 'MISSING_SERVICE_NAME',
|
||||
CompositionFailure: 'COMPOSITION_FAILURE',
|
||||
BreakingChanges: 'BREAKING_CHANGES',
|
||||
MetadataParsingFailure: 'METADATA_PARSING_FAILURE',
|
||||
|
|
@ -192,9 +191,6 @@ export type PublishFailureReasonCode =
|
|||
(typeof PublishFailureReasonCode)[keyof typeof PublishFailureReasonCode];
|
||||
|
||||
export type SchemaPublishFailureReason =
|
||||
| {
|
||||
code: (typeof PublishFailureReasonCode)['MissingServiceName'];
|
||||
}
|
||||
| {
|
||||
code: (typeof PublishFailureReasonCode)['MissingServiceUrl'];
|
||||
}
|
||||
|
|
@ -235,8 +231,8 @@ type SchemaPublishSuccess = {
|
|||
message: string;
|
||||
}> | null;
|
||||
compositionErrors: Array<SchemaCompositionError> | null;
|
||||
schema: SingleSchema | PushedCompositeSchema;
|
||||
schemas: [SingleSchema] | PushedCompositeSchema[];
|
||||
schema: SingleSchemaInput | CompositeSchemaInput;
|
||||
schemas: [SingleSchemaInput] | CompositeSchemaInput[];
|
||||
supergraph: string | null;
|
||||
fullSchemaSdl: string | null;
|
||||
tags: null | Array<string>;
|
||||
|
|
@ -285,7 +281,7 @@ export type SchemaDeleteSuccess = {
|
|||
conclusion: (typeof SchemaDeleteConclusion)['Accept'];
|
||||
state: {
|
||||
changes: Array<SchemaChangeType> | null;
|
||||
schemas: PushedCompositeSchema[];
|
||||
schemas: CompositeSchemaInput[];
|
||||
breakingChanges: Array<SchemaChangeType> | null;
|
||||
compositionErrors: Array<SchemaCompositionError> | null;
|
||||
coordinatesDiff: SchemaCoordinatesDiffResult | null;
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import {
|
|||
GetAffectedAppDeployments,
|
||||
RegistryChecks,
|
||||
} from '../registry-checks';
|
||||
import type { PublishInput } from '../schema-publisher';
|
||||
import type { Organization, Project, SingleSchema, Target } from './../../../../shared/entities';
|
||||
import { SingleSchemaInput } from '../schema-helper';
|
||||
import type { Organization, Project, Target } from './../../../../shared/entities';
|
||||
import { Logger } from './../../../shared/providers/logger';
|
||||
import {
|
||||
buildSchemaCheckFailureState,
|
||||
|
|
@ -32,21 +32,11 @@ export class SingleModel {
|
|||
) {}
|
||||
|
||||
@traceFn('Single modern: diffSchema')
|
||||
async diffSchema({
|
||||
input,
|
||||
latest,
|
||||
}: {
|
||||
input: {
|
||||
sdl: string;
|
||||
};
|
||||
latest: {
|
||||
schemas: [SingleSchema];
|
||||
} | null;
|
||||
async diffSchema(args: {
|
||||
incoming: Pick<SingleSchemaInput, 'sdl'>;
|
||||
existing: Pick<SingleSchemaInput, 'sdl'> | null;
|
||||
}) {
|
||||
return this.checks.serviceDiff({
|
||||
existingSdl: latest?.schemas[0]?.sdl ?? null,
|
||||
incomingSdl: input.sdl,
|
||||
});
|
||||
return this.checks.serviceDiff(args);
|
||||
}
|
||||
|
||||
@traceFn('Single modern: check', {
|
||||
|
|
@ -69,9 +59,7 @@ export class SingleModel {
|
|||
failDiffOnDangerousChange,
|
||||
filterNestedChanges,
|
||||
}: {
|
||||
input: {
|
||||
sdl: string;
|
||||
};
|
||||
input: Pick<SingleSchemaInput, 'sdl'>;
|
||||
selector: {
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
|
|
@ -80,12 +68,12 @@ export class SingleModel {
|
|||
latest: {
|
||||
isComposable: boolean;
|
||||
sdl: string | null;
|
||||
schemas: [SingleSchema];
|
||||
schemas: [SingleSchemaInput];
|
||||
} | null;
|
||||
latestComposable: {
|
||||
isComposable: boolean;
|
||||
sdl: string | null;
|
||||
schemas: [SingleSchema];
|
||||
schemas: [SingleSchemaInput];
|
||||
} | null;
|
||||
baseSchema: string | null;
|
||||
project: Project;
|
||||
|
|
@ -95,21 +83,18 @@ export class SingleModel {
|
|||
failDiffOnDangerousChange: boolean;
|
||||
filterNestedChanges: boolean;
|
||||
}): Promise<SchemaCheckResult> {
|
||||
const incoming: SingleSchema = {
|
||||
kind: 'single',
|
||||
const incoming: SingleSchemaInput = {
|
||||
id: temp,
|
||||
author: temp,
|
||||
commit: temp,
|
||||
target: selector.targetId,
|
||||
date: Date.now(),
|
||||
sdl: input.sdl,
|
||||
metadata: null,
|
||||
serviceName: null,
|
||||
serviceUrl: null,
|
||||
};
|
||||
|
||||
const schemas = [incoming] as [SingleSchema];
|
||||
const compareToPreviousComposableVersion =
|
||||
organization.featureFlags.compareToPreviousComposableVersion;
|
||||
const comparedVersion = compareToPreviousComposableVersion ? latestComposable : latest;
|
||||
const schemas = [incoming] as [SingleSchemaInput];
|
||||
const comparedVersion = organization.featureFlags.compareToPreviousComposableVersion
|
||||
? latestComposable
|
||||
: latest;
|
||||
|
||||
const checksumResult = await this.checks.checksum({
|
||||
existing: latest
|
||||
|
|
@ -220,37 +205,37 @@ export class SingleModel {
|
|||
conditionalBreakingChangeDiffConfig,
|
||||
failDiffOnDangerousChange,
|
||||
}: {
|
||||
input: PublishInput;
|
||||
input: {
|
||||
sdl: string;
|
||||
metadata: string | null;
|
||||
};
|
||||
organization: Organization;
|
||||
project: Project;
|
||||
target: Target;
|
||||
latest: {
|
||||
isComposable: boolean;
|
||||
sdl: string | null;
|
||||
schemas: [SingleSchema];
|
||||
schemas: [SingleSchemaInput];
|
||||
} | null;
|
||||
latestComposable: {
|
||||
isComposable: boolean;
|
||||
sdl: string | null;
|
||||
schemas: [SingleSchema];
|
||||
schemas: [SingleSchemaInput];
|
||||
} | null;
|
||||
baseSchema: string | null;
|
||||
conditionalBreakingChangeDiffConfig: null | ConditionalBreakingChangeDiffConfig;
|
||||
failDiffOnDangerousChange: boolean;
|
||||
}): Promise<SchemaPublishResult> {
|
||||
const incoming: SingleSchema = {
|
||||
kind: 'single',
|
||||
const incoming: SingleSchemaInput = {
|
||||
id: temp,
|
||||
author: input.author,
|
||||
sdl: input.sdl,
|
||||
commit: input.commit,
|
||||
target: target.id,
|
||||
date: Date.now(),
|
||||
metadata: input.metadata ?? null,
|
||||
metadata: input.metadata,
|
||||
serviceName: null,
|
||||
serviceUrl: null,
|
||||
};
|
||||
|
||||
const latestVersion = latest;
|
||||
const schemas = [incoming] as [SingleSchema];
|
||||
const schemas = [incoming] as [SingleSchemaInput];
|
||||
const compareToPreviousComposableVersion =
|
||||
organization.featureFlags.compareToPreviousComposableVersion;
|
||||
const comparedVersion = compareToPreviousComposableVersion ? latestComposable : latest;
|
||||
|
|
|
|||
|
|
@ -20,14 +20,12 @@ import type {
|
|||
DateRange,
|
||||
Organization,
|
||||
Project,
|
||||
PushedCompositeSchema,
|
||||
SingleSchema,
|
||||
} from './../../../shared/entities';
|
||||
import { Logger } from './../../shared/providers/logger';
|
||||
import { diffSchemaCoordinates, Inspector, SchemaCoordinatesDiffResult } from './inspector';
|
||||
import { SchemaCheckWarning } from './models/shared';
|
||||
import { CompositionOrchestrator } from './orchestrator/composition-orchestrator';
|
||||
import { extendWithBase, isCompositeSchema, SchemaHelper } from './schema-helper';
|
||||
import { CompositeSchemaInput, extendWithBase, SchemaHelper, SchemaInput } from './schema-helper';
|
||||
|
||||
export type ConditionalBreakingChangeDiffConfig = {
|
||||
period: DateRange;
|
||||
|
|
@ -75,8 +73,6 @@ export type CheckResult<C = unknown, F = unknown, S = unknown> =
|
|||
data?: S;
|
||||
};
|
||||
|
||||
type Schemas = [SingleSchema] | PushedCompositeSchema[];
|
||||
|
||||
type CompositionValidationError = {
|
||||
message: string;
|
||||
source: 'composition';
|
||||
|
|
@ -205,11 +201,11 @@ export class RegistryChecks {
|
|||
*/
|
||||
async checksum(args: {
|
||||
incoming: {
|
||||
schema: SingleSchema | PushedCompositeSchema;
|
||||
schema: SchemaInput;
|
||||
contractNames: null | Array<string>;
|
||||
};
|
||||
existing: null | {
|
||||
schema: SingleSchema | PushedCompositeSchema;
|
||||
schema: SchemaInput;
|
||||
contractNames: null | Array<string>;
|
||||
};
|
||||
}) {
|
||||
|
|
@ -276,7 +272,7 @@ export class RegistryChecks {
|
|||
targetId: string;
|
||||
project: Project;
|
||||
organization: Organization;
|
||||
schemas: Schemas;
|
||||
schemas: Array<SchemaInput>;
|
||||
baseSchema: string | null;
|
||||
contracts: null | ContractsInputType;
|
||||
}) {
|
||||
|
|
@ -337,7 +333,7 @@ export class RegistryChecks {
|
|||
version: {
|
||||
isComposable: boolean;
|
||||
sdl: string | null;
|
||||
schemas: Schemas;
|
||||
schemas: Array<SchemaInput>;
|
||||
} | null;
|
||||
organization: Organization;
|
||||
project: Project;
|
||||
|
|
@ -433,28 +429,20 @@ export class RegistryChecks {
|
|||
*/
|
||||
async serviceDiff(args: {
|
||||
/** The existing SDL */
|
||||
existingSdl: string | null;
|
||||
existing: Pick<SchemaInput, 'sdl'> | null;
|
||||
/** The incoming SDL */
|
||||
incomingSdl: string | null;
|
||||
incoming: Pick<SchemaInput, 'sdl'> | null;
|
||||
}) {
|
||||
let existingSchema: GraphQLSchema | null;
|
||||
let incomingSchema: GraphQLSchema | null;
|
||||
|
||||
try {
|
||||
existingSchema = args.existingSdl
|
||||
? buildSortedSchemaFromSchemaObject(
|
||||
this.helper.createSchemaObject({
|
||||
sdl: args.existingSdl,
|
||||
}),
|
||||
)
|
||||
existingSchema = args.existing
|
||||
? buildSortedSchemaFromSchemaObject(this.helper.createSchemaObject(args.existing))
|
||||
: null;
|
||||
|
||||
incomingSchema = args.incomingSdl
|
||||
? buildSortedSchemaFromSchemaObject(
|
||||
this.helper.createSchemaObject({
|
||||
sdl: args.incomingSdl,
|
||||
}),
|
||||
)
|
||||
incomingSchema = args.incoming
|
||||
? buildSortedSchemaFromSchemaObject(this.helper.createSchemaObject(args.incoming))
|
||||
: null;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to build schema for diff. Skip diff check.');
|
||||
|
|
@ -496,8 +484,8 @@ export class RegistryChecks {
|
|||
includeUrlChanges:
|
||||
| false
|
||||
| {
|
||||
schemasBefore: [SingleSchema] | PushedCompositeSchema[];
|
||||
schemasAfter: [SingleSchema] | PushedCompositeSchema[];
|
||||
schemasBefore: CompositeSchemaInput[];
|
||||
schemasAfter: CompositeSchemaInput[];
|
||||
};
|
||||
/** Whether Federation directive related changes should be filtered out from the list of changes. These would only show up due to an internal bug. */
|
||||
filterOutFederationChanges: boolean;
|
||||
|
|
@ -522,6 +510,8 @@ export class RegistryChecks {
|
|||
? buildSortedSchemaFromSchemaObject(
|
||||
this.helper.createSchemaObject({
|
||||
sdl: args.existingSdl,
|
||||
serviceName: null,
|
||||
serviceUrl: null,
|
||||
}),
|
||||
)
|
||||
: null;
|
||||
|
|
@ -530,6 +520,8 @@ export class RegistryChecks {
|
|||
? buildSortedSchemaFromSchemaObject(
|
||||
this.helper.createSchemaObject({
|
||||
sdl: args.incomingSdl,
|
||||
serviceName: null,
|
||||
serviceUrl: null,
|
||||
}),
|
||||
)
|
||||
: null;
|
||||
|
|
@ -734,8 +726,8 @@ export class RegistryChecks {
|
|||
if (args.includeUrlChanges) {
|
||||
inspectorChanges.push(
|
||||
...detectUrlChanges(
|
||||
args.includeUrlChanges.schemasBefore.filter(isCompositeSchema),
|
||||
args.includeUrlChanges.schemasAfter.filter(isCompositeSchema),
|
||||
args.includeUrlChanges.schemasBefore,
|
||||
args.includeUrlChanges.schemasAfter,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -812,23 +804,6 @@ export class RegistryChecks {
|
|||
} satisfies SchemaDiffSuccess;
|
||||
}
|
||||
|
||||
async serviceName(service: { name: string | null }) {
|
||||
if (!service.name) {
|
||||
this.logger.debug('No service name');
|
||||
return {
|
||||
status: 'failed',
|
||||
reason: 'Service name is required',
|
||||
} satisfies CheckResult;
|
||||
}
|
||||
|
||||
this.logger.debug('Service name is defined');
|
||||
|
||||
return {
|
||||
status: 'completed',
|
||||
result: null,
|
||||
} satisfies CheckResult;
|
||||
}
|
||||
|
||||
private isValidURL(url: string): boolean {
|
||||
try {
|
||||
new URL(url);
|
||||
|
|
@ -839,21 +814,35 @@ export class RegistryChecks {
|
|||
}
|
||||
}
|
||||
|
||||
async serviceUrl(
|
||||
service: { url: string | null },
|
||||
existingService: { url: string | null } | null,
|
||||
) {
|
||||
if (!service.url) {
|
||||
this.logger.debug('No service url');
|
||||
async serviceUrl(newServiceUrl: string | null, existingServiceUrl: string | null) {
|
||||
if (newServiceUrl === null) {
|
||||
if (existingServiceUrl) {
|
||||
return {
|
||||
status: 'completed',
|
||||
result: {
|
||||
status: 'unchanged' as const,
|
||||
serviceUrl: existingServiceUrl,
|
||||
},
|
||||
} satisfies CheckResult;
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'failed',
|
||||
reason: 'Service url is required',
|
||||
} satisfies CheckResult;
|
||||
}
|
||||
|
||||
this.logger.debug('Service url is defined');
|
||||
if (newServiceUrl === existingServiceUrl) {
|
||||
return {
|
||||
status: 'completed',
|
||||
result: {
|
||||
status: 'unchanged' as const,
|
||||
serviceUrl: existingServiceUrl,
|
||||
},
|
||||
} satisfies CheckResult;
|
||||
}
|
||||
|
||||
if (!this.isValidURL(service.url)) {
|
||||
if (!this.isValidURL(newServiceUrl)) {
|
||||
return {
|
||||
status: 'failed',
|
||||
reason: 'Invalid service URL provided',
|
||||
|
|
@ -862,19 +851,13 @@ export class RegistryChecks {
|
|||
|
||||
return {
|
||||
status: 'completed',
|
||||
result:
|
||||
existingService && service.url !== existingService.url
|
||||
? {
|
||||
before: existingService.url,
|
||||
after: service.url,
|
||||
message: service.url
|
||||
? `New service url: ${service.url} (previously: ${existingService.url ?? 'none'})`
|
||||
: `Service url removed (previously: ${existingService.url ?? 'none'})`,
|
||||
status: 'modified' as const,
|
||||
}
|
||||
: {
|
||||
status: 'unchanged' as const,
|
||||
},
|
||||
result: {
|
||||
message: `New service url: ${newServiceUrl} (previously: ${existingServiceUrl ?? 'none'})`,
|
||||
status: 'modified' as const,
|
||||
before: existingServiceUrl,
|
||||
after: newServiceUrl,
|
||||
serviceUrl: newServiceUrl,
|
||||
},
|
||||
} satisfies CheckResult;
|
||||
}
|
||||
|
||||
|
|
@ -948,8 +931,8 @@ export class RegistryChecks {
|
|||
}
|
||||
|
||||
type SubgraphDefinition = {
|
||||
service_name: string;
|
||||
service_url: string | null;
|
||||
serviceName: string;
|
||||
serviceUrl: string | null;
|
||||
};
|
||||
|
||||
export function detectUrlChanges(
|
||||
|
|
@ -964,49 +947,49 @@ export function detectUrlChanges(
|
|||
return [];
|
||||
}
|
||||
|
||||
const nameToCompositeSchemaMap = new Map(subgraphsBefore.map(s => [s.service_name, s]));
|
||||
const nameToCompositeSchemaMap = new Map(subgraphsBefore.map(s => [s.serviceName, s]));
|
||||
const changes: Array<RegistryServiceUrlChangeSerializableChange> = [];
|
||||
|
||||
for (const schema of subgraphsAfter) {
|
||||
const before = nameToCompositeSchemaMap.get(schema.service_name);
|
||||
const before = nameToCompositeSchemaMap.get(schema.serviceName);
|
||||
|
||||
if (before && before.service_url !== schema.service_url) {
|
||||
if (before.service_url != null && schema.service_url != null) {
|
||||
if (before && before.serviceUrl !== schema.serviceUrl) {
|
||||
if (before.serviceUrl != null && schema.serviceUrl != null) {
|
||||
changes.push({
|
||||
type: 'REGISTRY_SERVICE_URL_CHANGED',
|
||||
meta: {
|
||||
serviceName: schema.service_name,
|
||||
serviceName: schema.serviceName,
|
||||
serviceUrls: {
|
||||
old: before.service_url,
|
||||
new: schema.service_url,
|
||||
old: before.serviceUrl,
|
||||
new: schema.serviceUrl,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else if (before.service_url != null && schema.service_url == null) {
|
||||
} else if (before.serviceUrl != null && schema.serviceUrl == null) {
|
||||
changes.push({
|
||||
type: 'REGISTRY_SERVICE_URL_CHANGED',
|
||||
meta: {
|
||||
serviceName: schema.service_name,
|
||||
serviceName: schema.serviceName,
|
||||
serviceUrls: {
|
||||
old: before.service_url,
|
||||
old: before.serviceUrl,
|
||||
new: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else if (before.service_url == null && schema.service_url != null) {
|
||||
} else if (before.serviceUrl == null && schema.serviceUrl != null) {
|
||||
changes.push({
|
||||
type: 'REGISTRY_SERVICE_URL_CHANGED',
|
||||
meta: {
|
||||
serviceName: schema.service_name,
|
||||
serviceName: schema.serviceName,
|
||||
serviceUrls: {
|
||||
old: null,
|
||||
new: schema.service_url,
|
||||
new: schema.serviceUrl,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
throw new Error(
|
||||
`This shouldn't happen (before.service_url=${JSON.stringify(before.service_url)}, schema.service_url=${JSON.stringify(schema.service_url)}).`,
|
||||
`This shouldn't happen (before.serviceUrl=${JSON.stringify(before.serviceUrl)}, schema.serviceUrl=${JSON.stringify(schema.serviceUrl)}).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { Injectable, Scope } from 'graphql-modules';
|
|||
import objectHash from 'object-hash';
|
||||
import type {
|
||||
CompositeSchema,
|
||||
CreateSchemaObjectInput,
|
||||
PushedCompositeSchema,
|
||||
Schema,
|
||||
SchemaObject,
|
||||
|
|
@ -46,15 +47,15 @@ export function serviceExists(schemas: CompositeSchema[], serviceName: string) {
|
|||
}
|
||||
|
||||
export function swapServices(
|
||||
schemas: CompositeSchema[],
|
||||
newSchema: CompositeSchema,
|
||||
schemas: CompositeSchemaInput[],
|
||||
newSchema: CompositeSchemaInput,
|
||||
): {
|
||||
schemas: CompositeSchema[];
|
||||
existing: CompositeSchema | null;
|
||||
schemas: CompositeSchemaInput[];
|
||||
existing: CompositeSchemaInput | null;
|
||||
} {
|
||||
let swapped: CompositeSchema | null = null;
|
||||
let swapped: CompositeSchemaInput | null = null;
|
||||
const output = schemas.map(existing => {
|
||||
if (existing.service_name === newSchema.service_name) {
|
||||
if (existing.serviceName === newSchema.serviceName) {
|
||||
swapped = existing;
|
||||
return newSchema;
|
||||
}
|
||||
|
|
@ -72,10 +73,7 @@ export function swapServices(
|
|||
};
|
||||
}
|
||||
|
||||
export function extendWithBase(
|
||||
schemas: CompositeSchema[] | [SingleSchema],
|
||||
baseSchema: string | null,
|
||||
) {
|
||||
export function extendWithBase(schemas: Array<SchemaInput>, baseSchema: string | null) {
|
||||
if (!baseSchema) {
|
||||
return schemas;
|
||||
}
|
||||
|
|
@ -105,8 +103,6 @@ export function removeDescriptions(documentNode: DocumentNode): DocumentNode {
|
|||
});
|
||||
}
|
||||
|
||||
type CreateSchemaObjectInput = Parameters<typeof createSchemaObject>[0];
|
||||
|
||||
@Injectable({
|
||||
scope: Scope.Operation,
|
||||
global: true,
|
||||
|
|
@ -117,31 +113,60 @@ export class SchemaHelper {
|
|||
return createSchemaObject(schema);
|
||||
}
|
||||
|
||||
createChecksum(schema: SingleSchema | PushedCompositeSchema): string {
|
||||
createChecksum(schema: SchemaInput): string {
|
||||
const hasher = createHash('md5');
|
||||
|
||||
hasher.update(print(sortDocumentNode(this.createSchemaObject(schema).document)), 'utf-8');
|
||||
hasher.update(`service_name: ${schema.serviceName ?? ''}`, 'utf-8');
|
||||
hasher.update(`service_url: ${schema.serviceUrl ?? ''}`, 'utf-8');
|
||||
hasher.update(
|
||||
`service_name: ${
|
||||
'service_name' in schema && typeof schema.service_name === 'string'
|
||||
? schema.service_name
|
||||
: ''
|
||||
}`,
|
||||
'utf-8',
|
||||
);
|
||||
hasher.update(
|
||||
`service_url: ${
|
||||
'service_url' in schema && typeof schema.service_url === 'string' ? schema.service_url : ''
|
||||
}`,
|
||||
'utf-8',
|
||||
);
|
||||
hasher.update(
|
||||
`metadata: ${
|
||||
'metadata' in schema && schema.metadata ? objectHash(JSON.parse(schema.metadata)) : ''
|
||||
}`,
|
||||
`metadata: ${schema.metadata ? objectHash(JSON.parse(schema.metadata)) : ''}`,
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
return hasher.digest('hex');
|
||||
}
|
||||
}
|
||||
|
||||
export function toCompositeSchemaInput(schema: PushedCompositeSchema): CompositeSchemaInput {
|
||||
return {
|
||||
id: schema.id,
|
||||
metadata: schema.metadata,
|
||||
sdl: schema.sdl,
|
||||
serviceName: schema.service_name,
|
||||
// service_url can be null for very old records from 2023
|
||||
// The default value mapping should happen on the database read level
|
||||
// but right now we are doing that here until we refactor the database read level (Storage class)
|
||||
serviceUrl: schema.service_url ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
export function toSingleSchemaInput(schema: SingleSchema): SingleSchemaInput {
|
||||
return {
|
||||
id: schema.id,
|
||||
metadata: schema.metadata,
|
||||
sdl: schema.sdl,
|
||||
// Note: due to a "bug" we inserted service_name for single schemas into the schema_log table.
|
||||
// We set it explicitly to null to avoid any confusion in other parts of the business logic
|
||||
serviceName: null,
|
||||
serviceUrl: null,
|
||||
};
|
||||
}
|
||||
|
||||
export type CompositeSchemaInput = {
|
||||
id: string;
|
||||
sdl: string;
|
||||
serviceName: string;
|
||||
serviceUrl: string;
|
||||
metadata: string | null;
|
||||
};
|
||||
|
||||
export type SingleSchemaInput = {
|
||||
id: string;
|
||||
sdl: string;
|
||||
serviceName: null;
|
||||
serviceUrl: null;
|
||||
metadata: string | null;
|
||||
};
|
||||
|
||||
export type SchemaInput = CompositeSchemaInput | SingleSchemaInput;
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ export class SchemaManager {
|
|||
},
|
||||
});
|
||||
|
||||
const [organization, project, latestSchemas] = await Promise.all([
|
||||
const [organization, project, target] = await Promise.all([
|
||||
this.storage.getOrganization({
|
||||
organizationId: selector.organizationId,
|
||||
}),
|
||||
|
|
@ -170,11 +170,10 @@ export class SchemaManager {
|
|||
organizationId: selector.organizationId,
|
||||
projectId: selector.projectId,
|
||||
}),
|
||||
this.storage.getLatestSchemas({
|
||||
this.storage.getTarget({
|
||||
organizationId: selector.organizationId,
|
||||
projectId: selector.projectId,
|
||||
targetId: selector.targetId,
|
||||
onlyComposable: input.onlyComposable,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
|
@ -185,21 +184,26 @@ export class SchemaManager {
|
|||
};
|
||||
}
|
||||
|
||||
const latestSchemas = await this.getLatestSchemaVersionWithSchemaLogs({
|
||||
target,
|
||||
onlyComposable: input.onlyComposable,
|
||||
});
|
||||
|
||||
const existingServices = ensureCompositeSchemas(latestSchemas ? latestSchemas.schemas : []);
|
||||
const services = existingServices
|
||||
// remove provided services from the list
|
||||
.filter(service => !input.services.some(s => s.name === service.service_name))
|
||||
.map(service => ({
|
||||
service_name: service.service_name,
|
||||
serviceName: service.service_name,
|
||||
sdl: service.sdl,
|
||||
service_url: service.service_url,
|
||||
serviceUrl: service.service_url,
|
||||
}))
|
||||
// add provided services to the list
|
||||
.concat(
|
||||
input.services.map(service => ({
|
||||
service_name: service.name,
|
||||
serviceName: service.name,
|
||||
sdl: service.sdl,
|
||||
service_url: service.url ?? null,
|
||||
serviceUrl: service.url ?? null,
|
||||
})),
|
||||
)
|
||||
.map(service => this.schemaHelper.createSchemaObject(service));
|
||||
|
|
@ -346,6 +350,29 @@ export class SchemaManager {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the latest schema version including the schema logs.
|
||||
*/
|
||||
async getLatestSchemaVersionWithSchemaLogs(args: { target: Target; onlyComposable?: boolean }) {
|
||||
const schemaVersion = await (args.onlyComposable
|
||||
? this.getMaybeLatestValidVersion(args.target)
|
||||
: this.getMaybeLatestVersion(args.target));
|
||||
|
||||
if (!schemaVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const schemas = await this.storage.getSchemasOfVersion({
|
||||
versionId: schemaVersion.id,
|
||||
includeMetadata: true,
|
||||
});
|
||||
|
||||
return {
|
||||
version: schemaVersion,
|
||||
schemas,
|
||||
};
|
||||
}
|
||||
|
||||
async getPaginatedSchemaVersionsForTargetId(args: {
|
||||
targetId: string;
|
||||
organizationId: string;
|
||||
|
|
@ -1228,8 +1255,8 @@ export class SchemaManager {
|
|||
ensureCompositeSchemas(schemas).map(s =>
|
||||
this.schemaHelper.createSchemaObject({
|
||||
sdl: s.sdl,
|
||||
service_name: s.service_name,
|
||||
service_url: s.service_url,
|
||||
serviceName: s.service_name,
|
||||
serviceUrl: s.service_url,
|
||||
}),
|
||||
),
|
||||
{
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import type {
|
|||
} from '@hive/storage';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import * as Types from '../../../__generated__/types';
|
||||
import { Organization, Project, ProjectType, Schema, Target } from '../../../shared/entities';
|
||||
import { Organization, Project, ProjectType, Target } from '../../../shared/entities';
|
||||
import { HiveError } from '../../../shared/errors';
|
||||
import { createPeriod } from '../../../shared/helpers';
|
||||
import { isGitHubRepositoryString } from '../../../shared/is-github-repository-string';
|
||||
|
|
@ -54,7 +54,13 @@ import {
|
|||
} from './models/shared';
|
||||
import { SingleModel } from './models/single';
|
||||
import type { ConditionalBreakingChangeDiffConfig } from './registry-checks';
|
||||
import { ensureCompositeSchemas, ensureSingleSchema } from './schema-helper';
|
||||
import {
|
||||
ensureCompositeSchemas,
|
||||
ensureSingleSchema,
|
||||
SchemaInput,
|
||||
toCompositeSchemaInput,
|
||||
toSingleSchemaInput,
|
||||
} from './schema-helper';
|
||||
import { SchemaManager, shouldUseLatestComposableVersion } from './schema-manager';
|
||||
import { SchemaVersionHelper } from './schema-version-helper';
|
||||
|
||||
|
|
@ -102,6 +108,10 @@ type BreakPromise<T> = T extends Promise<infer U> ? U : never;
|
|||
|
||||
type PublishResult =
|
||||
| BreakPromise<ReturnType<SchemaPublisher['internalPublish']>>
|
||||
| {
|
||||
readonly __typename: 'SchemaPublishMissingServiceError';
|
||||
message: 'Missing service name';
|
||||
}
|
||||
| {
|
||||
readonly __typename: 'SchemaPublishRetry';
|
||||
readonly reason: string;
|
||||
|
|
@ -327,37 +337,26 @@ export class SchemaPublisher {
|
|||
},
|
||||
});
|
||||
|
||||
const [target, project, organization, latestVersion, latestComposableVersion, schemaProposal] =
|
||||
await Promise.all([
|
||||
this.storage.getTarget({
|
||||
organizationId: selector.organizationId,
|
||||
projectId: selector.projectId,
|
||||
targetId: selector.targetId,
|
||||
}),
|
||||
this.storage.getProject({
|
||||
organizationId: selector.organizationId,
|
||||
projectId: selector.projectId,
|
||||
}),
|
||||
this.storage.getOrganization({
|
||||
organizationId: selector.organizationId,
|
||||
}),
|
||||
this.storage.getLatestSchemas({
|
||||
organizationId: selector.organizationId,
|
||||
projectId: selector.projectId,
|
||||
targetId: selector.targetId,
|
||||
}),
|
||||
this.storage.getLatestSchemas({
|
||||
organizationId: selector.organizationId,
|
||||
projectId: selector.projectId,
|
||||
targetId: selector.targetId,
|
||||
onlyComposable: true,
|
||||
}),
|
||||
input.schemaProposalId
|
||||
? this.schemaProposals.getProposal({
|
||||
id: input.schemaProposalId,
|
||||
})
|
||||
: null,
|
||||
]);
|
||||
const [target, project, organization, schemaProposal] = await Promise.all([
|
||||
this.storage.getTarget({
|
||||
organizationId: selector.organizationId,
|
||||
projectId: selector.projectId,
|
||||
targetId: selector.targetId,
|
||||
}),
|
||||
this.storage.getProject({
|
||||
organizationId: selector.organizationId,
|
||||
projectId: selector.projectId,
|
||||
}),
|
||||
this.storage.getOrganization({
|
||||
organizationId: selector.organizationId,
|
||||
}),
|
||||
|
||||
input.schemaProposalId
|
||||
? this.schemaProposals.getProposal({
|
||||
id: input.schemaProposalId,
|
||||
})
|
||||
: null,
|
||||
]);
|
||||
|
||||
if (input.schemaProposalId && schemaProposal?.targetId !== selector.targetId) {
|
||||
return {
|
||||
|
|
@ -374,6 +373,16 @@ export class SchemaPublisher {
|
|||
} as const;
|
||||
}
|
||||
|
||||
const [latestVersion, latestComposableVersion] = await Promise.all([
|
||||
this.schemaManager.getLatestSchemaVersionWithSchemaLogs({
|
||||
target,
|
||||
}),
|
||||
this.schemaManager.getLatestSchemaVersionWithSchemaLogs({
|
||||
target,
|
||||
onlyComposable: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
if (input.service) {
|
||||
let serviceExists = false;
|
||||
if (latestVersion?.schemas) {
|
||||
|
|
@ -398,11 +407,6 @@ export class SchemaPublisher {
|
|||
}
|
||||
}
|
||||
|
||||
const [latestSchemaVersion, latestComposableSchemaVersion] = await Promise.all([
|
||||
this.schemaManager.getMaybeLatestVersion(target),
|
||||
this.schemaManager.getMaybeLatestValidVersion(target),
|
||||
]);
|
||||
|
||||
function increaseSchemaCheckCountMetric(conclusion: 'rejected' | 'accepted') {
|
||||
schemaCheckCount.inc({
|
||||
model: 'modern',
|
||||
|
|
@ -593,9 +597,9 @@ export class SchemaPublisher {
|
|||
selector,
|
||||
});
|
||||
|
||||
const latestSchemaVersionContracts = latestSchemaVersion
|
||||
const latestSchemaVersionContracts = latestVersion
|
||||
? await this.contracts.getContractVersionsForSchemaVersion({
|
||||
schemaVersionId: latestSchemaVersion.id,
|
||||
schemaVersionId: latestVersion.version.id,
|
||||
})
|
||||
: null;
|
||||
|
||||
|
|
@ -607,14 +611,12 @@ export class SchemaPublisher {
|
|||
if (input.schemaProposalId) {
|
||||
try {
|
||||
const diffSchema = await this.models[project.type].diffSchema({
|
||||
input: {
|
||||
existing: latestVersion
|
||||
? toSingleSchemaInput(ensureSingleSchema(latestVersion.schemas))
|
||||
: null,
|
||||
incoming: {
|
||||
sdl,
|
||||
},
|
||||
latest: latestVersion
|
||||
? {
|
||||
schemas: [ensureSingleSchema(latestVersion.schemas)],
|
||||
}
|
||||
: null,
|
||||
});
|
||||
if ('result' in diffSchema) {
|
||||
proposalChanges = diffSchema.result ?? null;
|
||||
|
|
@ -625,20 +627,22 @@ export class SchemaPublisher {
|
|||
}
|
||||
|
||||
checkResult = await this.models[ProjectType.SINGLE].check({
|
||||
input,
|
||||
input: {
|
||||
sdl: input.sdl,
|
||||
},
|
||||
selector,
|
||||
latest: latestVersion
|
||||
? {
|
||||
isComposable: latestVersion.valid,
|
||||
sdl: latestSchemaVersion?.compositeSchemaSDL ?? null,
|
||||
schemas: [ensureSingleSchema(latestVersion.schemas)],
|
||||
isComposable: latestVersion.version.isComposable,
|
||||
sdl: latestVersion.version.compositeSchemaSDL,
|
||||
schemas: [toSingleSchemaInput(ensureSingleSchema(latestVersion.schemas))],
|
||||
}
|
||||
: null,
|
||||
latestComposable: latestComposableVersion
|
||||
? {
|
||||
isComposable: latestComposableVersion.valid,
|
||||
sdl: latestComposableSchemaVersion?.compositeSchemaSDL ?? null,
|
||||
schemas: [ensureSingleSchema(latestComposableVersion.schemas)],
|
||||
isComposable: latestComposableVersion.version.isComposable,
|
||||
sdl: latestComposableVersion.version.compositeSchemaSDL,
|
||||
schemas: [toSingleSchemaInput(ensureSingleSchema(latestComposableVersion.schemas))],
|
||||
}
|
||||
: null,
|
||||
baseSchema,
|
||||
|
|
@ -665,12 +669,9 @@ export class SchemaPublisher {
|
|||
input: {
|
||||
sdl,
|
||||
serviceName: input.service,
|
||||
url: input.url ?? null,
|
||||
},
|
||||
latest: latestVersion
|
||||
? {
|
||||
schemas: ensureCompositeSchemas(latestVersion.schemas),
|
||||
}
|
||||
existing: latestVersion
|
||||
? ensureCompositeSchemas(latestVersion.schemas).map(toCompositeSchemaInput)
|
||||
: null,
|
||||
});
|
||||
if ('result' in diffSchema) {
|
||||
|
|
@ -685,23 +686,25 @@ export class SchemaPublisher {
|
|||
input: {
|
||||
sdl,
|
||||
serviceName: input.service,
|
||||
url: input.url ?? null,
|
||||
serviceUrl: input.url ?? null,
|
||||
},
|
||||
selector,
|
||||
latest: latestVersion
|
||||
? {
|
||||
isComposable: latestVersion.valid,
|
||||
sdl: latestSchemaVersion?.compositeSchemaSDL ?? null,
|
||||
schemas: ensureCompositeSchemas(latestVersion.schemas),
|
||||
isComposable: latestVersion.version.isComposable,
|
||||
sdl: latestVersion.version.compositeSchemaSDL,
|
||||
schemas: ensureCompositeSchemas(latestVersion.schemas).map(toCompositeSchemaInput),
|
||||
contractNames:
|
||||
latestSchemaVersionContracts?.edges.map(edge => edge.node.contractName) ?? null,
|
||||
}
|
||||
: null,
|
||||
latestComposable: latestComposableVersion
|
||||
? {
|
||||
isComposable: latestComposableVersion.valid,
|
||||
sdl: latestComposableSchemaVersion?.compositeSchemaSDL ?? null,
|
||||
schemas: ensureCompositeSchemas(latestComposableVersion.schemas),
|
||||
isComposable: latestComposableVersion.version.isComposable,
|
||||
sdl: latestComposableVersion.version.compositeSchemaSDL,
|
||||
schemas: ensureCompositeSchemas(latestComposableVersion.schemas).map(
|
||||
toCompositeSchemaInput,
|
||||
),
|
||||
}
|
||||
: null,
|
||||
baseSchema,
|
||||
|
|
@ -716,7 +719,12 @@ export class SchemaPublisher {
|
|||
conditionalBreakingChangeDiffConfig:
|
||||
conditionalBreakingChangeConfiguration?.conditionalBreakingChangeDiffConfig ?? null,
|
||||
failDiffOnDangerousChange,
|
||||
filterNestedChanges: true,
|
||||
compareToLatestComposableVersion: shouldUseLatestComposableVersion(
|
||||
selector.targetId,
|
||||
project,
|
||||
organization,
|
||||
),
|
||||
filterNestedChanges: !input.schemaProposalId,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
|
|
@ -736,7 +744,7 @@ export class SchemaPublisher {
|
|||
serviceUrl: input.url ?? null,
|
||||
meta: input.meta ?? null,
|
||||
targetId: target.id,
|
||||
schemaVersionId: latestVersion?.versionId ?? null,
|
||||
schemaVersionId: latestVersion?.version.id ?? null,
|
||||
isSuccess: false,
|
||||
breakingSchemaChanges: checkResult.state.schemaChanges?.breaking ?? null,
|
||||
safeSchemaChanges: checkResult.state.schemaChanges?.safe ?? null,
|
||||
|
|
@ -792,7 +800,7 @@ export class SchemaPublisher {
|
|||
serviceUrl: input.url ?? null,
|
||||
meta: input.meta ?? null,
|
||||
targetId: target.id,
|
||||
schemaVersionId: latestVersion?.versionId ?? null,
|
||||
schemaVersionId: latestVersion?.version.id ?? null,
|
||||
isSuccess: true,
|
||||
breakingSchemaChanges: checkResult.state?.schemaChanges?.breaking ?? null,
|
||||
safeSchemaChanges: checkResult.state?.schemaChanges?.safe ?? null,
|
||||
|
|
@ -834,14 +842,14 @@ export class SchemaPublisher {
|
|||
});
|
||||
this.logger.info('created successful schema check. (schemaCheckId=%s)', schemaCheck.id);
|
||||
} else if (checkResult.conclusion === SchemaCheckConclusion.Skip) {
|
||||
if (!latestVersion || !latestSchemaVersion) {
|
||||
if (!latestVersion) {
|
||||
throw new Error('This cannot happen 1 :)');
|
||||
}
|
||||
|
||||
const [compositeSchemaSdl, supergraphSdl, compositionErrors] = await Promise.all([
|
||||
this.schemaVersionHelper.getCompositeSchemaSdl(latestSchemaVersion),
|
||||
this.schemaVersionHelper.getSupergraphSdl(latestSchemaVersion),
|
||||
this.schemaVersionHelper.getSchemaCompositionErrors(latestSchemaVersion),
|
||||
this.schemaVersionHelper.getCompositeSchemaSdl(latestVersion.version),
|
||||
this.schemaVersionHelper.getSupergraphSdl(latestVersion.version),
|
||||
this.schemaVersionHelper.getSchemaCompositionErrors(latestVersion.version),
|
||||
]);
|
||||
|
||||
schemaCheck = await this.storage.createSchemaCheck({
|
||||
|
|
@ -850,7 +858,7 @@ export class SchemaPublisher {
|
|||
serviceUrl: input.url ?? null,
|
||||
meta: input.meta ?? null,
|
||||
targetId: target.id,
|
||||
schemaVersionId: latestSchemaVersion.id ?? null,
|
||||
schemaVersionId: latestVersion.version.id ?? null,
|
||||
breakingSchemaChanges: null,
|
||||
safeSchemaChanges: null,
|
||||
schemaPolicyWarnings: null,
|
||||
|
|
@ -963,14 +971,14 @@ export class SchemaPublisher {
|
|||
|
||||
// SchemaCheckConclusion.Skip
|
||||
|
||||
if (!latestVersion || !latestSchemaVersion) {
|
||||
if (!latestVersion) {
|
||||
throw new Error('This cannot happen 2 :)');
|
||||
}
|
||||
|
||||
if (latestSchemaVersion.isComposable) {
|
||||
if (latestVersion.version.isComposable) {
|
||||
increaseSchemaCheckCountMetric('accepted');
|
||||
const contracts = await this.contracts.getContractVersionsForSchemaVersion({
|
||||
schemaVersionId: latestSchemaVersion.id,
|
||||
schemaVersionId: latestVersion.version.id,
|
||||
});
|
||||
const failedContractCompositionCount =
|
||||
contracts?.edges.filter(edge => edge.node.schemaCompositionErrors !== null).length ?? 0;
|
||||
|
|
@ -999,7 +1007,7 @@ export class SchemaPublisher {
|
|||
conclusion: SchemaCheckConclusion.Failure,
|
||||
changes: null,
|
||||
breakingChanges: null,
|
||||
compositionErrors: latestSchemaVersion.schemaCompositionErrors,
|
||||
compositionErrors: latestVersion.version.schemaCompositionErrors,
|
||||
warnings: null,
|
||||
errors: null,
|
||||
schemaCheckId: schemaCheck?.id ?? null,
|
||||
|
|
@ -1084,11 +1092,11 @@ export class SchemaPublisher {
|
|||
|
||||
// SchemaCheckConclusion.Skip
|
||||
|
||||
if (!latestVersion || !latestSchemaVersion) {
|
||||
if (!latestVersion) {
|
||||
throw new Error('This cannot happen 3 :)');
|
||||
}
|
||||
|
||||
if (latestSchemaVersion.isComposable) {
|
||||
if (latestVersion.version.isComposable) {
|
||||
increaseSchemaCheckCountMetric('accepted');
|
||||
return {
|
||||
__typename: 'SchemaCheckSuccess',
|
||||
|
|
@ -1101,7 +1109,7 @@ export class SchemaPublisher {
|
|||
}
|
||||
|
||||
const contractVersions = await this.contracts.getContractVersionsForSchemaVersion({
|
||||
schemaVersionId: latestSchemaVersion.id,
|
||||
schemaVersionId: latestVersion.version.id,
|
||||
});
|
||||
|
||||
increaseSchemaCheckCountMetric('rejected');
|
||||
|
|
@ -1111,7 +1119,7 @@ export class SchemaPublisher {
|
|||
changes: [],
|
||||
warnings: [],
|
||||
errors: [
|
||||
...(latestSchemaVersion.schemaCompositionErrors?.map(error => ({
|
||||
...(latestVersion.version.schemaCompositionErrors?.map(error => ({
|
||||
message: error.message,
|
||||
source: error.source,
|
||||
})) ?? []),
|
||||
|
|
@ -1184,32 +1192,40 @@ export class SchemaPublisher {
|
|||
selector.targetId,
|
||||
);
|
||||
|
||||
const target = await this.storage.getTarget({
|
||||
organizationId: selector.organizationId,
|
||||
projectId: selector.projectId,
|
||||
targetId: selector.targetId,
|
||||
});
|
||||
|
||||
const [contracts, latestVersion, latestSchemas] = await Promise.all([
|
||||
this.contracts.getActiveContractsByTargetId({ targetId: selector.targetId }),
|
||||
this.schemaManager.getMaybeLatestVersion(target),
|
||||
input.service
|
||||
? this.storage.getLatestSchemas({
|
||||
organizationId: selector.organizationId,
|
||||
projectId: selector.projectId,
|
||||
targetId: selector.targetId,
|
||||
})
|
||||
: Promise.resolve(),
|
||||
const [target, project] = await Promise.all([
|
||||
this.storage.getTarget({
|
||||
organizationId: selector.organizationId,
|
||||
projectId: selector.projectId,
|
||||
targetId: selector.targetId,
|
||||
}),
|
||||
this.storage.getProject({
|
||||
organizationId: selector.organizationId,
|
||||
projectId: selector.projectId,
|
||||
}),
|
||||
]);
|
||||
|
||||
// If trying to push with a service name and there are existing services
|
||||
if (input.service) {
|
||||
const [contracts, latestVersion] = await Promise.all([
|
||||
this.contracts.getActiveContractsByTargetId({ targetId: selector.targetId }),
|
||||
this.schemaManager.getLatestSchemaVersionWithSchemaLogs({
|
||||
target,
|
||||
}),
|
||||
]);
|
||||
|
||||
if (project.type !== Types.ProjectType.SINGLE) {
|
||||
if (!input.service) {
|
||||
return {
|
||||
__typename: 'SchemaPublishMissingServiceError' as const,
|
||||
message: 'Missing service name',
|
||||
} as const;
|
||||
}
|
||||
|
||||
let serviceExists = false;
|
||||
if (latestSchemas?.schemas) {
|
||||
serviceExists = !!ensureCompositeSchemas(latestSchemas.schemas).find(
|
||||
if (latestVersion?.schemas) {
|
||||
serviceExists = !!ensureCompositeSchemas(latestVersion.schemas).find(
|
||||
({ service_name }) => service_name === input.service,
|
||||
);
|
||||
}
|
||||
|
||||
// this is a new service. Validate the service name.
|
||||
if (!serviceExists && !isValidServiceName(input.service)) {
|
||||
return {
|
||||
|
|
@ -1241,7 +1257,7 @@ export class SchemaPublisher {
|
|||
// We include the latest version ID to avoid caching a schema publication that targets different versions.
|
||||
// When deleting a schema, and publishing it again, the latest version ID will be different.
|
||||
// If we don't include it, the cache will return the previous result.
|
||||
latestVersionId: latestVersion?.id,
|
||||
latestVersionId: latestVersion?.version.id,
|
||||
}),
|
||||
)
|
||||
.update(this.session.id)
|
||||
|
|
@ -1346,55 +1362,48 @@ export class SchemaPublisher {
|
|||
signal,
|
||||
},
|
||||
async () => {
|
||||
const [organization, project, target, latestVersion, latestComposableVersion, baseSchema] =
|
||||
await Promise.all([
|
||||
this.storage.getOrganization({
|
||||
organizationId: selector.organizationId,
|
||||
}),
|
||||
this.storage.getProject({
|
||||
organizationId: selector.organizationId,
|
||||
projectId: selector.projectId,
|
||||
}),
|
||||
this.storage.getTarget({
|
||||
organizationId: selector.organizationId,
|
||||
projectId: selector.projectId,
|
||||
targetId: selector.targetId,
|
||||
}),
|
||||
this.storage.getLatestSchemas({
|
||||
organizationId: selector.organizationId,
|
||||
projectId: selector.projectId,
|
||||
targetId: selector.targetId,
|
||||
}),
|
||||
this.storage.getLatestSchemas({
|
||||
organizationId: selector.organizationId,
|
||||
projectId: selector.projectId,
|
||||
targetId: selector.targetId,
|
||||
onlyComposable: true,
|
||||
}),
|
||||
this.storage.getBaseSchema({
|
||||
organizationId: selector.organizationId,
|
||||
projectId: selector.projectId,
|
||||
targetId: selector.targetId,
|
||||
}),
|
||||
]);
|
||||
|
||||
const [latestSchemaVersion, latestComposableSchemaVersion] = await Promise.all([
|
||||
this.schemaManager.getMaybeLatestVersion(target),
|
||||
this.schemaManager.getMaybeLatestValidVersion(target),
|
||||
const [organization, project, target] = await Promise.all([
|
||||
this.storage.getOrganization({
|
||||
organizationId: selector.organizationId,
|
||||
}),
|
||||
this.storage.getProject({
|
||||
organizationId: selector.organizationId,
|
||||
projectId: selector.projectId,
|
||||
}),
|
||||
this.storage.getTarget({
|
||||
organizationId: selector.organizationId,
|
||||
projectId: selector.projectId,
|
||||
targetId: selector.targetId,
|
||||
}),
|
||||
]);
|
||||
|
||||
const compareToPreviousComposableVersion = shouldUseLatestComposableVersion(
|
||||
selector.targetId,
|
||||
project,
|
||||
organization,
|
||||
);
|
||||
|
||||
schemaDeleteCount.inc({ model: 'modern', projectType: project.type });
|
||||
|
||||
if (project.type !== ProjectType.FEDERATION && project.type !== ProjectType.STITCHING) {
|
||||
throw new HiveError(`${project.type} project not supported`);
|
||||
}
|
||||
|
||||
const [latestVersion, latestComposableVersion, baseSchema] = await Promise.all([
|
||||
this.schemaManager.getLatestSchemaVersionWithSchemaLogs({
|
||||
target,
|
||||
}),
|
||||
this.schemaManager.getLatestSchemaVersionWithSchemaLogs({
|
||||
target,
|
||||
onlyComposable: true,
|
||||
}),
|
||||
this.storage.getBaseSchema({
|
||||
organizationId: selector.organizationId,
|
||||
projectId: selector.projectId,
|
||||
targetId: selector.targetId,
|
||||
}),
|
||||
]);
|
||||
|
||||
const compareToLatestComposableVersion = shouldUseLatestComposableVersion(
|
||||
selector.targetId,
|
||||
project,
|
||||
organization,
|
||||
);
|
||||
|
||||
if (!latestVersion || latestVersion.schemas.length === 0) {
|
||||
throw new HiveError('Registry is empty');
|
||||
}
|
||||
|
|
@ -1412,7 +1421,7 @@ export class SchemaPublisher {
|
|||
if (!serviceExists) {
|
||||
return {
|
||||
__typename: 'SchemaDeleteError',
|
||||
valid: latestVersion.valid,
|
||||
valid: false,
|
||||
errors: [
|
||||
{
|
||||
message: `Service "${input.serviceName}" not found`,
|
||||
|
|
@ -1442,15 +1451,17 @@ export class SchemaPublisher {
|
|||
serviceName: input.serviceName,
|
||||
},
|
||||
latest: {
|
||||
isComposable: latestVersion.valid,
|
||||
sdl: latestSchemaVersion?.compositeSchemaSDL ?? null,
|
||||
schemas,
|
||||
isComposable: latestVersion.version.isComposable,
|
||||
sdl: latestVersion.version.compositeSchemaSDL,
|
||||
schemas: schemas.map(toCompositeSchemaInput),
|
||||
},
|
||||
latestComposable: latestComposableVersion
|
||||
? {
|
||||
isComposable: latestComposableVersion.valid,
|
||||
sdl: latestComposableSchemaVersion?.compositeSchemaSDL ?? null,
|
||||
schemas: ensureCompositeSchemas(latestComposableVersion.schemas),
|
||||
isComposable: latestComposableVersion.version.isComposable,
|
||||
sdl: latestComposableVersion.version.compositeSchemaSDL ?? null,
|
||||
schemas: ensureCompositeSchemas(latestComposableVersion.schemas).map(
|
||||
toCompositeSchemaInput,
|
||||
),
|
||||
}
|
||||
: null,
|
||||
baseSchema,
|
||||
|
|
@ -1465,15 +1476,16 @@ export class SchemaPublisher {
|
|||
conditionalBreakingChangeConfiguration?.conditionalBreakingChangeDiffConfig ?? null,
|
||||
contracts,
|
||||
failDiffOnDangerousChange,
|
||||
compareToLatestComposableVersion,
|
||||
});
|
||||
|
||||
let diffSchemaVersionId: string | null = null;
|
||||
if (compareToPreviousComposableVersion && latestComposableSchemaVersion) {
|
||||
diffSchemaVersionId = latestComposableSchemaVersion.id;
|
||||
if (compareToLatestComposableVersion && latestComposableVersion) {
|
||||
diffSchemaVersionId = latestComposableVersion.version.id;
|
||||
}
|
||||
|
||||
if (!compareToPreviousComposableVersion && latestSchemaVersion) {
|
||||
diffSchemaVersionId = latestSchemaVersion.id;
|
||||
if (!compareToLatestComposableVersion && latestVersion) {
|
||||
diffSchemaVersionId = latestVersion.version.id;
|
||||
}
|
||||
|
||||
if (deleteResult.conclusion === SchemaDeleteConclusion.Accept) {
|
||||
|
|
@ -1599,12 +1611,6 @@ export class SchemaPublisher {
|
|||
DeleteFailureReasonCode.CompositionFailure,
|
||||
)?.compositionErrors;
|
||||
|
||||
if (getReasonByCode(deleteResult.reasons, DeleteFailureReasonCode.MissingServiceName)) {
|
||||
errors.push({
|
||||
message: 'Service name is required',
|
||||
});
|
||||
}
|
||||
|
||||
if (compositionErrors?.length) {
|
||||
errors.push(...compositionErrors);
|
||||
}
|
||||
|
|
@ -1640,41 +1646,35 @@ export class SchemaPublisher {
|
|||
metadata: !!input.metadata,
|
||||
});
|
||||
|
||||
const [organization, project, target, latestVersion, latestComposable, baseSchema] =
|
||||
await Promise.all([
|
||||
this.storage.getOrganization({
|
||||
organizationId: organizationId,
|
||||
}),
|
||||
this.storage.getProject({
|
||||
organizationId: organizationId,
|
||||
projectId: projectId,
|
||||
}),
|
||||
this.storage.getTarget({
|
||||
organizationId: organizationId,
|
||||
projectId: projectId,
|
||||
targetId: targetId,
|
||||
}),
|
||||
this.storage.getLatestSchemas({
|
||||
organizationId: organizationId,
|
||||
projectId: projectId,
|
||||
targetId: targetId,
|
||||
}),
|
||||
this.storage.getLatestSchemas({
|
||||
organizationId: organizationId,
|
||||
projectId: projectId,
|
||||
targetId: targetId,
|
||||
onlyComposable: true,
|
||||
}),
|
||||
this.storage.getBaseSchema({
|
||||
organizationId: organizationId,
|
||||
projectId: projectId,
|
||||
targetId: targetId,
|
||||
}),
|
||||
]);
|
||||
const [organization, project, target, baseSchema] = await Promise.all([
|
||||
this.storage.getOrganization({
|
||||
organizationId: organizationId,
|
||||
}),
|
||||
this.storage.getProject({
|
||||
organizationId: organizationId,
|
||||
projectId: projectId,
|
||||
}),
|
||||
this.storage.getTarget({
|
||||
organizationId: organizationId,
|
||||
projectId: projectId,
|
||||
targetId: targetId,
|
||||
}),
|
||||
|
||||
const [latestSchemaVersion, latestComposableSchemaVersion] = await Promise.all([
|
||||
this.schemaManager.getMaybeLatestVersion(target),
|
||||
this.schemaManager.getMaybeLatestValidVersion(target),
|
||||
this.storage.getBaseSchema({
|
||||
organizationId: organizationId,
|
||||
projectId: projectId,
|
||||
targetId: targetId,
|
||||
}),
|
||||
]);
|
||||
|
||||
const [latestVersion, latestComposable] = await Promise.all([
|
||||
this.schemaManager.getLatestSchemaVersionWithSchemaLogs({
|
||||
target,
|
||||
}),
|
||||
this.schemaManager.getLatestSchemaVersionWithSchemaLogs({
|
||||
target,
|
||||
onlyComposable: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
function increaseSchemaPublishCountMetric(conclusion: 'rejected' | 'accepted' | 'ignored') {
|
||||
|
|
@ -1771,18 +1771,18 @@ export class SchemaPublisher {
|
|||
})
|
||||
: null;
|
||||
|
||||
const compareToPreviousComposableVersion = shouldUseLatestComposableVersion(
|
||||
const compareToLatestComposableVersion = shouldUseLatestComposableVersion(
|
||||
target.id,
|
||||
project,
|
||||
organization,
|
||||
);
|
||||
const comparedSchemaVersion = compareToPreviousComposableVersion
|
||||
? latestComposableSchemaVersion
|
||||
: latestSchemaVersion;
|
||||
const comparedSchemaVersion = compareToLatestComposableVersion
|
||||
? latestComposable
|
||||
: latestVersion;
|
||||
|
||||
const latestSchemaVersionContracts = latestSchemaVersion
|
||||
const latestSchemaVersionContracts = latestVersion
|
||||
? await this.contracts.getContractVersionsForSchemaVersion({
|
||||
schemaVersionId: latestSchemaVersion.id,
|
||||
schemaVersionId: latestVersion.version.id,
|
||||
})
|
||||
: null;
|
||||
|
||||
|
|
@ -1795,19 +1795,22 @@ export class SchemaPublisher {
|
|||
organization.featureFlags,
|
||||
);
|
||||
publishResult = await this.models[ProjectType.SINGLE].publish({
|
||||
input,
|
||||
input: {
|
||||
sdl: input.sdl,
|
||||
metadata: input.metadata ?? null,
|
||||
},
|
||||
latest: latestVersion
|
||||
? {
|
||||
isComposable: latestVersion.valid,
|
||||
sdl: latestSchemaVersion?.compositeSchemaSDL ?? null,
|
||||
schemas: [ensureSingleSchema(latestVersion.schemas)],
|
||||
isComposable: latestVersion.version.isComposable,
|
||||
sdl: latestVersion.version.compositeSchemaSDL,
|
||||
schemas: [toSingleSchemaInput(ensureSingleSchema(latestVersion.schemas))],
|
||||
}
|
||||
: null,
|
||||
latestComposable: latestComposable
|
||||
? {
|
||||
isComposable: latestComposable.valid,
|
||||
sdl: latestComposableSchemaVersion?.compositeSchemaSDL ?? null,
|
||||
schemas: [ensureSingleSchema(latestComposable.schemas)],
|
||||
isComposable: latestComposable.version.isComposable,
|
||||
sdl: latestComposable.version.compositeSchemaSDL,
|
||||
schemas: [toSingleSchemaInput(ensureSingleSchema(latestComposable.schemas))],
|
||||
}
|
||||
: null,
|
||||
organization,
|
||||
|
|
@ -1826,22 +1829,34 @@ export class SchemaPublisher {
|
|||
project.type,
|
||||
organization.featureFlags,
|
||||
);
|
||||
|
||||
if (!input.service) {
|
||||
throw new Error('Invalid state. input.service should have been validated by now.');
|
||||
}
|
||||
|
||||
publishResult = await this.models[project.type].publish({
|
||||
input,
|
||||
input: {
|
||||
sdl: input.sdl,
|
||||
service: input.service,
|
||||
metadata: input.metadata ?? null,
|
||||
url: input.url ?? null,
|
||||
},
|
||||
latest: latestVersion
|
||||
? {
|
||||
isComposable: latestVersion.valid,
|
||||
sdl: latestSchemaVersion?.compositeSchemaSDL ?? null,
|
||||
schemas: ensureCompositeSchemas(latestVersion.schemas),
|
||||
isComposable: latestVersion.version.isComposable,
|
||||
sdl: latestVersion.version.compositeSchemaSDL,
|
||||
schemas: ensureCompositeSchemas(latestVersion.schemas).map(toCompositeSchemaInput),
|
||||
contractNames:
|
||||
latestSchemaVersionContracts?.edges.map(edge => edge.node.contractName) ?? null,
|
||||
}
|
||||
: null,
|
||||
latestComposable: latestComposable
|
||||
? {
|
||||
isComposable: latestComposable.valid,
|
||||
sdl: latestComposableSchemaVersion?.compositeSchemaSDL ?? null,
|
||||
schemas: ensureCompositeSchemas(latestComposable.schemas),
|
||||
isComposable: latestComposable.version.isComposable,
|
||||
sdl: latestComposable.version.compositeSchemaSDL,
|
||||
schemas: ensureCompositeSchemas(latestComposable.schemas).map(
|
||||
toCompositeSchemaInput,
|
||||
),
|
||||
}
|
||||
: null,
|
||||
organization,
|
||||
|
|
@ -1852,6 +1867,7 @@ export class SchemaPublisher {
|
|||
conditionalBreakingChangeDiffConfig:
|
||||
conditionalBreakingChangeConfiguration?.conditionalBreakingChangeDiffConfig ?? null,
|
||||
failDiffOnDangerousChange,
|
||||
compareToLatestComposableVersion: compareToLatestComposableVersion,
|
||||
});
|
||||
break;
|
||||
default: {
|
||||
|
|
@ -1877,7 +1893,7 @@ export class SchemaPublisher {
|
|||
target: {
|
||||
slug: target.slug,
|
||||
},
|
||||
version: latestVersion ? { id: latestVersion.versionId } : undefined,
|
||||
version: latestVersion ? { id: latestVersion.version.id } : undefined,
|
||||
})
|
||||
: null;
|
||||
|
||||
|
|
@ -1912,13 +1928,6 @@ export class SchemaPublisher {
|
|||
|
||||
increaseSchemaPublishCountMetric('rejected');
|
||||
|
||||
if (getReasonByCode(publishResult.reasons, PublishFailureReasonCode.MissingServiceName)) {
|
||||
return {
|
||||
__typename: 'SchemaPublishMissingServiceError' as const,
|
||||
message: 'Missing service name',
|
||||
} as const;
|
||||
}
|
||||
|
||||
if (getReasonByCode(publishResult.reasons, PublishFailureReasonCode.MissingServiceUrl)) {
|
||||
return {
|
||||
__typename: 'SchemaPublishMissingUrlError' as const,
|
||||
|
|
@ -2000,7 +2009,7 @@ export class SchemaPublisher {
|
|||
|
||||
const supergraph = publishResult.state.supergraph ?? null;
|
||||
|
||||
const diffSchemaVersionId = comparedSchemaVersion?.id ?? null;
|
||||
const diffSchemaVersionId = comparedSchemaVersion?.version.id ?? null;
|
||||
|
||||
this.logger.debug(`Assigning ${schemaLogIds.length} schemas to new version`);
|
||||
|
||||
|
|
@ -2010,9 +2019,9 @@ export class SchemaPublisher {
|
|||
if (
|
||||
(project.type === ProjectType.FEDERATION || project.type === ProjectType.STITCHING) &&
|
||||
serviceUrl == null &&
|
||||
pushedSchema.kind === 'composite'
|
||||
pushedSchema.serviceName
|
||||
) {
|
||||
serviceUrl = pushedSchema.service_url;
|
||||
serviceUrl = pushedSchema.serviceUrl;
|
||||
}
|
||||
|
||||
const schemaVersion = await this.schemaManager.createVersion({
|
||||
|
|
@ -2057,7 +2066,7 @@ export class SchemaPublisher {
|
|||
changes,
|
||||
coordinatesDiff: publishResult.state.coordinatesDiff,
|
||||
diffSchemaVersionId,
|
||||
previousSchemaVersion: latestVersion?.versionId ?? null,
|
||||
previousSchemaVersion: latestVersion?.version.id ?? null,
|
||||
conditionalBreakingChangeMetadata: await this.getConditionalBreakingChangeMetadata({
|
||||
conditionalBreakingChangeConfiguration,
|
||||
organizationId,
|
||||
|
|
@ -2336,7 +2345,7 @@ export class SchemaPublisher {
|
|||
project: Project;
|
||||
supergraph: string | null;
|
||||
fullSchemaSdl: string;
|
||||
schemas: readonly Schema[];
|
||||
schemas: readonly SchemaInput[];
|
||||
contracts: null | Array<{ name: string; supergraph: string; sdl: string }>;
|
||||
versionId: string;
|
||||
}) {
|
||||
|
|
@ -2362,16 +2371,14 @@ export class SchemaPublisher {
|
|||
};
|
||||
|
||||
const publishCompositeSchema = async () => {
|
||||
const compositeSchema = ensureCompositeSchemas(schemas);
|
||||
|
||||
await Promise.all([
|
||||
await this.artifactStorageWriter.writeArtifact({
|
||||
targetId: target.id,
|
||||
artifactType: 'services',
|
||||
artifact: compositeSchema.map(s => ({
|
||||
name: s.service_name,
|
||||
artifact: schemas.map(s => ({
|
||||
name: s.serviceName,
|
||||
sdl: s.sdl,
|
||||
url: s.service_url,
|
||||
url: s.serviceUrl,
|
||||
})),
|
||||
contractName: null,
|
||||
versionId,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { Logger } from '../../shared/providers/logger';
|
|||
import { Storage } from '../../shared/providers/storage';
|
||||
import { CompositionOrchestrator } from './orchestrator/composition-orchestrator';
|
||||
import { RegistryChecks } from './registry-checks';
|
||||
import { ensureCompositeSchemas, SchemaHelper } from './schema-helper';
|
||||
import { ensureCompositeSchemas, SchemaHelper, toCompositeSchemaInput } from './schema-helper';
|
||||
import { SchemaManager } from './schema-manager';
|
||||
|
||||
@Injectable({
|
||||
|
|
@ -69,7 +69,13 @@ export class SchemaVersionHelper {
|
|||
|
||||
const validation = await this.compositionOrchestrator.composeAndValidate(
|
||||
CompositionOrchestrator.projectTypeToOrchestratorType(project.type),
|
||||
schemas.map(s => this.schemaHelper.createSchemaObject(s)),
|
||||
schemas.map(s =>
|
||||
this.schemaHelper.createSchemaObject({
|
||||
sdl: s.sdl,
|
||||
serviceName: s.kind === 'composite' ? s.service_name : null,
|
||||
serviceUrl: s.kind === 'composite' ? s.service_url : null,
|
||||
}),
|
||||
),
|
||||
{
|
||||
external: project.externalComposition,
|
||||
native: this.schemaManager.checkProjectNativeFederationSupport({
|
||||
|
|
@ -237,8 +243,8 @@ export class SchemaVersionHelper {
|
|||
existingSdl,
|
||||
incomingSdl,
|
||||
includeUrlChanges: {
|
||||
schemasBefore: ensureCompositeSchemas(schemaBefore),
|
||||
schemasAfter: ensureCompositeSchemas(schemasAfter),
|
||||
schemasBefore: ensureCompositeSchemas(schemaBefore).map(toCompositeSchemaInput),
|
||||
schemasAfter: ensureCompositeSchemas(schemasAfter).map(toCompositeSchemaInput),
|
||||
},
|
||||
filterOutFederationChanges: project.type === ProjectType.FEDERATION,
|
||||
conditionalBreakingChangeConfig: null,
|
||||
|
|
|
|||
|
|
@ -352,16 +352,6 @@ export interface Storage {
|
|||
|
||||
hasSchema(_: TargetSelector): Promise<boolean>;
|
||||
|
||||
getLatestSchemas(
|
||||
_: {
|
||||
onlyComposable?: boolean;
|
||||
} & TargetSelector,
|
||||
): Promise<{
|
||||
schemas: Schema[];
|
||||
versionId: string;
|
||||
valid: boolean;
|
||||
} | null>;
|
||||
|
||||
getLatestValidVersion(_: { targetId: string }): Promise<SchemaVersion | never>;
|
||||
|
||||
getMaybeLatestValidVersion(_: { targetId: string }): Promise<SchemaVersion | null | never>;
|
||||
|
|
|
|||
|
|
@ -116,11 +116,13 @@ export function createSDLHash(sdl: string): string {
|
|||
);
|
||||
}
|
||||
|
||||
export function createSchemaObject(
|
||||
schema:
|
||||
| Pick<SingleSchema, 'sdl'>
|
||||
| Pick<PushedCompositeSchema, 'sdl' | 'service_name' | 'service_url'>,
|
||||
): SchemaObject {
|
||||
export type CreateSchemaObjectInput = {
|
||||
sdl: string;
|
||||
serviceName?: string | null;
|
||||
serviceUrl?: string | null;
|
||||
};
|
||||
|
||||
export function createSchemaObject(schema: CreateSchemaObjectInput): SchemaObject {
|
||||
let document: DocumentNode;
|
||||
|
||||
try {
|
||||
|
|
@ -135,8 +137,8 @@ export function createSchemaObject(
|
|||
return {
|
||||
document,
|
||||
raw: schema.sdl,
|
||||
source: 'service_name' in schema ? schema.service_name : emptySource,
|
||||
url: 'service_url' in schema ? schema.service_url : null,
|
||||
source: schema.serviceName ?? emptySource,
|
||||
url: schema.serviceUrl ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2125,36 +2125,6 @@ export async function createStorage(
|
|||
|
||||
return SchemaVersionModel.parse(version);
|
||||
},
|
||||
async getLatestSchemas({ projectId: project, targetId: target, onlyComposable }) {
|
||||
const latest = await pool.maybeOne<
|
||||
Pick<schema_versions, 'id' | 'is_composable'>
|
||||
>(sql`/* getLatestSchemas */
|
||||
SELECT sv.id, sv.is_composable
|
||||
FROM schema_versions as sv
|
||||
LEFT JOIN targets as t ON (t.id = sv.target_id)
|
||||
LEFT JOIN schema_log as sl ON (sl.id = sv.action_id)
|
||||
WHERE t.id = ${target} AND t.project_id = ${project} AND ${
|
||||
onlyComposable ? sql`sv.is_composable IS TRUE` : true
|
||||
}
|
||||
ORDER BY sv.created_at DESC
|
||||
LIMIT 1
|
||||
`);
|
||||
|
||||
if (!latest) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const schemas = await storage.getSchemasOfVersion({
|
||||
versionId: latest.id,
|
||||
includeMetadata: true,
|
||||
});
|
||||
|
||||
return {
|
||||
versionId: latest.id,
|
||||
valid: latest.is_composable,
|
||||
schemas,
|
||||
};
|
||||
},
|
||||
async getSchemaByNameOfVersion(args) {
|
||||
const result = await pool.maybeOne<
|
||||
Pick<
|
||||
|
|
|
|||
Loading…
Reference in a new issue