in controlplane/src/core/bufservices/proposal/createProposal.ts [28:452]
export function createProposal(
opts: RouterOptions,
req: CreateProposalRequest,
ctx: HandlerContext,
): Promise<PlainMessage<CreateProposalResponse>> {
let logger = getLogger(ctx, opts.logger);
return handleError<PlainMessage<CreateProposalResponse>>(ctx, logger, async () => {
const authContext = await opts.authenticator.authenticate(ctx.requestHeader);
logger = enrichLogger(ctx, logger, authContext);
const federatedGraphRepo = new FederatedGraphRepository(logger, opts.db, authContext.organizationId);
const subgraphRepo = new SubgraphRepository(logger, opts.db, authContext.organizationId);
const proposalRepo = new ProposalRepository(opts.db);
const auditLogRepo = new AuditLogRepository(opts.db);
const namespaceRepo = new NamespaceRepository(opts.db, authContext.organizationId);
req.namespace = req.namespace || DefaultNamespace;
if (authContext.organizationDeactivated) {
throw new UnauthorizedError();
}
const namespace = await namespaceRepo.byName(req.namespace);
if (!namespace) {
return {
response: {
code: EnumStatusCode.ERR_NOT_FOUND,
details: `Namespace ${req.namespace} not found`,
},
proposalId: '',
breakingChanges: [],
nonBreakingChanges: [],
compositionErrors: [],
checkId: '',
lintWarnings: [],
lintErrors: [],
graphPruneWarnings: [],
graphPruneErrors: [],
compositionWarnings: [],
lintingSkipped: false,
graphPruningSkipped: false,
checkUrl: '',
proposalUrl: '',
proposalName: '',
};
}
const federatedGraph = await federatedGraphRepo.byName(req.federatedGraphName, req.namespace);
if (!federatedGraph) {
return {
response: {
code: EnumStatusCode.ERR_NOT_FOUND,
details: `Federated graph ${req.federatedGraphName} not found`,
},
proposalId: '',
breakingChanges: [],
nonBreakingChanges: [],
compositionErrors: [],
checkId: '',
lintWarnings: [],
lintErrors: [],
graphPruneWarnings: [],
graphPruneErrors: [],
compositionWarnings: [],
lintingSkipped: false,
graphPruningSkipped: false,
checkUrl: '',
proposalUrl: '',
proposalName: '',
};
}
// check whether the user is authorized to perform the action
if (!authContext.rbac.hasFederatedGraphWriteAccess(federatedGraph)) {
throw new UnauthorizedError();
}
if (!namespace.enableProposals) {
return {
response: {
code: EnumStatusCode.ERR,
details: `Proposals are not enabled for namespace ${req.namespace}`,
},
proposalId: '',
breakingChanges: [],
nonBreakingChanges: [],
compositionErrors: [],
checkId: '',
lintWarnings: [],
lintErrors: [],
graphPruneWarnings: [],
graphPruneErrors: [],
compositionWarnings: [],
lintingSkipped: false,
graphPruningSkipped: false,
checkUrl: '',
proposalUrl: '',
proposalName: '',
};
}
let proposalName = req.name;
if (req.namingConvention === ProposalNamingConvention.NORMAL) {
// checking if the name starts with p- and followed by any integer
const proposalNameRegex = /^p-\d+$/;
if (proposalNameRegex.test(req.name)) {
return {
response: {
code: EnumStatusCode.ERR,
details: `Proposal name cannot start with p-`,
},
proposalId: '',
breakingChanges: [],
nonBreakingChanges: [],
compositionErrors: [],
checkId: '',
lintWarnings: [],
lintErrors: [],
graphPruneWarnings: [],
graphPruneErrors: [],
compositionWarnings: [],
lintingSkipped: false,
graphPruningSkipped: false,
checkUrl: '',
proposalUrl: '',
proposalName: '',
};
}
} else {
const count = await proposalRepo.countByFederatedGraphId({
federatedGraphId: federatedGraph.id,
});
proposalName = `p-${count + 1}/${req.name}`;
}
const existingProposal = await proposalRepo.ByName({
name: proposalName,
federatedGraphId: federatedGraph.id,
});
if (existingProposal) {
return {
response: {
code: EnumStatusCode.ERR_ALREADY_EXISTS,
details: `Proposal ${proposalName} already exists.`,
},
proposalId: '',
breakingChanges: [],
nonBreakingChanges: [],
compositionErrors: [],
checkId: '',
lintWarnings: [],
lintErrors: [],
graphPruneWarnings: [],
graphPruneErrors: [],
compositionWarnings: [],
lintingSkipped: false,
graphPruningSkipped: false,
checkUrl: '',
proposalUrl: '',
proposalName: '',
};
}
if (req.subgraphs.length === 0) {
return {
response: {
code: EnumStatusCode.ERR,
details: `No subgraphs provided. At least one subgraph is required to create a proposal.`,
},
proposalId: '',
breakingChanges: [],
nonBreakingChanges: [],
compositionErrors: [],
checkId: '',
lintWarnings: [],
lintErrors: [],
graphPruneWarnings: [],
graphPruneErrors: [],
compositionWarnings: [],
lintingSkipped: false,
graphPruningSkipped: false,
checkUrl: '',
proposalUrl: '',
proposalName: '',
};
}
const subgraphNames = req.subgraphs.map((subgraph) => subgraph.name);
const uniqueSubgraphNames = new Set(subgraphNames);
if (uniqueSubgraphNames.size !== subgraphNames.length) {
return {
response: {
code: EnumStatusCode.ERR,
details: `The subgraphs provided in the proposal have to be unique. Please check the names of the subgraphs and try again.`,
},
proposalId: '',
breakingChanges: [],
nonBreakingChanges: [],
compositionErrors: [],
checkId: '',
lintWarnings: [],
lintErrors: [],
graphPruneWarnings: [],
graphPruneErrors: [],
compositionWarnings: [],
lintingSkipped: false,
graphPruningSkipped: false,
checkUrl: '',
proposalUrl: '',
proposalName: '',
};
}
const subgraphsOfFedGraph = await subgraphRepo.listByFederatedGraph({
federatedGraphTargetId: federatedGraph.targetId,
});
const proposalSubgraphs: {
subgraphId?: string;
subgraphName: string;
schemaSDL: string;
isDeleted: boolean;
isNew: boolean;
currentSchemaVersionId?: string;
labels: Label[];
}[] = [];
for (const proposalSubgraph of req.subgraphs) {
const subgraph = await subgraphRepo.byName(proposalSubgraph.name, req.namespace);
if (subgraph) {
const isSubgraphPartOfFedGraph = subgraphsOfFedGraph.some((s) => s.name === proposalSubgraph.name);
if (!isSubgraphPartOfFedGraph) {
return {
response: {
code: EnumStatusCode.ERR,
details: `Subgraph ${proposalSubgraph.name} is not part of the federated graph ${federatedGraph.name}`,
},
proposalId: '',
breakingChanges: [],
nonBreakingChanges: [],
compositionErrors: [],
checkId: '',
lintWarnings: [],
lintErrors: [],
graphPruneWarnings: [],
graphPruneErrors: [],
compositionWarnings: [],
lintingSkipped: false,
graphPruningSkipped: false,
checkUrl: '',
proposalUrl: '',
proposalName: '',
};
}
if (subgraph.isFeatureSubgraph) {
return {
response: {
code: EnumStatusCode.ERR,
details:
`The subgraph "${subgraph.name}" is a feature subgraph.` +
` Feature subgraphs are not currently supported for proposals.`,
},
proposalId: '',
breakingChanges: [],
nonBreakingChanges: [],
compositionErrors: [],
checkId: '',
lintWarnings: [],
lintErrors: [],
graphPruneWarnings: [],
graphPruneErrors: [],
compositionWarnings: [],
lintingSkipped: false,
graphPruningSkipped: false,
checkUrl: '',
proposalUrl: '',
proposalName: '',
};
}
if (proposalSubgraph.isNew) {
return {
response: {
code: EnumStatusCode.ERR,
details: `Subgraph ${proposalSubgraph.name} is marked as new, but a subgraph with the same name already exists.`,
},
proposalId: '',
breakingChanges: [],
nonBreakingChanges: [],
compositionErrors: [],
checkId: '',
lintWarnings: [],
lintErrors: [],
graphPruneWarnings: [],
graphPruneErrors: [],
compositionWarnings: [],
lintingSkipped: false,
graphPruningSkipped: false,
checkUrl: '',
proposalUrl: '',
proposalName: '',
};
}
}
proposalSubgraphs.push({
subgraphId: subgraph?.id,
subgraphName: proposalSubgraph.name,
schemaSDL: proposalSubgraph.schemaSDL,
isDeleted: proposalSubgraph.isDeleted,
isNew: !subgraph,
currentSchemaVersionId: subgraph?.schemaVersionId,
labels: proposalSubgraph.labels,
});
}
const proposal = await proposalRepo.createProposal({
federatedGraphId: federatedGraph.id,
name: proposalName,
userId: authContext.userId,
proposalSubgraphs,
});
await auditLogRepo.addAuditLog({
organizationId: authContext.organizationId,
organizationSlug: authContext.organizationSlug,
auditAction: 'proposal.created',
action: 'created',
actorId: authContext.userId,
auditableType: 'proposal',
auditableDisplayName: proposal.name,
actorDisplayName: authContext.userDisplayName,
apiKeyName: authContext.apiKeyName,
actorType: authContext.auth === 'api_key' ? 'api_key' : 'user',
targetNamespaceId: federatedGraph.namespaceId,
targetNamespaceDisplayName: federatedGraph.namespace,
});
const fedGraphRepo = new FederatedGraphRepository(logger, opts.db, authContext.organizationId);
const orgRepo = new OrganizationRepository(logger, opts.db, opts.billingDefaultPlanId);
const schemaLintRepo = new SchemaLintRepository(opts.db);
const schemaGraphPruningRepo = new SchemaGraphPruningRepository(opts.db);
const schemaCheckRepo = new SchemaCheckRepository(opts.db);
const contractRepo = new ContractRepository(logger, opts.db, authContext.organizationId);
const graphCompostionRepo = new GraphCompositionRepository(logger, opts.db);
const trafficInspector = new SchemaUsageTrafficInspector(opts.chClient!);
const composer = new Composer(
logger,
opts.db,
fedGraphRepo,
subgraphRepo,
contractRepo,
graphCompostionRepo,
opts.chClient,
);
const {
response,
breakingChanges,
nonBreakingChanges,
compositionErrors,
checkId,
lintWarnings,
lintErrors,
graphPruneWarnings,
graphPruneErrors,
compositionWarnings,
operationUsageStats,
} = await schemaCheckRepo.checkMultipleSchemas({
organizationId: authContext.organizationId,
orgRepo,
subgraphRepo,
fedGraphRepo,
schemaLintRepo,
schemaGraphPruningRepo,
proposalRepo,
trafficInspector,
composer,
subgraphs: proposalSubgraphs.map(
(subgraph) =>
new ProposalSubgraph({
name: subgraph.subgraphName,
schemaSDL: subgraph.schemaSDL,
labels: subgraph.labels,
isDeleted: subgraph.isDeleted,
isNew: subgraph.isNew,
}),
),
namespace,
logger,
chClient: opts.chClient,
skipProposalMatchCheck: true,
});
if (checkId) {
await schemaCheckRepo.createSchemaCheckProposal({
schemaCheckID: checkId,
proposalID: proposal.id,
});
}
return {
response,
proposalId: proposal.id,
breakingChanges,
nonBreakingChanges,
compositionErrors,
checkId,
lintWarnings,
lintErrors,
graphPruneWarnings,
graphPruneErrors,
compositionWarnings,
operationUsageStats,
lintingSkipped: !namespace.enableLinting,
graphPruningSkipped: !namespace.enableGraphPruning,
checkUrl: `${process.env.WEB_BASE_URL}/${authContext.organizationSlug}/${namespace.name}/graph/${federatedGraph.name}/checks/${checkId}`,
proposalUrl: `${process.env.WEB_BASE_URL}/${authContext.organizationSlug}/${namespace.name}/graph/${federatedGraph.name}/proposals/${proposal.id}`,
proposalName: proposal.name,
};
});
}