Skip to content

Commit

Permalink
wip: quickfix for add type
Browse files Browse the repository at this point in the history
  • Loading branch information
Yogu committed Aug 8, 2024
1 parent fce8b7e commit 86ff5ad
Show file tree
Hide file tree
Showing 9 changed files with 122 additions and 18 deletions.
2 changes: 1 addition & 1 deletion core-exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ export { TransactionError } from './src/execution/transaction-error';
export { ConflictRetriesExhaustedError } from './src/execution/runtime-errors';
export { AuthContext } from './src/authorization/auth-basics';
export { ChangeSet, TextChange } from './src/model/change-set/change-set';
export { applyChangeSet, applyChanges } from './src/model/change-set/apply-changes';
export { applyChangeSet, applyChanges } from './src/model/change-set/apply-change-set';
5 changes: 4 additions & 1 deletion spec/model/change-set/apply-change-set.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { expect } from 'chai';
import { MessageLocation } from '../../../src/model';
import { applyChangeSet, InvalidChangeSetError } from '../../../src/model/change-set/apply-changes';
import {
applyChangeSet,
InvalidChangeSetError,
} from '../../../src/model/change-set/apply-change-set';
import { ChangeSet, TextChange } from '../../../src/model/change-set/change-set';
import { Project } from '../../../src/project/project';
import { ProjectSource } from '../../../src/project/source';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,40 @@ export class InvalidChangeSetError extends Error {
}

export function applyChangeSet(project: Project, changeSet: ChangeSet): Project {
const newSources = project.sources.map((source) => {
const changes = changeSet.changes.filter((c) => c.source.name === source.name);
return applyChanges(source, changes);
const changedSources = project.sources.map((source) => {
const textChanges = changeSet.textChanges.filter((c) => c.source.name === source.name);
const appendChanges = changeSet.appendChanges.filter((c) => c.sourceName === source.name);
let newText = applyChanges(source, textChanges);
for (const change of appendChanges) {
newText += (newText.length ? '\n' : '') + change.text;
}
if (newText === source.body) {
return source;
} else {
return new ProjectSource(source.name, newText, source.filePath);
}
});
const existingSourceNames = new Set(project.sources.map((s) => s.name));
const appendSourceNames = new Set(changeSet.appendChanges.map((c) => c.sourceName));
const newSourceNames = [...appendSourceNames].filter((name) => !existingSourceNames.has(name));
const newSources = [...newSourceNames].map((sourceName) => {
const appendChanges = changeSet.appendChanges.filter((c) => c.sourceName === sourceName);
let newText = '';
for (const change of appendChanges) {
newText += (newText.length ? '\n' : '') + change.text;
}
return new ProjectSource(sourceName, newText);
});

return new Project({
...project,
sources: newSources,
sources: [...changedSources, ...newSources],
});
}

export function applyChanges(
source: ProjectSource,
changes: ReadonlyArray<TextChange>,
): ProjectSource {
function applyChanges(source: ProjectSource, changes: ReadonlyArray<TextChange>): string {
if (!changes.length) {
return source;
return source.body;
}

const sortedChanges = [...changes].sort(
Expand Down Expand Up @@ -55,7 +72,7 @@ export function applyChanges(
}
lastChange = change;
}
return new ProjectSource(source.name, output, source.filePath);
return output;
}

function formatChangeLocation(change: TextChange | undefined): string {
Expand Down
35 changes: 32 additions & 3 deletions src/model/change-set/change-set.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import { Location } from 'graphql';
import { Project } from '../../project/project';
import { ProjectSource } from '../../project/source';
import { LocationLike, MessageLocation } from '../validation/location';
import { MessageLocation } from '../validation/location';

export type Change = TextChange | AppendChange;

/**
* A set of changes to one or multiple project sources
*/
export class ChangeSet {
constructor(readonly changes: ReadonlyArray<TextChange>) {}
readonly textChanges: ReadonlyArray<TextChange>;
readonly appendChanges: ReadonlyArray<AppendChange>;

constructor(readonly changes: ReadonlyArray<Change>) {
this.textChanges = changes.filter((c) => c instanceof TextChange);
this.appendChanges = changes.filter((c) => c instanceof AppendChange);
}
}

/**
* A change that either deletes a span in an existing source or replaces that span with new text
*/
export class TextChange {
readonly source: ProjectSource;

Expand Down Expand Up @@ -37,3 +47,22 @@ export class TextChange {
this.source = this.location.source;
}
}

/**
* A change that creates a new source with a given name or appends text to an existing source
*
* If there are multiple AppendToNewSource instances with the same sourceName in a changeset, the content of all of them will be appended to the source, separated by a newline.
*/
export class AppendChange {
constructor(
/**
* The name of the new source
*/
readonly sourceName: string,

/**
* The text for the new source
*/
readonly text: string,
) {}
}
43 changes: 42 additions & 1 deletion src/model/compatibility-check/check-model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { FieldDefinitionNode, print, TypeDefinitionNode } from 'graphql';

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.8, 20.x)

Cannot find name 'typeToCheck'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.8, 20.x)

Cannot find name 'typeToCheck'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.8, 20.x)

Cannot find name 'baselineField'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.8, 18.x)

Cannot find name 'typeToCheck'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.8, 18.x)

Cannot find name 'typeToCheck'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.8, 18.x)

Cannot find name 'baselineField'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.8, 22.x)

Cannot find name 'typeToCheck'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.8, 22.x)

Cannot find name 'typeToCheck'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.8, 22.x)

Cannot find name 'baselineField'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.12, 20.x)

Cannot find name 'typeToCheck'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.12, 20.x)

Cannot find name 'typeToCheck'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.12, 20.x)

Cannot find name 'baselineField'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.11, 20.x)

Cannot find name 'typeToCheck'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.11, 20.x)

Cannot find name 'typeToCheck'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.11, 20.x)

Cannot find name 'baselineField'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.12, 22.x)

Cannot find name 'typeToCheck'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.12, 22.x)

Cannot find name 'typeToCheck'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.12, 22.x)

Cannot find name 'baselineField'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.11, 22.x)

Cannot find name 'typeToCheck'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.11, 22.x)

Cannot find name 'typeToCheck'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.11, 22.x)

Cannot find name 'baselineField'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.12, 18.x)

Cannot find name 'typeToCheck'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.12, 18.x)

Cannot find name 'typeToCheck'.

Check failure on line 1 in src/model/compatibility-check/check-model.ts

View workflow job for this annotation

GitHub Actions / test (arangodb:3.12, 18.x)

Cannot find name 'baselineField'.
import { MODULES_DIRECTIVE } from '../../schema/constants';
import { ChangeSet, TextChange } from '../change-set/change-set';
import { Model } from '../implementation';
import { ValidationResult, ValidationMessage, ValidationContext } from '../validation';
import {
ValidationResult,
ValidationMessage,
ValidationContext,
MessageLocation,
QuickFix,
} from '../validation';
import { checkType } from './check-type';
import { getRequiredBySuffix } from './describe-module-specification';

Expand All @@ -12,10 +21,42 @@ export function checkModel(modelToCheck: Model, baselineModel: Model): Validatio
for (const baselineType of baselineModel.types) {
const matchingType = modelToCheck.getType(baselineType.name);
if (!matchingType) {
const quickFixes: QuickFix[] = [];
if (baselineType.astNode && modelToCheck.project) {
const cleanedAstNode: TypeDefinitionNode = {
...baselineType.astNode,
directives: (baselineType.astNode.directives ?? []).filter(
(d) => d.name.value !== MODULES_DIRECTIVE,
),
};
const sourceName = baselineType.astNode.loc?.source.name ?? 'new.graphqls';
const existingSource = modelToCheck.project.sources.find(s => s.name === sourceName);

// end is the closing }, we want to add just before that
// we will be inserting " field: Type @directives\n" just before that closing }
const offset = typeToCheck.astNode.loc.end - 1;
const quickFixLocation = new MessageLocation(
MessageLocation.fromGraphQLLocation(typeToCheck.astNode.loc).source,
offset,
offset,
);

quickFixes.push(
new QuickFix({
description: `Add field "${baselineField.name}"`,
isPreferred: true,
changeSet: new ChangeSet([
new TextChange(quickFixLocation, ' ' + print(cleanedAstNode) + '\n'),
]),
}),
);
}

context.addMessage(
ValidationMessage.compatibilityIssue(
`Type "${baselineType.name}" is missing${getRequiredBySuffix(baselineType)}.`,
undefined,
{ quickFixes },
),
);
continue;
Expand Down
2 changes: 2 additions & 0 deletions src/model/config/model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ModelOptions } from '../../config/interfaces';
import { Project } from '../../project/project';
import { ValidationMessage } from '../validation';
import { BillingConfig } from './billing';
import { LocalizationConfig } from './i18n';
Expand All @@ -7,6 +8,7 @@ import { ModuleConfig } from './module';
import { TypeConfig } from './type';

export interface ModelConfig {
readonly project?: Project
readonly types: ReadonlyArray<TypeConfig>;
readonly permissionProfiles?: ReadonlyArray<NamespacedPermissionProfileConfigMap>;
readonly validationMessages?: ReadonlyArray<ValidationMessage>;
Expand Down
11 changes: 10 additions & 1 deletion src/model/create-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,19 @@ import { parseI18nConfigs } from './parse-i18n';
import { parseModuleConfigs } from './parse-modules';
import { parseTTLConfigs } from './parse-ttl';
import { ValidationContext, ValidationMessage } from './validation';
import { Project } from '../project/project';

export function createModel(parsedProject: ParsedProject, options: ModelOptions = {}): Model {
export interface CreateModelOptions extends ModelOptions {
project?: Project;
}

export function createModel(
parsedProject: ParsedProject,
{ project, ...options }: CreateModelOptions = {},
): Model {
const validationContext = new ValidationContext();
return new Model({
project,
types: createTypeInputs(parsedProject, validationContext, options),
permissionProfiles: extractPermissionProfiles(parsedProject),
i18n: extractI18n(parsedProject),
Expand Down
3 changes: 3 additions & 0 deletions src/model/implementation/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ import { TimeToLiveType } from './time-to-live';
import { createType, InvalidType, ObjectType, Type } from './type';
import { ValueObjectType } from './value-object-type';
import { ModuleDeclaration } from './modules/module-declaration';
import { Project } from '../../project/project';

export class Model implements ModelComponent {
private readonly typeMap: ReadonlyMap<string, Type>;
private readonly builtInTypes: ReadonlyArray<Type>;

readonly project: Project | undefined;
readonly rootNamespace: Namespace;
readonly namespaces: ReadonlyArray<Namespace>;
readonly types: ReadonlyArray<Type>;
Expand All @@ -41,6 +43,7 @@ export class Model implements ModelComponent {
readonly modules: ReadonlyArray<ModuleDeclaration>;

constructor(private input: ModelConfig) {
this.project = input.project;
this.modules = input.modules ? input.modules.map((m) => new ModuleDeclaration(m)) : [];
// do this after the modules have been set because it uses the module list to make built-in types available in all modules
this.builtInTypes = createBuiltInTypes(this);
Expand Down
2 changes: 1 addition & 1 deletion src/schema/schema-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export function validateAndPrepareSchema(project: Project): {

const preparedProject = executePreMergeTransformationPipeline({ sources: validParsedSources });

const model = createModel(preparedProject, project.options.modelOptions);
const model = createModel(preparedProject, { ...project.options.modelOptions, project });

const mergedSchema: DocumentNode = mergeSchemaDefinition(preparedProject);

Expand Down

0 comments on commit 86ff5ad

Please sign in to comment.