Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Comment only fixes #340

Merged
merged 2 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 96 additions & 2 deletions spec/project/project.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { expect } from 'chai';
import { graphql } from 'graphql';
import { Logger, LoggerProvider } from '../../src/config/logging';
import { DatabaseAdapter, FlexSearchTokenizable } from '../../src/database/database-adapter';
import { FlexSearchLanguage, Model } from '../../src/model';
import { Model } from '../../src/model';
import { Project } from '../../src/project/project';
import { ProjectSource } from '../../src/project/source';
import { QueryNode } from '../../src/query-tree';
import { FlexSearchTokenization } from '../../src/query-tree/flex-search';
import { createSchema } from '../../src/schema/schema-builder';
import gql from 'graphql-tag';
import { expectSingleError, expectToBeValid } from '../model/implementation/validation-utils';

class FakeDBAdatper implements DatabaseAdapter {
async execute(queryTree: QueryNode): Promise<any> {
Expand All @@ -30,6 +31,99 @@ class FakeDBAdatper implements DatabaseAdapter {
}

describe('project', () => {
describe('validate', () => {
it('accepts a valid simple project', async () => {
const project = new Project([
gql`
type Test @rootEntity {
key: String @key
}
`.loc!.source,
]);
expectToBeValid(project);
});

it('accepts a valid project with multiple sources', async () => {
const project = new Project([
gql`
type Test @rootEntity {
key: String @key
children: [Child]
}
`.loc!.source,
gql`
# make sure this file is not skipped just because it begins with a comment
type Child @childEntity {
key: String
}
`.loc!.source,
]);
expectToBeValid(project);
});

it('rejects an invalid project with multiple sources', async () => {
const project = new Project([
gql`
type Test @rootEntity {
key: String @key
children: [Child]
}
`.loc!.source,
gql`
type OtherChild @childEntity {
key: String
}
`.loc!.source,
]);
expectSingleError(project, 'Type "Child" not found.');
});

it('accepts a valid project with an additional empty file', async () => {
const project = new Project([
gql`
type Test @rootEntity {
key: String @key
}
`.loc!.source,
{
name: 'other.graphqls',
body: '',
},
]);
expectToBeValid(project);
});

it('accepts a valid project with an additional file that only contains comments', async () => {
const project = new Project([
gql`
type Test @rootEntity {
key: String @key
}
`.loc!.source,
{
name: 'other.graphqls',
body: '# this is a comment',
},
]);
expectToBeValid(project);
});

it('accepts a project without any source', async () => {
const project = new Project([]);
expectToBeValid(project);
});

it('accepts a project with just a comment-only source', async () => {
const project = new Project([
{
name: 'other.graphqls',
body: '# this is a comment',
},
]);
expectToBeValid(project);
});
});

describe('createSchema', () => {
it('schema resolvers log to logger specified in project', async () => {
let logs: string[] = [];
Expand Down
47 changes: 47 additions & 0 deletions spec/project/select-modules-in-sources.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,53 @@ describe('selectModulesInProjectSource', () => {
expect(result.getModel().modules).to.deep.equal([]);
});

it('removes files that become comment-only', () => {
const project = new Project({
sources: [
new ProjectSource(
'modules.json',
JSON.stringify({
modules: ['module1', 'module2', 'module3', 'extra1', 'extra2'],
}),
),
gql`
# a comment in the keeper file
type Keeper @rootEntity @modules(in: "module1", includeAllFields: true) {
key: String @key
}
`.loc!.source,
gql`
# a comment in the discard file
type Discard @rootEntity @modules(in: "extra2", includeAllFields: true) {
key: String @key
}
`.loc!.source,
// will also remove this one because we're throwing away comment-only file when
// parsing a project. Documenting that behavior in this test case, but it's
// probably fine either way
{
name: 'empty.graphqls',
body: "# a file that's already comment-only",
},
],
modelOptions: { withModuleDefinitions: true },
});
expectToBeValid(project);

const result = project.withModuleSelection(['module1', 'module2'], {
removeModuleDeclarations: true,
});
expectToBeValid(result);
expect(result.sources.map((s) => s.body)).to.deep.equal([
`
# a comment in the keeper file
type Keeper @rootEntity {
key: String @key
}
`,
]);
});

it('removes the modules part in an object file with modules and something else', () => {
const project = new Project({
sources: [
Expand Down
21 changes: 21 additions & 0 deletions src/graphql/is-comment-only-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { GraphQLError, Lexer, TokenKind } from 'graphql/index';
import { Source } from 'graphql';

/**
* Checks if the given graphql source string only contains comments and whitespace
* @param source
*/
export function isCommentOnlySource(source: string) {
const lexer = new Lexer(new Source(source));
try {
// lookahead() gets the first non-comment token
const firstToken = lexer.lookahead();
return firstToken.kind === TokenKind.EOF;
} catch (e) {
if (e instanceof GraphQLError) {
// syntax error means there is something
return false;
}
throw e;
}
}
10 changes: 10 additions & 0 deletions src/project/select-modules-in-sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { findDirectiveWithName } from '../schema/schema-utils';
import { Project, ProjectOptions } from './project';
import { ProjectSource } from './source';
import { isReadonlyArray } from '../utils/utils';
import { isCommentOnlySource } from '../graphql/is-comment-only-source';

export interface ModuleSelectionOptions {
/**
Expand Down Expand Up @@ -255,6 +256,10 @@ function selectModulesInGraphQLSource({
}
}

if (!changes) {
return source.body;
}

let currentPosition = 0;
let output = '';
for (let i = 0; i <= changes.length; i++) {
Expand All @@ -275,6 +280,11 @@ function selectModulesInGraphQLSource({
}
}

// if we removed everything except comments, delete the file
if (isCommentOnlySource(output)) {
return undefined;
}

return output;
}

Expand Down
9 changes: 7 additions & 2 deletions src/schema/schema-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import {
getLocation,
GraphQLError,
GraphQLSchema,
parse,
Kind as GraphQLKind,
Lexer,
parse,
TokenKind,
} from 'graphql';
import { parse as JSONparse } from 'json-source-map';
import { compact } from 'lodash';
Expand Down Expand Up @@ -53,6 +55,7 @@ import {
import { getLineEndPosition } from './schema-utils';
import jsonLint = require('json-lint');
import stripJsonComments = require('strip-json-comments');
import { isCommentOnlySource } from '../graphql/is-comment-only-source';

/**
* Validates a project and thus determines whether createSchema() would succeed
Expand Down Expand Up @@ -311,7 +314,9 @@ function parseGraphQLsSource(
options: ProjectOptions,
validationContext: ValidationContext,
): ParsedGraphQLProjectSource | undefined {
if (projectSource.body.trim() === '') {
// parse() does not accept documents, and there is no option to make it accept them either
// it would be annoying if you could not have empty files
if (isCommentOnlySource(projectSource.body)) {
return undefined;
}

Expand Down
Loading