From 350a9f604135d758f808645c3eeb4dd32677bf1a Mon Sep 17 00:00:00 2001 From: Jan Melcher Date: Fri, 13 Sep 2024 11:25:43 +0200 Subject: [PATCH] fix: accept graphql files that only consist of comments We already allowed whitespace-only files, so it only makes sense to also allow comment-only files. --- spec/project/project.spec.ts | 98 ++++++++++++++++++++++++++- src/graphql/is-comment-only-source.ts | 21 ++++++ src/schema/schema-builder.ts | 9 ++- 3 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 src/graphql/is-comment-only-source.ts diff --git a/spec/project/project.spec.ts b/spec/project/project.spec.ts index d40c8747..2670d6de 100644 --- a/spec/project/project.spec.ts +++ b/spec/project/project.spec.ts @@ -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 { @@ -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[] = []; diff --git a/src/graphql/is-comment-only-source.ts b/src/graphql/is-comment-only-source.ts new file mode 100644 index 00000000..78767070 --- /dev/null +++ b/src/graphql/is-comment-only-source.ts @@ -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; + } +} diff --git a/src/schema/schema-builder.ts b/src/schema/schema-builder.ts index 4b911512..216434a2 100644 --- a/src/schema/schema-builder.ts +++ b/src/schema/schema-builder.ts @@ -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'; @@ -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 @@ -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; }