diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 00000000..e69de29b diff --git a/spec/authorization/permission-descriptor.spec.ts b/spec/authorization/permission-descriptor.spec.ts index f43c3ca7..beb81ab6 100644 --- a/spec/authorization/permission-descriptor.spec.ts +++ b/spec/authorization/permission-descriptor.spec.ts @@ -7,7 +7,7 @@ import { BinaryOperationQueryNode, BinaryOperator, ConstBoolQueryNode, FieldQueryNode, LiteralQueryNode, QueryNode, VariableQueryNode } from '../../src/query-tree'; -import { ACCESS_GROUP_FIELD } from '../../src/schema/schema-defaults'; +import { ACCESS_GROUP_FIELD } from '../../src/schema/constants'; import { expect } from 'chai'; diff --git a/spec/performance/query-pipeline.perf.ts b/spec/performance/query-pipeline.perf.ts index fad559c4..ec7b83c4 100644 --- a/spec/performance/query-pipeline.perf.ts +++ b/spec/performance/query-pipeline.perf.ts @@ -1,18 +1,17 @@ -import { BenchmarkConfig, BenchmarkFactories } from './support/async-bench'; import { DocumentNode, GraphQLSchema, parse, validate } from 'graphql'; import * as path from 'path'; -import { DistilledOperation, distillQuery } from '../../src/graphql/query-distiller'; -import { createQueryTree } from '../../src/query/query-tree-builder'; +import { applyAuthorizationToQueryTree } from '../../src/authorization/execution'; import { getAQLQuery } from '../../src/database/arangodb/aql-generator'; -import { QueryNode } from '../../src/query-tree'; +import { DistilledOperation, distillQuery } from '../../src/graphql/query-distiller'; +import { Model } from '../../src/model'; +import { ObjectQueryNode, QueryNode } from '../../src/query-tree'; +import { buildConditionalObjectQueryNode, QueryNodeObjectType, RootTypesGenerator } from '../../src/schema-generation'; import { compact } from '../../src/utils/utils'; -import { applyAuthorizationToQueryTree } from '../../src/authorization/execution'; -import { Project } from '../../src/project/project'; +import { BenchmarkConfig, BenchmarkFactories } from './support/async-bench'; import { createTestProject } from './support/helpers'; -import { Model } from '../../src/model'; const QUERIES = [ -`{ + `{ allDeliveries { id items { @@ -28,14 +27,14 @@ const QUERIES = [ } }`, -`mutation d { + `mutation d { deleteDelivery(id: "15027307") { id deliveryNumber } }`, -` + ` mutation m { updateDelivery(input: { id: "15116232", @@ -88,20 +87,34 @@ interface PreparedQuery { gql: string; document: DocumentNode; distilledOperation: DistilledOperation; + queryType: QueryNodeObjectType; + mutationType: QueryNodeObjectType; queryTree: QueryNode; authorizedQueryTree: QueryNode; } +function buildQueryTree({distilledOperation, queryType, mutationType}: { distilledOperation: DistilledOperation, queryType: QueryNodeObjectType, mutationType: QueryNodeObjectType }): QueryNode { + if (distilledOperation.operation == 'mutation') { + return buildConditionalObjectQueryNode(ObjectQueryNode.EMPTY, mutationType, distilledOperation.selectionSet); + } else { + return buildConditionalObjectQueryNode(ObjectQueryNode.EMPTY, queryType, distilledOperation.selectionSet); + } +} + function prepareQuery(gql: string, schema: GraphQLSchema, model: Model): PreparedQuery { const document = parse(gql); validate(schema, document); const distilledOperation = distillQuery(document, schema, {}); - const queryTree = createQueryTree(distilledOperation, model); - const authorizedQueryTree = applyAuthorizationToQueryTree(queryTree, { authRoles: []}); + const queryType = new RootTypesGenerator().generateQueryType(model); + const mutationType = new RootTypesGenerator().generateMutationType(model); + const queryTree = buildQueryTree({queryType, mutationType, distilledOperation}); + const authorizedQueryTree = applyAuthorizationToQueryTree(queryTree, {authRoles: []}); return { gql, document, distilledOperation, + queryType, + mutationType, queryTree, authorizedQueryTree }; @@ -112,7 +125,8 @@ function testQueryPipeline(params: { parser: boolean, queryDistiller: boolean, q params.parser ? 'parser' : undefined, params.queryDistiller ? 'query-distiller' : undefined, params.queryTree ? 'query-tree' : undefined, - params.aql ? 'aql' : undefined + params.aql ? 'aql' : undefined, + params.auth ? 'auth' : undefined ]).join(', '); let schema: GraphQLSchema; @@ -132,16 +146,16 @@ function testQueryPipeline(params: { parser: boolean, queryDistiller: boolean, q fn() { const preparedQuery = preparedQueries[Math.floor(Math.random() * preparedQueries.length)]; if (params.parser) { - parse(preparedQuery.gql) + parse(preparedQuery.gql); } if (params.queryDistiller) { distillQuery(preparedQuery.document, schema, {}); } if (params.queryTree) { - createQueryTree(preparedQuery.distilledOperation, model); + buildQueryTree(preparedQuery); } if (params.auth) { - applyAuthorizationToQueryTree(preparedQuery.queryTree, { authRoles: []}); + applyAuthorizationToQueryTree(preparedQuery.queryTree, {authRoles: []}); } if (params.aql) { const transaction = getAQLQuery(preparedQuery.authorizedQueryTree); @@ -152,12 +166,12 @@ function testQueryPipeline(params: { parser: boolean, queryDistiller: boolean, q } const benchmarks: BenchmarkFactories = [ - () => testQueryPipeline({parser: true, queryDistiller: false, queryTree: false, auth: false, aql: false }), - () => testQueryPipeline({parser: false, queryDistiller: true, queryTree: false, auth: false, aql: false }), - () => testQueryPipeline({parser: false, queryDistiller: false, queryTree: true, auth: false, aql: false }), - () => testQueryPipeline({parser: false, queryDistiller: false, queryTree: true, auth: true, aql: false }), - () => testQueryPipeline({parser: false, queryDistiller: false, queryTree: false, auth: false, aql: true }), - () => testQueryPipeline({parser: true, queryDistiller: true, queryTree: true, auth: true, aql: true }) + () => testQueryPipeline({parser: true, queryDistiller: false, queryTree: false, auth: false, aql: false}), + () => testQueryPipeline({parser: false, queryDistiller: true, queryTree: false, auth: false, aql: false}), + () => testQueryPipeline({parser: false, queryDistiller: false, queryTree: true, auth: false, aql: false}), + () => testQueryPipeline({parser: false, queryDistiller: false, queryTree: true, auth: true, aql: false}), + () => testQueryPipeline({parser: false, queryDistiller: false, queryTree: false, auth: false, aql: true}), + () => testQueryPipeline({parser: true, queryDistiller: true, queryTree: true, auth: true, aql: true}) ]; -export default benchmarks; \ No newline at end of file +export default benchmarks; diff --git a/spec/performance/support/helpers.ts b/spec/performance/support/helpers.ts index 2f428223..7bb00d40 100644 --- a/spec/performance/support/helpers.ts +++ b/spec/performance/support/helpers.ts @@ -21,8 +21,8 @@ export interface TestEnvironment { const schemaContext: SchemaContext = { loggerProvider: new Log4jsLoggerProvider('warn') }; -export async function createTestProject(modelPath: string): Promise<{project: Project, schema: GraphQLSchema}> { - const project = await loadProjectFromDir(MODEL_PATH, schemaContext); +export async function createTestProject(modelPath: string = MODEL_PATH): Promise<{project: Project, schema: GraphQLSchema}> { + const project = await loadProjectFromDir(modelPath, schemaContext); const dbConfig = await createTempDatabase(); const dbAdapter = new ArangoDBAdapter(dbConfig, schemaContext); const schema = project.createSchema(dbAdapter); diff --git a/spec/query/query-tree-utils.spec.ts b/spec/query-tree/utils/simplify-booleans.ts similarity index 97% rename from spec/query/query-tree-utils.spec.ts rename to spec/query-tree/utils/simplify-booleans.ts index caaf123a..51ad9dba 100644 --- a/spec/query/query-tree-utils.spec.ts +++ b/spec/query-tree/utils/simplify-booleans.ts @@ -1,9 +1,9 @@ +import { expect } from 'chai'; import { BinaryOperationQueryNode, BinaryOperator, ConstBoolQueryNode, LiteralQueryNode, QueryNode, UnaryOperationQueryNode, UnaryOperator -} from '../../src/query-tree'; -import { simplifyBooleans } from '../../src/query/query-tree-utils'; -import { expect } from 'chai'; +} from '../../../src/query-tree'; +import { simplifyBooleans } from '../../../src/query-tree/utils'; describe('query-tree-utils', () => { describe('simplifyBooleans', () => { diff --git a/spec/query/query-tree-builder.spec.ts b/spec/query/query-tree-builder.spec.ts deleted file mode 100644 index b42a1166..00000000 --- a/spec/query/query-tree-builder.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { buildASTSchema, parse } from 'graphql'; -import { distillQuery } from '../../src/graphql/query-distiller'; -import { createQueryTree } from '../../src/query/query-tree-builder'; -import { - EntitiesQueryNode, FieldQueryNode, ObjectQueryNode, RootEntityIDQueryNode, TransformListQueryNode -} from '../../src/query-tree'; -import { expect } from 'chai'; -import { createModel, Model } from '../../src/model'; -import { SchemaConfig } from '../../src/config/schema-config'; - -describe('query-tree-builder', () => { - const ast = parse(` - schema { - query: Query - } - type Query { - allUsers: [User] - } - type User { - id: ID - name: String - } - `); - const schema = buildASTSchema(ast); - - it('builds a simple entity fetch tree', () => { - async function test() { - await Promise.resolve(123); - } - test(); - const query = `{ allUsers { code: id, name } }`; - const op = distillQuery(parse(query), schema); - const queryTree = createQueryTree(op, createModel({ schemaParts: [ { document: ast } ] })); - expect(queryTree.properties.length).to.equal(1); - expect(queryTree.properties[0].propertyName).to.equal('allUsers'); - expect(queryTree.properties[0].valueNode).to.be.an.instanceof(TransformListQueryNode); - const listNode = queryTree.properties[0].valueNode as TransformListQueryNode; - expect(listNode.listNode).to.be.an.instanceof(EntitiesQueryNode); - const entitiesNode = listNode.listNode as EntitiesQueryNode; - expect(entitiesNode.rootEntityType.name).to.equal('User'); - expect(listNode.innerNode).to.be.an.instanceof(ObjectQueryNode); - const objectNode = listNode.innerNode as ObjectQueryNode; - expect(objectNode.properties.length).to.equal(2); - expect(objectNode.properties[0].propertyName).to.equal('code'); - expect(objectNode.properties[0].valueNode).to.be.an.instanceof(RootEntityIDQueryNode); - expect(objectNode.properties[1].propertyName).to.equal('name'); - expect(objectNode.properties[1].valueNode).to.be.an.instanceof(FieldQueryNode); - expect((objectNode.properties[1].valueNode as FieldQueryNode).field.name).to.equal('name'); - console.log(queryTree.describe()); - }); -}); diff --git a/spec/regression/initialization.ts b/spec/regression/initialization.ts index a10bfdf3..aa955f1b 100644 --- a/spec/regression/initialization.ts +++ b/spec/regression/initialization.ts @@ -3,7 +3,7 @@ import { Database } from 'arangojs'; import {ExecutionResult, graphql, GraphQLSchema} from 'graphql'; import * as fs from 'fs'; import stripJsonComments = require('strip-json-comments'); -import {NAMESPACE_SEPARATOR} from "../../src/schema/schema-defaults"; +import {NAMESPACE_SEPARATOR} from "../../src/schema/constants"; const DATABASE_NAME = 'cruddl-test-temp'; const DATABASE_URL = 'http://root:@localhost:8529'; diff --git a/spec/query/createdat-updatedat-modification.spec.ts b/spec/schema-generation/createdat-updatedat-modification.spec.ts similarity index 100% rename from spec/query/createdat-updatedat-modification.spec.ts rename to spec/schema-generation/createdat-updatedat-modification.spec.ts diff --git a/spec/schema/preparation/ast-transformation-modules/add-filter-arguments-to-fields-transformer.spec.ts b/spec/schema/preparation/ast-transformation-modules/add-filter-arguments-to-fields-transformer.spec.ts deleted file mode 100644 index 81d60134..00000000 --- a/spec/schema/preparation/ast-transformation-modules/add-filter-arguments-to-fields-transformer.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import {NamedTypeNode, DefinitionNode, ObjectTypeDefinitionNode, parse} from "graphql"; -import {NAMED_TYPE, OBJECT_TYPE_DEFINITION} from "../../../../src/graphql/kinds"; -import {FILTER_ARG} from "../../../../src/schema/schema-defaults"; -import {AddFilterArgumentsToFieldsTransformer} from "../../../../src/schema/preparation/post-merge-ast-transformation-modules/add-filter-arguments-to-fields-transformer"; -import { expect } from 'chai'; - -const sdl = ` - type Foo @rootEntity { - id: ID - createdAt: DateTime - updatedAt: DateTime - foo: String! - bar: Bar - } - - type Bar @embedded { - size: Int! - name: String - } - - scalar DateTime - - # the next three types are not defined in AST, yet. Normally, they are created along with a new GraphQLSchema. - scalar String - scalar ID - scalar Int - - type Query { - allFoos: [Foo!]! - } - - schema { - query: Query - } - - input FooFilter { - bla: String - } - `; - -describe('add-filter-arguments-to-fields', () => { - it('meets preconditions', () => { - const ast = parse(sdl); - // there are no filter arguments before running the transformer. - const queryType: ObjectTypeDefinitionNode = ast.definitions.find(def => def.kind === OBJECT_TYPE_DEFINITION && def.name.value === 'Query') as ObjectTypeDefinitionNode; - const allFoosField = queryType.fields.find(field => field.name.value === 'allFoos'); - const filterArg = allFoosField!.arguments.find(arg => arg.name.value === FILTER_ARG); - expect(filterArg).to.be.undefined; - }); - - const ast = parse(sdl); - new AddFilterArgumentsToFieldsTransformer().transform(ast); - - it ('adds a new arg `filter` to Query.allFoos() of type FooFooFilter', () => { - const queryType: ObjectTypeDefinitionNode = ast.definitions.find(def => def.kind === OBJECT_TYPE_DEFINITION && def.name.value === 'Query') as ObjectTypeDefinitionNode; - const allFoosField = queryType.fields.find(field => field.name.value === 'allFoos'); - const filterArg = allFoosField!.arguments.find(arg => arg.name.value === FILTER_ARG); - expect(filterArg).to.not.be.undefined; - expect(filterArg!.type.kind).to.equal(NAMED_TYPE); - expect((filterArg!.type as NamedTypeNode).name.value).to.equal('FooFilter'); - }); - -}); diff --git a/spec/schema/preparation/ast-transformation-modules/add-filter-input-types-transformer.spec.ts b/spec/schema/preparation/ast-transformation-modules/add-filter-input-types-transformer.spec.ts deleted file mode 100644 index 4f293e4b..00000000 --- a/spec/schema/preparation/ast-transformation-modules/add-filter-input-types-transformer.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import {parse} from "graphql"; -import {INPUT_OBJECT_TYPE_DEFINITION} from "../../../../src/graphql/kinds"; -import {AddFilterInputTypesTransformer} from "../../../../src/schema/preparation/post-merge-ast-transformation-modules/add-filter-input-types-transformer"; -import {getNamedInputTypeDefinitionAST} from "../../../../src/schema/schema-utils"; -import { expect } from 'chai'; - -const sdl = ` - type Foo @rootEntity { - id: ID - createdAt: DateTime - updatedAt: DateTime - foo: String! - bar: Bar - } - - type Bar @embedded { - size: Int! - name: String - } - - scalar DateTime - - # the next three types are not defined in AST, yet. Normally, they are created along with a new GraphQLSchema. - scalar String - scalar ID - scalar Int - - `; - -describe('add-input-types', () => { - it('meets preconditions', () => { - const ast = parse(sdl); - // there are no filter/input types before running the transformer. - expect(ast.definitions.find(def => def.kind === INPUT_OBJECT_TYPE_DEFINITION)).to.be.undefined; - }); - - const ast = parse(sdl); - new AddFilterInputTypesTransformer().transform(ast); - - it ('contains a filter type for Foo', () => { - const fooFilter = getNamedInputTypeDefinitionAST(ast, 'FooFilter') - expect(fooFilter).to.not.be.undefined; - expect(fooFilter.kind).to.equal(INPUT_OBJECT_TYPE_DEFINITION); - // TODO add more tests here. - }); - - it ('contains a filter type for Bar', () => { - const barFilter = getNamedInputTypeDefinitionAST(ast, 'BarFilter') - expect(barFilter).to.not.be.undefined; - expect(barFilter.kind).to.equal(INPUT_OBJECT_TYPE_DEFINITION); - // TODO add more tests here. - }); - -}); diff --git a/spec/schema/preparation/ast-transformation-modules/add-missing-entity-fields-transformer.spec.ts b/spec/schema/preparation/ast-transformation-modules/add-missing-entity-fields-transformer.spec.ts deleted file mode 100644 index 44ae791f..00000000 --- a/spec/schema/preparation/ast-transformation-modules/add-missing-entity-fields-transformer.spec.ts +++ /dev/null @@ -1,67 +0,0 @@ -import {ObjectTypeDefinitionNode, parse} from "graphql"; -import {ENTITY_CREATED_AT, ENTITY_ID, ENTITY_UPDATED_AT, SCALAR_DATETIME} from "../../../../src/schema/schema-defaults"; -import {objectTypeFieldsWithNameOfNamedType} from "../../schema-test-utils"; -import {AddMissingEntityFieldsTransformer} from "../../../../src/schema/preparation/post-merge-ast-transformation-modules/add-missing-entity-fields-transformer"; -import { expect } from 'chai'; - -const sdl = ` - type Foo @rootEntity { - foo: String - } - - type Bar @rootEntity { - id: ID - createdAt: DateTime - updatedAt: DateTime - name: String - } - - scalar DateTime - - `; - -describe('add-missing-entity-fields transformer', () => { - it('meets preconditions', () => { - const ast = parse(sdl); - - const fooDefinition = ast.definitions[0] as ObjectTypeDefinitionNode; - expect(objectTypeFieldsWithNameOfNamedType(fooDefinition, ENTITY_ID, 'ID').length).to.equal(0); - expect(objectTypeFieldsWithNameOfNamedType(fooDefinition, ENTITY_CREATED_AT, SCALAR_DATETIME).length).to.equal(0); - expect(objectTypeFieldsWithNameOfNamedType(fooDefinition, ENTITY_UPDATED_AT, SCALAR_DATETIME).length).to.equal(0); - - const barDefinition = ast.definitions[1] as ObjectTypeDefinitionNode; - expect(objectTypeFieldsWithNameOfNamedType(barDefinition, ENTITY_ID, 'ID').length).to.equal(1); - expect(objectTypeFieldsWithNameOfNamedType(barDefinition, ENTITY_CREATED_AT, SCALAR_DATETIME).length).to.equal(1); - expect(objectTypeFieldsWithNameOfNamedType(barDefinition, ENTITY_UPDATED_AT, SCALAR_DATETIME).length).to.equal(1); - }); - - const ast = parse(sdl); - new AddMissingEntityFieldsTransformer().transform(ast); - const fooDefinition = ast.definitions[0] as ObjectTypeDefinitionNode; - const barDefinition = ast.definitions[1] as ObjectTypeDefinitionNode; - - it ('adds an id', () => { - expect(objectTypeFieldsWithNameOfNamedType(fooDefinition, ENTITY_ID, 'ID').length).to.equal(1); - }); - - it ('adds a createdAt DateTime', () => { - expect(objectTypeFieldsWithNameOfNamedType(fooDefinition, ENTITY_CREATED_AT, SCALAR_DATETIME).length).to.equal(1); - }); - - it ('adds an updatedAt DateTime', () => { - expect(objectTypeFieldsWithNameOfNamedType(fooDefinition, ENTITY_UPDATED_AT, SCALAR_DATETIME).length).to.equal(1); - }); - - it ('keeps an id', () => { - expect(objectTypeFieldsWithNameOfNamedType(barDefinition, ENTITY_ID, 'ID').length).to.equal(1); - }); - - it ('keeps a createdAt DateTime', () => { - expect(objectTypeFieldsWithNameOfNamedType(barDefinition, ENTITY_CREATED_AT, SCALAR_DATETIME).length).to.equal(1); - }); - - it ('keeps an updatedAt DateTime', () => { - expect(objectTypeFieldsWithNameOfNamedType(barDefinition, ENTITY_UPDATED_AT, SCALAR_DATETIME).length).to.equal(1); - }); - -}); diff --git a/spec/schema/preparation/ast-transformation-modules/add-namespaces-to-types-transformer.spec.ts b/spec/schema/preparation/ast-transformation-modules/add-namespaces-to-types-transformer.spec.ts index 45263a08..a15bfb71 100644 --- a/spec/schema/preparation/ast-transformation-modules/add-namespaces-to-types-transformer.spec.ts +++ b/spec/schema/preparation/ast-transformation-modules/add-namespaces-to-types-transformer.spec.ts @@ -1,7 +1,7 @@ import { parse } from 'graphql'; import { findDirectiveWithName, getObjectTypes } from '../../../../src/schema/schema-utils'; import { AddNamespacesToTypesTransformer } from '../../../../src/schema/preparation/pre-merge-ast-transformation-modules/add-namespaces-to-types-transformer'; -import { NAMESPACE_DIRECTIVE, ROOT_ENTITY_DIRECTIVE } from '../../../../src/schema/schema-defaults'; +import { NAMESPACE_DIRECTIVE, ROOT_ENTITY_DIRECTIVE } from '../../../../src/schema/constants'; import { STRING } from '../../../../src/graphql/kinds'; import { expect } from 'chai'; diff --git a/spec/schema/preparation/ast-transformation-modules/add-order-by-enums-transformer.spec.ts b/spec/schema/preparation/ast-transformation-modules/add-order-by-enums-transformer.spec.ts deleted file mode 100644 index 914d6549..00000000 --- a/spec/schema/preparation/ast-transformation-modules/add-order-by-enums-transformer.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -import {parse} from "graphql"; -import {ENUM_TYPE_DEFINITION, INPUT_OBJECT_TYPE_DEFINITION} from "../../../../src/graphql/kinds"; -import {AddFilterInputTypesTransformer} from "../../../../src/schema/preparation/post-merge-ast-transformation-modules/add-filter-input-types-transformer"; -import {getNamedInputTypeDefinitionAST, getNamedTypeDefinitionAST} from "../../../../src/schema/schema-utils"; -import {AddOrderbyInputEnumsTransformer} from "../../../../src/schema/preparation/post-merge-ast-transformation-modules/add-orderby-enums-transformer"; -import { expect } from 'chai'; - -const sdl = ` - type Foo @rootEntity { - id: ID - createdAt: DateTime - updatedAt: DateTime - foo: String! - bar: Bar - } - - type Bar @embedded { - size: Int! - name: String - } - - scalar DateTime - - # the next three types are not defined in AST, yet. Normally, they are created along with a new GraphQLSchema. - scalar String - scalar ID - scalar Int - - `; - -describe('add-order-by-enums', () => { - it('meets preconditions', () => { - const ast = parse(sdl); - // there are no filter/input types before running the transformer. - expect(ast.definitions.find(def => def.kind === ENUM_TYPE_DEFINITION)).to.be.undefined; - }); - - const ast = parse(sdl); - new AddOrderbyInputEnumsTransformer().transform(ast); - - it ('contains an enum for Foo', () => { - const fooOrderByEnum = getNamedTypeDefinitionAST(ast, 'FooOrderBy'); - expect(fooOrderByEnum).not.to.be.undefined; - expect(fooOrderByEnum.kind).to.equal(ENUM_TYPE_DEFINITION); - // TODO add more tests here. - }); - - it ('contains an enum for Bar', () => { - const barOrderByEnum = getNamedTypeDefinitionAST(ast, 'BarOrderBy'); - expect(barOrderByEnum).to.not.be.undefined; - expect(barOrderByEnum.kind).to.equal(ENUM_TYPE_DEFINITION); - // TODO add more tests here. - }); - -}); diff --git a/spec/schema/preparation/ast-transformation-modules/add-orderby-arguments-to-fields-transformer.spec.ts b/spec/schema/preparation/ast-transformation-modules/add-orderby-arguments-to-fields-transformer.spec.ts deleted file mode 100644 index d6265068..00000000 --- a/spec/schema/preparation/ast-transformation-modules/add-orderby-arguments-to-fields-transformer.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import {ListTypeNode, NamedTypeNode, NonNullTypeNode, ObjectTypeDefinitionNode, parse} from "graphql"; -import {LIST_TYPE, NAMED_TYPE, NON_NULL_TYPE, OBJECT_TYPE_DEFINITION} from "../../../../src/graphql/kinds"; -import {ORDER_BY_ARG} from "../../../../src/schema/schema-defaults"; -import {AddOrderbyArgumentsToFieldsTransformer} from "../../../../src/schema/preparation/post-merge-ast-transformation-modules/add-orderby-arguments-to-fields-transformer"; -import { expect } from 'chai'; - -const sdl = ` - type Foo @rootEntity { - id: ID - createdAt: DateTime - updatedAt: DateTime - foo: String! - bar: Bar - } - - type Bar @embedded { - size: Int! - name: String - } - - scalar DateTime - - # the next three types are not defined in AST, yet. Normally, they are created along with a new GraphQLSchema. - scalar String - scalar ID - scalar Int - - type Query { - allFoos: [Foo!]! - } - - schema { - query: Query - } - - enum FooOrderBy { - A, B, C - } - `; - -describe('add-orderby-arguments-to-fields', () => { - it('meets preconditions', () => { - const ast = parse(sdl); - // there are no orderBy arguments before running the transformer. - const queryType: ObjectTypeDefinitionNode = ast.definitions.find(def => def.kind === OBJECT_TYPE_DEFINITION && def.name.value === 'Query') as ObjectTypeDefinitionNode; - const allFoosField = queryType.fields.find(field => field.name.value === 'allFoos'); - const orderByArg = allFoosField!.arguments.find(arg => arg.name.value === ORDER_BY_ARG); - expect(orderByArg).to.be.undefined; - }); - - const ast = parse(sdl); - new AddOrderbyArgumentsToFieldsTransformer().transform(ast); - - it ('adds a new arg `order by` to Query.allFoos() of type FooOrderBy', () => { - const queryType: ObjectTypeDefinitionNode = ast.definitions.find(def => def.kind === OBJECT_TYPE_DEFINITION && def.name.value === 'Query') as ObjectTypeDefinitionNode; - const allFoosField = queryType.fields.find(field => field.name.value === 'allFoos'); - const orderByArg = allFoosField!.arguments.find(arg => arg.name.value === ORDER_BY_ARG); - expect(orderByArg).to.not.be.undefined; - expect(orderByArg!.type.kind).to.equal(LIST_TYPE); - expect((orderByArg!.type as ListTypeNode).type.kind).to.equal(NON_NULL_TYPE); - expect((((orderByArg!.type as ListTypeNode).type) as NonNullTypeNode).type.kind).to.equal(NAMED_TYPE); - expect(((((orderByArg!.type as ListTypeNode).type) as NonNullTypeNode).type as NamedTypeNode).name.value).to.equal('FooOrderBy'); - }); - -}); diff --git a/spec/schema/preparation/ast-transformation-modules/add-root-query-type-transformer.spec.ts b/spec/schema/preparation/ast-transformation-modules/add-root-query-type-transformer.spec.ts deleted file mode 100644 index 566724b4..00000000 --- a/spec/schema/preparation/ast-transformation-modules/add-root-query-type-transformer.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import {ObjectTypeDefinitionNode, parse} from "graphql"; -import {OBJECT_TYPE_DEFINITION} from "../../../../src/graphql/kinds"; -import {getNamedTypeDefinitionAST} from "../../../../src/schema/schema-utils"; -import {AddRootQueryTypeTransformer} from "../../../../src/schema/preparation/post-merge-ast-transformation-modules/add-root-query-type-transformer"; -import { expect } from 'chai'; - -const sdl = ` - type Foo @rootEntity { - id: ID - createdAt: DateTime - updatedAt: DateTime - foo: String! - bar: Bar - } - - type Bar @embedded { - size: Int! - name: String - } - - scalar DateTime - - # the next three types are not defined in AST, yet. Normally, they are created along with a new GraphQLSchema. - scalar String - scalar ID - scalar Int - - `; - -describe('add-root-query-type', () => { - it('meets preconditions', () => { - const ast = parse(sdl); - // there are no filter/input types before running the transformer. - expect(ast.definitions.find(def => def.kind === OBJECT_TYPE_DEFINITION && def.name.value === 'Query')).to.be.undefined; - }); - - const ast = parse(sdl); - new AddRootQueryTypeTransformer().transform(ast); - - it ('contains an object type called Query', () => { - const queryType = getNamedTypeDefinitionAST(ast, 'Query') - expect(queryType).to.not.be.undefined; - expect(queryType.kind).to.equal(OBJECT_TYPE_DEFINITION); - expect(( queryType).fields.find(field => field.name.value === 'Foo')).to.not.be.undefined; - expect(( queryType).fields.find(field => field.name.value === 'allFoos')).to.not.be.undefined; - // Bar is an embedded type => no root fields for Bar - expect(( queryType).fields.find(field => field.name.value === 'Bar')).to.be.undefined; - expect(( queryType).fields.find(field => field.name.value === 'allBar')).to.be.undefined; - - // TODO add more tests here. - }); - -}); diff --git a/spec/schema/preparation/ast-transformation-modules/add-root-schema-transformer.spec.ts b/spec/schema/preparation/ast-transformation-modules/add-root-schema-transformer.spec.ts deleted file mode 100644 index 45138814..00000000 --- a/spec/schema/preparation/ast-transformation-modules/add-root-schema-transformer.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import {parse, SchemaDefinitionNode} from "graphql"; -import {SCHEMA_DEFINITION} from "../../../../src/graphql/kinds"; -import {AddRootSchemaTransformer} from "../../../../src/schema/preparation/post-merge-ast-transformation-modules/add-root-schema-transformer"; -import { expect } from 'chai'; - -const sdl = ` - type Foo @rootEntity { - id: ID - createdAt: DateTime - updatedAt: DateTime - foo: String! - bar: Bar - } - - type Bar @embedded { - size: Int! - name: String - } - - scalar DateTime - - # the next three types are not defined in AST, yet. Normally, they are created along with a new GraphQLSchema. - scalar String - scalar ID - scalar Int - - `; - -describe('add-root-schema', () => { - it('meets preconditions', () => { - const ast = parse(sdl); - // there are no filter/input types before running the transformer. - expect(ast.definitions.find(def => def.kind === SCHEMA_DEFINITION)).to.be.undefined; - }); - - const ast = parse(sdl); - new AddRootSchemaTransformer().transform(ast); - - it ('contains a schema', () => { - const schema = ast.definitions.find(def => def.kind === SCHEMA_DEFINITION); - expect(schema).to.not.be.undefined; - // check for a query operation type - expect(schema.operationTypes.find(opType => opType.operation === 'query')).to.not.be.undefined; - }); - -}); diff --git a/spec/schema/preparation/ast-transformation-modules/non-nullable-lists-transformer.spec.ts b/spec/schema/preparation/ast-transformation-modules/non-nullable-lists-transformer.spec.ts deleted file mode 100644 index 42701ee8..00000000 --- a/spec/schema/preparation/ast-transformation-modules/non-nullable-lists-transformer.spec.ts +++ /dev/null @@ -1,127 +0,0 @@ -import {ListTypeNode, NonNullTypeNode, ObjectTypeDefinitionNode, parse} from "graphql"; -import {NonNullableListsTransformer} from "../../../../src/schema/preparation/post-merge-ast-transformation-modules/non-nullable-lists-transformer"; -import {LIST_TYPE, NAMED_TYPE, NON_NULL_TYPE} from "../../../../src/graphql/kinds"; -import { expect } from 'chai'; - -const sdl1 = ` - type Stuff { - foo: String - bar: Int! - baz: [ListElementStuff] - } - type ListElementStuff { - name: String - } - `; - -const sdl2 = ` - type Stuff { - foo: String - bar: Int! - baz: [ListElementStuff]! - } - type ListElementStuff { - name: String - } - `; - -const sdl3 = ` - type Stuff { - foo: String - bar: Int! - baz: [ListElementStuff!] - } - type ListElementStuff { - name: String - } - `; - -const sdl4 = ` - type Stuff { - foo: String - bar: Int! - baz: [ListElementStuff!]! - } - - type ListElementStuff { - name: String - } - `; - -describe('non-nullable-lists transformer', () => { - it ('keep non list types', () => { - const ast = parse(sdl1); - const stuffDefinition = ast.definitions[0] as ObjectTypeDefinitionNode; - const listElementStuffDefinition = ast.definitions[1] as ObjectTypeDefinitionNode; - // check old ast - expect(listElementStuffDefinition.fields[0].type.kind).to.equal(NAMED_TYPE); - }); - - it('transforms [ListElem]', () => { - const ast = parse(sdl1); - const stuffDefinition = ast.definitions[0] as ObjectTypeDefinitionNode; - const listElementStuffDefinition = ast.definitions[1] as ObjectTypeDefinitionNode; - // check old ast - expect(stuffDefinition.fields[2].type.kind).to.equal(LIST_TYPE); - expect((stuffDefinition.fields[2].type).type.kind).to.equal(NAMED_TYPE); - - // check new ast - new NonNullableListsTransformer().transform(ast); - const stuffDefinitionNew = ast.definitions[0] as ObjectTypeDefinitionNode; - const listElementStuffDefinitionNew = ast.definitions[1] as ObjectTypeDefinitionNode; - - // remains the same, non-list types will not be touched. - expect(listElementStuffDefinitionNew.fields[0].type.kind).to.equal(NAMED_TYPE); - - expect(stuffDefinitionNew.fields[2].type.kind).to.equal(NON_NULL_TYPE); - expect((stuffDefinitionNew.fields[2].type).type.kind).to.equal(LIST_TYPE); - expect(((stuffDefinitionNew.fields[2].type).type).type.kind).to.equal(NON_NULL_TYPE); - - }); - - it('transforms [ListElem]!', () => { - const ast = parse(sdl2); - new NonNullableListsTransformer().transform(ast); - const stuffDefinitionNew = ast.definitions[0] as ObjectTypeDefinitionNode; - const listElementStuffDefinitionNew = ast.definitions[1] as ObjectTypeDefinitionNode; - - // remains the same, non-list types will not be touched. - expect(listElementStuffDefinitionNew.fields[0].type.kind).to.equal(NAMED_TYPE); - - expect(stuffDefinitionNew.fields[2].type.kind).to.equal(NON_NULL_TYPE); - expect((stuffDefinitionNew.fields[2].type).type.kind).to.equal(LIST_TYPE); - expect(((stuffDefinitionNew.fields[2].type).type).type.kind).to.equal(NON_NULL_TYPE); - - }); - - it('transforms [ListElem!]', () => { - const ast = parse(sdl3); - new NonNullableListsTransformer().transform(ast); - const stuffDefinitionNew = ast.definitions[0] as ObjectTypeDefinitionNode; - const listElementStuffDefinitionNew = ast.definitions[1] as ObjectTypeDefinitionNode; - - // remains the same, non-list types will not be touched. - expect(listElementStuffDefinitionNew.fields[0].type.kind).to.equal(NAMED_TYPE); - - expect(stuffDefinitionNew.fields[2].type.kind).to.equal(NON_NULL_TYPE); - expect((stuffDefinitionNew.fields[2].type).type.kind).to.equal(LIST_TYPE); - expect(((stuffDefinitionNew.fields[2].type).type).type.kind).to.equal(NON_NULL_TYPE); - - }); - - it('keeps [ListElem!]!', () => { - const ast = parse(sdl4); - new NonNullableListsTransformer().transform(ast); - const stuffDefinitionNew = ast.definitions[0] as ObjectTypeDefinitionNode; - const listElementStuffDefinitionNew = ast.definitions[1] as ObjectTypeDefinitionNode; - - // remains the same, non-list types will not be touched. - expect(listElementStuffDefinitionNew.fields[0].type.kind).to.equal(NAMED_TYPE); - - expect(stuffDefinitionNew.fields[2].type.kind).to.equal(NON_NULL_TYPE); - expect((stuffDefinitionNew.fields[2].type).type.kind).to.equal(LIST_TYPE); - expect(((stuffDefinitionNew.fields[2].type).type).type.kind).to.equal(NON_NULL_TYPE); - - }); - -}); diff --git a/spec/schema/schema-test-utils.ts b/spec/schema/schema-test-utils.ts deleted file mode 100644 index e18baab8..00000000 --- a/spec/schema/schema-test-utils.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {FieldDefinitionNode, NamedTypeNode, ObjectTypeDefinitionNode} from "graphql"; - -export function objectTypeFieldsWithNameOfNamedType(objectType: ObjectTypeDefinitionNode, name: string, namedType: string): FieldDefinitionNode[] { - return objectType.fields.filter(field => field.name.value === name && (field.type).name.value === namedType); -} \ No newline at end of file diff --git a/src/authorization/permission-descriptors.ts b/src/authorization/permission-descriptors.ts index 20ec66aa..4470c1bb 100644 --- a/src/authorization/permission-descriptors.ts +++ b/src/authorization/permission-descriptors.ts @@ -2,11 +2,11 @@ import { BinaryOperationQueryNode, BinaryOperator, ConstBoolQueryNode, FieldQueryNode, LiteralQueryNode, QueryNode, UnknownValueQueryNode } from '../query-tree'; -import { simplifyBooleans } from '../query/query-tree-utils'; +import { simplifyBooleans } from '../query-tree/utils'; import { Field, Permission, PermissionProfile, RootEntityType } from '../model'; import { flatMap } from 'lodash'; import { AccessOperation, AuthContext } from './auth-basics'; -import { ACCESS_GROUP_FIELD } from '../schema/schema-defaults'; +import { ACCESS_GROUP_FIELD } from '../schema/constants'; export enum ConditionExplanationContext { BEFORE_WRITE, diff --git a/src/authorization/transformers/affected-field-info.ts b/src/authorization/transformers/affected-field-info.ts index 4c1070ff..d5feb2a8 100644 --- a/src/authorization/transformers/affected-field-info.ts +++ b/src/authorization/transformers/affected-field-info.ts @@ -1,7 +1,7 @@ import { AccessOperation, AuthContext, AUTHORIZATION_ERROR_NAME } from '../auth-basics'; import { AffectedFieldInfoQueryNode, QueryNode, RuntimeErrorQueryNode } from '../../query-tree'; import { PermissionResult } from '../permission-descriptors'; -import { ACCESS_GROUP_FIELD } from '../../schema/schema-defaults'; +import { ACCESS_GROUP_FIELD } from '../../schema/constants'; import { getPermissionDescriptorOfField, getPermissionDescriptorOfRootEntityType } from '../permission-descriptors-in-model'; diff --git a/src/database/arangodb/aql-generator.ts b/src/database/arangodb/aql-generator.ts index 63e2c465..1c0de717 100644 --- a/src/database/arangodb/aql-generator.ts +++ b/src/database/arangodb/aql-generator.ts @@ -9,7 +9,7 @@ import { UnaryOperationQueryNode, UnaryOperator, UpdateEntitiesQueryNode, VariableAssignmentQueryNode, VariableQueryNode, WithPreExecutionQueryNode } from '../../query-tree'; -import { simplifyBooleans } from '../../query/query-tree-utils'; +import { simplifyBooleans } from '../../query-tree/utils'; import { decapitalize } from '../../utils/utils'; import { aql, AQLCompoundQuery, AQLFragment, AQLQueryResultVariable, AQLVariable } from './aql'; import { getCollectionNameForRelation, getCollectionNameForRootEntity } from './arango-basics'; diff --git a/src/graphql/language-utils.ts b/src/graphql/language-utils.ts index b84bc637..0900ea2b 100644 --- a/src/graphql/language-utils.ts +++ b/src/graphql/language-utils.ts @@ -373,6 +373,3 @@ export function getAliasOrName(fieldNode: FieldNode) { } return fieldNode.name.value; } - -export const introspectionTypes = ['_ExtendedIntrospection', '_ExtendedType', '_ExtendedField', '_FieldMetadata', '_FieldLink', '_FieldJoin']; - diff --git a/src/graphql/names.ts b/src/graphql/names.ts deleted file mode 100644 index 5bc3e721..00000000 --- a/src/graphql/names.ts +++ /dev/null @@ -1,202 +0,0 @@ -import {ObjectTypeDefinitionNode, TypeDefinitionNode} from "graphql"; -import * as pluralize from "pluralize"; -import { - ADD_CHILD_ENTITIES_FIELD_PREFIX, ADD_EDGES_FIELD_PREFIX, - ALL_ENTITIES_FIELD_PREFIX, - CHILD_ENTITY_DIRECTIVE, - CREATE_ENTITY_FIELD_PREFIX, DELETE_ALL_ENTITIES_FIELD_PREFIX, - DELETE_ENTITY_FIELD_PREFIX, - INPUT_FIELD_CONTAINS, - INPUT_FIELD_ENDS_WITH, - INPUT_FIELD_EVERY, - INPUT_FIELD_GT, - INPUT_FIELD_GTE, INPUT_FIELD_IN, - INPUT_FIELD_LT, - INPUT_FIELD_LTE, - INPUT_FIELD_NONE, - INPUT_FIELD_NOT, - INPUT_FIELD_NOT_CONTAINS, - INPUT_FIELD_NOT_ENDS_WITH, - INPUT_FIELD_NOT_IN, - INPUT_FIELD_NOT_STARTS_WITH, - INPUT_FIELD_SEPARATOR, - INPUT_FIELD_SOME, - INPUT_FIELD_STARTS_WITH, - ORDER_BY_ASC_SUFFIX, - ORDER_BY_DESC_SUFFIX, - REMOVE_CHILD_ENTITIES_FIELD_PREFIX, REMOVE_EDGES_FIELD_PREFIX, - ROOT_ENTITY_DIRECTIVE, UPDATE_ALL_ENTITIES_FIELD_PREFIX, - UPDATE_CHILD_ENTITIES_FIELD_PREFIX, - UPDATE_ENTITY_FIELD_PREFIX -} from '../schema/schema-defaults'; -import {capitalize} from "../utils/utils"; -import {hasDirectiveWithName} from "../schema/schema-utils"; - -export function getFilterTypeName(entityDefinition: TypeDefinitionNode|string) { - if (typeof(entityDefinition) === 'string') { - return entityDefinition + 'Filter'; - } - return entityDefinition.name.value + 'Filter'; -} - -/** - * Don't use for root entities and child entities, use getCreateInputTypeName or getUpdateInputTypeName instead. - */ -export function getInputTypeName(objectType: ObjectTypeDefinitionNode) { - if (hasDirectiveWithName(objectType, ROOT_ENTITY_DIRECTIVE) || hasDirectiveWithName(objectType, CHILD_ENTITY_DIRECTIVE)) { - throw new Error(`Can't call getInputTypeName() on root and child entities`); - } - return objectType.name.value + 'Input'; -} - -export function getCreateInputTypeName(objectType: ObjectTypeDefinitionNode) { - if (hasDirectiveWithName(objectType, ROOT_ENTITY_DIRECTIVE) || hasDirectiveWithName(objectType, CHILD_ENTITY_DIRECTIVE)) { - return 'Create' + objectType.name.value + 'Input'; - } - return getInputTypeName(objectType); -} - -export function getUpdateInputTypeName(objectType: ObjectTypeDefinitionNode) { - if (hasDirectiveWithName(objectType, ROOT_ENTITY_DIRECTIVE) || hasDirectiveWithName(objectType, CHILD_ENTITY_DIRECTIVE)) { - return 'Update' + objectType.name.value + 'Input'; - } - return getInputTypeName(objectType); -} - -export function getUpdateAllInputTypeName(objectType: ObjectTypeDefinitionNode) { - if (!hasDirectiveWithName(objectType, ROOT_ENTITY_DIRECTIVE)) { - throw new Error(`Calling getUpdateAllInputTypeName() is allowed only for root entities`); - } - return 'UpdateAll' + pluralize(objectType.name.value) + 'Input'; -} - -export function getAddRelationFieldName(fieldName: string) { - return ADD_EDGES_FIELD_PREFIX + capitalize(fieldName); -} - -export function getRemoveRelationFieldName(fieldName: string) { - return REMOVE_EDGES_FIELD_PREFIX + capitalize(fieldName); -} - -export function getAddChildEntitiesFieldName(fieldName: string) { - return ADD_CHILD_ENTITIES_FIELD_PREFIX + capitalize(fieldName); -} - -export function getUpdateChildEntitiesFieldName(fieldName: string) { - return UPDATE_CHILD_ENTITIES_FIELD_PREFIX + capitalize(fieldName); -} - -export function getRemoveChildEntitiesFieldName(fieldName: string) { - return REMOVE_CHILD_ENTITIES_FIELD_PREFIX + capitalize(fieldName); -} - -export function getOrderByEnumTypeName(entityDefinition: ObjectTypeDefinitionNode) { - return entityDefinition.name.value + 'OrderBy'; -} - - -// identifier cross reference: query/filtering.ts - -export function notField(name: string) { - return name + INPUT_FIELD_SEPARATOR + INPUT_FIELD_NOT; -} - -export function inField(name: string) { - return name + INPUT_FIELD_SEPARATOR + INPUT_FIELD_IN; -} - -export function notInField(name: string) { - return name + INPUT_FIELD_SEPARATOR + INPUT_FIELD_NOT_IN; -} - -export function ltField(name: string) { - return name + INPUT_FIELD_SEPARATOR + INPUT_FIELD_LT; -} - -export function lteField(name: string) { - return name + INPUT_FIELD_SEPARATOR + INPUT_FIELD_LTE; -} - -export function gtField(name: string) { - return name + INPUT_FIELD_SEPARATOR + INPUT_FIELD_GT; -} - -export function gteField(name: string) { - return name + INPUT_FIELD_SEPARATOR + INPUT_FIELD_GTE; -} - -export function containsField(name: string) { - return name + INPUT_FIELD_SEPARATOR + INPUT_FIELD_CONTAINS; -} - -export function notContainsField(name: string) { - return name + INPUT_FIELD_SEPARATOR + INPUT_FIELD_NOT_CONTAINS; -} - -export function startsWithField(name: string) { - return name + INPUT_FIELD_SEPARATOR + INPUT_FIELD_STARTS_WITH; -} - -export function notStartsWithField(name: string) { - return name + INPUT_FIELD_SEPARATOR + INPUT_FIELD_NOT_STARTS_WITH; -} - -export function endsWithField(name: string) { - return name + INPUT_FIELD_SEPARATOR + INPUT_FIELD_ENDS_WITH; -} - -export function notEndsWithField(name: string) { - return name + INPUT_FIELD_SEPARATOR + INPUT_FIELD_NOT_ENDS_WITH; -} - -export function someField(name: string) { - return name + INPUT_FIELD_SEPARATOR + INPUT_FIELD_SOME; -} - -export function everyField(name: string) { - return name + INPUT_FIELD_SEPARATOR + INPUT_FIELD_EVERY; -} - -export function noneField(name: string) { - return name + INPUT_FIELD_SEPARATOR + INPUT_FIELD_NONE; -} - -export function getAllEntitiesFieldName(entityName: string) { - return ALL_ENTITIES_FIELD_PREFIX + pluralize(entityName); -} - -export function getMetaFieldName(field: string) { - return '_' + field + 'Meta'; -} - -export function createEntityQuery(entityName: string) { - return CREATE_ENTITY_FIELD_PREFIX + entityName; -} - -export function updateEntityQuery(entityName: string) { - return UPDATE_ENTITY_FIELD_PREFIX + entityName; -} - -export function updateAllEntitiesQuery(entityName: string) { - return UPDATE_ALL_ENTITIES_FIELD_PREFIX + pluralize(entityName); -} - -export function deleteEntityQuery(entityName: string) { - return DELETE_ENTITY_FIELD_PREFIX + entityName; -} - -export function deleteAllEntitiesQuery(entityName: string) { - return DELETE_ALL_ENTITIES_FIELD_PREFIX + pluralize(entityName); -} - -export function sortedByAsc(name: string) { - return name + ORDER_BY_ASC_SUFFIX; -} - -export function sortedByDesc(name: string) { - return name + ORDER_BY_DESC_SUFFIX; -} - -export function namespacedType(namespace: string, opType: string) { - return capitalize(namespace) + opType; -} diff --git a/src/graphql/query-distiller.ts b/src/graphql/query-distiller.ts index c5e2e802..f3228842 100644 --- a/src/graphql/query-distiller.ts +++ b/src/graphql/query-distiller.ts @@ -1,16 +1,15 @@ +import { blue, cyan, green } from 'colors/safe'; import { DocumentNode, FieldNode, FragmentDefinitionNode, getNamedType, GraphQLCompositeType, GraphQLField, GraphQLObjectType, GraphQLOutputType, GraphQLSchema, isCompositeType, OperationDefinitionNode, OperationTypeNode, SelectionNode } from 'graphql'; - -import { resolveSelections } from './field-collection'; import { arrayToObject, flatMap, groupArray, indent, INDENTATION } from '../utils/utils'; +import { getArgumentValues } from './argument-values'; +import { resolveSelections } from './field-collection'; +import { getAliasOrName } from './language-utils'; import { extractOperation } from './operations'; import { getOperationRootType } from './schema-utils'; -import { getAliasOrName } from './language-utils'; -import { getArgumentValues } from './argument-values'; -import { blue, cyan, green } from 'colors/safe'; /** * A request for the value of one field with a specific argument set and selection set @@ -122,13 +121,6 @@ export function distillOperation(params: OperationDistillationParams): Distilled return new DistilledOperation(params.operation.operation, selections) } -export interface QueryDistillationParams { - schema: GraphQLSchema - document: DocumentNode - variableValues: { [name: string]: any } - operationName?: string -} - /** * Creates a simplified description of an operation in a query */ diff --git a/src/graphql/value-from-ast.ts b/src/graphql/value-from-ast.ts new file mode 100644 index 00000000..a6087d31 --- /dev/null +++ b/src/graphql/value-from-ast.ts @@ -0,0 +1,29 @@ +import { ValueNode } from 'graphql'; +import { BOOLEAN, ENUM, FLOAT, INT, LIST, OBJECT, STRING } from './kinds'; +import { PlainObject } from '../utils/utils'; + +/** + * Gets the value of a ValueNode + * + * Like valueFromAST from graphqljs, but without typechecking so it does not need the input type + */ +export function getValueFromAST(valueNode: ValueNode): any { + switch (valueNode.kind) { + case STRING: + case INT: + case FLOAT: + case BOOLEAN: + case ENUM: + return valueNode.value; + case LIST: + return [...valueNode.values.map(value => getValueFromAST(value))]; + case OBJECT: + const obj: PlainObject = {}; + valueNode.fields.forEach(field => { + obj[field.name.value] = getValueFromAST(field.value); + }); + return obj; + default: + return undefined; + } +} diff --git a/src/model/create-model.ts b/src/model/create-model.ts index 19177e66..0e65373d 100644 --- a/src/model/create-model.ts +++ b/src/model/create-model.ts @@ -17,13 +17,13 @@ import { import { CALC_MUTATIONS_DIRECTIVE, CALC_MUTATIONS_OPERATORS_ARG, CHILD_ENTITY_DIRECTIVE, DEFAULT_VALUE_DIRECTIVE, ENTITY_EXTENSION_DIRECTIVE, INDEX_DEFINITION_INPUT_TYPE, INDEX_DIRECTIVE, INDICES_ARG, INVERSE_OF_ARG, - KEY_FIELD_DIRECTIVE, NAMESPACE_DIRECTIVE, NAMESPACE_NAME_ARG, NAMESPACE_SEPARATOR, OBJECT_TYPE_ENTITY_DIRECTIVES, + KEY_FIELD_DIRECTIVE, NAMESPACE_DIRECTIVE, NAMESPACE_NAME_ARG, NAMESPACE_SEPARATOR, OBJECT_TYPE_KIND_DIRECTIVES, PERMISSION_PROFILE_ARG, REFERENCE_DIRECTIVE, RELATION_DIRECTIVE, ROLES_DIRECTIVE, ROLES_READ_ARG, ROLES_READ_WRITE_ARG, ROOT_ENTITY_DIRECTIVE, UNIQUE_DIRECTIVE, VALUE_ARG, VALUE_OBJECT_DIRECTIVE -} from '../schema/schema-defaults'; +} from '../schema/constants'; import { ValidationMessage } from './validation'; import { PermissionsConfig, RolesSpecifierConfig } from './config'; -import { flattenValueNode } from '../schema/directive-arg-flattener'; +import { getValueFromAST } from '../graphql/value-from-ast'; export function createModel(input: SchemaConfig): Model { const validationMessages: ValidationMessage[] = []; @@ -132,7 +132,7 @@ function getDefaultValue(fieldNode: FieldDefinitionNode, validationMessages: Val validationMessages.push(ValidationMessage.error(VALIDATION_ERROR_MISSING_ARGUMENT_DEFAULT_VALUE, {}, defaultValueDirective.loc)); return undefined; } - return flattenValueNode(defaultValueArg.value); + return getValueFromAST(defaultValueArg.value); } function createFieldInput(fieldNode: FieldDefinitionNode, validationMessages: ValidationMessage[]): FieldConfig { @@ -244,7 +244,7 @@ function mapIndexDefinition(index: ObjectValueNode): IndexDefinitionConfig { } function getKindOfObjectTypeNode(definition: ObjectTypeDefinitionNode, validationMessages?: ValidationMessage[]): string | undefined { - const kindDirectives = (definition.directives || []).filter(dir => OBJECT_TYPE_ENTITY_DIRECTIVES.includes(dir.name.value)); + const kindDirectives = (definition.directives || []).filter(dir => OBJECT_TYPE_KIND_DIRECTIVES.includes(dir.name.value)); if (kindDirectives.length !== 1) { if (validationMessages) { if (kindDirectives.length === 0) { diff --git a/src/model/implementation/field.ts b/src/model/implementation/field.ts index bb234502..9b56a1dc 100644 --- a/src/model/implementation/field.ts +++ b/src/model/implementation/field.ts @@ -5,7 +5,7 @@ import { FieldDefinitionNode } from 'graphql'; import { ObjectType, Type } from './type'; import { PermissionProfile } from './permission-profile'; import { Model } from './model'; -import { CALC_MUTATIONS_OPERATORS } from '../../schema/schema-defaults'; +import { CALC_MUTATIONS_OPERATORS } from '../../schema/constants'; import { RolesSpecifier } from './roles-specifier'; import { Relation } from './relation'; diff --git a/src/model/implementation/model.ts b/src/model/implementation/model.ts index 0bc92fe0..d9abbec3 100644 --- a/src/model/implementation/model.ts +++ b/src/model/implementation/model.ts @@ -13,7 +13,7 @@ import { ScalarType } from './scalar-type'; import { EnumType } from './enum-type'; import { groupBy } from 'lodash'; import { flatMap, objectValues } from '../../utils/utils'; -import { DEFAULT_PERMISSION_PROFILE } from '../../schema/schema-defaults'; +import { DEFAULT_PERMISSION_PROFILE } from '../../schema/constants'; import { Relation } from './relation'; export class Model implements ModelComponent{ diff --git a/src/model/implementation/permission-profile.ts b/src/model/implementation/permission-profile.ts index 7d349e77..58e75e7c 100644 --- a/src/model/implementation/permission-profile.ts +++ b/src/model/implementation/permission-profile.ts @@ -1,4 +1,4 @@ -import { WILDCARD_CHARACTER } from '../../schema/schema-defaults'; +import { WILDCARD_CHARACTER } from '../../schema/constants'; import { escapeRegExp, mapValues } from '../../utils/utils'; import { AccessOperation, AuthContext } from '../../authorization/auth-basics'; import { PermissionAccessKind, PermissionConfig, PermissionProfileConfig, PermissionProfileConfigMap } from '../index'; diff --git a/src/model/implementation/root-entity-type.ts b/src/model/implementation/root-entity-type.ts index bced322c..b009f74b 100644 --- a/src/model/implementation/root-entity-type.ts +++ b/src/model/implementation/root-entity-type.ts @@ -6,7 +6,7 @@ import { ScalarType } from './scalar-type'; import { ValidationContext } from './validation'; import { ValidationMessage } from '../validation'; import { Index } from './indices'; -import { ACCESS_GROUP_FIELD, DEFAULT_PERMISSION_PROFILE } from '../../schema/schema-defaults'; +import { ACCESS_GROUP_FIELD, DEFAULT_PERMISSION_PROFILE } from '../../schema/constants'; import { PermissionProfile } from './permission-profile'; import { Relation } from './relation'; import { GraphQLString } from 'graphql'; diff --git a/src/query-tree/utils/extract-variable-assignments.ts b/src/query-tree/utils/extract-variable-assignments.ts new file mode 100644 index 00000000..90178e42 --- /dev/null +++ b/src/query-tree/utils/extract-variable-assignments.ts @@ -0,0 +1,55 @@ +import { + BinaryOperationQueryNode, FieldQueryNode, FirstOfListQueryNode, ObjectQueryNode, OrderClause, OrderSpecification, + PropertySpecification, QueryNode, RootEntityIDQueryNode, UnaryOperationQueryNode, VariableAssignmentQueryNode +} from '..'; + +/** + * Traverses recursively through Unary/Binary operations, extracts all variable definitions and replaces them by their + * variable nodes + * @param {QueryNode} node + * @param variableAssignmentsList: (will be modified) list to which to add the variable assignment nodes + */ +export function extractVariableAssignments(node: QueryNode, variableAssignmentsList: VariableAssignmentQueryNode[]): QueryNode { + if (node instanceof UnaryOperationQueryNode) { + return new UnaryOperationQueryNode(extractVariableAssignments(node.valueNode, variableAssignmentsList), node.operator); + } + if (node instanceof BinaryOperationQueryNode) { + return new BinaryOperationQueryNode( + extractVariableAssignments(node.lhs, variableAssignmentsList), + node.operator, + extractVariableAssignments(node.rhs, variableAssignmentsList)); + } + if (node instanceof VariableAssignmentQueryNode) { + variableAssignmentsList.push(node); + return node.resultNode; + } + if (node instanceof FirstOfListQueryNode) { + return new FirstOfListQueryNode(extractVariableAssignments(node.listNode, variableAssignmentsList)); + } + if (node instanceof FieldQueryNode) { + return new FieldQueryNode(extractVariableAssignments(node.objectNode, variableAssignmentsList), node.field); + } + if (node instanceof RootEntityIDQueryNode) { + return new RootEntityIDQueryNode(extractVariableAssignments(node.objectNode, variableAssignmentsList)); + } + if (node instanceof ObjectQueryNode) { + return new ObjectQueryNode(node.properties.map(p => new PropertySpecification(p.propertyName, extractVariableAssignments(p.valueNode, variableAssignmentsList)))); + } + return node; +} + +/** + * Wraps a queryNode in a list of variable assignments (the logical counterpart to extractVariableAssignments) + */ +export function prependVariableAssignments(node: QueryNode, variableAssignmentsList: VariableAssignmentQueryNode[]) { + return variableAssignmentsList.reduce((currentNode, assignmentNode) => new VariableAssignmentQueryNode({ + resultNode: currentNode, + variableNode: assignmentNode.variableNode, + variableValueNode: assignmentNode.variableValueNode + }), node); +} + +export function extractVariableAssignmentsInOrderSpecification(orderSpecification: OrderSpecification, variableAssignmentsList: VariableAssignmentQueryNode[]): OrderSpecification { + return new OrderSpecification(orderSpecification.clauses + .map(clause => new OrderClause(extractVariableAssignments(clause.valueNode, variableAssignmentsList), clause.direction))); +} diff --git a/src/query-tree/utils/index.ts b/src/query-tree/utils/index.ts new file mode 100644 index 00000000..8670d8de --- /dev/null +++ b/src/query-tree/utils/index.ts @@ -0,0 +1,3 @@ +export * from './extract-variable-assignments'; +export * from './simplify-booleans'; +export * from './static-evaluation'; diff --git a/src/query-tree/utils/simplify-booleans.ts b/src/query-tree/utils/simplify-booleans.ts new file mode 100644 index 00000000..5860a4c1 --- /dev/null +++ b/src/query-tree/utils/simplify-booleans.ts @@ -0,0 +1,75 @@ +import { + BinaryOperationQueryNode, BinaryOperator, ConstBoolQueryNode, QueryNode, UnaryOperationQueryNode, UnaryOperator +} from '..'; + +/** + * Simplifies a boolean expression (does not recurse into non-boolean-related nodes) + */ +export function simplifyBooleans(node: QueryNode): QueryNode { + if (node instanceof UnaryOperationQueryNode && node.operator == UnaryOperator.NOT) { + const inner = simplifyBooleans(node.valueNode); + if (inner instanceof ConstBoolQueryNode) { + return new ConstBoolQueryNode(!inner.value); + } + return new UnaryOperationQueryNode(inner, UnaryOperator.NOT); + } + if (node instanceof BinaryOperationQueryNode) { + const lhs = simplifyBooleans(node.lhs); + const rhs = simplifyBooleans(node.rhs); + if (node.operator == BinaryOperator.AND) { + if (lhs instanceof ConstBoolQueryNode) { + if (rhs instanceof ConstBoolQueryNode) { + // constant evaluation + return ConstBoolQueryNode.for(lhs.value && rhs.value); + } + if (lhs.value == false) { + // FALSE && anything == FALSE + return ConstBoolQueryNode.FALSE; + } + if (lhs.value == true) { + // TRUE && other == other + return rhs; + } + } + if (rhs instanceof ConstBoolQueryNode) { + if (rhs.value == false) { + // anything && FALSE == FALSE + return ConstBoolQueryNode.FALSE; + } + // const case is handled above + if (rhs.value == true) { + // other && TRUE == other + return lhs; + } + } + } + + if (node.operator == BinaryOperator.OR) { + if (lhs instanceof ConstBoolQueryNode) { + if (rhs instanceof ConstBoolQueryNode) { + // constant evaluation + return ConstBoolQueryNode.for(lhs.value || rhs.value); + } + if (lhs.value == true) { + // TRUE || anything == TRUE + return ConstBoolQueryNode.TRUE; + } + if (lhs.value == false) { + // FALSE || other == other + return rhs; + } + } + if (rhs instanceof ConstBoolQueryNode) { + if (rhs.value == true) { + // anything || TRUE == TRUE + return ConstBoolQueryNode.TRUE; + } + if (rhs.value == false) { + // other || FALSE == other + return lhs; + } + } + } + } + return node; +} diff --git a/src/query/static-evaluation.ts b/src/query-tree/utils/static-evaluation.ts similarity index 87% rename from src/query/static-evaluation.ts rename to src/query-tree/utils/static-evaluation.ts index aebb080d..b3793cc5 100644 --- a/src/query/static-evaluation.ts +++ b/src/query-tree/utils/static-evaluation.ts @@ -1,4 +1,4 @@ -import { ObjectQueryNode, QueryNode } from '../query-tree'; +import { ObjectQueryNode, QueryNode } from '..'; export interface StaticEvaluationResult { canEvaluateStatically: boolean @@ -15,4 +15,4 @@ export function evaluateQueryStatically(queryNode: QueryNode): StaticEvaluationR return { canEvaluateStatically: false } -} \ No newline at end of file +} diff --git a/src/query/fields.ts b/src/query/fields.ts deleted file mode 100644 index 0c6255c2..00000000 --- a/src/query/fields.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { - BinaryOperationQueryNode, BinaryOperator, EntitiesQueryNode, FieldQueryNode, FirstOfListQueryNode, - FollowEdgeQueryNode, QueryNode, RootEntityIDQueryNode, TransformListQueryNode, VariableAssignmentQueryNode, - VariableQueryNode -} from '../query-tree'; -import { ID_FIELD } from '../schema/schema-defaults'; -import { createSafeListQueryNode } from './queries'; -import { Field, ObjectType, RootEntityType, TypeKind } from '../model'; - -export function createScalarFieldValueNode(objectType: ObjectType, fieldName: string, contextNode: QueryNode): QueryNode { - const field = objectType.getFieldOrThrow(fieldName); - if (field.type.isObjectType) { - throw new Error(`Expected field "${objectType.name}.${fieldName} to be of scalar type, but is ${field.type.kind}`); - } - return createNonListFieldValueNode({ - parentType: objectType, - field, - objectNode: contextNode - }); -} - -/** - * Creates a query node that evaluates to the value of a field, and optionally maps that value using a mapping function - * Complex field lookups are cached for the mapping function. - * If a mapping function is specified and the field lookup is complex, wraps the value in a variable. - */ -export function createNonListFieldValueNode(params: {field: Field, parentType: ObjectType, objectNode: QueryNode, innerNodeFn?: (valueNode: QueryNode) => QueryNode }) { - params.innerNodeFn = params.innerNodeFn || (a => a); - - function mapInnerFn(node: QueryNode) { - if (params.innerNodeFn) { - return params.innerNodeFn(node); - } - return node; - } - - function mapInnerFnWithVariable(node: QueryNode) { - if (params.innerNodeFn) { - return VariableAssignmentQueryNode.create(node, params.innerNodeFn, params.field.name); - } - return node; - } - - if (params.field.isList) { - throw new Error(`Type of ${params.field} is unexpectedly a list type`); - } - if (params.field.isRelation) { - return mapInnerFnWithVariable(createTo1RelationNode(params.field, params.objectNode)); - } - if (params.field.isReference) { - return mapInnerFnWithVariable(createTo1ReferenceNode(params.field, params.objectNode)); - } - if (params.parentType.kind == TypeKind.ROOT_ENTITY && params.field.name == ID_FIELD) { - return mapInnerFn(new RootEntityIDQueryNode(params.objectNode)); - } - return mapInnerFn(new FieldQueryNode(params.objectNode, params.field)); -} - -function createTo1RelationNode(field: Field, objectNode: QueryNode): QueryNode { - const relation = field.getRelationOrThrow(); - const followNode = new FollowEdgeQueryNode(relation, objectNode, relation.getFieldSide(field)); - return new FirstOfListQueryNode(followNode); -} - -function createTo1ReferenceNode(field: Field, objectNode: QueryNode): QueryNode { - const referencedEntityType = field.type as RootEntityType; - const keyFieldInReferencedEntity = referencedEntityType.getKeyFieldOrThrow(); - - const keyNode = new FieldQueryNode(objectNode, field); - const listItemVar = new VariableQueryNode(field.name); - const filterNode = new BinaryOperationQueryNode( - new FieldQueryNode(listItemVar, keyFieldInReferencedEntity), - BinaryOperator.EQUAL, - keyNode - ); - - const listNode = new EntitiesQueryNode(referencedEntityType); - const filteredListNode = new TransformListQueryNode({ - listNode, - filterNode, - maxCount: 1, - itemVariable: listItemVar - }); - return new FirstOfListQueryNode(filteredListNode); -} - -export function createListFieldValueNode(params: { field: Field, objectNode: QueryNode, parentType: ObjectType}) { - if (params.field.isRelation) { - return createToNRelationQueryNode(params.field, params.parentType, params.objectNode); - } - if (params.field.isReference) { - throw new Error(`${params.field.name}: references in lists are not supported yet`); - } - return createSafeListQueryNode(new FieldQueryNode(params.objectNode, params.field)); -} - -function createToNRelationQueryNode(field: Field, parentType: ObjectType, sourceEntityNode: QueryNode): QueryNode { - const relation = field.getRelationOrThrow(); - return new FollowEdgeQueryNode(relation, sourceEntityNode, relation.getFieldSide(field)); -} diff --git a/src/query/filtering.ts b/src/query/filtering.ts deleted file mode 100644 index 804310f4..00000000 --- a/src/query/filtering.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { - BasicType, BinaryOperationQueryNode, BinaryOperator, ConstBoolQueryNode, CountQueryNode, LiteralQueryNode, - QueryNode, TransformListQueryNode, TypeCheckQueryNode, UnaryOperationQueryNode, UnaryOperator, VariableQueryNode -} from '../query-tree'; -import { isArray } from 'util'; -import { - AND_FILTER_FIELD, OR_FILTER_FIELD, INPUT_FIELD_CONTAINS, INPUT_FIELD_ENDS_WITH, INPUT_FIELD_EQUAL, INPUT_FIELD_GT, - INPUT_FIELD_GTE, INPUT_FIELD_IN, INPUT_FIELD_LT, INPUT_FIELD_LTE, INPUT_FIELD_NOT, INPUT_FIELD_NOT_CONTAINS, - INPUT_FIELD_NOT_ENDS_WITH, INPUT_FIELD_NOT_IN, INPUT_FIELD_NOT_STARTS_WITH, INPUT_FIELD_SEPARATOR, - INPUT_FIELD_STARTS_WITH -} from '../schema/schema-defaults'; -import { createListFieldValueNode, createNonListFieldValueNode, createScalarFieldValueNode } from './fields'; -import { decapitalize } from '../utils/utils'; -import { ObjectType, Type } from '../model'; - -export function createFilterNode(filterArg: any, type: Type, contextNode: QueryNode): QueryNode { - if (!filterArg || !Object.keys(filterArg).length) { - return new ConstBoolQueryNode(true); - } - let filterNode: QueryNode | undefined = undefined; - for (const key of Object.getOwnPropertyNames(filterArg)) { - let newClause; - if (type.isObjectType) { - newClause = getObjectTypeFilterClauseNode(key, filterArg[key], contextNode, type); - } else { - // handle scalars and enums - newClause = getScalarTypeFilterClauseNode(key, filterArg[key], contextNode); - } - - if (filterNode && newClause) { - filterNode = new BinaryOperationQueryNode(filterNode, BinaryOperator.AND, newClause); - } else { - filterNode = newClause; - } - } - return filterNode || new ConstBoolQueryNode(true); -} - -const filterOperators: { [suffix: string]: (fieldNode: QueryNode, valueNode: QueryNode) => QueryNode } = { - // not's before the normal fields because they need to be matched first in the suffix-matching algorithm - [INPUT_FIELD_EQUAL]: (fieldNode, valueNode) => new BinaryOperationQueryNode(fieldNode, BinaryOperator.EQUAL, valueNode), - [INPUT_FIELD_NOT]: (fieldNode, valueNode) => new BinaryOperationQueryNode(fieldNode, BinaryOperator.UNEQUAL, valueNode), - [INPUT_FIELD_LT]: (fieldNode, valueNode) => new BinaryOperationQueryNode(fieldNode, BinaryOperator.LESS_THAN, valueNode), - [INPUT_FIELD_LTE]: (fieldNode, valueNode) => new BinaryOperationQueryNode(fieldNode, BinaryOperator.LESS_THAN_OR_EQUAL, valueNode), - [INPUT_FIELD_GT]: (fieldNode, valueNode) => new BinaryOperationQueryNode(fieldNode, BinaryOperator.GREATER_THAN, valueNode), - [INPUT_FIELD_GTE]: (fieldNode, valueNode) => new BinaryOperationQueryNode(fieldNode, BinaryOperator.GREATER_THAN_OR_EQUAL, valueNode), - [INPUT_FIELD_NOT_IN]: (fieldNode, valueNode) => not(new BinaryOperationQueryNode(fieldNode, BinaryOperator.IN, valueNode)), - [INPUT_FIELD_IN]: (fieldNode, valueNode) => new BinaryOperationQueryNode(fieldNode, BinaryOperator.IN, valueNode), - [INPUT_FIELD_NOT_CONTAINS]: (fieldNode, valueNode) => not(new BinaryOperationQueryNode(fieldNode, BinaryOperator.CONTAINS, valueNode)), - [INPUT_FIELD_CONTAINS]: (fieldNode, valueNode) => new BinaryOperationQueryNode(fieldNode, BinaryOperator.CONTAINS, valueNode), - [INPUT_FIELD_NOT_STARTS_WITH]: (fieldNode, valueNode) => not(new BinaryOperationQueryNode(fieldNode, BinaryOperator.STARTS_WITH, valueNode)), - [INPUT_FIELD_STARTS_WITH]: (fieldNode, valueNode) => new BinaryOperationQueryNode(fieldNode, BinaryOperator.STARTS_WITH, valueNode), - [INPUT_FIELD_NOT_ENDS_WITH]: (fieldNode, valueNode) => not(new BinaryOperationQueryNode(fieldNode, BinaryOperator.ENDS_WITH, valueNode)), - [INPUT_FIELD_ENDS_WITH]: (fieldNode, valueNode) => new BinaryOperationQueryNode(fieldNode, BinaryOperator.ENDS_WITH, valueNode) -}; - -function getObjectTypeFilterClauseNode(key: string, value: any, contextNode: QueryNode, objectType: ObjectType): QueryNode { - // special nodes - switch (key) { - case AND_FILTER_FIELD: - if (!isArray(value) || !value.length) { - return new ConstBoolQueryNode(true); - } - return value - .map(itemValue => createFilterNode(itemValue, objectType, contextNode)) - .reduce((prev, current) => new BinaryOperationQueryNode(prev, BinaryOperator.AND, current)); - case OR_FILTER_FIELD: - if (!isArray(value)) { - return new ConstBoolQueryNode(true); // regard as omitted - } - if (!value.length) { - return new ConstBoolQueryNode(false); // proper boolean logic (neutral element of OR is FALSE) - } - return value - .map(itemValue => createFilterNode(itemValue, objectType, contextNode)) - .reduce((prev, current) => new BinaryOperationQueryNode(prev, BinaryOperator.OR, current)); - } - - // check for _some, _every, _none filters - const quantifierData = captureListQuantifiers(key); - if (quantifierData) { - let { fieldKey, quantifier } = quantifierData; - const field = objectType.getFieldOrThrow(fieldKey); - - const listNode = createListFieldValueNode({ - objectNode: contextNode, - parentType: objectType, - field: field - }); - - if (quantifier === 'every') { - // every(P(x)) === none(!P(x)) - // so just flip it - quantifier = 'none'; - value = { not: value } - } - const binaryOp = quantifier === 'some' ? BinaryOperator.GREATER_THAN : BinaryOperator.EQUAL; - const itemVarNode = new VariableQueryNode(decapitalize(field.type.name)); - const filteredListNode = new TransformListQueryNode({ - listNode, - filterNode: createFilterNode(value, field.type, itemVarNode), - itemVariable: itemVarNode - }); - - return new BinaryOperationQueryNode(new CountQueryNode(filteredListNode), binaryOp, new LiteralQueryNode(0)); - } - - // check for filters in embedded objects - const field = objectType.getField(key); - if (field) { - if (field.type.isObjectType) { - if (value === null) { - // don't check inside the object but check if the object an object anyway. - const fieldNode = createNonListFieldValueNode({ - parentType: objectType, - field, - objectNode: contextNode - }); - const isObjectNode = new TypeCheckQueryNode(fieldNode, BasicType.OBJECT); - return new UnaryOperationQueryNode(isObjectNode, UnaryOperator.NOT); - } - - // possibly complex field lookup (references, relations) - return createNonListFieldValueNode({ - field, - parentType: objectType, - objectNode: contextNode, - innerNodeFn: (valueNode: QueryNode) => { - // this function maps the object (field value, referenced object, related object) to the filter condition - const isObjectNode = new TypeCheckQueryNode(valueNode, BasicType.OBJECT); - const rawFilterNode = createFilterNode(value, field.type, valueNode); - if (!rawFilterNode) { - return isObjectNode; // an empty filter only checks if the object is present and a real object - } - // make sure to check for object type before doing the filter - return new BinaryOperationQueryNode(isObjectNode, BinaryOperator.AND, rawFilterNode); - } - }); - } else { - // simple scalar equality filter - const fieldNode = createScalarFieldValueNode(objectType, key, contextNode); - const valueNode = new LiteralQueryNode(value); - return new BinaryOperationQueryNode(fieldNode, BinaryOperator.EQUAL, valueNode); - } - } - - // operator on scalar - for (const operatorKey in filterOperators) { - const suffix = INPUT_FIELD_SEPARATOR + operatorKey; - if (key.endsWith(suffix)) { - const fieldName = key.substr(0, key.length - suffix.length); - const fieldNode = createScalarFieldValueNode(objectType, fieldName, contextNode); - const valueNode = new LiteralQueryNode(value); - return filterOperators[operatorKey](fieldNode, valueNode); - } - } - - throw new Error(`Invalid filter field: ${key}`); -} - -function captureListQuantifiers(key: string) { - const data = key.match(/^(.+)_(some|every|none)$/); - if (data == undefined) { - return undefined; - } - return { - fieldKey: data[1], - quantifier: data[2] - } -} - -function not(value: QueryNode) { - return new UnaryOperationQueryNode(value, UnaryOperator.NOT); -} - -function getScalarTypeFilterClauseNode(key: string, value: any, contextNode: QueryNode): QueryNode { - const valueNode = new LiteralQueryNode(value); - return filterOperators[key](contextNode, valueNode); -} diff --git a/src/query/list-meta.ts b/src/query/list-meta.ts deleted file mode 100644 index 0b8a0a22..00000000 --- a/src/query/list-meta.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { FieldRequest } from '../graphql/query-distiller'; -import { - CountQueryNode, ObjectQueryNode, PropertySpecification, QueryNode, TransformListQueryNode, VariableQueryNode -} from '../query-tree'; -import { createFilterNode } from './filtering'; -import { COUNT_META_FIELD, FILTER_ARG } from '../schema/schema-defaults'; -import { decapitalize } from '../utils/utils'; -import { Type } from '../model'; - -export function createListMetaNode(fieldRequest: FieldRequest, listNode: QueryNode, itemType: Type) { - const itemVarNode = new VariableQueryNode(decapitalize(itemType.name)); - - if (FILTER_ARG in fieldRequest.args) { - const filterNode = createFilterNode(fieldRequest.args[FILTER_ARG], itemType, itemVarNode); - listNode = new TransformListQueryNode({ - listNode, - filterNode, - itemVariable: itemVarNode - }); - } - return new ObjectQueryNode(fieldRequest.selectionSet - .map(selection => new PropertySpecification(selection.propertyName, createMetaFieldNode(selection.fieldRequest, listNode)))); -} - -function createMetaFieldNode(fieldRequest: FieldRequest, listNode: QueryNode) { - switch (fieldRequest.fieldName) { - case COUNT_META_FIELD: - return new CountQueryNode(listNode); - default: - throw new Error(`Unsupported meta field: ${fieldRequest.fieldName}`); - } -} diff --git a/src/query/mutations.ts b/src/query/mutations.ts deleted file mode 100644 index 216f8de6..00000000 --- a/src/query/mutations.ts +++ /dev/null @@ -1,639 +0,0 @@ -import { getNamedType } from 'graphql'; -import { - getAddChildEntitiesFieldName, getAddRelationFieldName, getRemoveChildEntitiesFieldName, getRemoveRelationFieldName, - getUpdateChildEntitiesFieldName -} from '../graphql/names'; -import { FieldRequest } from '../graphql/query-distiller'; -import { - CalcMutationsOperator, Field, Namespace, ObjectType, Relation, RelationFieldSide, RootEntityType -} from '../model'; -import { - AddEdgesQueryNode, AffectedFieldInfoQueryNode, BasicType, BinaryOperationQueryNode, BinaryOperator, - ConcatListsQueryNode, ConditionalQueryNode, CreateEntityQueryNode, DeleteEntitiesQueryNode, EdgeFilter, - EdgeIdentifier, EntitiesQueryNode, EntityFromIdQueryNode, ErrorIfEmptyResultValidator, FieldQueryNode, - FirstOfListQueryNode, ListQueryNode, LiteralQueryNode, MergeObjectsQueryNode, NullQueryNode, ObjectQueryNode, - PartialEdgeIdentifier, PreExecQueryParms, PropertySpecification, QueryNode, QueryResultValidator, - RemoveEdgesQueryNode, RootEntityIDQueryNode, SetEdgeQueryNode, SetFieldQueryNode, TransformListQueryNode, - TypeCheckQueryNode, UnaryOperationQueryNode, UnaryOperator, UpdateEntitiesQueryNode, VariableAssignmentQueryNode, - VariableQueryNode, WithPreExecutionQueryNode -} from '../query-tree'; -import { - ADD_CHILD_ENTITIES_FIELD_PREFIX, CALC_MUTATIONS_OPERATORS, CREATE_ENTITY_FIELD_PREFIX, - DELETE_ALL_ENTITIES_FIELD_PREFIX, DELETE_ENTITY_FIELD_PREFIX, ENTITY_CREATED_AT, ENTITY_UPDATED_AT, ID_FIELD, - MUTATION_ID_ARG, MUTATION_INPUT_ARG, MutationType, REMOVE_CHILD_ENTITIES_FIELD_PREFIX, - UPDATE_ALL_ENTITIES_FIELD_PREFIX, UPDATE_CHILD_ENTITIES_FIELD_PREFIX, UPDATE_ENTITY_FIELD_PREFIX, WILDCARD_CHARACTER -} from '../schema/schema-defaults'; -import { AnyValue, decapitalize, filterProperties, mapValues, PlainObject } from '../utils/utils'; -import { createEntityObjectNode, createTransformListQueryNode } from './queries'; -import uuid = require('uuid'); - -/** - * Creates a QueryNode for a field of the root mutation type - * @param {FieldRequest} fieldRequest the mutation field, such as createSomeEntity - * @param fieldRequestStack - */ -export function createMutationNamespaceNode(fieldRequest: FieldRequest, fieldRequestStack: FieldRequest[], namespace: Namespace): QueryNode { - if (fieldRequest.fieldName.startsWith(CREATE_ENTITY_FIELD_PREFIX)) { - return createCreateEntityQueryNode(fieldRequest, [...fieldRequestStack, fieldRequest], namespace.getRootEntityTypeOrThrow(getNamedType(fieldRequest.field.type).name)); - } - - if (fieldRequest.fieldName.startsWith(UPDATE_ENTITY_FIELD_PREFIX) || fieldRequest.fieldName.startsWith(UPDATE_ALL_ENTITIES_FIELD_PREFIX)) { - return createUpdateEntityQueryNode(fieldRequest, [...fieldRequestStack, fieldRequest], namespace.getRootEntityTypeOrThrow(getNamedType(fieldRequest.field.type).name)); - } - - if (fieldRequest.fieldName.startsWith(DELETE_ENTITY_FIELD_PREFIX) || fieldRequest.fieldName.startsWith(DELETE_ALL_ENTITIES_FIELD_PREFIX)) { - return createDeleteEntityQueryNode(fieldRequest, [...fieldRequestStack, fieldRequest], namespace.getRootEntityTypeOrThrow(getNamedType(fieldRequest.field.type).name)); - } - - const childNamespace = namespace.getChildNamespace(fieldRequest.fieldName); - if (childNamespace) { - return createMutationNamespaceFieldNode(fieldRequest, [...fieldRequestStack, fieldRequest], childNamespace); - } - - throw new Error(`Mutation field "${fieldRequest.fieldName}" is not known in namespace "${namespace.dotSeparatedPath}"`); -} - -function createMutationNamespaceFieldNode(fieldRequest: FieldRequest, fieldRequestStack: FieldRequest[], namespace: Namespace): QueryNode { - return new ObjectQueryNode(fieldRequest.selectionSet.map( - sel => new PropertySpecification(sel.propertyName, - // a namespace can be interpreted as pushing the root node down. - createMutationNamespaceNode(sel.fieldRequest, fieldRequestStack, namespace)))); -} - -function createCreateEntityQueryNode(fieldRequest: FieldRequest, fieldRequestStack: FieldRequest[], rootEntityType: RootEntityType): QueryNode { - const input = fieldRequest.args[MUTATION_INPUT_ARG]; - const fieldCollector = new Set(); - const objectNode = new LiteralQueryNode(prepareMutationInput(input, rootEntityType, MutationType.CREATE, fieldCollector)); - - // Create new entity - const createEntityNode = new CreateEntityQueryNode(rootEntityType, objectNode, - Array.from(fieldCollector.values()).map(field => new AffectedFieldInfoQueryNode(field))); - const newEntityIdVarNode = new VariableQueryNode('newEntityId'); - const newEntityPreExec = new PreExecQueryParms({query: createEntityNode, resultVariable: newEntityIdVarNode}); - - // Add relations if needed - let createRelationsPreExec: PreExecQueryParms|undefined = undefined; - const relationStatements = getRelationAddRemoveStatements(input, rootEntityType, newEntityIdVarNode, false); - if (relationStatements.length) { - createRelationsPreExec = new PreExecQueryParms({ query: - new FirstOfListQueryNode(new ListQueryNode([new NullQueryNode(),...relationStatements]))}); - } - - // Build up result query node - const newEntityVarNode = new VariableQueryNode('newEntity'); - const objectQueryNode = createEntityObjectNode(fieldRequest.selectionSet, newEntityVarNode, rootEntityType, fieldRequestStack); - const resultNode = new VariableAssignmentQueryNode({ - variableNode: newEntityVarNode, - variableValueNode: new EntityFromIdQueryNode(rootEntityType, newEntityIdVarNode), - resultNode: objectQueryNode - }); - - // PreExecute creation and relation queries and return result - return new WithPreExecutionQueryNode({ - resultNode, - preExecQueries: [newEntityPreExec, createRelationsPreExec] - }); -} - -export function prepareMutationInput(input: PlainObject, objectType: ObjectType, mutationType: MutationType, fieldCollector: Set): PlainObject { - - let preparedInput: PlainObject = { ...input }; - - // Apply default values from model - // This is treated as user input, so the default - // values are applied as the very first step. - if (mutationType === MutationType.CREATE) { - // add default values - const defaultValues: any = {}; - for (const field of objectType.fields) { - if (field.defaultValue !== undefined) { - defaultValues[field.name] = field.defaultValue; - } - } - preparedInput = { - ...defaultValues, - ...input - } - } - - const now = getCurrentISODate(); - - if (objectType.isChildEntityType) { - preparedInput = { - ...preparedInput, - [ENTITY_UPDATED_AT]: now - }; - if (mutationType === MutationType.CREATE) { - preparedInput = { - ...preparedInput, - [ENTITY_CREATED_AT]: now, - [ID_FIELD]: uuid() - }; - } - } - - if (objectType.isRootEntityType) { - if (mutationType === MutationType.CREATE) { - // remove relation fields as they are treated by createCreateEntityQueryNode directly and should not be part - // of the created object - preparedInput = { - ...filterProperties(preparedInput, (value, key) => !objectType.getFieldOrThrow(key).isRelation), - [ENTITY_CREATED_AT]: now, - [ENTITY_UPDATED_AT]: now - }; - } else { - // We don't want to increment updatedAt if only relations are touched. - if (inputIncludesValueFields(preparedInput, objectType)) { - preparedInput = { - ...preparedInput, - [ENTITY_UPDATED_AT]: now - }; - } - } - } - - function isValueField(field: Field|undefined): boolean { - if (!field) { - // fail safe, e.g. for calcMutations or add/update/delete - return true; - } - // system: createdAt/updatedAt/id - those are not specified by the user - // relation: those do not affect the root entity - if (field.isSystemField || field.isRelation) { - return false; - } - // regular field - return true; - } - - /** - * Checks whether the input object defines fields that alter the root entity's value - */ - function inputIncludesValueFields(preparedInput: PlainObject, objectType: RootEntityType): boolean { - return Object - .keys(preparedInput) - .map(key => objectType.getField(key)) - .filter(isValueField) - .length > 0; - } - - function getObjectTypeFieldOfInputKey(objectType: ObjectType, key: string): Field|undefined { - const field = objectType.getField(key); - if (field) { - return field; - } - - if (key.startsWith(ADD_CHILD_ENTITIES_FIELD_PREFIX)) { - const descendantKey = decapitalize(key.substring(ADD_CHILD_ENTITIES_FIELD_PREFIX.length, key.length)); - return objectType.getField(descendantKey); - } else if (key.startsWith(REMOVE_CHILD_ENTITIES_FIELD_PREFIX)) { - const descendantKey = decapitalize(key.substring(REMOVE_CHILD_ENTITIES_FIELD_PREFIX.length, key.length)); - return objectType.getField(descendantKey); - } - return undefined; - } - - function prepareFieldValue(value: AnyValue, field: Field, mutationType: MutationType, withinList: boolean = false): AnyValue { - if (field.isList && !withinList) { - return (value as AnyValue[]).map(itemValue => prepareFieldValue(itemValue, field, mutationType, true)); - } - // Make sure to test value for being an object to not prepare "null" values or keys for reference fields (for which rawType would indeed be an object) - if (typeof value == 'object' && value !== null && field.type.isObjectType) { - return prepareMutationInput(value as PlainObject, field.type, mutationType, fieldCollector); - } - // scalars can be used as-is - return value; - } - - function keyWithoutPrefix(key: string, prefix: string): string | undefined { - if(key.startsWith(prefix)) { - return decapitalize(key.substring(prefix.length)); - } else { - return undefined; - } - } - - // recursive calls - return mapValues(preparedInput, (fieldValue, key) => { - const field = objectType.getField(key); - - if (field) { - fieldCollector.add(field); - // only within entity extension, updates stay update-like. Within value objects, we fall back to create logic - const fieldMutationType = field.type.isEntityExtensionType ? mutationType : MutationType.CREATE; - return prepareFieldValue(fieldValue, field, fieldMutationType) - } else { - // must be a (gnerated) special input field - const possiblePrefixes: string[] = [ - ADD_CHILD_ENTITIES_FIELD_PREFIX, - UPDATE_CHILD_ENTITIES_FIELD_PREFIX, - REMOVE_CHILD_ENTITIES_FIELD_PREFIX, - ...CALC_MUTATIONS_OPERATORS.map(op => op.prefix)]; - for (const prefix of possiblePrefixes) { - let withoutPrefix = keyWithoutPrefix(key, prefix); - if(withoutPrefix) { - const field = objectType.getField(withoutPrefix); - if (field) { - fieldCollector.add(field); - switch(prefix) { - case ADD_CHILD_ENTITIES_FIELD_PREFIX: { - // oh, adding child entities is create, not update! - // btw, once create => always create! - return prepareFieldValue(fieldValue, field, MutationType.CREATE) - } - case UPDATE_CHILD_ENTITIES_FIELD_PREFIX: { - return prepareFieldValue(fieldValue, field, mutationType) - } - default: { - // the remaining special input fields do not need further treatment. - // e.g. REMOVE_CHILD_ENTITIES_FIELD_PREFIX, CALC_MUTATIONS_OPERATORS - return fieldValue; - } - } - } - } - } - throw new Error(`Mutation input field named "${key}" does neither match to a plain object field, nor to a known special input field pattern.`); - } - }); -} - -function createUpdateEntityQueryNode(fieldRequest: FieldRequest, fieldRequestStack: FieldRequest[], rootEntityType: RootEntityType): QueryNode { - const mutationType: MutationType = fieldRequest.fieldName.startsWith(UPDATE_ALL_ENTITIES_FIELD_PREFIX) ? - MutationType.UPDATE_ALL : - MutationType.UPDATE; - - const fieldCollector = new Set(); - const input = prepareMutationInput(fieldRequest.args[MUTATION_INPUT_ARG], rootEntityType, mutationType, fieldCollector); - - // Update entity query - let listNode: QueryNode; - const listItemVar = new VariableQueryNode(decapitalize(rootEntityType.name)); - const allEntitiesNode = new EntitiesQueryNode(rootEntityType); - if (mutationType === MutationType.UPDATE_ALL) { - listNode = createTransformListQueryNode(fieldRequest, allEntitiesNode, listItemVar, listItemVar, rootEntityType, fieldRequestStack); - } else { - const filterNode = new BinaryOperationQueryNode(new RootEntityIDQueryNode(listItemVar), - BinaryOperator.EQUAL, - new LiteralQueryNode(input[ID_FIELD])); - listNode = new TransformListQueryNode({ - listNode: allEntitiesNode, - filterNode: filterNode, - maxCount: 1, - itemVariable: listItemVar - }); - } - - const currentEntityVarNode = new VariableQueryNode('currentEntity'); - const updateEntitiesNode = new UpdateEntitiesQueryNode({ - rootEntityType: rootEntityType, - listNode, - updates: createUpdatePropertiesSpecification(input, rootEntityType, currentEntityVarNode), - currentEntityVariable: currentEntityVarNode, - affectedFields: Array.from(fieldCollector.values()).map(field => new AffectedFieldInfoQueryNode(field)) - }); - - const updatedEntitiesIDsVarNode = new VariableQueryNode('updatedEntitiesId'); - let updateEntityResultValidator: QueryResultValidator | undefined = undefined; - if (mutationType === MutationType.UPDATE) { - updateEntityResultValidator = new ErrorIfEmptyResultValidator(`${rootEntityType.name} with id ${input[ID_FIELD]} could not be found.`, 'NotFoundError'); - } - const updateEntitiesPreExec = new PreExecQueryParms({query: updateEntitiesNode, resultVariable: updatedEntitiesIDsVarNode, resultValidator: updateEntityResultValidator}); - - // update relations if needed - let updateRelationsPreExec: PreExecQueryParms|undefined = undefined; - const updatedEntityIdVarNode1 = new VariableQueryNode('updatedEntityId1'); - const relationStatements = getRelationAddRemoveStatements(input, rootEntityType, updatedEntityIdVarNode1, true); - if (relationStatements.length) { - const updateRelationsNode: QueryNode = new TransformListQueryNode({ - listNode: updatedEntitiesIDsVarNode, - itemVariable: updatedEntityIdVarNode1, - innerNode: new FirstOfListQueryNode(new ListQueryNode([new NullQueryNode(), ...relationStatements])) - }); - updateRelationsPreExec = new PreExecQueryParms({ query: new FirstOfListQueryNode(updateRelationsNode) }); - } - - // Build up result query node - const updatedEntityIdVarNode2 = new VariableQueryNode('updatedEntityId2'); - let resultNode: QueryNode = new TransformListQueryNode({ - listNode: updatedEntitiesIDsVarNode, - itemVariable: updatedEntityIdVarNode2, - innerNode: createEntityObjectNode( - fieldRequest.selectionSet, - new EntityFromIdQueryNode(rootEntityType, updatedEntityIdVarNode2), - rootEntityType, - fieldRequestStack) - }); - - if (mutationType === MutationType.UPDATE) { - resultNode = new FirstOfListQueryNode(resultNode); - } - - // PreExecute update and relation queries and return result - return new WithPreExecutionQueryNode({ - resultNode, - preExecQueries: [updateEntitiesPreExec, updateRelationsPreExec] - }); -} - -export function getRelationAddRemoveStatements(obj: PlainObject, parentType: ObjectType, sourceIDNode: QueryNode, isAddRemove: boolean): QueryNode[] { - // note: we don't check if the target ids exists. This would be a constraint that will be checked in Foxx once we - // implement Foxx. It's not easy to do this in AQL because we can't throw errors in AQL. - - const relationFields = parentType.fields.filter(f => f.isRelation); - const statements: QueryNode[] = []; - for (const field of relationFields) { - const relation = field.getRelationOrThrow(); - if (field.isList) { - // to-n relation - const idsToBeAdded = (isAddRemove ? obj[getAddRelationFieldName(field.name)] : obj[field.name]) as string[] | undefined || []; - const idsToBeRemoved = isAddRemove ? (obj[getRemoveRelationFieldName(field.name)] || []) as string[] : []; - if (idsToBeAdded.length && idsToBeRemoved.length) { - throw new Error(`Currently, it is not possible to use add and remove on the same relation in one mutation`); - } - - if (idsToBeAdded.length) { - const edgeNodes = idsToBeAdded.map(id => getEdgeIdentifier({ - relation, - sourceIDNode, - targetIDNode: new LiteralQueryNode(id), - sourceField: field - })); - statements.push(new AddEdgesQueryNode(relation, edgeNodes)); - } else if (idsToBeRemoved.length) { - let targetIds; - if(idsToBeRemoved.includes(WILDCARD_CHARACTER)) { - // target IDs undefined => no target ID filter => remove all edges from source ignoring target - targetIds = undefined; - }else { - targetIds = idsToBeRemoved.map(id => new LiteralQueryNode(id)); - } - - statements.push(new RemoveEdgesQueryNode(relation, getEdgeFilter({ - relation, - sourceIDNodes: [sourceIDNode], - targetIDNodes: targetIds, - sourceField: field - }))); - } - } else if (field.name in obj) { - // to-1 relation - const newID = obj[field.name]; - if (newID) { - // set related entity - statements.push(new SetEdgeQueryNode({ - relation: relation, - existingEdge: getPartialEdgeIdentifier({ - relation, - sourceIDNode, - sourceField: field - }), - newEdge: getEdgeIdentifier({ - relation, - sourceIDNode, - targetIDNode: new LiteralQueryNode(newID), - sourceField: field - }) - })); - } else { - // remove relation - statements.push(new RemoveEdgesQueryNode( - relation, - getEdgeFilter({ - relation, - sourceIDNodes: [sourceIDNode], - sourceField: field - }) - )); - } - } - } - return statements; -} - -/** - * Creates an Edge identifier. Reorders source/target so that they match from/to in the relation - */ -function getEdgeIdentifier(param: { relation: Relation; sourceIDNode: QueryNode; targetIDNode: QueryNode; sourceField: Field }): EdgeIdentifier { - switch (param.relation.getFieldSide(param.sourceField)) { - case RelationFieldSide.FROM_SIDE: - return new EdgeIdentifier(param.sourceIDNode, param.targetIDNode); - case RelationFieldSide.TO_SIDE: - return new EdgeIdentifier(param.targetIDNode, param.sourceIDNode); - } -} - -/** - * Creates a partial edge identifier of the format ?->id or id->? - */ -function getPartialEdgeIdentifier(param: { relation: Relation; sourceIDNode: QueryNode; sourceField: Field }): PartialEdgeIdentifier { - switch (param.relation.getFieldSide(param.sourceField)) { - case RelationFieldSide.FROM_SIDE: - return new PartialEdgeIdentifier(param.sourceIDNode, undefined); - case RelationFieldSide.TO_SIDE: - return new PartialEdgeIdentifier(undefined, param.sourceIDNode); - } -} - -/** - * Creates an Edge filter. Reorders source/target so that they match from/to in the relation - */ -function getEdgeFilter(param: { relation: Relation; sourceIDNodes?: QueryNode[]; targetIDNodes?: QueryNode[]; sourceField: Field }): EdgeFilter { - switch (param.relation.getFieldSide(param.sourceField)) { - case RelationFieldSide.FROM_SIDE: - return new EdgeFilter(param.sourceIDNodes, param.targetIDNodes); - case RelationFieldSide.TO_SIDE: - return new EdgeFilter(param.targetIDNodes, param.sourceIDNodes); - } -} - -function getCurrentISODate() { - return new Date().toISOString(); -} - -function createUpdatePropertiesSpecification(obj: any, objectType: ObjectType, oldEntityNode: QueryNode): SetFieldQueryNode[] { - if (typeof obj != 'object') { - return []; - } - - const properties: SetFieldQueryNode[] = []; - for (const field of objectType.fields) { - if (field.type.isEntityExtensionType && field.name in obj) { - // call recursively and use update semantic (leave fields that are not specified as-is - const sourceNode = new FieldQueryNode(oldEntityNode, field); - const valueNode = new MergeObjectsQueryNode([ - createSafeObjectQueryNode(sourceNode), - new ObjectQueryNode(createUpdatePropertiesSpecification(obj[field.name], field.type, sourceNode)) - ]); - properties.push(new SetFieldQueryNode(field, valueNode)); - } else if (field.type.isChildEntityType) { - const childEntityType = field.type; - const idField = childEntityType.getFieldOrThrow(ID_FIELD); - - // first add, then delete, then update - // -> delete trumps add - // -> new values can be updated - // -> update operates on reduced list (delete ones do not generate overhead) - // generates a query like this: - // FOR obj IN [...existing, ...newValues] FILTER !(obj.id IN removedIDs) RETURN obj.id == updateID ? update(obj) : obj - const rawExistingNode = new FieldQueryNode(oldEntityNode, field); - let currentNode: QueryNode = new ConditionalQueryNode( // fall back to empty list if property is not a list - new TypeCheckQueryNode(rawExistingNode, BasicType.LIST), rawExistingNode, new ListQueryNode([])); - - const newValues: any[] | undefined = obj[getAddChildEntitiesFieldName(field.name)]; - if (newValues) { - // newValues is already prepared, just like everything passed to this function, so uuids are already there - // wrap the whole thing into a LiteralQueryNode instead of them individually so that only one bound variable is used - const newNode = new LiteralQueryNode(newValues); - currentNode = new ConcatListsQueryNode([currentNode, newNode]); - } - - const childEntityVarNode = new VariableQueryNode(decapitalize(childEntityType.name)); - const childIDQueryNode = new FieldQueryNode(childEntityVarNode, idField); - - const removedIDs: number[] | undefined = obj[getRemoveChildEntitiesFieldName(field.name)]; - let removalFilterNode: QueryNode | undefined = undefined; - if (removedIDs && removedIDs.length) { - // FILTER !(obj.id IN [...removedIDs]) - removalFilterNode = new UnaryOperationQueryNode( - new BinaryOperationQueryNode( - childIDQueryNode, - BinaryOperator.IN, - new LiteralQueryNode(removedIDs) - ), - UnaryOperator.NOT - ); - } - - const updatedValues: any[] | undefined = obj[getUpdateChildEntitiesFieldName(field.name)]; - let updateMapNode: QueryNode | undefined = undefined; - if (updatedValues && updatedValues.length) { - // build an ugly conditional tree - // looks like this: - // - item - // - item.id == 1 ? update1(item) : item - // - item.id == 2 ? update2(item) : (item.id == 1 ? update1(item) : item) - // ... - updateMapNode = childEntityVarNode; - - for (const value of updatedValues) { - const filterNode = new BinaryOperationQueryNode(childIDQueryNode, BinaryOperator.EQUAL, new LiteralQueryNode(value[ID_FIELD])); - const updateNode = new MergeObjectsQueryNode([ - createSafeObjectQueryNode(childEntityVarNode), - new ObjectQueryNode(createUpdatePropertiesSpecification(value, childEntityType, childEntityVarNode)) - ]); - updateMapNode = new ConditionalQueryNode(filterNode, updateNode, updateMapNode); - } - } - - if (removalFilterNode || updateMapNode) { - currentNode = new TransformListQueryNode({ - listNode: currentNode, - filterNode: removalFilterNode, - innerNode: updateMapNode, - itemVariable: childEntityVarNode - }); - } - - properties.push(new SetFieldQueryNode(field, currentNode)); - } else if (field.isRelation) { - // do nothing because relations are not represented in the update property specification, they are - // considered by createUpdateEntityQueryNode directly - } else if (field.isReadOnly) { - // this field must not be updated (may exist in schema for other purposes like filtering for an entity) - } else { - // scalars and value objects - let valueNode: QueryNode | undefined = undefined; - if (field.name in obj) { - valueNode = new LiteralQueryNode(obj[field.name]); - } - - // note: the loop order defines the application order of the operators so it is significant - for (const operator of CALC_MUTATIONS_OPERATORS) { - if (!(field.calcMutationOperators.has(operator.name as CalcMutationsOperator))) { - continue; - } - const inputCalcFieldName = operator.prefix + field.name; - const binaryOperator: BinaryOperator = BinaryOperator[operator.name]; - if(!(inputCalcFieldName in obj)) { - continue; - } - - // TODO ArangoDB implicitly converts null to 0 during arithmetic operations. Is this ok? - valueNode = new BinaryOperationQueryNode( - valueNode || new FieldQueryNode(oldEntityNode, field), - binaryOperator, - new LiteralQueryNode(obj[inputCalcFieldName]) - ); - } - - if(valueNode) { - properties.push(new SetFieldQueryNode(field, valueNode)); - } - } - } - - // if any property has been updated on an entity, set its update timestamp - if (properties.length && (objectType.isChildEntityType || objectType.isRootEntityType)) { - const field = objectType.getFieldOrThrow(ENTITY_UPDATED_AT); - properties.push(new SetFieldQueryNode(field, new LiteralQueryNode(getCurrentISODate()))); - } - - return properties; -} - -function createDeleteEntityQueryNode(fieldRequest: FieldRequest, fieldRequestStack: FieldRequest[], rootEntityType: RootEntityType): QueryNode { - const mutationType: MutationType = fieldRequest.fieldName.startsWith(DELETE_ALL_ENTITIES_FIELD_PREFIX) ? - MutationType.DELETE_ALL : - MutationType.DELETE; - - // Delete entity query - let listNode: QueryNode; - const listItemVar = new VariableQueryNode(decapitalize(rootEntityType.name)); - const allEntitiesNode = new EntitiesQueryNode(rootEntityType); - if (mutationType === MutationType.DELETE_ALL) { - listNode = createTransformListQueryNode(fieldRequest, allEntitiesNode, listItemVar, listItemVar, rootEntityType, fieldRequestStack); - } else { - const filterNode = new BinaryOperationQueryNode(new RootEntityIDQueryNode(listItemVar), - BinaryOperator.EQUAL, - new LiteralQueryNode(fieldRequest.args[MUTATION_ID_ARG])); - listNode = new TransformListQueryNode({ - listNode: allEntitiesNode, - filterNode: filterNode, - maxCount: 1, - itemVariable: listItemVar - }); - } - - const deleteEntitiesNode = new DeleteEntitiesQueryNode({ - rootEntityType: rootEntityType, - listNode - }); - - // Build up result query node - const deletedEntityVarNode = new VariableQueryNode('deletedEntity'); - let resultNode: QueryNode = new TransformListQueryNode({ - listNode: deleteEntitiesNode, - itemVariable: deletedEntityVarNode, - innerNode: createEntityObjectNode( - fieldRequest.selectionSet, - deletedEntityVarNode, - rootEntityType, - fieldRequestStack) - }); - - if (mutationType === MutationType.DELETE) { - resultNode = new FirstOfListQueryNode(resultNode); - } - - return resultNode; -} - -export function createSafeObjectQueryNode(objectNode: QueryNode) { - return new ConditionalQueryNode( - new TypeCheckQueryNode(objectNode, BasicType.OBJECT), - objectNode, - new ObjectQueryNode([]) - ); -} - diff --git a/src/query/pagination-and-sorting.ts b/src/query/pagination-and-sorting.ts deleted file mode 100644 index 64da3002..00000000 --- a/src/query/pagination-and-sorting.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { FieldRequest } from '../graphql/query-distiller'; -import { getNamedType, GraphQLField, GraphQLObjectType, GraphQLType } from 'graphql'; -import { - BinaryOperationQueryNode, BinaryOperator, ConstBoolQueryNode, LiteralQueryNode, NullQueryNode, ObjectQueryNode, - OrderClause, OrderDirection, OrderSpecification, PropertySpecification, QueryNode, UnaryOperationQueryNode, - UnaryOperator -} from '../query-tree'; -import { isArray } from 'util'; -import { isListType } from '../graphql/schema-utils'; -import { - AFTER_ARG, CURSOR_FIELD, ID_FIELD, ORDER_BY_ARG, ORDER_BY_ASC_SUFFIX, ORDER_BY_DESC_SUFFIX -} from '../schema/schema-defaults'; -import { sortedByAsc, sortedByDesc } from '../graphql/names'; -import { createNonListFieldValueNode } from './fields'; -import { ObjectType, Type } from '../model/implementation'; - -export function createPaginationFilterNode(objectType: ObjectType, listFieldRequest: FieldRequest, itemNode: QueryNode) { - const afterArg = listFieldRequest.args[AFTER_ARG]; - if (!afterArg) { - return new ConstBoolQueryNode(true); - } - - let cursorObj: any; - try { - cursorObj = JSON.parse(afterArg); - if (typeof cursorObj != 'object' || cursorObj === null) { - throw new Error('The JSON value provided as "after" argument is not an object'); - } - } catch (e) { - throw new Error(`Invalid cursor ${JSON.stringify(afterArg)} supplied to "after": ${e.message}`); - } - - // Make sure we only select items after the cursor - // Thus, we need to implement the 'comparator' based on the order-by-specification - // Haskell-like pseudo-code because it's easier ;-) - // filterForClause :: Clause[] -> FilterNode: - // filterForClause([{field, ASC}, ...tail]) = - // (context[clause.field] > cursor[clause.field] || (context[clause.field] == cursor[clause.field] && filterForClause(tail)) - // filterForClause([{field, DESC}, ...tail]) = - // (context[clause.field] < cursor[clause.field] || (context[clause.field] == cursor[clause.field] && filterForClause(tail)) - // filterForClause([]) = FALSE # arbitrary; if order is absolute, this case should never occur - function filterForClause(clauses: {fieldPath: string, valueNode: QueryNode, direction: OrderDirection}[]): QueryNode { - if (clauses.length == 0) { - return new ConstBoolQueryNode(false); - } - - const clause = clauses[0]; - const cursorValue = cursorObj[clause.fieldPath]; - - const operator = clause.direction == OrderDirection.ASCENDING ? BinaryOperator.GREATER_THAN : BinaryOperator.LESS_THAN; - return new BinaryOperationQueryNode( - new BinaryOperationQueryNode(clause.valueNode, operator, new LiteralQueryNode(cursorValue)), - BinaryOperator.OR, - new BinaryOperationQueryNode( - new BinaryOperationQueryNode(clause.valueNode, BinaryOperator.EQUAL, new LiteralQueryNode(cursorValue)), - BinaryOperator.AND, - filterForClause(clauses.slice(1)) - ) - ); - } - - const clauseNames = getOrderByClauseNames(objectType, listFieldRequest); - const clauses = clauseNames.map(name => { - let direction = name.endsWith(ORDER_BY_DESC_SUFFIX) ? OrderDirection.DESCENDING : OrderDirection.ASCENDING; - const fieldPath = getFieldPathFromOrderByClause(name); - const valueNode = createScalarFieldPathValueNode(objectType, fieldPath, itemNode); - return { - fieldPath, - direction, - valueNode - }; - }); - return filterForClause(clauses); -} - -export function createOrderSpecification(objectType: ObjectType, listFieldRequest: FieldRequest, itemNode: QueryNode) { - const clauseNames = getOrderByClauseNames(objectType, listFieldRequest); - const clauses = clauseNames.map(name => { - let dir = name.endsWith(ORDER_BY_DESC_SUFFIX) ? OrderDirection.DESCENDING : OrderDirection.ASCENDING; - const fieldPath = getFieldPathFromOrderByClause(name); - const fieldQuery = createScalarFieldPathValueNode(objectType, fieldPath, itemNode); - return new OrderClause(fieldQuery, dir); - }); - return new OrderSpecification(clauses); -} - - -/** - * Creates a query node for the cursor of itemNode when being requested in listFieldRequest - * @param {FieldRequest} listFieldRequest - * @param {QueryNode} itemNode - */ -export function createCursorQueryNode(listFieldRequest: FieldRequest, itemNode: QueryNode, itemType: ObjectType) { - if (!listFieldRequest || !isListType(listFieldRequest.field.type)) { - return new NullQueryNode(); // not in context of a list - } - - const clauses = getOrderByClauseNames(itemType, listFieldRequest); - const fieldPaths = clauses.map(clause => getFieldPathFromOrderByClause(clause)).sort(); - const objectNode = new ObjectQueryNode(fieldPaths.map( fieldPath => - new PropertySpecification(fieldPath, createScalarFieldPathValueNode(itemType, fieldPath, itemNode)))); - return new UnaryOperationQueryNode(objectNode, UnaryOperator.JSON_STRINGIFY); -} - -function getFieldPathFromOrderByClause(clause: string): string { - if (clause.endsWith(ORDER_BY_ASC_SUFFIX)) { - return clause.substr(0, clause.length - ORDER_BY_ASC_SUFFIX.length); - } - if (clause.endsWith(ORDER_BY_DESC_SUFFIX)) { - return clause.substr(0, clause.length - ORDER_BY_DESC_SUFFIX.length); - } - return clause; -} - -function createScalarFieldPathValueNode(baseType: ObjectType, fieldPath: string, baseNode: QueryNode): QueryNode { - const segments = fieldPath.split('_'); - if (!segments.length) { - throw new Error(`Invalid field path: ${fieldPath}`); - } - let currentType: Type = baseType; - let currentNode = baseNode; - for (const fieldName of segments) { - if (!currentType.isObjectType) { - throw new Error(`Invalid field path on ${baseType.name}: ${fieldPath} (type ${currentType} does not have subfields`); - } - const field = currentType.getFieldOrThrow(fieldName); - - // we don't do type checks for object because in AQL, property lookups on non-objects yield NULL anyway - // we might need to change this when targeting different data bases - - currentNode = createNonListFieldValueNode({ - field, - parentType: currentType, - objectNode: currentNode - }); - - currentType = field.type; - } - - return currentNode; -} - -function getOrderByClauseNames(objectType: ObjectType, listFieldRequest: FieldRequest): string[] { - const orderBy = listFieldRequest.args[ORDER_BY_ARG]; - const clauseNames = !orderBy ? [] : isArray(orderBy) ? orderBy : [orderBy]; - - // if pagination is enabled on a list of entities, make sure we filter after a unique key - // TODO figure a way to do proper pagination on a simple list of embedded objects - - const isCursorRequested = listFieldRequest.selectionSet.some(sel => sel.fieldRequest.fieldName == CURSOR_FIELD); - if (isCursorRequested && objectType.getField(ID_FIELD)) { - const includesID = clauseNames.some(name => name == sortedByAsc(ID_FIELD) || name == sortedByDesc(ID_FIELD)); - if (!includesID) { - return [...clauseNames, sortedByAsc(ID_FIELD)]; - } - return clauseNames; - } - return clauseNames; -} diff --git a/src/query/queries.ts b/src/query/queries.ts deleted file mode 100644 index cc64df65..00000000 --- a/src/query/queries.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { FieldRequest, FieldSelection } from '../graphql/query-distiller'; -import { getNamedType, GraphQLField, GraphQLObjectType } from 'graphql'; -import { - BasicType, BinaryOperationQueryNode, BinaryOperator, ConditionalQueryNode, EntitiesQueryNode, FirstOfListQueryNode, - ListQueryNode, LiteralQueryNode, NullQueryNode, ObjectQueryNode, PropertySpecification, QueryNode, - TransformListQueryNode, TypeCheckQueryNode, VariableQueryNode -} from '../query-tree'; -import { createCursorQueryNode, createOrderSpecification, createPaginationFilterNode } from './pagination-and-sorting'; -import { createFilterNode } from './filtering'; -import { ALL_ENTITIES_FIELD_PREFIX, CURSOR_FIELD, FILTER_ARG, FIRST_ARG } from '../schema/schema-defaults'; -import { decapitalize, objectEntries } from '../utils/utils'; -import { createListFieldValueNode, createNonListFieldValueNode, createScalarFieldValueNode } from './fields'; -import { createListMetaNode } from './list-meta'; -import { Namespace, ObjectType, RootEntityType } from '../model'; - -/** - * Creates a QueryNode for a field of the root query type - * @param {FieldRequest} fieldRequest the query field, such as allEntities - */ -export function createQueryNamespaceNode(fieldRequest: FieldRequest, fieldRequestStack: FieldRequest[], namespace: Namespace): QueryNode { - if (isEntitiesQueryField(fieldRequest.field)) { - return createAllEntitiesFieldNode(fieldRequest, [...fieldRequestStack, fieldRequest], namespace); - } - if (isMetaField(fieldRequest.field)) { - return createEntitiesMetaFieldNode(fieldRequest, fieldRequestStack, namespace); - } - const childNamespace = namespace.getChildNamespace(fieldRequest.fieldName); - if (childNamespace) { - return createQueryNamespaceFieldNode(fieldRequest, [...fieldRequestStack, fieldRequest], childNamespace) - } - const rootEntityType = namespace.getRootEntityType(getNamedType(fieldRequest.field.type).name); - if (rootEntityType) { - return createSingleEntityFieldNode(fieldRequest, [...fieldRequestStack, fieldRequest], rootEntityType); - } - - throw new Error(`Field "${fieldRequest.fieldName}" is not known in namespace "${namespace.dotSeparatedPath}"`); -} - -function createQueryNamespaceFieldNode(fieldRequest: FieldRequest, fieldRequestStack: FieldRequest[], namespace: Namespace): QueryNode { - return new ObjectQueryNode(fieldRequest.selectionSet.map( - sel => new PropertySpecification(sel.propertyName, - // a namespace can be interpreted as pushing the root node down. - createQueryNamespaceNode(sel.fieldRequest, fieldRequestStack, namespace)))); -} - -function createAllEntitiesFieldNode(fieldRequest: FieldRequest, fieldRequestStack: FieldRequest[], namespace: Namespace): QueryNode { - const rootEntityType = namespace.getRootEntityTypeOrThrow(getNamedType(fieldRequest.field.type).name); - const listNode = new EntitiesQueryNode(rootEntityType); - const itemVariable = new VariableQueryNode(decapitalize(rootEntityType.name)); - const innerNode = createEntityObjectNode(fieldRequest.selectionSet, itemVariable, rootEntityType, fieldRequestStack); - return createTransformListQueryNode(fieldRequest, listNode, itemVariable, innerNode, rootEntityType, fieldRequestStack); -} - -function createEntitiesMetaFieldNode(fieldRequest: FieldRequest, fieldRequestStack: FieldRequest[], namespace: Namespace): QueryNode { - const listField = findOriginalFieldForMetaFieldInNamespace(fieldRequest, fieldRequestStack); - const objectType = namespace.getRootEntityTypeOrThrow(getNamedType(listField.type).name); - const listNode = new EntitiesQueryNode(objectType); - return createListMetaNode(fieldRequest, listNode, objectType); -} - -function findOriginalFieldForMetaFieldInNamespace(fieldRequest: FieldRequest, fieldRequestStack: FieldRequest[]) { - const listFieldName = unwrapMetaFieldName(fieldRequest.field); - // walk through namespaces, start at root query type. - let currentNamespaceNode = fieldRequest.schema.getQueryType(); - const pathRemaining = [...fieldRequestStack]; - while(pathRemaining.length) { - const nextFieldNode = pathRemaining.shift()!; - const nextType = getNamedType(currentNamespaceNode.getFields()[nextFieldNode.field.name].type); - if (!(nextType instanceof GraphQLObjectType)) { - throw new Error("Expected object type"); - } - currentNamespaceNode = nextType; - } - const listField = currentNamespaceNode.getFields()[listFieldName]; - if (!listField) { - throw new Error(`Requesting meta field ${fieldRequest.fieldName}, but list field ${listFieldName} does not exist on Query`); - } - return listField; -} - -function createSingleEntityFieldNode(fieldRequest: FieldRequest, fieldRequestStack: FieldRequest[], rootEntityType: RootEntityType): QueryNode { - const entityVarNode = new VariableQueryNode(decapitalize(rootEntityType.name)); - const filterClauses = objectEntries(fieldRequest.args).map(([fieldName, value]) => - new BinaryOperationQueryNode(createScalarFieldValueNode(rootEntityType, fieldName, entityVarNode), BinaryOperator.EQUAL, new LiteralQueryNode(value))); - if (filterClauses.length != 1) { - throw new Error(`Must specify exactly one argument to ${fieldRequest.parentType.toString()}.${fieldRequest.field.name}`); - } - const filterNode = filterClauses[0]; - const innerNode = createConditionalObjectNode(fieldRequest.selectionSet, entityVarNode, rootEntityType, fieldRequestStack); - const listNode = new EntitiesQueryNode(rootEntityType); - const filteredListNode = new TransformListQueryNode({ - listNode, - filterNode, - innerNode, - itemVariable: entityVarNode - }); - return new FirstOfListQueryNode(filteredListNode); -} - -/** - * Creates a QueryNode for the value of querying a specific entity - * @param {FieldSelection[]} fieldSelections specifies what to select in the entity (e.g. the fieldSelections of an allEntities query) - * @param {QueryNode} objectNode a node that evaluates to the entity - * @param {FieldRequest[]} fieldRequestStack parent field requests, up to (including) the enclosing fieldRequest of fieldSeletions - * @returns {ObjectQueryNode} - */ -export function createEntityObjectNode(fieldSelections: FieldSelection[], objectNode: QueryNode, objectType: ObjectType, fieldRequestStack: FieldRequest[]) { - return new ObjectQueryNode(fieldSelections.map( - sel => new PropertySpecification(sel.propertyName, - createEntityFieldQueryNode(sel.fieldRequest, objectNode, objectType, [...fieldRequestStack, sel.fieldRequest])))); -} - -/** - * Creates a QueryNode for a specific field to query from an entity - * @param {FieldRequest} fieldRequest the field, e.g. "id" - * @param {QueryNode} objectNode the QueryNode that evaluates to the entity - * @param {FieldRequest[]} fieldRequestStack parent field requests, up to (including) the fieldRequest arg - * @returns {QueryNode} - */ -function createEntityFieldQueryNode(fieldRequest: FieldRequest, objectNode: QueryNode, objectType: ObjectType, fieldRequestStack: FieldRequest[]): QueryNode { - if (fieldRequest.fieldName == CURSOR_FIELD) { - return createCursorQueryNode(fieldRequestStack[fieldRequestStack.length - 2], objectNode, objectType); - } - - if (isMetaField(fieldRequest.field)) { - const listFieldName = unwrapMetaFieldName(fieldRequest.field); - const listField = objectType.getFieldOrThrow(listFieldName); - const listNode = createListFieldValueNode({ - objectNode, - parentType: objectType, - field: listField}); - return createListMetaNode(fieldRequest, listNode, listField.type); - } - - const field = objectType.getFieldOrThrow(fieldRequest.fieldName); - - if (field.isList) { - const listNode = createListFieldValueNode({ - objectNode, - parentType: objectType, - field}); - if (field.type.isObjectType) { - // support filters, order by and pagination - const itemVariable = new VariableQueryNode(decapitalize(field.type.name)); - const innerNode = createEntityObjectNode(fieldRequest.selectionSet, itemVariable, field.type, fieldRequestStack); - return createTransformListQueryNode(fieldRequest, listNode, itemVariable, innerNode, field.type, fieldRequestStack); - } else { - return listNode; - } - } - - return createNonListFieldValueNode({ - objectNode, - parentType: objectType, - field, - innerNodeFn: valueNode => field.type.isObjectType ? createConditionalObjectNode(fieldRequest.selectionSet, valueNode, field.type, fieldRequestStack) : valueNode - }); -} - -function createConditionalObjectNode(fieldSelections: FieldSelection[], contextNode: QueryNode, objectType: ObjectType, fieldRequestStack: FieldRequest[]) { - return new ConditionalQueryNode( - new TypeCheckQueryNode(contextNode, BasicType.OBJECT), - createEntityObjectNode(fieldSelections, contextNode, objectType, fieldRequestStack), - new NullQueryNode()); -} - -export function createTransformListQueryNode(fieldRequest: FieldRequest, listNode: QueryNode, itemVariable: VariableQueryNode, innerNode: QueryNode, itemType: ObjectType, fieldRequestStack: FieldRequest[]): QueryNode { - let orderBy = createOrderSpecification(itemType, fieldRequest, itemVariable); - const basicFilterNode = createFilterNode(fieldRequest.args[FILTER_ARG], itemType, itemVariable); - const paginationFilterNode = createPaginationFilterNode(itemType, fieldRequest, itemVariable); - let filterNode: QueryNode = new BinaryOperationQueryNode(basicFilterNode, BinaryOperator.AND, paginationFilterNode); - const maxCount = fieldRequest.args[FIRST_ARG]; - - return new TransformListQueryNode({ - listNode, - innerNode, - filterNode, - orderBy, - maxCount, - itemVariable - }); -} - -export function createSafeListQueryNode(listNode: QueryNode) { - // to avoid errors because of eagerly evaluated list expression, we just convert non-lists to an empty list - return new ConditionalQueryNode( - new TypeCheckQueryNode(listNode, BasicType.LIST), - listNode, - new ListQueryNode([]) - ); -} - -function isEntitiesQueryField(field: GraphQLField) { - return field.name.startsWith(ALL_ENTITIES_FIELD_PREFIX); -} - -function isMetaField(field: GraphQLField) { - return field.name.startsWith('_') && field.name.endsWith('Meta'); -} - -function unwrapMetaFieldName(field: GraphQLField): string { - return field.name.substr(1, field.name.length - '_Meta'.length); -} diff --git a/src/query/query-tree-builder.ts b/src/query/query-tree-builder.ts deleted file mode 100644 index 19d83c06..00000000 --- a/src/query/query-tree-builder.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { DistilledOperation, FieldRequest } from '../graphql/query-distiller'; -import { NullQueryNode, ObjectQueryNode, PropertySpecification, QueryNode } from '../query-tree'; -import { MUTATION_TYPE, QUERY_TYPE } from '../schema/schema-defaults'; -import { createQueryNamespaceNode } from './queries'; -import { createMutationNamespaceNode } from './mutations'; -import { globalContext } from '../config/global'; -import { Model } from '../model'; - -/** - * Creates a QueryTree that is used to instruct the DataBase how to perform a GraphQL query - * @param {FieldRequest} operation the graphql query - */ -export function createQueryTree(operation: DistilledOperation, model: Model) { - return new ObjectQueryNode(operation.selectionSet.map( - sel => new PropertySpecification(sel.propertyName, - createQueryNodeForField(sel.fieldRequest, model)))); -} - -function createQueryNodeForField(fieldRequest: FieldRequest, model: Model): QueryNode { - switch (fieldRequest.parentType.name) { - case QUERY_TYPE: - return createQueryNamespaceNode(fieldRequest, [], model.rootNamespace); - case MUTATION_TYPE: - return createMutationNamespaceNode(fieldRequest, [], model.rootNamespace); - default: - globalContext.loggerProvider.getLogger('query-tree-builder').warn(`unknown root field: ${fieldRequest.fieldName}`); - return new NullQueryNode(); - } -} diff --git a/src/query/query-tree-utils.ts b/src/query/query-tree-utils.ts deleted file mode 100644 index 1df4492a..00000000 --- a/src/query/query-tree-utils.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { - BinaryOperationQueryNode, BinaryOperator, ConstBoolQueryNode, FieldQueryNode, FirstOfListQueryNode, ObjectQueryNode, - OrderClause, OrderSpecification, PropertySpecification, QueryNode, RootEntityIDQueryNode, UnaryOperationQueryNode, - UnaryOperator, VariableAssignmentQueryNode -} from '../query-tree'; - -/** - * Traverses recursively through Unary/Binary operations, extracts all variable definitions and replaces them by their - * variable nodes - * @param {QueryNode} node - * @param variableAssignmentsList: (will be modified) list to which to add the variable assignment nodes - */ -export function extractVariableAssignments(node: QueryNode, variableAssignmentsList: VariableAssignmentQueryNode[]): QueryNode { - if (node instanceof UnaryOperationQueryNode) { - return new UnaryOperationQueryNode(extractVariableAssignments(node.valueNode, variableAssignmentsList), node.operator); - } - if (node instanceof BinaryOperationQueryNode) { - return new BinaryOperationQueryNode( - extractVariableAssignments(node.lhs, variableAssignmentsList), - node.operator, - extractVariableAssignments(node.rhs, variableAssignmentsList)); - } - if (node instanceof VariableAssignmentQueryNode) { - variableAssignmentsList.push(node); - return node.resultNode; - } - if (node instanceof FirstOfListQueryNode) { - return new FirstOfListQueryNode(extractVariableAssignments(node.listNode, variableAssignmentsList)); - } - if (node instanceof FieldQueryNode) { - return new FieldQueryNode(extractVariableAssignments(node.objectNode, variableAssignmentsList), node.field); - } - if (node instanceof RootEntityIDQueryNode) { - return new RootEntityIDQueryNode(extractVariableAssignments(node.objectNode, variableAssignmentsList)); - } - if (node instanceof ObjectQueryNode) { - return new ObjectQueryNode(node.properties.map(p => new PropertySpecification(p.propertyName, extractVariableAssignments(p.valueNode, variableAssignmentsList)))); - } - return node; -} - -/** - * Wraps a queryNode in a list of variable assignments (the logical counterpart to extractVariableAssignments) - */ -export function prependVariableAssignments(node: QueryNode, variableAssignmentsList: VariableAssignmentQueryNode[]) { - return variableAssignmentsList.reduce((currentNode, assignmentNode) => new VariableAssignmentQueryNode({ - resultNode: currentNode, - variableNode: assignmentNode.variableNode, - variableValueNode: assignmentNode.variableValueNode - }), node); -} - -export function extractVariableAssignmentsInOrderSpecification(orderSpecification: OrderSpecification, variableAssignmentsList: VariableAssignmentQueryNode[]): OrderSpecification { - return new OrderSpecification(orderSpecification.clauses - .map(clause => new OrderClause(extractVariableAssignments(clause.valueNode, variableAssignmentsList), clause.direction))); -} - -/** - * Simplifies a boolean expression (does not recurse into non-boolean-related nodes) - */ -export function simplifyBooleans(node: QueryNode): QueryNode { - if (node instanceof UnaryOperationQueryNode && node.operator == UnaryOperator.NOT) { - const inner = simplifyBooleans(node.valueNode); - if (inner instanceof ConstBoolQueryNode) { - return new ConstBoolQueryNode(!inner.value); - } - return new UnaryOperationQueryNode(inner, UnaryOperator.NOT); - } - if (node instanceof BinaryOperationQueryNode) { - const lhs = simplifyBooleans(node.lhs); - const rhs = simplifyBooleans(node.rhs); - if (node.operator == BinaryOperator.AND) { - if (lhs instanceof ConstBoolQueryNode) { - if (rhs instanceof ConstBoolQueryNode) { - // constant evaluation - return ConstBoolQueryNode.for(lhs.value && rhs.value); - } - if (lhs.value == false) { - // FALSE && anything == FALSE - return ConstBoolQueryNode.FALSE; - } - if (lhs.value == true) { - // TRUE && other == other - return rhs; - } - } - if (rhs instanceof ConstBoolQueryNode) { - if (rhs.value == false) { - // anything && FALSE == FALSE - return ConstBoolQueryNode.FALSE; - } - // const case is handled above - if (rhs.value == true) { - // other && TRUE == other - return lhs; - } - } - } - - if (node.operator == BinaryOperator.OR) { - if (lhs instanceof ConstBoolQueryNode) { - if (rhs instanceof ConstBoolQueryNode) { - // constant evaluation - return ConstBoolQueryNode.for(lhs.value || rhs.value); - } - if (lhs.value == true) { - // TRUE || anything == TRUE - return ConstBoolQueryNode.TRUE; - } - if (lhs.value == false) { - // FALSE || other == other - return rhs; - } - } - if (rhs instanceof ConstBoolQueryNode) { - if (rhs.value == true) { - // anything || TRUE == TRUE - return ConstBoolQueryNode.TRUE; - } - if (rhs.value == false) { - // other || FALSE == other - return lhs; - } - } - } - } - return node; -} \ No newline at end of file diff --git a/src/query/runtime-errors.ts b/src/query/runtime-errors.ts deleted file mode 100644 index 384859bb..00000000 --- a/src/query/runtime-errors.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { defaultFieldResolver, GraphQLSchema } from 'graphql'; -import { transformSchema } from 'graphql-transformer/dist'; -import { extractRuntimeError, isRuntimeErrorValue } from '../query-tree'; -import { isPromise } from '../utils/utils'; - -export function addRuntimeErrorResolvers(schema: GraphQLSchema) { - return transformSchema(schema, { - transformField(config) { - return { - ...config, - resolve(source, args, context, info) { - const result = (config.resolve || defaultFieldResolver)(source, args, context, info); - if (isPromise(result)) { - return result.then(res => { - if (isRuntimeErrorValue(res)) { - throw extractRuntimeError(res); - } - return res; - }) - } - if (isRuntimeErrorValue(result)) { - throw extractRuntimeError(result); - } - return result; - } - } - } - }); -} diff --git a/src/schema-generation/create-input-types/generator.ts b/src/schema-generation/create-input-types/generator.ts index c24d377c..3c1b34dc 100644 --- a/src/schema-generation/create-input-types/generator.ts +++ b/src/schema-generation/create-input-types/generator.ts @@ -2,6 +2,7 @@ import { GraphQLList, GraphQLNonNull } from 'graphql'; import { flatMap } from 'lodash'; import memorize from 'memorize-decorator'; import { ChildEntityType, EntityExtensionType, Field, ObjectType, RootEntityType, ValueObjectType } from '../../model'; +import { getCreateInputTypeName } from '../../schema/names'; import { EnumTypeGenerator } from '../enum-type-generator'; import { BasicCreateInputField, BasicListCreateInputField, CreateInputField, ObjectCreateInputField, @@ -32,25 +33,25 @@ export class CreateInputTypeGenerator { @memorize() generateForRootEntityType(type: RootEntityType): CreateRootEntityInputType { - return new CreateRootEntityInputType(`Create${type.name}Input`, + return new CreateRootEntityInputType(getCreateInputTypeName(type.name), () => flatMap(type.fields, (field: Field) => this.generateFields(field))); } @memorize() generateForChildEntityType(type: ChildEntityType): CreateChildEntityInputType { - return new CreateChildEntityInputType(`Create${type.name}Input`, + return new CreateChildEntityInputType(getCreateInputTypeName(type.name), () => flatMap(type.fields, (field: Field) => this.generateFields(field))); } @memorize() generateForEntityExtensionType(type: EntityExtensionType): CreateObjectInputType { - return new CreateObjectInputType(`Create${type.name}Input`, + return new CreateObjectInputType(getCreateInputTypeName(type.name), () => flatMap(type.fields, (field: Field) => this.generateFields(field))); } @memorize() generateForValueObjectType(type: ValueObjectType): CreateObjectInputType { - return new CreateObjectInputType(`${type.name}Input`, + return new CreateObjectInputType(getCreateInputTypeName(type.name), () => flatMap(type.fields, (field: Field) => this.generateFields(field))); } diff --git a/src/schema-generation/create-input-types/input-types.ts b/src/schema-generation/create-input-types/input-types.ts index e7ce75c7..d88f1da1 100644 --- a/src/schema-generation/create-input-types/input-types.ts +++ b/src/schema-generation/create-input-types/input-types.ts @@ -2,7 +2,7 @@ import { Thunk } from 'graphql'; import { fromPairs, toPairs } from 'lodash'; import { Field } from '../../model'; import { PreExecQueryParms, QueryNode } from '../../query-tree'; -import { ENTITY_CREATED_AT, ENTITY_UPDATED_AT, ID_FIELD } from '../../schema/schema-defaults'; +import { ENTITY_CREATED_AT, ENTITY_UPDATED_AT, ID_FIELD } from '../../schema/constants'; import { flatMap, PlainObject } from '../../utils/utils'; import { TypedInputObjectType } from '../typed-input-object-type'; import { CreateInputField } from './input-fields'; diff --git a/src/schema-generation/field-nodes.ts b/src/schema-generation/field-nodes.ts index 30ce35d2..dad61743 100644 --- a/src/schema-generation/field-nodes.ts +++ b/src/schema-generation/field-nodes.ts @@ -1,14 +1,11 @@ import { RootEntityType } from '../model'; import { Field } from '../model/implementation'; import { - BasicType, - BinaryOperationQueryNode, BinaryOperator, ConditionalQueryNode, EntitiesQueryNode, FieldQueryNode, - FirstOfListQueryNode, - FollowEdgeQueryNode, NullQueryNode, QueryNode, RootEntityIDQueryNode, TransformListQueryNode, TypeCheckQueryNode, - VariableQueryNode + BasicType, BinaryOperationQueryNode, BinaryOperator, ConditionalQueryNode, EntitiesQueryNode, FieldQueryNode, + FirstOfListQueryNode, FollowEdgeQueryNode, ListQueryNode, NullQueryNode, QueryNode, RootEntityIDQueryNode, + TransformListQueryNode, TypeCheckQueryNode, VariableQueryNode } from '../query-tree'; -import { createSafeListQueryNode } from '../query/queries'; -import { ID_FIELD } from '../schema/schema-defaults'; +import { ID_FIELD } from '../schema/constants'; export function createFieldNode(field: Field, sourceNode: QueryNode): QueryNode { if (field.isList) { @@ -69,3 +66,12 @@ function createToNRelationNode(field: Field, sourceNode: QueryNode): QueryNode { const relation = field.getRelationOrThrow(); return new FollowEdgeQueryNode(relation, sourceNode, relation.getFieldSide(field)); } + +function createSafeListQueryNode(listNode: QueryNode) { + // to avoid errors because of eagerly evaluated list expression, we just convert non-lists to an empty list + return new ConditionalQueryNode( + new TypeCheckQueryNode(listNode, BasicType.LIST), + listNode, + new ListQueryNode([]) + ); +} diff --git a/src/schema-generation/filter-augmentation.ts b/src/schema-generation/filter-augmentation.ts index ce039887..3dc49448 100644 --- a/src/schema-generation/filter-augmentation.ts +++ b/src/schema-generation/filter-augmentation.ts @@ -1,5 +1,5 @@ import { Type } from '../model'; -import { FILTER_ARG } from '../schema/schema-defaults'; +import { FILTER_ARG } from '../schema/constants'; import { FilterTypeGenerator } from './filter-input-types'; import { QueryNodeField } from './query-node-object-type'; import { buildFilteredListNode } from './utils/filtering'; diff --git a/src/schema-generation/filter-input-types/constants.ts b/src/schema-generation/filter-input-types/constants.ts index c5a480c0..03e78558 100644 --- a/src/schema-generation/filter-input-types/constants.ts +++ b/src/schema-generation/filter-input-types/constants.ts @@ -8,7 +8,7 @@ import { INPUT_FIELD_IN, INPUT_FIELD_LT, INPUT_FIELD_LTE, INPUT_FIELD_NONE, INPUT_FIELD_NOT, INPUT_FIELD_NOT_CONTAINS, INPUT_FIELD_NOT_ENDS_WITH, INPUT_FIELD_NOT_IN, INPUT_FIELD_NOT_STARTS_WITH, INPUT_FIELD_SOME, INPUT_FIELD_STARTS_WITH, SCALAR_DATE, SCALAR_TIME -} from '../../schema/schema-defaults'; +} from '../../schema/constants'; export const FILTER_OPERATORS: { [suffix: string]: (fieldNode: QueryNode, valueNode: QueryNode) => QueryNode } = { [INPUT_FIELD_EQUAL]: binaryOp(BinaryOperator.EQUAL), diff --git a/src/schema-generation/filter-input-types/filter-fields.ts b/src/schema-generation/filter-input-types/filter-fields.ts index f18821de..265dad98 100644 --- a/src/schema-generation/filter-input-types/filter-fields.ts +++ b/src/schema-generation/filter-input-types/filter-fields.ts @@ -5,7 +5,9 @@ import { BinaryOperationQueryNode, BinaryOperator, ConstBoolQueryNode, CountQueryNode, LiteralQueryNode, QueryNode, TransformListQueryNode, VariableQueryNode } from '../../query-tree'; -import { AND_FILTER_FIELD, INPUT_FIELD_EVERY, INPUT_FIELD_NONE, OR_FILTER_FIELD } from '../../schema/schema-defaults'; +import { + AND_FILTER_FIELD, INPUT_FIELD_EVERY, INPUT_FIELD_NONE, FILTER_FIELD_PREFIX_SEPARATOR, OR_FILTER_FIELD +} from '../../schema/constants'; import { AnyValue, decapitalize, PlainObject } from '../../utils/utils'; import { createFieldNode } from '../field-nodes'; import { TypedInputFieldBase } from '../typed-input-object-type'; @@ -33,7 +35,7 @@ export class ScalarOrEnumFieldFilterField implements FilterField { if (this.operatorPrefix == undefined) { return this.field.name; } - return `${this.field.name}_${this.operatorPrefix}`; + return this.field.name + FILTER_FIELD_PREFIX_SEPARATOR + this.operatorPrefix; } getFilterNode(sourceNode: QueryNode, filterValue: AnyValue): QueryNode { @@ -160,4 +162,3 @@ export class OrFilterField implements FilterField { return nodes.reduce((prev, node) => new BinaryOperationQueryNode(prev, BinaryOperator.OR, node)); } } - diff --git a/src/schema-generation/filter-input-types/generator.ts b/src/schema-generation/filter-input-types/generator.ts index 1013d869..eefb2f13 100644 --- a/src/schema-generation/filter-input-types/generator.ts +++ b/src/schema-generation/filter-input-types/generator.ts @@ -5,7 +5,8 @@ import { EnumType, Field, ScalarType, Type } from '../../model/index'; import { BinaryOperationQueryNode, BinaryOperator, ConstBoolQueryNode, NullQueryNode, QueryNode } from '../../query-tree'; -import { INPUT_FIELD_EQUAL } from '../../schema/schema-defaults'; +import { INPUT_FIELD_EQUAL } from '../../schema/constants'; +import { getFilterTypeName } from '../../schema/names'; import { AnyValue, objectEntries } from '../../utils/utils'; import { EnumTypeGenerator } from '../enum-type-generator'; import { resolveThunk } from '../query-node-object-type'; @@ -62,7 +63,7 @@ export class FilterTypeGenerator { ] } - const filterType = new FilterObjectType(`${type.name}Filter`, getFields); + const filterType = new FilterObjectType(getFilterTypeName(type.name), getFields); return filterType; } diff --git a/src/schema-generation/index.ts b/src/schema-generation/index.ts new file mode 100644 index 00000000..e5bfac05 --- /dev/null +++ b/src/schema-generation/index.ts @@ -0,0 +1,4 @@ +export { SchemaGenerator } from './schema-generator'; +export { RootTypesGenerator } from './root-types-generator'; +export * from './query-node-object-type/definition'; +export { buildConditionalObjectQueryNode }from './query-node-object-type/query-node-generator'; diff --git a/src/schema-generation/meta-type-generator.ts b/src/schema-generation/meta-type-generator.ts index 005e1a49..5ff974f1 100644 --- a/src/schema-generation/meta-type-generator.ts +++ b/src/schema-generation/meta-type-generator.ts @@ -1,23 +1,23 @@ import { GraphQLInt } from 'graphql'; import memorize from 'memorize-decorator'; -import { ObjectType } from '../model/implementation'; import { CountQueryNode } from '../query-tree'; +import { COUNT_META_FIELD, QUERY_META_TYPE } from '../schema/constants'; import { QueryNodeField, QueryNodeObjectType } from './query-node-object-type'; export class MetaTypeGenerator { @memorize() - generate(objectType: ObjectType): QueryNodeObjectType { + generate(): QueryNodeObjectType { return { - name: `${objectType.name}Meta`, + name: QUERY_META_TYPE, fields: [this.getCountField()] }; } private getCountField(): QueryNodeField { return { - name: 'count', + name: COUNT_META_FIELD, type: GraphQLInt, resolve: listNode => new CountQueryNode(listNode) }; } -} \ No newline at end of file +} diff --git a/src/schema-generation/mutation-type-generator.ts b/src/schema-generation/mutation-type-generator.ts index 83b61a67..e3654bbb 100644 --- a/src/schema-generation/mutation-type-generator.ts +++ b/src/schema-generation/mutation-type-generator.ts @@ -1,7 +1,6 @@ import { GraphQLNonNull } from 'graphql'; import { flatMap } from 'lodash'; import memorize from 'memorize-decorator'; -import * as pluralize from 'pluralize'; import { Namespace, RootEntityType } from '../model'; import { AffectedFieldInfoQueryNode, BinaryOperationQueryNode, BinaryOperator, CreateEntityQueryNode, @@ -9,10 +8,11 @@ import { FirstOfListQueryNode, LiteralQueryNode, ObjectQueryNode, PreExecQueryParms, QueryNode, RootEntityIDQueryNode, TransformListQueryNode, UnknownValueQueryNode, UpdateEntitiesQueryNode, VariableQueryNode, WithPreExecutionQueryNode } from '../query-tree'; +import { ID_FIELD, MUTATION_INPUT_ARG, MUTATION_TYPE } from '../schema/constants'; import { - CREATE_ENTITY_FIELD_PREFIX, DELETE_ALL_ENTITIES_FIELD_PREFIX, DELETE_ENTITY_FIELD_PREFIX, ID_FIELD, - MUTATION_INPUT_ARG, UPDATE_ALL_ENTITIES_FIELD_PREFIX, UPDATE_ENTITY_FIELD_PREFIX -} from '../schema/schema-defaults'; + getCreateEntityFieldName, getDeleteAllEntitiesFieldName, getDeleteEntityFieldName, getUpdateAllEntitiesFieldName, + getUpdateEntityFieldName +} from '../schema/names'; import { decapitalize, PlainObject } from '../utils/utils'; import { CreateInputTypeGenerator, CreateRootEntityInputType } from './create-input-types'; import { ListAugmentation } from './list-augmentation'; @@ -44,7 +44,7 @@ export class MutationTypeGenerator { const rootEntityFields = flatMap(namespace.rootEntityTypes, type => this.generateFields(type)); return { - name: `${namespace.pascalCasePath}Mutation`, + name: namespace.pascalCasePath + MUTATION_TYPE, fields: [ ...namespaceFields, ...rootEntityFields @@ -66,7 +66,7 @@ export class MutationTypeGenerator { const inputType = this.createTypeGenerator.generateForRootEntityType(rootEntityType); return { - name: `${CREATE_ENTITY_FIELD_PREFIX}${rootEntityType.name}`, + name: getCreateEntityFieldName(rootEntityType.name), type: new QueryNodeNonNullType(this.outputTypeGenerator.generate(rootEntityType)), args: { [MUTATION_INPUT_ARG]: { @@ -101,7 +101,7 @@ export class MutationTypeGenerator { const inputType = this.updateTypeGenerator.generateForRootEntityType(rootEntityType); return { - name: `${UPDATE_ENTITY_FIELD_PREFIX}${rootEntityType.name}`, + name: getUpdateEntityFieldName(rootEntityType.name), type: this.outputTypeGenerator.generate(rootEntityType), args: { [MUTATION_INPUT_ARG]: { @@ -162,7 +162,7 @@ export class MutationTypeGenerator { private generateUpdateAllField(rootEntityType: RootEntityType): QueryNodeField { // we construct this field like a regular query field first so that the list augmentation works const fieldBase: QueryNodeField = { - name: `${UPDATE_ALL_ENTITIES_FIELD_PREFIX}${pluralize(rootEntityType.name)}`, + name: getUpdateAllEntitiesFieldName(rootEntityType.name), type: makeNonNullableList(this.outputTypeGenerator.generate(rootEntityType)), resolve: () => new EntitiesQueryNode(rootEntityType) }; @@ -226,7 +226,7 @@ export class MutationTypeGenerator { private generateDeleteField(rootEntityType: RootEntityType): QueryNodeField { return { - name: `${DELETE_ENTITY_FIELD_PREFIX}${rootEntityType.name}`, + name: getDeleteEntityFieldName(rootEntityType.name), type: this.outputTypeGenerator.generate(rootEntityType), args: getArgumentsForUniqueFields(rootEntityType), resolve: (source, args) => this.generateDeleteQueryNode(rootEntityType, args) @@ -248,7 +248,7 @@ export class MutationTypeGenerator { private generateDeleteAllField(rootEntityType: RootEntityType): QueryNodeField { // we construct this field like a regular query field first so that the list augmentation works const fieldBase: QueryNodeField = { - name: `${DELETE_ALL_ENTITIES_FIELD_PREFIX}${pluralize(rootEntityType.name)}`, + name: getDeleteAllEntitiesFieldName(rootEntityType.name), type: makeNonNullableList(this.outputTypeGenerator.generate(rootEntityType)), resolve: () => new EntitiesQueryNode(rootEntityType) }; diff --git a/src/schema-generation/order-by-and-pagination-augmentation.ts b/src/schema-generation/order-by-and-pagination-augmentation.ts index f9caef80..fd676d28 100644 --- a/src/schema-generation/order-by-and-pagination-augmentation.ts +++ b/src/schema-generation/order-by-and-pagination-augmentation.ts @@ -4,7 +4,7 @@ import { BinaryOperationQueryNode, BinaryOperator, ConstBoolQueryNode, LiteralQueryNode, OrderDirection, OrderSpecification, QueryNode, RuntimeErrorQueryNode, TransformListQueryNode, VariableQueryNode } from '../query-tree'; -import { AFTER_ARG, FIRST_ARG, ORDER_BY_ARG } from '../schema/schema-defaults'; +import { AFTER_ARG, FIRST_ARG, ORDER_BY_ARG } from '../schema/constants'; import { decapitalize } from '../utils/utils'; import { OrderByEnumGenerator, OrderByEnumType, OrderByEnumValue } from './order-by-enum-generator'; import { QueryNodeField } from './query-node-object-type'; diff --git a/src/schema-generation/order-by-enum-generator.ts b/src/schema-generation/order-by-enum-generator.ts index 0fd55efb..0a474267 100644 --- a/src/schema-generation/order-by-enum-generator.ts +++ b/src/schema-generation/order-by-enum-generator.ts @@ -1,8 +1,10 @@ import { GraphQLEnumType } from 'graphql'; import { chain } from 'lodash'; import memorize from 'memorize-decorator'; -import { Field, ObjectType } from '../model/implementation'; +import { Field, ObjectType } from '../model'; import { OrderClause, OrderDirection, QueryNode } from '../query-tree'; +import { ORDER_BY_ASC_SUFFIX, ORDER_BY_DESC_SUFFIX } from '../schema/constants'; +import { getOrderByTypeName } from '../schema/names'; import { flatMap } from '../utils/utils'; import { createFieldNode } from './field-nodes'; @@ -12,7 +14,7 @@ export class OrderByEnumType { } get name() { - return `${this.objectType.name}OrderBy`; + return getOrderByTypeName(this.objectType.name); } @memorize() @@ -54,7 +56,7 @@ export class OrderByEnumValue { } get name() { - return this.underscoreSeparatedPath + '_' + (this.direction == OrderDirection.ASCENDING ? 'ASC' : 'DESC'); + return this.underscoreSeparatedPath + (this.direction == OrderDirection.ASCENDING ? ORDER_BY_ASC_SUFFIX : ORDER_BY_DESC_SUFFIX); } getValueNode(itemNode: QueryNode): QueryNode { diff --git a/src/schema-generation/output-type-generator.ts b/src/schema-generation/output-type-generator.ts index 11ad5d2f..6aa9611c 100644 --- a/src/schema-generation/output-type-generator.ts +++ b/src/schema-generation/output-type-generator.ts @@ -1,14 +1,14 @@ import { GraphQLString } from 'graphql'; import { sortBy } from 'lodash'; import memorize from 'memorize-decorator'; -import { getMetaFieldName } from '../graphql/names'; +import { getMetaFieldName } from '../schema/names'; import { FieldRequest } from '../graphql/query-distiller'; import { isListType } from '../graphql/schema-utils'; import { Field, ObjectType, Type } from '../model'; import { NullQueryNode, ObjectQueryNode, PropertySpecification, QueryNode, UnaryOperationQueryNode, UnaryOperator } from '../query-tree'; -import { CURSOR_FIELD } from '../schema/schema-defaults'; +import { CURSOR_FIELD } from '../schema/constants'; import { flatMap } from '../utils/utils'; import { EnumTypeGenerator } from './enum-type-generator'; import { createFieldNode } from './field-nodes'; @@ -112,7 +112,7 @@ export class OutputTypeGenerator { throw new Error(`Can only create meta field for object types`); } - const metaType = this.metaTypeGenerator.generate(field.type); + const metaType = this.metaTypeGenerator.generate(); const plainField: QueryNodeField = { name: getMetaFieldName(field.name), type: new QueryNodeNonNullType(metaType), diff --git a/src/schema-generation/query-type-generator.ts b/src/schema-generation/query-type-generator.ts index 080bc6e8..4a290abb 100644 --- a/src/schema-generation/query-type-generator.ts +++ b/src/schema-generation/query-type-generator.ts @@ -1,9 +1,8 @@ import memorize from 'memorize-decorator'; -import * as pluralize from 'pluralize'; -import { getAllEntitiesFieldName, getMetaFieldName } from '../graphql/names'; import { Namespace, RootEntityType } from '../model'; import { EntitiesQueryNode, FirstOfListQueryNode, ObjectQueryNode, QueryNode } from '../query-tree'; -import { ALL_ENTITIES_FIELD_PREFIX } from '../schema/schema-defaults'; +import { QUERY_TYPE } from '../schema/constants'; +import { getAllEntitiesFieldName, getMetaFieldName } from '../schema/names'; import { flatMap } from '../utils/utils'; import { FilterAugmentation } from './filter-augmentation'; import { ListAugmentation } from './list-augmentation'; @@ -25,7 +24,7 @@ export class QueryTypeGenerator { @memorize() generate(namespace: Namespace): QueryNodeObjectType { return { - name: `${namespace.pascalCasePath}Query`, + name: namespace.pascalCasePath + QUERY_TYPE, fields: [ ...namespace.childNamespaces.map(namespace => this.getNamespaceField(namespace)), ...flatMap(namespace.rootEntityTypes, type => this.getFields(type)), @@ -76,7 +75,7 @@ export class QueryTypeGenerator { } private getAllRootEntitiesMetaField(rootEntityType: RootEntityType): QueryNodeField { - const metaType = this.metaTypeGenerator.generate(rootEntityType); + const metaType = this.metaTypeGenerator.generate(); const fieldConfig = ({ name: getMetaFieldName(getAllEntitiesFieldName(rootEntityType.name)), type: new QueryNodeNonNullType(metaType), diff --git a/src/schema-generation/root-types-generator.ts b/src/schema-generation/root-types-generator.ts new file mode 100644 index 00000000..dacbe308 --- /dev/null +++ b/src/schema-generation/root-types-generator.ts @@ -0,0 +1,44 @@ +import memorize from 'memorize-decorator'; +import { Model } from '../model'; +import { CreateInputTypeGenerator } from './create-input-types'; +import { EnumTypeGenerator } from './enum-type-generator'; +import { FilterAugmentation } from './filter-augmentation'; +import { FilterTypeGenerator } from './filter-input-types'; +import { ListAugmentation } from './list-augmentation'; +import { MetaTypeGenerator } from './meta-type-generator'; +import { MutationTypeGenerator } from './mutation-type-generator'; +import { OrderByAndPaginationAugmentation } from './order-by-and-pagination-augmentation'; +import { OrderByEnumGenerator } from './order-by-enum-generator'; +import { OutputTypeGenerator } from './output-type-generator'; +import { QueryNodeObjectType, QueryNodeObjectTypeConverter } from './query-node-object-type'; +import { QueryTypeGenerator } from './query-type-generator'; +import { UpdateInputTypeGenerator } from './update-input-types'; + +export class RootTypesGenerator { + private readonly enumTypeGenerator = new EnumTypeGenerator(); + private readonly filterTypeGenerator = new FilterTypeGenerator(this.enumTypeGenerator); + private readonly orderByEnumGenerator = new OrderByEnumGenerator(); + private readonly orderByAugmentation = new OrderByAndPaginationAugmentation(this.orderByEnumGenerator); + private readonly filterAugmentation = new FilterAugmentation(this.filterTypeGenerator); + private readonly listAugmentation = new ListAugmentation(this.filterAugmentation, this.orderByAugmentation); + private readonly metaTypeGenerator = new MetaTypeGenerator(); + private readonly outputTypeGenerator = new OutputTypeGenerator(this.listAugmentation, this.filterAugmentation, + this.enumTypeGenerator, this.orderByEnumGenerator, this.metaTypeGenerator); + private readonly createTypeGenerator = new CreateInputTypeGenerator(this.enumTypeGenerator); + private readonly updateTypeGenerator = new UpdateInputTypeGenerator(this.enumTypeGenerator, this.createTypeGenerator); + private readonly queryTypeGenerator = new QueryTypeGenerator(this.outputTypeGenerator, this.listAugmentation, + this.filterAugmentation, this.metaTypeGenerator); + private readonly mutationTypeGenerator = new MutationTypeGenerator(this.outputTypeGenerator, this.createTypeGenerator, + this.updateTypeGenerator, this.listAugmentation); + + @memorize() + generateQueryType(model: Model): QueryNodeObjectType { + return this.queryTypeGenerator.generate(model.rootNamespace); + } + + @memorize() + generateMutationType(model: Model): QueryNodeObjectType { + return this.mutationTypeGenerator.generate(model.rootNamespace); + } + +} diff --git a/src/schema-generation/schema-generator.ts b/src/schema-generation/schema-generator.ts index 19116a82..f2d8bfa2 100644 --- a/src/schema-generation/schema-generator.ts +++ b/src/schema-generation/schema-generator.ts @@ -5,40 +5,15 @@ import { addOperationBasedResolvers, OperationParams } from '../graphql/operatio import { distillOperation } from '../graphql/query-distiller'; import { Model } from '../model'; import { ObjectQueryNode, QueryNode } from '../query-tree'; -import { evaluateQueryStatically } from '../query/static-evaluation'; +import { evaluateQueryStatically } from '../query-tree/utils'; import { SchemaTransformationContext } from '../schema/preparation/transformation-pipeline'; -import { CreateInputTypeGenerator } from './create-input-types'; -import { EnumTypeGenerator } from './enum-type-generator'; -import { FilterAugmentation } from './filter-augmentation'; -import { FilterTypeGenerator } from './filter-input-types'; -import { ListAugmentation } from './list-augmentation'; -import { MetaTypeGenerator } from './meta-type-generator'; -import { MutationTypeGenerator } from './mutation-type-generator'; -import { OrderByAndPaginationAugmentation } from './order-by-and-pagination-augmentation'; -import { OrderByEnumGenerator } from './order-by-enum-generator'; -import { OutputTypeGenerator } from './output-type-generator'; import { buildConditionalObjectQueryNode, QueryNodeObjectType, QueryNodeObjectTypeConverter } from './query-node-object-type'; -import { QueryTypeGenerator } from './query-type-generator'; -import { UpdateInputTypeGenerator } from './update-input-types'; +import { RootTypesGenerator } from './root-types-generator'; export class SchemaGenerator { - private readonly enumTypeGenerator = new EnumTypeGenerator(); - private readonly filterTypeGenerator = new FilterTypeGenerator(this.enumTypeGenerator); - private readonly orderByEnumGenerator = new OrderByEnumGenerator(); - private readonly orderByAugmentation = new OrderByAndPaginationAugmentation(this.orderByEnumGenerator); - private readonly filterAugmentation = new FilterAugmentation(this.filterTypeGenerator); - private readonly listAugmentation = new ListAugmentation(this.filterAugmentation, this.orderByAugmentation); - private readonly metaTypeGenerator = new MetaTypeGenerator(); - private readonly outputTypeGenerator = new OutputTypeGenerator(this.listAugmentation, this.filterAugmentation, - this.enumTypeGenerator, this.orderByEnumGenerator, this.metaTypeGenerator); - private readonly createTypeGenerator = new CreateInputTypeGenerator(this.enumTypeGenerator); - private readonly updateTypeGenerator = new UpdateInputTypeGenerator(this.enumTypeGenerator, this.createTypeGenerator); - private readonly queryTypeGenerator = new QueryTypeGenerator(this.outputTypeGenerator, this.listAugmentation, - this.filterAugmentation, this.metaTypeGenerator); - private readonly mutationTypeGenerator = new MutationTypeGenerator(this.outputTypeGenerator, this.createTypeGenerator, - this.updateTypeGenerator, this.listAugmentation); + private readonly rootTypesGenerator = new RootTypesGenerator(); private readonly queryNodeObjectTypeConverter = new QueryNodeObjectTypeConverter(); constructor( @@ -48,8 +23,8 @@ export class SchemaGenerator { } generate(model: Model) { - const queryType = this.queryTypeGenerator.generate(model.rootNamespace); - const mutationType = this.mutationTypeGenerator.generate(model.rootNamespace); + const queryType = this.rootTypesGenerator.generateQueryType(model); + const mutationType = this.rootTypesGenerator.generateMutationType(model); const dumbSchema = new GraphQLSchema({ query: this.queryNodeObjectTypeConverter.convertToGraphQLObjectType(queryType), mutation: this.queryNodeObjectTypeConverter.convertToGraphQLObjectType(mutationType) diff --git a/src/schema-generation/update-input-types/generator.ts b/src/schema-generation/update-input-types/generator.ts index f29efb44..16757ba9 100644 --- a/src/schema-generation/update-input-types/generator.ts +++ b/src/schema-generation/update-input-types/generator.ts @@ -3,7 +3,8 @@ import { flatMap } from 'lodash'; import memorize from 'memorize-decorator'; import * as pluralize from 'pluralize'; import { CalcMutationsOperator, ChildEntityType, EntityExtensionType, Field, RootEntityType } from '../../model'; -import { CALC_MUTATIONS_OPERATORS, CalcMutationOperator, ID_FIELD } from '../../schema/schema-defaults'; +import { CALC_MUTATIONS_OPERATORS, CalcMutationOperator, ID_FIELD } from '../../schema/constants'; +import { getUpdateAllInputTypeName, getUpdateInputTypeName } from '../../schema/names'; import { CreateInputTypeGenerator } from '../create-input-types'; import { EnumTypeGenerator } from '../enum-type-generator'; import { @@ -39,13 +40,13 @@ export class UpdateInputTypeGenerator { @memorize() generateForRootEntityType(type: RootEntityType): UpdateRootEntityInputType { - return new UpdateRootEntityInputType(type, `Update${type.name}Input`, + return new UpdateRootEntityInputType(type, getUpdateInputTypeName(type.name), () => flatMap(type.fields, (field: Field) => this.generateFields(field))); } @memorize() generateUpdateAllRootEntitiesInputType(type: RootEntityType): UpdateRootEntityInputType { - return new UpdateRootEntityInputType(type, `UpdateAll${pluralize(type.name)}Input`, + return new UpdateRootEntityInputType(type, getUpdateAllInputTypeName(type.name), () => flatMap(type.fields, (field: Field) => this.generateFields(field, { skipID: true, skipRelations: true // can't do this properly at the moment because it would need a dynamic number of pre-execs @@ -54,13 +55,13 @@ export class UpdateInputTypeGenerator { @memorize() generateForEntityExtensionType(type: EntityExtensionType): UpdateEntityExtensionInputType { - return new UpdateEntityExtensionInputType(type, `Update${type.name}Input`, + return new UpdateEntityExtensionInputType(type, getUpdateInputTypeName(type.name), () => flatMap(type.fields, (field: Field) => this.generateFields(field))); } @memorize() generateForChildEntityType(type: ChildEntityType): UpdateChildEntityInputType { - return new UpdateChildEntityInputType(type, `Update${type.name}Input`, + return new UpdateChildEntityInputType(type, getUpdateInputTypeName(type.name), () => flatMap(type.fields, (field: Field) => this.generateFields(field))); } diff --git a/src/schema-generation/update-input-types/input-fields.ts b/src/schema-generation/update-input-types/input-fields.ts index 6a63a9fc..17171a0a 100644 --- a/src/schema-generation/update-input-types/input-fields.ts +++ b/src/schema-generation/update-input-types/input-fields.ts @@ -1,7 +1,7 @@ import { GraphQLID, GraphQLInputType, GraphQLList, GraphQLNonNull } from 'graphql'; import { getAddChildEntitiesFieldName, getRemoveChildEntitiesFieldName, getUpdateChildEntitiesFieldName -} from '../../graphql/names'; +} from '../../schema/names'; import { CalcMutationsOperator, Field } from '../../model'; import { BinaryOperationQueryNode, BinaryOperator, FieldQueryNode, LiteralQueryNode, MergeObjectsQueryNode, NullQueryNode, diff --git a/src/schema-generation/update-input-types/input-types.ts b/src/schema-generation/update-input-types/input-types.ts index 33b729dc..cdaad989 100644 --- a/src/schema-generation/update-input-types/input-types.ts +++ b/src/schema-generation/update-input-types/input-types.ts @@ -2,7 +2,7 @@ import { Thunk } from 'graphql'; import { groupBy } from 'lodash'; import { getAddChildEntitiesFieldName, getRemoveChildEntitiesFieldName, getUpdateChildEntitiesFieldName -} from '../../graphql/names'; +} from '../../schema/names'; import { ChildEntityType, Field, ObjectType, RootEntityType } from '../../model'; import { BasicType, BinaryOperationQueryNode, BinaryOperator, ConcatListsQueryNode, ConditionalQueryNode, FieldQueryNode, @@ -10,8 +10,7 @@ import { RuntimeErrorQueryNode, SetFieldQueryNode, TransformListQueryNode, TypeCheckQueryNode, UnaryOperationQueryNode, UnaryOperator, VariableQueryNode } from '../../query-tree'; -import { createSafeObjectQueryNode } from '../../query/mutations'; -import { ENTITY_UPDATED_AT, ID_FIELD } from '../../schema/schema-defaults'; +import { ENTITY_UPDATED_AT, ID_FIELD } from '../../schema/constants'; import { AnyValue, decapitalize, flatMap, joinWithAnd, objectEntries, PlainObject } from '../../utils/utils'; import { TypedInputObjectType } from '../typed-input-object-type'; import { AddChildEntitiesInputField, UpdateChildEntitiesInputField, UpdateInputField } from './input-fields'; @@ -125,7 +124,7 @@ export class UpdateObjectInputType extends TypedInputObjectType - new BinaryOperationQueryNode(createScalarFieldValueNode(rootEntityType, fieldName, entityVarNode), BinaryOperator.EQUAL, new LiteralQueryNode(value))); + new BinaryOperationQueryNode(createFieldNode(rootEntityType.getFieldOrThrow(fieldName), entityVarNode), BinaryOperator.EQUAL, new LiteralQueryNode(value))); if (filterClauses.length != 1) { throw new Error(`Must specify exactly one argument to ${rootEntityType.toString()}`); // TODO throw this at the correct GraphQL query location } diff --git a/src/schema-generation/utils/filtering.ts b/src/schema-generation/utils/filtering.ts index 7da0a7b0..4a17737c 100644 --- a/src/schema-generation/utils/filtering.ts +++ b/src/schema-generation/utils/filtering.ts @@ -1,6 +1,6 @@ import { Type } from '../../model'; import { QueryNode, TransformListQueryNode, VariableQueryNode } from '../../query-tree'; -import { FILTER_ARG } from '../../schema/schema-defaults'; +import { FILTER_ARG } from '../../schema/constants'; import { decapitalize } from '../../utils/utils'; import { FilterObjectType } from '../filter-input-types'; diff --git a/src/schema-generation/utils/pagination.ts b/src/schema-generation/utils/pagination.ts index 19c3aac5..268dbbdc 100644 --- a/src/schema-generation/utils/pagination.ts +++ b/src/schema-generation/utils/pagination.ts @@ -1,5 +1,5 @@ import { OrderDirection } from '../../query-tree'; -import { ID_FIELD, ORDER_BY_ARG } from '../../schema/schema-defaults'; +import { ID_FIELD, ORDER_BY_ARG } from '../../schema/constants'; import { OrderByEnumType, OrderByEnumValue } from '../order-by-enum-generator'; export function getOrderByValues(args: any, orderByType: OrderByEnumType): ReadonlyArray { diff --git a/src/schema/schema-defaults.ts b/src/schema/constants.ts similarity index 84% rename from src/schema/schema-defaults.ts rename to src/schema/constants.ts index 4781216c..4462845b 100644 --- a/src/schema/schema-defaults.ts +++ b/src/schema/constants.ts @@ -1,17 +1,3 @@ -export const defaultModelDefTypes = ` - scalar DateTime - - type EntityMeta { - createdAt: DateTime! - updatedAt: DateTime! - createdBy: User - } - - type User @rootEntity { - name: String! - } - `; - export const WILDCARD_CHARACTER = '*'; export const ROOT_ENTITY_DIRECTIVE = 'rootEntity'; @@ -56,11 +42,6 @@ export const SCALAR_DATE = 'Date'; export const SCALAR_TIME = 'Time'; export const SCALAR_JSON = 'JSON'; -export const ENTITY_ID = 'id'; - -export const AND_FILTER_FIELD = 'AND'; -export const OR_FILTER_FIELD = 'OR'; - export const ORDER_BY_ASC_SUFFIX = '_ASC'; export const ORDER_BY_DESC_SUFFIX = '_DESC'; @@ -73,7 +54,7 @@ export const FIRST_ARG = 'first'; export const VALUE_ARG = 'value'; export const KEY_FIELD_DIRECTIVE = 'key'; -export const INPUT_FIELD_SEPARATOR = '_'; +export const FILTER_FIELD_PREFIX_SEPARATOR = '_'; export const INPUT_FIELD_EQUAL = 'equal'; export const INPUT_FIELD_NOT = 'not'; export const INPUT_FIELD_IN = 'in'; @@ -91,6 +72,8 @@ export const INPUT_FIELD_NOT_ENDS_WITH = 'not_ends_with'; export const INPUT_FIELD_SOME = 'some'; export const INPUT_FIELD_EVERY = 'every'; export const INPUT_FIELD_NONE = 'none'; +export const AND_FILTER_FIELD = 'AND'; +export const OR_FILTER_FIELD = 'OR'; export const CALC_MUTATIONS_DIRECTIVE = 'calcMutations'; export const CALC_MUTATIONS_OPERATORS_ARG = 'operators'; @@ -108,7 +91,6 @@ export const CALC_MUTATIONS_OPERATORS: CalcMutationOperator[] = [ export const COUNT_META_FIELD = 'count'; export const MUTATION_INPUT_ARG = 'input'; -export const MUTATION_ID_ARG = 'id'; export const ROLES_DIRECTIVE = 'roles'; export const ROLES_READ_ARG = 'read'; @@ -117,21 +99,17 @@ export const PERMISSION_PROFILE_ARG = 'permissionProfile'; export const DEFAULT_PERMISSION_PROFILE = 'default'; -export const OBJECT_TYPE_ENTITY_DIRECTIVES = [ROOT_ENTITY_DIRECTIVE, CHILD_ENTITY_DIRECTIVE, ENTITY_EXTENSION_DIRECTIVE, VALUE_OBJECT_DIRECTIVE]; +export const OBJECT_TYPE_KIND_DIRECTIVES = [ROOT_ENTITY_DIRECTIVE, CHILD_ENTITY_DIRECTIVE, ENTITY_EXTENSION_DIRECTIVE, VALUE_OBJECT_DIRECTIVE]; export const NAMESPACE_DIRECTIVE = 'namespace'; export const NAMESPACE_NAME_ARG = 'name'; export const NAMESPACE_SEPARATOR = '.'; -export const NAMESPACE_FIELD_PATH_DIRECTIVE = 'namespaceFieldPath'; export const INDICES_DIRECTIVE = 'indices'; export const INDEX_DIRECTIVE = 'index'; // for fields export const UNIQUE_DIRECTIVE = 'unique'; // for fields export const INDICES_ARG = INDICES_DIRECTIVE; export const INDEX_DEFINITION_INPUT_TYPE = 'IndexDefinition'; -export const INDEX_NAME_FIELD = 'name'; -export const INDEX_FIELDS_FIELD = 'fields'; -export const INDEX_UNIQUE_FIELD = 'unique'; export const ALL_FIELD_DIRECTIVES = [KEY_FIELD_DIRECTIVE, RELATION_DIRECTIVE, REFERENCE_DIRECTIVE, ROLES_DIRECTIVE, CALC_MUTATIONS_DIRECTIVE, DEFAULT_VALUE_DIRECTIVE, INDEX_DIRECTIVE, UNIQUE_DIRECTIVE]; export const ALL_OBJECT_TYPE_DIRECTIVES = [ @@ -143,13 +121,3 @@ export const ALL_OBJECT_TYPE_DIRECTIVES = [ ROLES_DIRECTIVE, INDICES_DIRECTIVE ]; - -export const MUTATION_FIELD = 'mutation'; - -export enum MutationType { - CREATE, - UPDATE, - UPDATE_ALL, - DELETE, - DELETE_ALL -} diff --git a/src/schema/directive-arg-flattener.ts b/src/schema/directive-arg-flattener.ts deleted file mode 100644 index e92f92d1..00000000 --- a/src/schema/directive-arg-flattener.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {AnyValue, PlainObject} from "../utils/utils"; -import {ValueNode, VariableNode} from "graphql"; -import {BOOLEAN, ENUM, FLOAT, INT, LIST, NULL, OBJECT, STRING, VARIABLE} from "../graphql/kinds"; - -export function flattenValueNode(valueNode: ValueNode): any { - switch (valueNode.kind) { - case STRING: - case INT: - case FLOAT: - case BOOLEAN: - case ENUM: - return valueNode.value; - case LIST: - return [...valueNode.values.map(value => flattenValueNode(value))]; - case OBJECT: - const obj: PlainObject = {}; - valueNode.fields.forEach(field => { - obj[field.name.value] = flattenValueNode(field.value); - }); - return obj; - default: - return undefined; - } -} diff --git a/src/schema/names.ts b/src/schema/names.ts new file mode 100644 index 00000000..93ad9445 --- /dev/null +++ b/src/schema/names.ts @@ -0,0 +1,76 @@ +import * as pluralize from 'pluralize'; +import { capitalize } from '../utils/utils'; +import { + ADD_CHILD_ENTITIES_FIELD_PREFIX, ADD_EDGES_FIELD_PREFIX, ALL_ENTITIES_FIELD_PREFIX, CREATE_ENTITY_FIELD_PREFIX, + DELETE_ALL_ENTITIES_FIELD_PREFIX, DELETE_ENTITY_FIELD_PREFIX, REMOVE_CHILD_ENTITIES_FIELD_PREFIX, + REMOVE_EDGES_FIELD_PREFIX, UPDATE_ALL_ENTITIES_FIELD_PREFIX, UPDATE_CHILD_ENTITIES_FIELD_PREFIX, + UPDATE_ENTITY_FIELD_PREFIX +} from './constants'; + +export function getAllEntitiesFieldName(entityName: string) { + return ALL_ENTITIES_FIELD_PREFIX + pluralize(entityName); +} + +export function getCreateEntityFieldName(entityName: string) { + return CREATE_ENTITY_FIELD_PREFIX + entityName; +} + +export function getUpdateEntityFieldName(entityName: string) { + return UPDATE_ENTITY_FIELD_PREFIX + entityName; +} + +export function getUpdateAllEntitiesFieldName(entityName: string) { + return UPDATE_ALL_ENTITIES_FIELD_PREFIX + pluralize(entityName); +} + +export function getDeleteEntityFieldName(entityName: string) { + return DELETE_ENTITY_FIELD_PREFIX + entityName; +} + +export function getDeleteAllEntitiesFieldName(entityName: string) { + return DELETE_ALL_ENTITIES_FIELD_PREFIX + pluralize(entityName); +} + +export function getMetaFieldName(field: string) { + return '_' + field + 'Meta'; +} + +export function getAddRelationFieldName(fieldName: string) { + return ADD_EDGES_FIELD_PREFIX + capitalize(fieldName); +} + +export function getRemoveRelationFieldName(fieldName: string) { + return REMOVE_EDGES_FIELD_PREFIX + capitalize(fieldName); +} + +export function getAddChildEntitiesFieldName(fieldName: string) { + return ADD_CHILD_ENTITIES_FIELD_PREFIX + capitalize(fieldName); +} + +export function getUpdateChildEntitiesFieldName(fieldName: string) { + return UPDATE_CHILD_ENTITIES_FIELD_PREFIX + capitalize(fieldName); +} + +export function getRemoveChildEntitiesFieldName(fieldName: string) { + return REMOVE_CHILD_ENTITIES_FIELD_PREFIX + capitalize(fieldName); +} + +export function getFilterTypeName(typeName: string) { + return `${typeName}Filter`; +} + +export function getOrderByTypeName(typeName: string) { + return `${typeName}OrderBy`; +} + +export function getCreateInputTypeName(typeName: string) { + return `Create${typeName}Input`; +} + +export function getUpdateInputTypeName(typeName: string) { + return `Update${typeName}Input`; +} + +export function getUpdateAllInputTypeName(typeName: string) { + return `UpdateAll${pluralize(typeName)}Input`; +} diff --git a/src/schema/preparation/ast-validation-modules/indices-validator.ts b/src/schema/preparation/ast-validation-modules/indices-validator.ts index 7d11d6ec..30b80b65 100644 --- a/src/schema/preparation/ast-validation-modules/indices-validator.ts +++ b/src/schema/preparation/ast-validation-modules/indices-validator.ts @@ -4,7 +4,7 @@ import { ValidationMessage } from '../../../model'; import { findDirectiveWithName, getChildEntityTypes, getEntityExtensionTypes, getValueObjectTypes } from '../../schema-utils'; -import { INDEX_DIRECTIVE, UNIQUE_DIRECTIVE } from '../../schema-defaults'; +import { INDEX_DIRECTIVE, UNIQUE_DIRECTIVE } from '../../constants'; export const VALIDATION_ERROR_INDICES_ONLY_ON_ROOT_ENTITIES = "Indices are only allowed in root entity fields. You can add indices to fields of embedded objects with @rootEntities(indices: [...])."; diff --git a/src/schema/preparation/ast-validation-modules/key-field-validator.ts b/src/schema/preparation/ast-validation-modules/key-field-validator.ts index 7447f043..5f1ba0f2 100644 --- a/src/schema/preparation/ast-validation-modules/key-field-validator.ts +++ b/src/schema/preparation/ast-validation-modules/key-field-validator.ts @@ -2,7 +2,7 @@ import { ASTValidator } from '../ast-validator'; import { DocumentNode } from 'graphql'; import { ValidationMessage } from '../../../model'; import { getObjectTypes } from '../../schema-utils'; -import { KEY_FIELD_DIRECTIVE, ROOT_ENTITY_DIRECTIVE } from '../../schema-defaults'; +import { KEY_FIELD_DIRECTIVE, ROOT_ENTITY_DIRECTIVE } from '../../constants'; export const VALIDATION_ERROR_INVALID_OBJECT_TYPE = "A @key field can only be declared on root entities."; diff --git a/src/schema/preparation/ast-validation-modules/no-unused-non-root-object-types-validator.ts b/src/schema/preparation/ast-validation-modules/no-unused-non-root-object-types-validator.ts index dd1c61f6..6223ac1c 100644 --- a/src/schema/preparation/ast-validation-modules/no-unused-non-root-object-types-validator.ts +++ b/src/schema/preparation/ast-validation-modules/no-unused-non-root-object-types-validator.ts @@ -4,7 +4,7 @@ import { ValidationMessage } from '../../../model'; import { getNamedTypeDefinitionAST, getObjectTypes, getRootEntityTypes, getTypeNameIgnoringNonNullAndList } from '../../schema-utils'; -import { OBJECT_TYPE_ENTITY_DIRECTIVES } from '../../schema-defaults'; +import { OBJECT_TYPE_KIND_DIRECTIVES } from '../../constants'; export const VALIDATION_WARNING_UNUSED_OBJECT_TYPE = "Unused object type."; @@ -24,7 +24,7 @@ export class NoUnusedNonRootObjectTypesValidator implements ASTValidator { VALIDATION_WARNING_UNUSED_OBJECT_TYPE, { entityKind: unusedType.directives!.find( - directive => OBJECT_TYPE_ENTITY_DIRECTIVES.includes(directive.name.value))!.name.value + directive => OBJECT_TYPE_KIND_DIRECTIVES.includes(directive.name.value))!.name.value }, unusedType.loc) ); diff --git a/src/schema/preparation/ast-validation-modules/roles-on-non-root-entity-types.ts b/src/schema/preparation/ast-validation-modules/roles-on-non-root-entity-types.ts index 0424c4f5..69e5dc36 100644 --- a/src/schema/preparation/ast-validation-modules/roles-on-non-root-entity-types.ts +++ b/src/schema/preparation/ast-validation-modules/roles-on-non-root-entity-types.ts @@ -2,7 +2,7 @@ import { ASTValidator } from '../ast-validator'; import { DocumentNode } from 'graphql'; import { ValidationMessage } from '../../../model'; import { findDirectiveWithName, getObjectTypes } from '../../schema-utils'; -import { ROLES_DIRECTIVE, ROOT_ENTITY_DIRECTIVE } from '../../schema-defaults'; +import { ROLES_DIRECTIVE, ROOT_ENTITY_DIRECTIVE } from '../../constants'; export const VALIDATION_ERROR_ROLES_ON_NON_ROOT_ENTITY_TYPE = '@roles is only allowed on fields and on root entity types.'; diff --git a/src/schema/preparation/post-merge-ast-transformation-modules/add-create-entity-input-types-transformer.ts b/src/schema/preparation/post-merge-ast-transformation-modules/add-create-entity-input-types-transformer.ts deleted file mode 100644 index 3a5366e7..00000000 --- a/src/schema/preparation/post-merge-ast-transformation-modules/add-create-entity-input-types-transformer.ts +++ /dev/null @@ -1,87 +0,0 @@ -import {ASTTransformer} from '../transformation-pipeline'; -import { - DocumentNode, - FieldDefinitionNode, - GraphQLID, - InputObjectTypeDefinitionNode, - InputValueDefinitionNode, - ObjectTypeDefinitionNode, - TypeNode -} from 'graphql'; -import { - findDirectiveWithName, - getChildEntityTypes, - getNamedTypeDefinitionAST, - getRootEntityTypes, - hasDirectiveWithName -} from '../../schema-utils'; -import { - INPUT_OBJECT_TYPE_DEFINITION, - LIST_TYPE, - NAMED_TYPE, - NON_NULL_TYPE, - OBJECT_TYPE_DEFINITION -} from '../../../graphql/kinds'; -import {getCreateInputTypeName} from '../../../graphql/names'; -import { - ENTITY_CREATED_AT, ENTITY_UPDATED_AT, ID_FIELD, RELATION_DIRECTIVE, ROLES_DIRECTIVE -} from '../../schema-defaults'; -import { - buildInputFieldFromNonListField, - buildInputValueListNodeFromField -} from './add-input-type-transformation-helper-transformer'; -import { compact } from '../../../utils/utils'; - -export class AddCreateEntityInputTypesTransformer implements ASTTransformer { - - transform(ast: DocumentNode): void { - getRootEntityTypes(ast).forEach(objectType => { - ast.definitions.push(this.createCreateInputTypeForObjectType(ast, objectType)); - }); - getChildEntityTypes(ast).forEach(objectType => { - ast.definitions.push(this.createCreateInputTypeForObjectType(ast, objectType)); - }); - } - - protected createCreateInputTypeForObjectType(ast: DocumentNode, objectType: ObjectTypeDefinitionNode): InputObjectTypeDefinitionNode { - // create input fields for all entity fields except ID, createdAt, updatedAt - const skip = [ID_FIELD, ENTITY_CREATED_AT, ENTITY_UPDATED_AT]; - const args = [ - ...objectType.fields.filter(field => !skip.includes(field.name.value)).map(field => this.createInputTypeField(ast, field, field.type)) - ]; - return { - kind: INPUT_OBJECT_TYPE_DEFINITION, - name: { kind: "Name", value: getCreateInputTypeName(objectType) }, - fields: args, - loc: objectType.loc, - directives: compact([ findDirectiveWithName(objectType, ROLES_DIRECTIVE) ]) - } - } - - protected createInputTypeField(ast: DocumentNode, field: FieldDefinitionNode, type: TypeNode): InputValueDefinitionNode { - switch (type.kind) { - case NON_NULL_TYPE: - return this.createInputTypeField(ast, field, type.type); - case NAMED_TYPE: - return buildInputFieldFromNonListField(ast, field, type); - case LIST_TYPE: - const effectiveType = type.type.kind === NON_NULL_TYPE ? type.type.type : type.type; - if (effectiveType.kind === LIST_TYPE) { - throw new Error('Lists of lists are not allowed.'); - } - const namedTypeOfList = getNamedTypeDefinitionAST(ast, effectiveType.name.value); - if (namedTypeOfList.kind === OBJECT_TYPE_DEFINITION) { - // relations are referenced via IDs - if (hasDirectiveWithName(field, RELATION_DIRECTIVE)) { - return buildInputValueListNodeFromField(field.name.value, GraphQLID.name, field) - } - return buildInputValueListNodeFromField(field.name.value, getCreateInputTypeName(namedTypeOfList), field) - } else { - return buildInputValueListNodeFromField(field.name.value, effectiveType.name.value, field); - } - } - } - -} - - diff --git a/src/schema/preparation/post-merge-ast-transformation-modules/add-cursor-field-to-entities-transformer.ts b/src/schema/preparation/post-merge-ast-transformation-modules/add-cursor-field-to-entities-transformer.ts deleted file mode 100644 index 10c1a387..00000000 --- a/src/schema/preparation/post-merge-ast-transformation-modules/add-cursor-field-to-entities-transformer.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {ASTTransformer} from "../transformation-pipeline"; -import {DocumentNode, FieldDefinitionNode} from "graphql"; -import {buildNameNode, getChildEntityTypes, getRootEntityTypes} from "../../schema-utils"; -import {FIELD_DEFINITION, NAMED_TYPE} from "../../../graphql/kinds"; -import {CURSOR_FIELD} from "../../schema-defaults"; - -export class AddCursorFieldToEntitiesTransformer implements ASTTransformer { - - transform(ast: DocumentNode): void { - - const cursorFieldDefinition : FieldDefinitionNode = { - kind: FIELD_DEFINITION, - type: { - kind: NAMED_TYPE, - name: buildNameNode('String') - }, - name: buildNameNode(CURSOR_FIELD), - arguments: [] - }; - - getRootEntityTypes(ast).forEach(rootEntityType => { - rootEntityType.fields.push(cursorFieldDefinition) - }); - getChildEntityTypes(ast).forEach(childEntityType => { - childEntityType.fields.push(cursorFieldDefinition) - }); - - } - -} \ No newline at end of file diff --git a/src/schema/preparation/post-merge-ast-transformation-modules/add-extension-input-types-transformer.ts b/src/schema/preparation/post-merge-ast-transformation-modules/add-extension-input-types-transformer.ts deleted file mode 100644 index 7d4c248c..00000000 --- a/src/schema/preparation/post-merge-ast-transformation-modules/add-extension-input-types-transformer.ts +++ /dev/null @@ -1,78 +0,0 @@ -import {ASTTransformer} from "../transformation-pipeline"; -import { - DocumentNode, - FieldDefinitionNode, - GraphQLID, - InputObjectTypeDefinitionNode, - InputValueDefinitionNode, - ObjectTypeDefinitionNode, - TypeNode -} from "graphql"; -import { - findDirectiveWithName, getEntityExtensionTypes, getNamedTypeDefinitionAST, hasDirectiveWithName -} from '../../schema-utils'; -import { - INPUT_OBJECT_TYPE_DEFINITION, - LIST_TYPE, - NAMED_TYPE, - NON_NULL_TYPE, - OBJECT_TYPE_DEFINITION -} from "../../../graphql/kinds"; -import {getCreateInputTypeName} from "../../../graphql/names"; -import { ROLES_DIRECTIVE, ROOT_ENTITY_DIRECTIVE } from '../../schema-defaults'; -import { - buildInputFieldFromNonListField, - buildInputValueListNodeFromField -} from './add-input-type-transformation-helper-transformer'; -import { compact } from '../../../utils/utils'; - -export class AddExtensionInputTypesTransformer implements ASTTransformer { - - transform(ast: DocumentNode): void { - getEntityExtensionTypes(ast).forEach(objectType => { - ast.definitions.push(this.createCreateInputTypeForObjectType(ast, objectType)); - }); - } - - protected createCreateInputTypeForObjectType(ast: DocumentNode, objectType: ObjectTypeDefinitionNode): InputObjectTypeDefinitionNode { - // create input fields for all entity fields except ID, createdAt, updatedAt - const args = [ - ...objectType.fields.map(field => this.createInputTypeField(ast, field, field.type)) - ]; - return { - kind: INPUT_OBJECT_TYPE_DEFINITION, - name: { kind: "Name", value: getCreateInputTypeName(objectType) }, - fields: args, - loc: objectType.loc, - directives: compact([ findDirectiveWithName(objectType, ROLES_DIRECTIVE) ]) - } - } - - // undefined currently means not supported. - protected createInputTypeField(ast: DocumentNode, field: FieldDefinitionNode, type: TypeNode): InputValueDefinitionNode { - switch (type.kind) { - case NON_NULL_TYPE: - return this.createInputTypeField(ast, field, type.type); - case NAMED_TYPE: - return buildInputFieldFromNonListField(ast, field, type); - case LIST_TYPE: - const effectiveType = type.type.kind === NON_NULL_TYPE ? type.type.type : type.type; - if (effectiveType.kind === LIST_TYPE) { - throw new Error('Lists of lists are not allowed.'); - } - const namedTypeOfList = getNamedTypeDefinitionAST(ast, effectiveType.name.value); - switch (namedTypeOfList.kind) { - case OBJECT_TYPE_DEFINITION: - if (hasDirectiveWithName(namedTypeOfList, ROOT_ENTITY_DIRECTIVE)) { - // foreign key - return buildInputValueListNodeFromField(field.name.value, GraphQLID.name, field); - } else { - return buildInputValueListNodeFromField(field.name.value, getCreateInputTypeName(namedTypeOfList), field) - } - default: - return buildInputValueListNodeFromField(field.name.value, effectiveType.name.value, field); - } - } - } - -} \ No newline at end of file diff --git a/src/schema/preparation/post-merge-ast-transformation-modules/add-filter-arguments-to-fields-transformer.ts b/src/schema/preparation/post-merge-ast-transformation-modules/add-filter-arguments-to-fields-transformer.ts deleted file mode 100644 index d4804cc4..00000000 --- a/src/schema/preparation/post-merge-ast-transformation-modules/add-filter-arguments-to-fields-transformer.ts +++ /dev/null @@ -1,42 +0,0 @@ -import {ASTTransformer} from "../transformation-pipeline"; -import {DocumentNode} from "graphql"; -import { - buildNameNode, - getNamedInputTypeDefinitionAST, - getNamedTypeDefinitionAST, - getObjectTypes, - getTypeNameIgnoringNonNullAndList -} from "../../schema-utils"; -import { - INPUT_VALUE_DEFINITION, - LIST_TYPE, - NAMED_TYPE, - NON_NULL_TYPE, - OBJECT_TYPE_DEFINITION -} from "../../../graphql/kinds"; -import {getFilterTypeName} from "../../../graphql/names"; -import {FILTER_ARG} from "../../schema-defaults"; - -export class AddFilterArgumentsToFieldsTransformer implements ASTTransformer { - - transform(ast: DocumentNode): void { - getObjectTypes(ast).forEach(objectType => { - objectType.fields.forEach(field => { - // Only lists have query args - if (field.type.kind !== LIST_TYPE && !(field.type.kind === NON_NULL_TYPE && field.type.type.kind === LIST_TYPE)) { - return; - } - const resolvedType = getNamedTypeDefinitionAST(ast, getTypeNameIgnoringNonNullAndList(field.type)); - // if this field is of an object type and this object type has a *Filter type - if (resolvedType.kind === OBJECT_TYPE_DEFINITION && getNamedInputTypeDefinitionAST(ast, getFilterTypeName(resolvedType))) { - field.arguments.push({ - kind: INPUT_VALUE_DEFINITION, - name: buildNameNode(FILTER_ARG), - type: { kind: NAMED_TYPE, name: buildNameNode(getFilterTypeName(resolvedType))} - }) - } - }) - }) - } - -} \ No newline at end of file diff --git a/src/schema/preparation/post-merge-ast-transformation-modules/add-filter-input-types-transformer.ts b/src/schema/preparation/post-merge-ast-transformation-modules/add-filter-input-types-transformer.ts deleted file mode 100644 index de97629b..00000000 --- a/src/schema/preparation/post-merge-ast-transformation-modules/add-filter-input-types-transformer.ts +++ /dev/null @@ -1,357 +0,0 @@ -import {ASTTransformer} from "../transformation-pipeline"; -import { - DirectiveNode, - DocumentNode, - FieldDefinitionNode, GraphQLBoolean, GraphQLFloat, GraphQLInt, GraphQLString, - InputObjectTypeDefinitionNode, - InputValueDefinitionNode, - ObjectTypeDefinitionNode, - TypeNode -} from 'graphql'; -import { - buildNameNode, - findDirectiveWithName, getEnumTypes, - getNamedTypeDefinitionAST, - getObjectTypes, - getTypeNameIgnoringNonNullAndList, hasDirectiveWithName -} from '../../schema-utils'; -import { - ENUM_TYPE_DEFINITION, - INPUT_OBJECT_TYPE_DEFINITION, - LIST_TYPE, - NAME, - NAMED_TYPE, - NON_NULL_TYPE, - OBJECT_TYPE_DEFINITION, - SCALAR_TYPE_DEFINITION -} from "../../../graphql/kinds"; -import { - containsField, - endsWithField, - everyField, - getFilterTypeName, - gteField, - gtField, - inField, - lteField, - ltField, - noneField, - notContainsField, - notEndsWithField, - notField, - notInField, - notStartsWithField, - someField, - startsWithField -} from "../../../graphql/names"; -import {compact, flatMap} from '../../../utils/utils'; -import { - AND_FILTER_FIELD, - OR_FILTER_FIELD, - INPUT_FIELD_CONTAINS, - INPUT_FIELD_ENDS_WITH, - INPUT_FIELD_EQUAL, - INPUT_FIELD_GT, - INPUT_FIELD_GTE, - INPUT_FIELD_IN, - INPUT_FIELD_LT, - INPUT_FIELD_LTE, - INPUT_FIELD_NOT, - INPUT_FIELD_NOT_CONTAINS, - INPUT_FIELD_NOT_ENDS_WITH, - INPUT_FIELD_NOT_IN, - INPUT_FIELD_NOT_STARTS_WITH, - INPUT_FIELD_STARTS_WITH, - ROLES_DIRECTIVE, - SCALAR_DATE, - SCALAR_DATETIME, SCALAR_JSON, - SCALAR_TIME -} from "../../schema-defaults"; - -export class AddFilterInputTypesTransformer implements ASTTransformer { - - transform(ast: DocumentNode): void { - ast.definitions.push( - this.createStringFilterType(), - this.createIntFilterType(), - this.createFloatFilterType(), - this.createBooleanFilterType(), - this.createDateFilterType(), - this.createTimeFilterType(), - this.createDateTimeFilterType(), - this.createJSONFilterType(), - ...this.createEnumTypeFilters(ast) - ); - getObjectTypes(ast).forEach(objectType => { - ast.definitions.push(this.createInputFilterTypeForObjectType(ast, objectType)) - }) - } - - protected createEnumTypeFilters(ast: DocumentNode): InputObjectTypeDefinitionNode[] { - return getEnumTypes(ast).map(enumType => { - return { - kind: INPUT_OBJECT_TYPE_DEFINITION, - name: buildNameNode(getFilterTypeName(enumType)), - fields: [ - this.buildNamedTypeFieldForField(INPUT_FIELD_EQUAL, enumType.name.value), - this.buildNamedTypeFieldForField(INPUT_FIELD_NOT, enumType.name.value), - this.buildListOfNamedTypeFieldForField(INPUT_FIELD_IN, enumType.name.value), - this.buildListOfNamedTypeFieldForField(INPUT_FIELD_NOT_IN, enumType.name.value), - ] - } - }) - } - - protected createInputFilterTypeForObjectType(ast: DocumentNode, objectType: ObjectTypeDefinitionNode): InputObjectTypeDefinitionNode { - const args = [ - ...flatMap(objectType.fields, field => this.createInputFilterTypeFields(ast, field, field.type)), - this.buildListOfNamedTypeField(AND_FILTER_FIELD, getFilterTypeName(objectType)), - this.buildListOfNamedTypeField(OR_FILTER_FIELD, getFilterTypeName(objectType)), - // TODO add if supported: this.buildInputValueNamedType(ARGUMENT_NOT, getFilterTypeName(objectType)) - ]; - return { - kind: INPUT_OBJECT_TYPE_DEFINITION, - name: {kind: "Name", value: getFilterTypeName(objectType)}, - fields: args, - loc: objectType.loc, - directives: compact([ findDirectiveWithName(objectType, ROLES_DIRECTIVE) ]) - } - } - - // undefined currently means not supported. - protected createInputFilterTypeFields(ast: DocumentNode, field: FieldDefinitionNode, type: TypeNode): InputValueDefinitionNode[] { - const name = field.name.value; - switch (type.kind) { - case NON_NULL_TYPE: - return this.createInputFilterTypeFields(ast, field, type.type); - case LIST_TYPE: - const innerListType = getNamedTypeDefinitionAST(ast, getTypeNameIgnoringNonNullAndList(type.type)); - return [ - this.buildNamedTypeFieldForField(someField(name), getFilterTypeName(innerListType), field), - this.buildNamedTypeFieldForField(everyField(name), getFilterTypeName(innerListType), field), - this.buildNamedTypeFieldForField(noneField(name), getFilterTypeName(innerListType), field), - ]; - case NAMED_TYPE: - // get definition for named type - const namedTypeDefinition = getNamedTypeDefinitionAST(ast, type.name.value); - switch (namedTypeDefinition.kind) { - case SCALAR_TYPE_DEFINITION: - switch (namedTypeDefinition.name.value) { - case GraphQLString.name: - return [ - this.buildNamedTypeFieldForField(name, GraphQLString.name, field), - this.buildNamedTypeFieldForField(notField(name), GraphQLString.name, field), - this.buildListOfNamedTypeFieldForField(inField(name), GraphQLString.name, field), - this.buildListOfNamedTypeFieldForField(notInField(name), GraphQLString.name, field), - this.buildNamedTypeFieldForField(ltField(name), GraphQLString.name, field), - this.buildNamedTypeFieldForField(lteField(name), GraphQLString.name, field), - this.buildNamedTypeFieldForField(gtField(name), GraphQLString.name, field), - this.buildNamedTypeFieldForField(gteField(name), GraphQLString.name, field), - this.buildNamedTypeFieldForField(containsField(name), GraphQLString.name, field), - this.buildNamedTypeFieldForField(notContainsField(name), GraphQLString.name, field), - this.buildNamedTypeFieldForField(startsWithField(name), GraphQLString.name, field), - this.buildNamedTypeFieldForField(notStartsWithField(name), GraphQLString.name, field), - this.buildNamedTypeFieldForField(endsWithField(name), GraphQLString.name, field), - this.buildNamedTypeFieldForField(notEndsWithField(name), GraphQLString.name, field), - ]; - case SCALAR_TIME: - case SCALAR_DATE: - case SCALAR_DATETIME: - case GraphQLInt.name: // TODO: should't id have a reduced set? gt, lt, do they really make sense on ids? - case 'Float': - case 'ID': - return [ - this.buildNamedTypeFieldForField(name, namedTypeDefinition.name.value, field), - this.buildNamedTypeFieldForField(notField(name), namedTypeDefinition.name.value, field), - this.buildListOfNamedTypeFieldForField(inField(name), namedTypeDefinition.name.value, field), - this.buildListOfNamedTypeFieldForField(notInField(name), namedTypeDefinition.name.value, field), - this.buildNamedTypeFieldForField(ltField(name), namedTypeDefinition.name.value, field), - this.buildNamedTypeFieldForField(lteField(name), namedTypeDefinition.name.value, field), - this.buildNamedTypeFieldForField(gtField(name), namedTypeDefinition.name.value, field), - this.buildNamedTypeFieldForField(gteField(name), namedTypeDefinition.name.value, field), - ]; - case 'Boolean': - return [ - this.buildNamedTypeFieldForField(name, namedTypeDefinition.name.value, field), - ]; - default: - return []; - } - case ENUM_TYPE_DEFINITION: - return [ - this.buildNamedTypeFieldForField(name, namedTypeDefinition.name.value, field), - this.buildNamedTypeFieldForField(notField(name), namedTypeDefinition.name.value, field), - this.buildListOfNamedTypeFieldForField(inField(name), namedTypeDefinition.name.value, field), - this.buildListOfNamedTypeFieldForField(notInField(name), namedTypeDefinition.name.value, field), - ]; - case OBJECT_TYPE_DEFINITION: - // use the embedded object filter - return [this.buildNamedTypeFieldForField(name, getFilterTypeName(namedTypeDefinition), field)]; - default: - return [] - } - } - } - - protected buildNamedTypeFieldForField(name: string, typeName: string, sourceField?: FieldDefinitionNode): InputValueDefinitionNode { - const directives = sourceField ? this.getDirectivesForField(sourceField) : []; - return { - kind: "InputValueDefinition", - type: {kind: NAMED_TYPE, name: {kind: NAME, value: typeName}}, - name: {kind: NAME, value: name}, - directives: directives - } - } - - protected buildListOfNamedTypeField(name: string, innerListTypeName: string, directives?: DirectiveNode[]): InputValueDefinitionNode { - return { - kind: "InputValueDefinition", - type: { - kind: LIST_TYPE, - type: {kind: NON_NULL_TYPE, type: {kind: NAMED_TYPE, name: {kind: NAME, value: innerListTypeName}}} - }, - name: {kind: NAME, value: name}, - directives - } - } - - protected buildListOfNamedTypeFieldForField(name: string, typeName: string, sourceField?: FieldDefinitionNode): InputValueDefinitionNode { - const directives = sourceField ? this.getDirectivesForField(sourceField) : []; - return this.buildListOfNamedTypeField(name, typeName, directives); - } - - protected getDirectivesForField(field: FieldDefinitionNode) { - const directiveNames = [ROLES_DIRECTIVE]; - return compact(directiveNames.map(name => findDirectiveWithName(field, name))); - } - - protected createStringFilterType(): InputObjectTypeDefinitionNode { - return { - kind: INPUT_OBJECT_TYPE_DEFINITION, - name: buildNameNode(getFilterTypeName(GraphQLString.name)), - fields: [ - this.buildNamedTypeFieldForField(INPUT_FIELD_EQUAL, GraphQLString.name), - this.buildNamedTypeFieldForField(INPUT_FIELD_NOT, GraphQLString.name), - this.buildListOfNamedTypeFieldForField(INPUT_FIELD_IN, GraphQLString.name), - this.buildListOfNamedTypeFieldForField(INPUT_FIELD_NOT_IN, GraphQLString.name), - this.buildNamedTypeFieldForField(INPUT_FIELD_LT, GraphQLString.name), - this.buildNamedTypeFieldForField(INPUT_FIELD_LTE, GraphQLString.name), - this.buildNamedTypeFieldForField(INPUT_FIELD_GT, GraphQLString.name), - this.buildNamedTypeFieldForField(INPUT_FIELD_GTE, GraphQLString.name), - this.buildNamedTypeFieldForField(INPUT_FIELD_CONTAINS, GraphQLString.name), - this.buildNamedTypeFieldForField(INPUT_FIELD_NOT_CONTAINS, GraphQLString.name), - this.buildNamedTypeFieldForField(INPUT_FIELD_STARTS_WITH, GraphQLString.name), - this.buildNamedTypeFieldForField(INPUT_FIELD_NOT_STARTS_WITH, GraphQLString.name), - this.buildNamedTypeFieldForField(INPUT_FIELD_ENDS_WITH, GraphQLString.name), - this.buildNamedTypeFieldForField(INPUT_FIELD_NOT_ENDS_WITH, GraphQLString.name), - ] - } - } - - protected createIntFilterType(): InputObjectTypeDefinitionNode { - return { - kind: INPUT_OBJECT_TYPE_DEFINITION, - name: buildNameNode(getFilterTypeName(GraphQLInt.name)), - fields: [ - this.buildNamedTypeFieldForField(INPUT_FIELD_EQUAL, GraphQLInt.name), - this.buildNamedTypeFieldForField(INPUT_FIELD_NOT, GraphQLInt.name), - this.buildListOfNamedTypeFieldForField(INPUT_FIELD_IN, GraphQLInt.name), - this.buildListOfNamedTypeFieldForField(INPUT_FIELD_NOT_IN, GraphQLInt.name), - this.buildNamedTypeFieldForField(INPUT_FIELD_LT, GraphQLInt.name), - this.buildNamedTypeFieldForField(INPUT_FIELD_LTE, GraphQLInt.name), - this.buildNamedTypeFieldForField(INPUT_FIELD_GT, GraphQLInt.name), - this.buildNamedTypeFieldForField(INPUT_FIELD_GTE, GraphQLInt.name), - ] - } - } - - protected createDateFilterType(): InputObjectTypeDefinitionNode { - return { - kind: INPUT_OBJECT_TYPE_DEFINITION, - name: buildNameNode(getFilterTypeName(SCALAR_DATE)), - fields: [ - this.buildNamedTypeFieldForField(INPUT_FIELD_EQUAL, SCALAR_DATE), - this.buildNamedTypeFieldForField(INPUT_FIELD_NOT, SCALAR_DATE), - this.buildListOfNamedTypeFieldForField(INPUT_FIELD_IN, SCALAR_DATE), - this.buildListOfNamedTypeFieldForField(INPUT_FIELD_NOT_IN, SCALAR_DATE), - this.buildNamedTypeFieldForField(INPUT_FIELD_LT, SCALAR_DATE), - this.buildNamedTypeFieldForField(INPUT_FIELD_LTE, SCALAR_DATE), - this.buildNamedTypeFieldForField(INPUT_FIELD_GT, SCALAR_DATE), - this.buildNamedTypeFieldForField(INPUT_FIELD_GTE, SCALAR_DATE), - ] - } - } - - protected createTimeFilterType(): InputObjectTypeDefinitionNode { - return { - kind: INPUT_OBJECT_TYPE_DEFINITION, - name: buildNameNode(getFilterTypeName(SCALAR_TIME)), - fields: [ - this.buildNamedTypeFieldForField(INPUT_FIELD_EQUAL, SCALAR_TIME), - this.buildNamedTypeFieldForField(INPUT_FIELD_NOT, SCALAR_TIME), - this.buildListOfNamedTypeFieldForField(INPUT_FIELD_IN, SCALAR_TIME), - this.buildListOfNamedTypeFieldForField(INPUT_FIELD_NOT_IN, SCALAR_TIME), - this.buildNamedTypeFieldForField(INPUT_FIELD_LT, SCALAR_TIME), - this.buildNamedTypeFieldForField(INPUT_FIELD_LTE, SCALAR_TIME), - this.buildNamedTypeFieldForField(INPUT_FIELD_GT, SCALAR_TIME), - this.buildNamedTypeFieldForField(INPUT_FIELD_GTE, SCALAR_TIME), - ] - } - } - - protected createDateTimeFilterType(): InputObjectTypeDefinitionNode { - return { - kind: INPUT_OBJECT_TYPE_DEFINITION, - name: buildNameNode(getFilterTypeName(SCALAR_DATETIME)), - fields: [ - this.buildNamedTypeFieldForField(INPUT_FIELD_EQUAL, SCALAR_DATETIME), - this.buildNamedTypeFieldForField(INPUT_FIELD_NOT, SCALAR_DATETIME), - this.buildListOfNamedTypeFieldForField(INPUT_FIELD_IN, SCALAR_DATETIME), - this.buildListOfNamedTypeFieldForField(INPUT_FIELD_NOT_IN, SCALAR_DATETIME), - this.buildNamedTypeFieldForField(INPUT_FIELD_LT, SCALAR_DATETIME), - this.buildNamedTypeFieldForField(INPUT_FIELD_LTE, SCALAR_DATETIME), - this.buildNamedTypeFieldForField(INPUT_FIELD_GT, SCALAR_DATETIME), - this.buildNamedTypeFieldForField(INPUT_FIELD_GTE, SCALAR_DATETIME), - ] - } - } - - protected createFloatFilterType(): InputObjectTypeDefinitionNode { - return { - kind: INPUT_OBJECT_TYPE_DEFINITION, - name: buildNameNode(getFilterTypeName(GraphQLFloat.name)), - fields: [ - this.buildNamedTypeFieldForField(INPUT_FIELD_EQUAL, GraphQLFloat.name), - this.buildNamedTypeFieldForField(INPUT_FIELD_NOT, GraphQLFloat.name), - this.buildListOfNamedTypeFieldForField(INPUT_FIELD_IN, GraphQLFloat.name), - this.buildListOfNamedTypeFieldForField(INPUT_FIELD_NOT_IN, GraphQLFloat.name), - this.buildNamedTypeFieldForField(INPUT_FIELD_LT, GraphQLFloat.name), - this.buildNamedTypeFieldForField(INPUT_FIELD_LTE, GraphQLFloat.name), - this.buildNamedTypeFieldForField(INPUT_FIELD_GT, GraphQLFloat.name), - this.buildNamedTypeFieldForField(INPUT_FIELD_GTE, GraphQLFloat.name), - ] - } - } - - protected createBooleanFilterType(): InputObjectTypeDefinitionNode { - return { - kind: INPUT_OBJECT_TYPE_DEFINITION, - name: buildNameNode(getFilterTypeName(GraphQLBoolean.name)), - fields: [ - this.buildNamedTypeFieldForField(INPUT_FIELD_EQUAL, GraphQLBoolean.name), - ] - } - } - - protected createJSONFilterType(): InputObjectTypeDefinitionNode { - return { - kind: INPUT_OBJECT_TYPE_DEFINITION, - name: buildNameNode(getFilterTypeName(SCALAR_JSON)), - fields: [ - this.buildNamedTypeFieldForField(INPUT_FIELD_EQUAL, SCALAR_JSON), - this.buildNamedTypeFieldForField(INPUT_FIELD_NOT, SCALAR_JSON), - ] - } - } - -} \ No newline at end of file diff --git a/src/schema/preparation/post-merge-ast-transformation-modules/add-input-type-transformation-helper-transformer.ts b/src/schema/preparation/post-merge-ast-transformation-modules/add-input-type-transformation-helper-transformer.ts deleted file mode 100644 index 02dd04c0..00000000 --- a/src/schema/preparation/post-merge-ast-transformation-modules/add-input-type-transformation-helper-transformer.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { - INPUT_VALUE_DEFINITION, - LIST_TYPE, - NAMED_TYPE, - NON_NULL_TYPE, - OBJECT_TYPE_DEFINITION -} from '../../../graphql/kinds'; -import { - buildNameNode, - findDirectiveWithName, getCalcMutationOperatorsFromDirective, - getNamedTypeDefinitionAST, - getReferenceKeyField, - getRoleListFromDirective, - hasDirectiveWithName -} from '../../schema-utils'; -import { - ArgumentNode, - DirectiveNode, - DocumentNode, - FieldDefinitionNode, - GraphQLID, - InputValueDefinitionNode, - ListValueNode, - Location, - NamedTypeNode, - StringValueNode -} from 'graphql'; -import { - CALC_MUTATIONS_DIRECTIVE, - ID_FIELD, - REFERENCE_DIRECTIVE, - RELATION_DIRECTIVE, - ROLES_DIRECTIVE, - ROLES_READ_ARG, - ROLES_READ_WRITE_ARG -} from '../../schema-defaults'; -import {compact} from '../../../utils/utils'; -import {getInputTypeName} from '../../../graphql/names'; -import {intersection} from 'lodash'; - -export function buildInputValueNode(name: string, namedTypeName: string, loc?: Location, directives?: DirectiveNode[]): InputValueDefinitionNode { - return { - kind: INPUT_VALUE_DEFINITION, - type: { - kind: NAMED_TYPE, - name: buildNameNode(namedTypeName) - }, - name: buildNameNode(name), - loc: loc, - directives - }; -} - -export function buildInputValueNodeFromField(name: string, namedTypeName: string, sourceField: FieldDefinitionNode): InputValueDefinitionNode { - const directiveNames = [ROLES_DIRECTIVE]; - const directives = compact(directiveNames.map(name => findDirectiveWithName(sourceField, name))); - return buildInputValueNode(name, namedTypeName, sourceField.loc, directives.length ? directives : undefined); -} - -export function buildInputValueListNode(name: string, namedTypeName: string, loc?: Location, directives?: DirectiveNode[]): InputValueDefinitionNode { - return { - kind: INPUT_VALUE_DEFINITION, - type: { - kind: LIST_TYPE, - type: { - kind: NON_NULL_TYPE, - type: { - kind: NAMED_TYPE, - name: buildNameNode(namedTypeName) - } - } - }, - name: buildNameNode(name), - loc: loc - }; -} - -export function buildInputValueListNodeFromField(name: string, namedTypeName: string, sourceField: FieldDefinitionNode): InputValueDefinitionNode { - const directiveNames = [ROLES_DIRECTIVE]; - const directives = compact(directiveNames.map(name => findDirectiveWithName(sourceField, name))); - return buildInputValueListNode(name, namedTypeName, sourceField.loc, directives.length ? directives : undefined); -} - -export function buildInputValueNodeID(): InputValueDefinitionNode { - return { - kind: INPUT_VALUE_DEFINITION, - type: { - kind: NON_NULL_TYPE, - type: { - kind: NAMED_TYPE, - name: buildNameNode(GraphQLID.name) - } - }, - name: buildNameNode(ID_FIELD) - }; -} - -/** - * Creates a new @roles() directive which is equivalent to a list of directive nodes combined - * (getting more and more restrictive) - */ -export function intersectRolesDirectives(directiveNodes: DirectiveNode[]): DirectiveNode|undefined { - if (!directiveNodes.length) { - // no restriction - return undefined; - } - - function getArg(argName: string): ArgumentNode { - const roleSets = directiveNodes.map(directive => getRoleListFromDirective(directive, argName)); - const roles = intersection(...roleSets); - const argValue: ListValueNode = { - kind: 'ListValue', - values: roles.map((roleName: string): StringValueNode => ({ - kind: 'StringValue', - value: roleName - })) - }; - return { - kind: 'Argument', - name: { kind: 'Name', value: argName }, - value: argValue - }; - } - - return { - kind: 'Directive', - name: { kind: 'Name', value: ROLES_DIRECTIVE }, - arguments: [ - getArg(ROLES_READ_ARG), - getArg(ROLES_READ_WRITE_ARG) - ] - }; -} - -export function buildInputFieldFromNonListField(ast: DocumentNode, field: FieldDefinitionNode, namedType: NamedTypeNode ) { - const typeDefinition = getNamedTypeDefinitionAST(ast, namedType.name.value); - if (typeDefinition.kind === OBJECT_TYPE_DEFINITION) { - if (hasDirectiveWithName(field, REFERENCE_DIRECTIVE)) { - const keyType = getReferenceKeyField(typeDefinition); - return buildInputValueNodeFromField(field.name.value, keyType, field); - } - if (hasDirectiveWithName(field, RELATION_DIRECTIVE)) { - return buildInputValueNodeFromField(field.name.value, GraphQLID.name, field); - } - - // we excluded lists, relations and references -> no child entities, no root entities - // -> only extensions and value objects -> we don't need to distinguish between create and update - return buildInputValueNodeFromField(field.name.value, getInputTypeName(typeDefinition), field); - } else { - // scalars - return buildInputValueNodeFromField(field.name.value, namedType.name.value, field); - } -} - -export function buildInputFieldsFromCalcMutationField(ast: DocumentNode, field: FieldDefinitionNode, namedType: NamedTypeNode ): InputValueDefinitionNode[] { - const directive = findDirectiveWithName(field, CALC_MUTATIONS_DIRECTIVE); - if (!directive) { - // directive missing => no calcMutations - return []; - } - const operators = getCalcMutationOperatorsFromDirective(directive); - - return operators.map(operator => buildInputValueNodeFromField(operator.prefix + field.name.value, namedType.name.value, field)) -} \ No newline at end of file diff --git a/src/schema/preparation/post-merge-ast-transformation-modules/add-meta-fields-along-with-filterable-fields-transformer.ts b/src/schema/preparation/post-merge-ast-transformation-modules/add-meta-fields-along-with-filterable-fields-transformer.ts deleted file mode 100644 index 3480ab78..00000000 --- a/src/schema/preparation/post-merge-ast-transformation-modules/add-meta-fields-along-with-filterable-fields-transformer.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {ASTTransformer} from "../transformation-pipeline"; -import {DocumentNode, FieldDefinitionNode} from "graphql"; -import {buildNameNode, getObjectTypes} from "../../schema-utils"; -import {FILTER_ARG, QUERY_META_TYPE, ROLES_DIRECTIVE} from '../../schema-defaults'; -import {getMetaFieldName} from "../../../graphql/names"; -import {FIELD_DEFINITION, NAMED_TYPE, NON_NULL_TYPE} from "../../../graphql/kinds"; -import {mapNullable} from '../../../utils/utils'; - -export class AddMetaFieldsAlongWithFilterableFieldsTransformer implements ASTTransformer { - - transform(ast: DocumentNode): void { - getObjectTypes(ast).forEach(objectType => { - objectType.fields.forEach(field => { - if (field.arguments.some(arg => arg.name.value === FILTER_ARG)) { - objectType.fields.push(buildMetaField(field)) - } - }) - }) - } - -} - -function buildMetaField(field: FieldDefinitionNode): FieldDefinitionNode { - return { - name: buildNameNode(getMetaFieldName(field.name.value)), - // meta fields have only the filter arg of the original field. - arguments: field.arguments.filter(arg => arg.name.value === FILTER_ARG), - type: { kind: NON_NULL_TYPE, type: { kind: NAMED_TYPE, name: buildNameNode(QUERY_META_TYPE)}}, - kind: FIELD_DEFINITION, - loc: field.loc, - directives: mapNullable(field.directives, directives => directives.filter(dir => dir.name.value == ROLES_DIRECTIVE)) - } -} diff --git a/src/schema/preparation/post-merge-ast-transformation-modules/add-missing-entity-fields-transformer.ts b/src/schema/preparation/post-merge-ast-transformation-modules/add-missing-entity-fields-transformer.ts deleted file mode 100644 index e65c937d..00000000 --- a/src/schema/preparation/post-merge-ast-transformation-modules/add-missing-entity-fields-transformer.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {ASTTransformer} from "../transformation-pipeline"; -import {DocumentNode} from "graphql"; -import { - createFieldDefinitionNode, - fieldDefinitionNodeByNameExists, - getChildEntityTypes, - getRootEntityTypes -} from "../../schema-utils"; -import {ENTITY_CREATED_AT, ENTITY_ID, ENTITY_UPDATED_AT, SCALAR_DATETIME} from "../../schema-defaults"; - -/** - * Assert that all @rootEntity have the fields id, updatedAt, createdAt. - */ -export class AddMissingEntityFieldsTransformer implements ASTTransformer { - - transform(ast: DocumentNode): void { - this.extendRootEntityTypes(ast); - } - - protected extendRootEntityTypes(ast: DocumentNode) { - [...getRootEntityTypes(ast), ...getChildEntityTypes(ast)].forEach(moType => { - // assert existence of ID field - // TODO better remove existing fields with the following names because they could contain bullshit (wrong type, args...). - if (!fieldDefinitionNodeByNameExists(moType, ENTITY_ID)) { - moType.fields.push(createFieldDefinitionNode(ENTITY_ID, 'ID', moType.loc)); - } - // assert existence of createdAt field - if (!fieldDefinitionNodeByNameExists(moType, ENTITY_CREATED_AT)) { - moType.fields.push(createFieldDefinitionNode(ENTITY_CREATED_AT, SCALAR_DATETIME, moType.loc)); - } - // assert existence of updatedAt field - if (!fieldDefinitionNodeByNameExists(moType, ENTITY_UPDATED_AT)) { - moType.fields.push(createFieldDefinitionNode(ENTITY_UPDATED_AT, SCALAR_DATETIME, moType.loc)); - } - }); - } - -} \ No newline at end of file diff --git a/src/schema/preparation/post-merge-ast-transformation-modules/add-orderby-arguments-to-fields-transformer.ts b/src/schema/preparation/post-merge-ast-transformation-modules/add-orderby-arguments-to-fields-transformer.ts deleted file mode 100644 index 33c2f144..00000000 --- a/src/schema/preparation/post-merge-ast-transformation-modules/add-orderby-arguments-to-fields-transformer.ts +++ /dev/null @@ -1,47 +0,0 @@ -import {ASTTransformer} from "../transformation-pipeline"; -import {DocumentNode} from "graphql"; -import { - buildNameNode, - getNamedTypeDefinitionAST, - getObjectTypes, - getTypeNameIgnoringNonNullAndList -} from "../../schema-utils"; -import { - INPUT_VALUE_DEFINITION, - LIST_TYPE, - NAMED_TYPE, - NON_NULL_TYPE, - OBJECT_TYPE_DEFINITION -} from "../../../graphql/kinds"; -import {getOrderByEnumTypeName} from "../../../graphql/names"; -import {ORDER_BY_ARG} from "../../schema-defaults"; - -export class AddOrderbyArgumentsToFieldsTransformer implements ASTTransformer { - - transform(ast: DocumentNode): void { - getObjectTypes(ast).forEach(objectType => { - objectType.fields.forEach(field => { - // Only lists have query args - if (field.type.kind !== LIST_TYPE && !(field.type.kind === NON_NULL_TYPE && field.type.type.kind === LIST_TYPE)) { - return; - } - const resolvedType = getNamedTypeDefinitionAST(ast, getTypeNameIgnoringNonNullAndList(field.type)); - // if this field is of an object type and this object type has an *OrderBy type - if (resolvedType.kind === OBJECT_TYPE_DEFINITION && getNamedTypeDefinitionAST(ast, getOrderByEnumTypeName(resolvedType))) { - field.arguments.push({ - kind: INPUT_VALUE_DEFINITION, - name: buildNameNode(ORDER_BY_ARG), - type: { - kind: LIST_TYPE, - type: { - kind: NON_NULL_TYPE, - type: { kind: NAMED_TYPE, name: buildNameNode(getOrderByEnumTypeName(resolvedType)) } - } - } - }) - } - }) - }) - } - -} \ No newline at end of file diff --git a/src/schema/preparation/post-merge-ast-transformation-modules/add-orderby-enums-transformer.ts b/src/schema/preparation/post-merge-ast-transformation-modules/add-orderby-enums-transformer.ts deleted file mode 100644 index 7ee65b8e..00000000 --- a/src/schema/preparation/post-merge-ast-transformation-modules/add-orderby-enums-transformer.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ASTTransformer } from '../transformation-pipeline'; -import { DocumentNode, EnumTypeDefinitionNode, EnumValueDefinitionNode, ObjectTypeDefinitionNode } from 'graphql'; -import { - buildNameNode, getNamedTypeDefinitionAST, getObjectTypes, getTypeNameIgnoringNonNullAndList -} from '../../schema-utils'; -import { flatMap } from '../../../utils/utils'; -import { - ENUM_TYPE_DEFINITION, ENUM_VALUE_DEFINITION, LIST_TYPE, NON_NULL_TYPE, OBJECT_TYPE_DEFINITION, - SCALAR_TYPE_DEFINITION -} from '../../../graphql/kinds'; -import { getOrderByEnumTypeName, sortedByAsc, sortedByDesc } from '../../../graphql/names'; - -export class AddOrderbyInputEnumsTransformer implements ASTTransformer { - - transform(ast: DocumentNode): void { - getObjectTypes(ast).forEach(objectType => { - ast.definitions.push(this.createOrderByEnum(ast, objectType)) - }) - } - - protected createOrderByEnum(ast: DocumentNode, objectType: ObjectTypeDefinitionNode): EnumTypeDefinitionNode { - return { - name: buildNameNode(getOrderByEnumTypeName(objectType)), - kind: ENUM_TYPE_DEFINITION, - loc: objectType.loc, - values: flatMap(this.collectOrderByEnumFieldNamesRecurse(ast, objectType, [], ""), name => - [ - this.buildEnumValueDefinitionNode(sortedByAsc(name)), - this.buildEnumValueDefinitionNode(sortedByDesc(name)) - ] - ) - } - } - - protected buildEnumValueDefinitionNode(name: string): EnumValueDefinitionNode { - return { - kind: ENUM_VALUE_DEFINITION, - name: buildNameNode(name) - } - } - - protected collectOrderByEnumFieldNamesRecurse(ast: DocumentNode, objectType: ObjectTypeDefinitionNode, ignoreTypeNames: string[], prefix: string): string[] { - return flatMap(objectType.fields, field => { - // no sorting on nested lists - if (field.type.kind === LIST_TYPE || field.type.kind === NON_NULL_TYPE && field.type.type.kind === LIST_TYPE) { - // emtpy list will be auto-removed by flatMap - return [] - } - // prevent endless recursion by cycling through one-to-one relationships - const typeName = getTypeNameIgnoringNonNullAndList(field.type); - if (ignoreTypeNames.includes(typeName)) { - return [] - } - const type = getNamedTypeDefinitionAST(ast, typeName); - - switch (type.kind) { - case SCALAR_TYPE_DEFINITION: - case ENUM_TYPE_DEFINITION: - return [ prefix + field.name.value ]; - case OBJECT_TYPE_DEFINITION: - return this.collectOrderByEnumFieldNamesRecurse(ast, type, [...ignoreTypeNames, typeName], prefix + field.name.value + '_'); - default: - throw new Error(`Unexpected type of ${typeName} when creating order by enum.`); - } - }) - } - -} diff --git a/src/schema/preparation/post-merge-ast-transformation-modules/add-pagination-arguments-to-fields-transformer.ts b/src/schema/preparation/post-merge-ast-transformation-modules/add-pagination-arguments-to-fields-transformer.ts deleted file mode 100644 index f86165e9..00000000 --- a/src/schema/preparation/post-merge-ast-transformation-modules/add-pagination-arguments-to-fields-transformer.ts +++ /dev/null @@ -1,45 +0,0 @@ -import {ASTTransformer} from "../transformation-pipeline"; -import {DocumentNode, GraphQLInt, GraphQLString} from 'graphql'; -import { - buildNameNode, - getNamedTypeDefinitionAST, - getObjectTypes, - getTypeNameIgnoringNonNullAndList, - hasDirectiveWithName -} from "../../schema-utils"; -import { - INPUT_VALUE_DEFINITION, - LIST_TYPE, - NAMED_TYPE, - NON_NULL_TYPE, - OBJECT_TYPE_DEFINITION -} from "../../../graphql/kinds"; -import {AFTER_ARG, FIRST_ARG, VALUE_OBJECT_DIRECTIVE} from '../../schema-defaults'; - -export class AddPaginationArgumentsToFieldsTransformer implements ASTTransformer { - - transform(ast: DocumentNode): void { - getObjectTypes(ast).forEach(objectType => { - objectType.fields.forEach(field => { - // Only lists have pagination args - if (field.type.kind !== LIST_TYPE && !(field.type.kind === NON_NULL_TYPE && field.type.type.kind === LIST_TYPE)) { - return; - } - const resolvedType = getNamedTypeDefinitionAST(ast, getTypeNameIgnoringNonNullAndList(field.type)); - // if this field is of an object type (only object types can have a _cursor field, so pagination only makes sense for them) - if (resolvedType.kind === OBJECT_TYPE_DEFINITION && !hasDirectiveWithName(resolvedType, VALUE_OBJECT_DIRECTIVE)) { - field.arguments.push({ - kind: INPUT_VALUE_DEFINITION, - name: buildNameNode(FIRST_ARG), - type: { kind: NAMED_TYPE, name: buildNameNode(GraphQLInt.name)} - },{ - kind: INPUT_VALUE_DEFINITION, - name: buildNameNode(AFTER_ARG), - type: { kind: NAMED_TYPE, name: buildNameNode(GraphQLString.name)} - }); - } - }) - }) - } - -} \ No newline at end of file diff --git a/src/schema/preparation/post-merge-ast-transformation-modules/add-query-meta-type-transformer.ts b/src/schema/preparation/post-merge-ast-transformation-modules/add-query-meta-type-transformer.ts deleted file mode 100644 index 535cbb44..00000000 --- a/src/schema/preparation/post-merge-ast-transformation-modules/add-query-meta-type-transformer.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {ASTTransformer} from "../transformation-pipeline"; -import {DocumentNode, GraphQLInt} from "graphql"; -import {FIELD_DEFINITION, NAMED_TYPE, NON_NULL_TYPE, OBJECT_TYPE_DEFINITION} from "../../../graphql/kinds"; -import {buildNameNode} from "../../schema-utils"; -import {COUNT_META_FIELD, QUERY_META_TYPE} from "../../schema-defaults"; - -export class AddQueryMetaTypeTransformer implements ASTTransformer { - - transform(ast: DocumentNode): void { - ast.definitions.push({ - kind: OBJECT_TYPE_DEFINITION, - name: buildNameNode(QUERY_META_TYPE), - fields: [ - { - kind: FIELD_DEFINITION, - name: buildNameNode(COUNT_META_FIELD), - arguments: [], - type: { kind: NON_NULL_TYPE, type: { kind: NAMED_TYPE, name: buildNameNode(GraphQLInt.name) }}, - } - ] - }) - } - -} \ No newline at end of file diff --git a/src/schema/preparation/post-merge-ast-transformation-modules/add-root-mutation-type-transformer.ts b/src/schema/preparation/post-merge-ast-transformation-modules/add-root-mutation-type-transformer.ts deleted file mode 100644 index 63b4bfee..00000000 --- a/src/schema/preparation/post-merge-ast-transformation-modules/add-root-mutation-type-transformer.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { mapNullable } from '../../../utils/utils'; -import {ASTTransformer} from "../transformation-pipeline"; -import { DirectiveNode, DocumentNode, FieldDefinitionNode, GraphQLID, GraphQLInt, ObjectTypeDefinitionNode } from 'graphql'; -import { - buildNameNode, - createObjectTypeNode, - enterOrCreateNextNamespacePart, - findDirectiveWithName, - getNodeByName, - getRootEntityTypes -} from "../../schema-utils"; -import { - DIRECTIVE, FIELD_DEFINITION, INPUT_VALUE_DEFINITION, LIST_TYPE, NAMED_TYPE, NON_NULL_TYPE, - STRING -} from "../../../graphql/kinds"; -import { - FILTER_ARG, FIRST_ARG, - MUTATION_FIELD, - MUTATION_ID_ARG, - MUTATION_INPUT_ARG, - MUTATION_TYPE, - NAMESPACE_DIRECTIVE, - NAMESPACE_NAME_ARG, - NAMESPACE_SEPARATOR, ORDER_BY_ARG, - ROLES_DIRECTIVE -} from '../../schema-defaults'; -import { - createEntityQuery, deleteAllEntitiesQuery, - deleteEntityQuery, - getCreateInputTypeName, getFilterTypeName, getOrderByEnumTypeName, getUpdateAllInputTypeName, - getUpdateInputTypeName, updateAllEntitiesQuery, - updateEntityQuery -} from '../../../graphql/names'; -import {compact} from "graphql-transformer/dist/src/utils"; - -const MUTATION_FIELD_DIRECTIVE: DirectiveNode = { - name: buildNameNode(MUTATION_FIELD), kind: DIRECTIVE -}; - -export class AddRootMutationTypeTransformer implements ASTTransformer { - - transform(ast: DocumentNode): void { - const rootMutatuinField = createObjectTypeNode(MUTATION_TYPE); - ast.definitions.push(rootMutatuinField); - getRootEntityTypes(ast).forEach(rootEntityType => buildMutationTypeEntityFieldsIntoNamespace(ast, rootEntityType, rootMutatuinField)) - } -} - -function buildMutationTypeEntityFieldsIntoNamespace(ast: DocumentNode, rootEntityType: ObjectTypeDefinitionNode, rootQueryField: ObjectTypeDefinitionNode) { - let currentNode = rootQueryField; - const namespaceDirective = findDirectiveWithName(rootEntityType, NAMESPACE_DIRECTIVE); - if (namespaceDirective && namespaceDirective.arguments) { - const nameArg = getNodeByName(namespaceDirective.arguments, NAMESPACE_NAME_ARG); - if (nameArg && nameArg.value.kind === STRING && nameArg.value.value) { - const namespace = nameArg.value.value; - // loop through namespaces and create intermediate fields and types - namespace.split(NAMESPACE_SEPARATOR).forEach(namespacePart => { - currentNode = enterOrCreateNextNamespacePart(ast, currentNode, namespacePart, MUTATION_TYPE); - }); - } - } - currentNode.fields.push( - buildCreateMutation(rootEntityType), - buildUpdateMutation(rootEntityType), - buildUpdateAllMutation(rootEntityType), - buildDeleteMutation(rootEntityType), - buildDeleteAllMutation(rootEntityType) - ) -} - -function buildCreateMutation(rootEntityDef: ObjectTypeDefinitionNode): FieldDefinitionNode { - return { - kind: FIELD_DEFINITION, - name: buildNameNode(createEntityQuery(rootEntityDef.name.value)), - type: { kind: NON_NULL_TYPE, type: { kind: NAMED_TYPE, name: buildNameNode(rootEntityDef.name.value) } }, - arguments: [ - buildNonNullTypeInputArg(MUTATION_INPUT_ARG, getCreateInputTypeName(rootEntityDef)), - ], - loc: rootEntityDef.loc, - directives: compact([findDirectiveWithName(rootEntityDef, ROLES_DIRECTIVE), MUTATION_FIELD_DIRECTIVE]) - } -} - -function buildUpdateMutation(rootEntityDef: ObjectTypeDefinitionNode): FieldDefinitionNode { - return { - kind: FIELD_DEFINITION, - name: buildNameNode(updateEntityQuery(rootEntityDef.name.value)), - type: { kind: NAMED_TYPE, name: buildNameNode(rootEntityDef.name.value) }, - arguments: [ - buildNonNullTypeInputArg(MUTATION_INPUT_ARG, getUpdateInputTypeName(rootEntityDef)), - ], - loc: rootEntityDef.loc, - directives: compact([findDirectiveWithName(rootEntityDef, ROLES_DIRECTIVE), MUTATION_FIELD_DIRECTIVE]) - } -} - -function buildUpdateAllMutation(rootEntityDef: ObjectTypeDefinitionNode): FieldDefinitionNode { - return { - kind: FIELD_DEFINITION, - name: buildNameNode(updateAllEntitiesQuery(rootEntityDef.name.value)), - type: { kind: LIST_TYPE, type: { kind: NON_NULL_TYPE, type: { kind: NAMED_TYPE, name: buildNameNode(rootEntityDef.name.value) }} }, - arguments: [ - // TODO Refactor duplicate code (see AddFilterArgumentsToFieldsTransformer AddOrderbyArgumentsToFieldsTransformer AddPaginationArgumentsToFieldsTransformer) - buildNamedTypeInputArg(FILTER_ARG, getFilterTypeName(rootEntityDef)), - buildListTypeInputArg(ORDER_BY_ARG, getOrderByEnumTypeName(rootEntityDef)), - buildNamedTypeInputArg(FIRST_ARG, GraphQLInt.name), - buildNonNullTypeInputArg(MUTATION_INPUT_ARG, getUpdateAllInputTypeName(rootEntityDef)), - ], - loc: rootEntityDef.loc, - directives: compact([findDirectiveWithName(rootEntityDef, ROLES_DIRECTIVE), MUTATION_FIELD_DIRECTIVE]) - } -} - -function buildDeleteMutation(rootEntityDef: ObjectTypeDefinitionNode): FieldDefinitionNode { - return { - kind: FIELD_DEFINITION, - name: buildNameNode(deleteEntityQuery(rootEntityDef.name.value)), - type: { kind: NAMED_TYPE, name: buildNameNode(rootEntityDef.name.value) }, - arguments: [ - buildNonNullTypeInputArg(MUTATION_ID_ARG, GraphQLID.name), - ], - loc: rootEntityDef.loc, - directives: compact([findDirectiveWithName(rootEntityDef, ROLES_DIRECTIVE), MUTATION_FIELD_DIRECTIVE]) - } -} - -function buildDeleteAllMutation(rootEntityDef: ObjectTypeDefinitionNode): FieldDefinitionNode { - return { - kind: FIELD_DEFINITION, - name: buildNameNode(deleteAllEntitiesQuery(rootEntityDef.name.value)), - type: { kind: LIST_TYPE, type: { kind: NON_NULL_TYPE, type: { kind: NAMED_TYPE, name: buildNameNode(rootEntityDef.name.value) }} }, - arguments: [ - // TODO Refactor duplicate code (see AddFilterArgumentsToFieldsTransformer AddOrderbyArgumentsToFieldsTransformer AddPaginationArgumentsToFieldsTransformer) - buildNamedTypeInputArg(FILTER_ARG, getFilterTypeName(rootEntityDef)), - buildListTypeInputArg(ORDER_BY_ARG, getOrderByEnumTypeName(rootEntityDef)), - buildNamedTypeInputArg(FIRST_ARG, GraphQLInt.name), - ], - loc: rootEntityDef.loc, - directives: compact([findDirectiveWithName(rootEntityDef, ROLES_DIRECTIVE), MUTATION_FIELD_DIRECTIVE]) - } -} - -function buildNonNullTypeInputArg(name: string, namedTypeName: string) { - return { - kind: INPUT_VALUE_DEFINITION, - name: buildNameNode(name), - type: { - kind: NON_NULL_TYPE, - type: { - kind: NAMED_TYPE, - name: buildNameNode(namedTypeName) - } - } - }; -} - -function buildNamedTypeInputArg(name: string, namedTypeName: string) { - return { - kind: INPUT_VALUE_DEFINITION, - name: buildNameNode(name), - type: { - kind: NAMED_TYPE, - name: buildNameNode(namedTypeName) - } - }; -} - -function buildListTypeInputArg(name: string, namedTypeName: string) { - return { - kind: INPUT_VALUE_DEFINITION, - name: buildNameNode(name), - type: { - kind: LIST_TYPE, - type: { - kind: NON_NULL_TYPE, - type: { - kind: NAMED_TYPE, - name: buildNameNode(namedTypeName) - } - } - } - }; -} diff --git a/src/schema/preparation/post-merge-ast-transformation-modules/add-root-query-type-transformer.ts b/src/schema/preparation/post-merge-ast-transformation-modules/add-root-query-type-transformer.ts deleted file mode 100644 index fbd6e585..00000000 --- a/src/schema/preparation/post-merge-ast-transformation-modules/add-root-query-type-transformer.ts +++ /dev/null @@ -1,101 +0,0 @@ -import {ASTTransformer} from "../transformation-pipeline"; -import {DocumentNode, FieldDefinitionNode, InputValueDefinitionNode, ObjectTypeDefinitionNode} from "graphql"; -import { - buildNameNode, - createObjectTypeNode, - findDirectiveWithName, - getNodeByName, - getRootEntityTypes, - getTypeNameIgnoringNonNullAndList, - enterOrCreateNextNamespacePart -} from "../../schema-utils"; -import { - FIELD_DEFINITION, - INPUT_VALUE_DEFINITION, - LIST_TYPE, - NAMED_TYPE, - NON_NULL_TYPE, - STRING -} from "../../../graphql/kinds"; -import {mapNullable} from '../../../utils/utils'; -import { - ENTITY_ID, - KEY_FIELD_DIRECTIVE, - NAMESPACE_DIRECTIVE, - NAMESPACE_NAME_ARG, - NAMESPACE_SEPARATOR, - QUERY_TYPE, - ROLES_DIRECTIVE -} from '../../schema-defaults'; -import {getAllEntitiesFieldName} from "../../../graphql/names"; - -export class AddRootQueryTypeTransformer implements ASTTransformer { - - transform(ast: DocumentNode): void { - const rootQueryField = createObjectTypeNode(QUERY_TYPE); - ast.definitions.push(rootQueryField); - getRootEntityTypes(ast).forEach(rootEntityType => buildQueryTypeEntityFieldsIntoNamespace(ast, rootEntityType, rootQueryField)) - } -} - -function buildQueryTypeEntityFieldsIntoNamespace(ast: DocumentNode, rootEntityType: ObjectTypeDefinitionNode, rootQueryField: ObjectTypeDefinitionNode) { - let currentNode = rootQueryField; - const namespaceDirective = findDirectiveWithName(rootEntityType, NAMESPACE_DIRECTIVE); - if (namespaceDirective && namespaceDirective.arguments) { - const nameArg = getNodeByName(namespaceDirective.arguments, NAMESPACE_NAME_ARG); - if (nameArg && nameArg.value.kind === STRING && nameArg.value.value) { - const namespace = nameArg.value.value; - // loop through namespaces and create intermediate fields and types - namespace.split(NAMESPACE_SEPARATOR).forEach(namespacePart => { - currentNode = enterOrCreateNextNamespacePart(ast, currentNode, namespacePart, QUERY_TYPE); - }); - } - } - currentNode.fields.push( - buildQueryOneEntityField(rootEntityType), - buildQueryAllEntityField(rootEntityType), - ) -} - -function buildQueryOneEntityField(entityDef: ObjectTypeDefinitionNode): FieldDefinitionNode { - return { - kind: FIELD_DEFINITION, - name: buildNameNode(entityDef.name.value), - type: { kind: NAMED_TYPE, name: buildNameNode(entityDef.name.value) }, - arguments: [ - { - kind: INPUT_VALUE_DEFINITION, - name: buildNameNode(ENTITY_ID), - type: { kind: NAMED_TYPE, name: buildNameNode('ID')} - }, - ...buildQueryOneInputFiltersForKeyFields(entityDef) - ], - loc: entityDef.loc, - directives: mapNullable(entityDef.directives, directives => directives.filter(dir => dir.name.value == ROLES_DIRECTIVE)) - } -} - -function buildQueryAllEntityField(entityDef: ObjectTypeDefinitionNode): FieldDefinitionNode { - return { - kind: FIELD_DEFINITION, - name: buildNameNode(getAllEntitiesFieldName(entityDef.name.value)), - type: { kind: LIST_TYPE, type: { kind: NON_NULL_TYPE, type: { kind: NAMED_TYPE, name: buildNameNode(entityDef.name.value) }} }, - arguments: [], // arguments will be added later - loc: entityDef.loc, - directives: mapNullable(entityDef.directives, directives => directives.filter(dir => dir.name.value == ROLES_DIRECTIVE)) - } -} - -function buildQueryOneInputFiltersForKeyFields(entityDef: ObjectTypeDefinitionNode): InputValueDefinitionNode[] { - const keyFields = entityDef.fields.filter(field => field.directives && field.directives.some(directive => directive.name.value === KEY_FIELD_DIRECTIVE)); - return keyFields.map(field => ({ - kind: INPUT_VALUE_DEFINITION, - loc: field.loc, - name: field.name, - type: { - kind: NAMED_TYPE, - name: buildNameNode(getTypeNameIgnoringNonNullAndList(field.type)) - }, - directives: mapNullable(field.directives, directives => directives.filter(dir => dir.name.value == ROLES_DIRECTIVE)) - })); -} \ No newline at end of file diff --git a/src/schema/preparation/post-merge-ast-transformation-modules/add-root-schema-transformer.ts b/src/schema/preparation/post-merge-ast-transformation-modules/add-root-schema-transformer.ts deleted file mode 100644 index 8a5c9b02..00000000 --- a/src/schema/preparation/post-merge-ast-transformation-modules/add-root-schema-transformer.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {ASTTransformer} from "../transformation-pipeline"; -import {DocumentNode, SchemaDefinitionNode} from "graphql"; -import {NAMED_TYPE, OPERATION_TYPE_DEFINITION, SCHEMA_DEFINITION} from "../../../graphql/kinds"; -import {buildNameNode} from "../../schema-utils"; - -export class AddRootSchemaTransformer implements ASTTransformer { - - transform(ast: DocumentNode): void { - ast.definitions.push(this.buildRootSchema(ast)) - } - - protected buildRootSchema(ast: DocumentNode): SchemaDefinitionNode { - return { - // fields: this.buildQueryTypeEntityFields(getRootEntityTypes(ast)), - kind: SCHEMA_DEFINITION, - operationTypes: [ - { - kind: OPERATION_TYPE_DEFINITION, - type: { - kind: NAMED_TYPE, - name: buildNameNode('Query'), - }, - operation: "query" - }, - { - kind: OPERATION_TYPE_DEFINITION, - type: { - kind: NAMED_TYPE, - name: buildNameNode('Mutation'), - }, - operation: "mutation" - } - ], - directives: [] - } - } - -} \ No newline at end of file diff --git a/src/schema/preparation/post-merge-ast-transformation-modules/add-scalar-types-transformer.ts b/src/schema/preparation/post-merge-ast-transformation-modules/add-scalar-types-transformer.ts deleted file mode 100644 index 5c472756..00000000 --- a/src/schema/preparation/post-merge-ast-transformation-modules/add-scalar-types-transformer.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {ASTTransformer} from "../transformation-pipeline"; -import {DocumentNode} from "graphql"; -import {SCALAR_DATE, SCALAR_DATETIME, SCALAR_JSON, SCALAR_TIME} from "../../schema-defaults"; -import {buildScalarDefinitionNode} from "../../schema-utils"; - -export class AddScalarTypesTransformer implements ASTTransformer { - - transform(ast: DocumentNode): void { - ast.definitions.push(buildScalarDefinitionNode(SCALAR_DATETIME)); - ast.definitions.push(buildScalarDefinitionNode(SCALAR_DATE)); - ast.definitions.push(buildScalarDefinitionNode(SCALAR_TIME)); - ast.definitions.push(buildScalarDefinitionNode(SCALAR_JSON)); - } - -} \ No newline at end of file diff --git a/src/schema/preparation/post-merge-ast-transformation-modules/add-update-entity-input-types-transformer.ts b/src/schema/preparation/post-merge-ast-transformation-modules/add-update-entity-input-types-transformer.ts deleted file mode 100644 index 7c043d9f..00000000 --- a/src/schema/preparation/post-merge-ast-transformation-modules/add-update-entity-input-types-transformer.ts +++ /dev/null @@ -1,122 +0,0 @@ -import {ASTTransformer} from '../transformation-pipeline'; -import { - DocumentNode, - FieldDefinitionNode, - GraphQLID, - InputObjectTypeDefinitionNode, - InputValueDefinitionNode, - ObjectTypeDefinitionNode, - TypeNode -} from 'graphql'; -import { - findDirectiveWithName, - getChildEntityTypes, - getNamedTypeDefinitionAST, - getRootEntityTypes, - hasDirectiveWithName -} from '../../schema-utils'; -import { - INPUT_OBJECT_TYPE_DEFINITION, - LIST_TYPE, - NAMED_TYPE, - NON_NULL_TYPE, - OBJECT_TYPE_DEFINITION -} from '../../../graphql/kinds'; -import { - getAddChildEntitiesFieldName, - getAddRelationFieldName, - getCreateInputTypeName, - getRemoveChildEntitiesFieldName, - getRemoveRelationFieldName, getUpdateAllInputTypeName, - getUpdateChildEntitiesFieldName, - getUpdateInputTypeName -} from '../../../graphql/names'; -import { - CHILD_ENTITY_DIRECTIVE, - ENTITY_CREATED_AT, - ENTITY_UPDATED_AT, - ID_FIELD, - RELATION_DIRECTIVE, ROLES_DIRECTIVE -} from '../../schema-defaults'; -import { compact, flatMap } from '../../../utils/utils'; -import { - buildInputFieldFromNonListField, buildInputFieldsFromCalcMutationField, - buildInputValueListNodeFromField, - buildInputValueNodeID -} from './add-input-type-transformation-helper-transformer'; - -export class AddUpdateEntityInputTypesTransformer implements ASTTransformer { - - transform(ast: DocumentNode): void { - getRootEntityTypes(ast).forEach(objectType => { - ast.definitions.push(this.createUpdateInputTypeForObjectType(ast, objectType)); - ast.definitions.push(this.createUpdateAllInputTypeForObjectType(ast, objectType)); - }); - getChildEntityTypes(ast).forEach(objectType => { - ast.definitions.push(this.createUpdateInputTypeForObjectType(ast, objectType)); - }) - } - - protected createUpdateInputTypeForObjectType(ast: DocumentNode, objectType: ObjectTypeDefinitionNode): InputObjectTypeDefinitionNode { - return { - kind: INPUT_OBJECT_TYPE_DEFINITION, - name: { kind: "Name", value: getUpdateInputTypeName(objectType) }, - fields: [buildInputValueNodeID(), ...this.createInputTypeFieldsForObjectType(ast, objectType)], - loc: objectType.loc, - directives: compact([ findDirectiveWithName(objectType, ROLES_DIRECTIVE) ]) - } - } - - protected createUpdateAllInputTypeForObjectType(ast: DocumentNode, objectType: ObjectTypeDefinitionNode): InputObjectTypeDefinitionNode { - return { - kind: INPUT_OBJECT_TYPE_DEFINITION, - name: { kind: "Name", value: getUpdateAllInputTypeName(objectType) }, - fields: this.createInputTypeFieldsForObjectType(ast, objectType), - loc: objectType.loc, - directives: compact([ findDirectiveWithName(objectType, ROLES_DIRECTIVE) ]) - } - } - - protected createInputTypeFieldsForObjectType(ast: DocumentNode, objectType: ObjectTypeDefinitionNode): InputValueDefinitionNode[] { - // create input fields for all entity fields except createdAt, updatedAt - const skip = [ID_FIELD, ENTITY_CREATED_AT, ENTITY_UPDATED_AT]; - return flatMap(objectType.fields.filter(field => !skip.includes(field.name.value)), field => this.createInputTypeField(ast, field, field.type)) - } - - // undefined currently means not supported. - protected createInputTypeField(ast: DocumentNode, field: FieldDefinitionNode, type: TypeNode): InputValueDefinitionNode[] { - switch (type.kind) { - case NON_NULL_TYPE: - return this.createInputTypeField(ast, field, type.type); - case NAMED_TYPE: - return [ buildInputFieldFromNonListField(ast, field, type), ...buildInputFieldsFromCalcMutationField(ast, field, type) ]; - case LIST_TYPE: - const effectiveType = type.type.kind === NON_NULL_TYPE ? type.type.type : type.type; - if (effectiveType.kind === LIST_TYPE) { - throw new Error('Lists of lists are not allowed.'); - } - const namedTypeOfList = getNamedTypeDefinitionAST(ast, effectiveType.name.value); - if (namedTypeOfList.kind === OBJECT_TYPE_DEFINITION) { - if (hasDirectiveWithName(field, RELATION_DIRECTIVE)) { - // add/remove by foreign key - return [ - buildInputValueListNodeFromField(getAddRelationFieldName(field.name.value), GraphQLID.name, field), - buildInputValueListNodeFromField(getRemoveRelationFieldName(field.name.value), GraphQLID.name, field), - ]; - } - if (hasDirectiveWithName(namedTypeOfList, CHILD_ENTITY_DIRECTIVE)) { - // add / update /remove with data - return [ - buildInputValueListNodeFromField(getAddChildEntitiesFieldName(field.name.value), getCreateInputTypeName(namedTypeOfList), field), - buildInputValueListNodeFromField(getUpdateChildEntitiesFieldName(field.name.value), getUpdateInputTypeName(namedTypeOfList), field), - buildInputValueListNodeFromField(getRemoveChildEntitiesFieldName(field.name.value), GraphQLID.name, field), - ] - } - return [buildInputValueListNodeFromField(field.name.value, getUpdateInputTypeName(namedTypeOfList), field)]; - } else { - return [buildInputValueListNodeFromField(field.name.value, effectiveType.name.value, field)]; - } - } - } - -} \ No newline at end of file diff --git a/src/schema/preparation/post-merge-ast-transformation-modules/add-value-object-input-types-transformer.ts b/src/schema/preparation/post-merge-ast-transformation-modules/add-value-object-input-types-transformer.ts deleted file mode 100644 index 0edbed56..00000000 --- a/src/schema/preparation/post-merge-ast-transformation-modules/add-value-object-input-types-transformer.ts +++ /dev/null @@ -1,58 +0,0 @@ -import {ASTTransformer} from '../transformation-pipeline'; -import { - DocumentNode, - FieldDefinitionNode, - InputObjectTypeDefinitionNode, - InputValueDefinitionNode, - ObjectTypeDefinitionNode, - TypeNode -} from 'graphql'; -import { findDirectiveWithName, getValueObjectTypes } from '../../schema-utils'; -import {INPUT_OBJECT_TYPE_DEFINITION, LIST_TYPE, NAMED_TYPE, NON_NULL_TYPE} from '../../../graphql/kinds'; -import {getCreateInputTypeName} from '../../../graphql/names'; -import { - buildInputFieldFromNonListField, - buildInputValueListNodeFromField -} from './add-input-type-transformation-helper-transformer'; -import { ROLES_DIRECTIVE } from '../../schema-defaults'; -import { compact } from '../../../utils/utils'; - -export class AddValueObjectInputTypesTransformer implements ASTTransformer { - - transform(ast: DocumentNode): void { - getValueObjectTypes(ast).forEach(objectType => { - ast.definitions.push(this.createCreateInputTypeForObjectType(ast, objectType)); - }); - } - - protected createCreateInputTypeForObjectType(ast: DocumentNode, objectType: ObjectTypeDefinitionNode): InputObjectTypeDefinitionNode { - // create input fields for all entity fields except ID, createdAt, updatedAt - const args = [ - ...objectType.fields.map(field => this.createInputTypeField(ast, field, field.type)) - ]; - return { - kind: INPUT_OBJECT_TYPE_DEFINITION, - name: { kind: "Name", value: getCreateInputTypeName(objectType) }, - fields: args, - loc: objectType.loc, - directives: compact([ findDirectiveWithName(objectType, ROLES_DIRECTIVE) ]) - } - } - - // undefined currently means not supported. - protected createInputTypeField(ast: DocumentNode, field: FieldDefinitionNode, type: TypeNode): InputValueDefinitionNode { - switch (type.kind) { - case NON_NULL_TYPE: - return this.createInputTypeField(ast, field, type.type); - case NAMED_TYPE: - return buildInputFieldFromNonListField(ast, field, type); - case LIST_TYPE: - const effectiveType = type.type.kind === NON_NULL_TYPE ? type.type.type : type.type; - if (effectiveType.kind === LIST_TYPE) { - throw new Error('Lists of lists are not allowed.'); - } - return buildInputValueListNodeFromField(field.name.value, effectiveType.name.value, field); - } - } - -} \ No newline at end of file diff --git a/src/schema/preparation/post-merge-ast-transformation-modules/non-nullable-lists-transformer.ts b/src/schema/preparation/post-merge-ast-transformation-modules/non-nullable-lists-transformer.ts deleted file mode 100644 index d83311d5..00000000 --- a/src/schema/preparation/post-merge-ast-transformation-modules/non-nullable-lists-transformer.ts +++ /dev/null @@ -1,35 +0,0 @@ -import {ASTTransformer} from "../transformation-pipeline"; -import {DocumentNode, TypeNode} from "graphql"; -import {LIST_TYPE, NAMED_TYPE, NON_NULL_TYPE} from "../../../graphql/kinds"; -import {getFieldDefinitionNodes, nonNullifyType} from "../../schema-utils"; - -/** - * Asserts that all Lists used in FieldDefinitions are of Type [Obj!]! - */ -export class NonNullableListsTransformer implements ASTTransformer { - - transform(ast: DocumentNode): void { - getFieldDefinitionNodes(ast).forEach(field => field.type = this.nonNullifyListType(field.type)); - } - - /** - * Deep search on the FieldDefinitionTree for - * @param {TypeNode} parentType - * @param {TypeNode} type - * @returns {TypeNode} - */ - protected nonNullifyListType(type: TypeNode): TypeNode { - switch (type.kind) { - case NAMED_TYPE: - return type; - case LIST_TYPE: - return nonNullifyType({ ...type, type: nonNullifyType(type.type)}) - case NON_NULL_TYPE: - if (type.type.kind === LIST_TYPE) { - return { ...type, type: { ...type.type, type: nonNullifyType(type.type.type)} } - } else { - return type; - } - } - } -} diff --git a/src/schema/preparation/pre-merge-ast-transformation-modules/add-namespaces-to-types-transformer.ts b/src/schema/preparation/pre-merge-ast-transformation-modules/add-namespaces-to-types-transformer.ts index 94f6def4..b0b842e0 100644 --- a/src/schema/preparation/pre-merge-ast-transformation-modules/add-namespaces-to-types-transformer.ts +++ b/src/schema/preparation/pre-merge-ast-transformation-modules/add-namespaces-to-types-transformer.ts @@ -2,7 +2,7 @@ import { ASTTransformationContext, ASTTransformer } from '../transformation-pipe import {DocumentNode} from "graphql"; import {buildNameNode, getRootEntityTypes, hasDirectiveWithName} from "../../schema-utils"; import {ARGUMENT, DIRECTIVE, STRING} from "../../../graphql/kinds"; -import {NAMESPACE_DIRECTIVE, NAMESPACE_NAME_ARG} from "../../schema-defaults"; +import {NAMESPACE_DIRECTIVE, NAMESPACE_NAME_ARG} from "../../constants"; export class AddNamespacesToTypesTransformer implements ASTTransformer { diff --git a/src/schema/preparation/schema-transformation-modules/add-alias-based-resolvers.ts b/src/schema/preparation/schema-transformation-modules/add-alias-based-resolvers.ts deleted file mode 100644 index ed01af4e..00000000 --- a/src/schema/preparation/schema-transformation-modules/add-alias-based-resolvers.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { SchemaTransformer } from '../transformation-pipeline'; -import { addAliasBasedResolvers } from '../../../graphql/alias-based-resolvers'; -import { GraphQLSchema } from 'graphql'; - -export class AddAliasBasedResolversTransformer implements SchemaTransformer { - transform(schema: GraphQLSchema): GraphQLSchema { - return addAliasBasedResolvers(schema); - } -} diff --git a/src/schema/preparation/schema-transformation-modules/add-operation-resolvers.ts b/src/schema/preparation/schema-transformation-modules/add-operation-resolvers.ts deleted file mode 100644 index dfa2d724..00000000 --- a/src/schema/preparation/schema-transformation-modules/add-operation-resolvers.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { SchemaTransformationContext, SchemaTransformer } from '../transformation-pipeline'; -import { GraphQLSchema, print } from 'graphql'; -import { evaluateQueryStatically } from '../../../query/static-evaluation'; -import { globalContext } from '../../../config/global'; -import { distillOperation } from '../../../graphql/query-distiller'; -import { QueryNode } from '../../../query-tree'; -import { createQueryTree } from '../../../query/query-tree-builder'; -import { applyAuthorizationToQueryTree } from '../../../authorization/execution'; -import { addOperationBasedResolvers } from '../../../graphql/operation-based-resolvers'; -import { Model } from '../../../model'; - -/** - * Adds resolvers on operation level that actually execute an operation via the databaseAdapter - */ -export class AddOperationResolversTransformer implements SchemaTransformer { - transform(schema: GraphQLSchema, context: SchemaTransformationContext, model: Model) { - return addOperationBasedResolvers(schema, async operationInfo => { - globalContext.registerContext(context); - const logger = globalContext.loggerProvider.getLogger('query-resolvers'); - try { - let queryTree: QueryNode; - try { - logger.debug(`Executing ${operationInfo.operation.operation} ${operationInfo.operation.name ? operationInfo.operation.name.value : ''}`); - if (logger.isTraceEnabled()) { - logger.trace(`Operation: ${print(operationInfo.operation)}`); - } - const operation = distillOperation(operationInfo); - if (logger.isTraceEnabled()) { - logger.trace(`DistilledOperation: ${operation.describe()}`); - } - - const requestRoles = getRequestRoles(operationInfo.context); - logger.debug(`Request roles: ${requestRoles.join(', ')}`); - queryTree = createQueryTree(operation, model); - if (logger.isTraceEnabled()) { - logger.trace('Before authorization: ' + queryTree.describe()); - } - queryTree = applyAuthorizationToQueryTree(queryTree, { authRoles: requestRoles}); - if (logger.isTraceEnabled()) { - logger.trace('After authorization: ' + queryTree.describe()); - } - } finally { - globalContext.unregisterContext(); - } - let { canEvaluateStatically, result } = evaluateQueryStatically(queryTree); - if (!canEvaluateStatically) { - result = await context.databaseAdapter.execute(queryTree); - logger.debug(`Execution successful`) - } else { - logger.debug(`Execution successful (evaluated statically without database adapter))`); - } - if (logger.isTraceEnabled()) { - logger.trace('Result: ' + JSON.stringify(result, undefined, ' ')); - } - return result; - } catch (e) { - logger.error("Error evaluating GraphQL query: " + e.stack); - throw e; - } - }); - } -} - -function getRequestRoles(context: any): string[] { - return context.authRoles || []; -} diff --git a/src/schema/preparation/schema-transformation-modules/add-runtime-error-resolvers.ts b/src/schema/preparation/schema-transformation-modules/add-runtime-error-resolvers.ts index 4cf9db71..b4d384aa 100644 --- a/src/schema/preparation/schema-transformation-modules/add-runtime-error-resolvers.ts +++ b/src/schema/preparation/schema-transformation-modules/add-runtime-error-resolvers.ts @@ -1,9 +1,32 @@ +import { defaultFieldResolver, GraphQLSchema } from 'graphql'; +import { transformSchema } from 'graphql-transformer'; +import { extractRuntimeError, isRuntimeErrorValue } from '../../../query-tree'; +import { isPromise } from '../../../utils/utils'; import { SchemaTransformer } from '../transformation-pipeline'; -import { GraphQLSchema } from 'graphql'; -import { addRuntimeErrorResolvers } from '../../../query/runtime-errors'; export class AddRuntimeErrorResolversTransformer implements SchemaTransformer { transform(schema: GraphQLSchema): GraphQLSchema { - return addRuntimeErrorResolvers(schema); + return transformSchema(schema, { + transformField(config) { + return { + ...config, + resolve(source, args, context, info) { + const result = (config.resolve || defaultFieldResolver)(source, args, context, info); + if (isPromise(result)) { + return result.then(res => { + if (isRuntimeErrorValue(res)) { + throw extractRuntimeError(res); + } + return res; + }) + } + if (isRuntimeErrorValue(result)) { + throw extractRuntimeError(result); + } + return result; + } + } + } + }); } } diff --git a/src/schema/preparation/schema-transformation-modules/implement-scalar-types.ts b/src/schema/preparation/schema-transformation-modules/implement-scalar-types.ts deleted file mode 100644 index 066fd856..00000000 --- a/src/schema/preparation/schema-transformation-modules/implement-scalar-types.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { GraphQLScalarType, GraphQLSchema } from 'graphql'; -import { GraphQLDateTime } from '../../scalars/date-time'; -import { transformSchema } from 'graphql-transformer/dist'; -import { arrayToObject } from '../../../utils/utils'; -import { SchemaTransformer } from '../transformation-pipeline'; -import GraphQLJSON = require('graphql-type-json'); - -const scalars: GraphQLScalarType[] = [ - GraphQLDateTime, - GraphQLJSON -]; -const scalarMap = arrayToObject(scalars, scalar => scalar.name); - -export class ImplementScalarTypesTransformer implements SchemaTransformer { - transform(schema: GraphQLSchema) { - return transformSchema(schema, { - transformScalarType(config) { - const scalar = scalarMap[config.name]; - if (scalar) { - return { - ...scalar, - description: scalar.description, - serialize: scalar.serialize.bind(scalar), - parseLiteral: scalar.parseLiteral.bind(scalar), - parseValue: scalar.parseValue.bind(scalar) - } - } - return config; - } - }); - } -} diff --git a/src/schema/preparation/transformation-pipeline.ts b/src/schema/preparation/transformation-pipeline.ts index d2597a45..cc01e228 100644 --- a/src/schema/preparation/transformation-pipeline.ts +++ b/src/schema/preparation/transformation-pipeline.ts @@ -3,23 +3,6 @@ import { SchemaContext } from '../../config/global'; import { SchemaPartConfig } from '../../config/schema-config'; import { DatabaseAdapter } from '../../database/database-adapter'; import { Model, PermissionProfileMap } from '../../model'; -import { AddCreateEntityInputTypesTransformer } from './post-merge-ast-transformation-modules/add-create-entity-input-types-transformer'; -import { AddCursorFieldToEntitiesTransformer } from './post-merge-ast-transformation-modules/add-cursor-field-to-entities-transformer'; -import { AddExtensionInputTypesTransformer } from './post-merge-ast-transformation-modules/add-extension-input-types-transformer'; -import { AddFilterArgumentsToFieldsTransformer } from './post-merge-ast-transformation-modules/add-filter-arguments-to-fields-transformer'; -import { AddFilterInputTypesTransformer } from './post-merge-ast-transformation-modules/add-filter-input-types-transformer'; -import { AddMetaFieldsAlongWithFilterableFieldsTransformer } from './post-merge-ast-transformation-modules/add-meta-fields-along-with-filterable-fields-transformer'; -import { AddMissingEntityFieldsTransformer } from './post-merge-ast-transformation-modules/add-missing-entity-fields-transformer'; -import { AddOrderbyArgumentsToFieldsTransformer } from './post-merge-ast-transformation-modules/add-orderby-arguments-to-fields-transformer'; -import { AddOrderbyInputEnumsTransformer } from './post-merge-ast-transformation-modules/add-orderby-enums-transformer'; -import { AddPaginationArgumentsToFieldsTransformer } from './post-merge-ast-transformation-modules/add-pagination-arguments-to-fields-transformer'; -import { AddQueryMetaTypeTransformer } from './post-merge-ast-transformation-modules/add-query-meta-type-transformer'; -import { AddRootMutationTypeTransformer } from './post-merge-ast-transformation-modules/add-root-mutation-type-transformer'; -import { AddRootQueryTypeTransformer } from './post-merge-ast-transformation-modules/add-root-query-type-transformer'; -import { AddRootSchemaTransformer } from './post-merge-ast-transformation-modules/add-root-schema-transformer'; -import { AddScalarTypesTransformer } from './post-merge-ast-transformation-modules/add-scalar-types-transformer'; -import { AddUpdateEntityInputTypesTransformer } from './post-merge-ast-transformation-modules/add-update-entity-input-types-transformer'; -import { AddValueObjectInputTypesTransformer } from './post-merge-ast-transformation-modules/add-value-object-input-types-transformer'; import { AddNamespacesToTypesTransformer } from './pre-merge-ast-transformation-modules/add-namespaces-to-types-transformer'; import { AddRuntimeErrorResolversTransformer } from './schema-transformation-modules/add-runtime-error-resolvers'; @@ -27,49 +10,10 @@ const preMergePipeline: ASTTransformer[] = [ new AddNamespacesToTypesTransformer() ]; -const postMergePipeline: ASTTransformer[] = [ - // Add basic stuff to object types - new AddScalarTypesTransformer(), - new AddMissingEntityFieldsTransformer(), - // TODO: check if some input stuff should be nullable in schema. - // new NonNullableListsTransformer(, - - // add query parameters - new AddFilterInputTypesTransformer(), - new AddOrderbyInputEnumsTransformer(), - - // Input types for creation and manipulation of object types. - new AddCreateEntityInputTypesTransformer(), - new AddUpdateEntityInputTypesTransformer(), - new AddExtensionInputTypesTransformer(), - new AddValueObjectInputTypesTransformer(), - - // build query stuff - new AddRootQueryTypeTransformer(), - new AddFilterArgumentsToFieldsTransformer(), - new AddOrderbyArgumentsToFieldsTransformer(), - new AddCursorFieldToEntitiesTransformer(), - new AddPaginationArgumentsToFieldsTransformer(), - - new AddQueryMetaTypeTransformer(), - new AddMetaFieldsAlongWithFilterableFieldsTransformer(), - - // build mutation stuff - new AddRootMutationTypeTransformer(), - - // compose schema - new AddRootSchemaTransformer() - -]; - const schemaPipeline: SchemaTransformer[] = [ new AddRuntimeErrorResolversTransformer() ]; -export function executePostMergeTransformationPipeline(ast: DocumentNode, context: ASTTransformationContext, model: Model) { - postMergePipeline.forEach(transformer => transformer.transform(ast, context, model)); -} - export function executePreMergeTransformationPipeline(schemaParts: SchemaPartConfig[], rootContext: ASTTransformationContext, model: Model) { schemaParts.forEach(schemaPart => preMergePipeline.forEach(transformer => { diff --git a/src/schema/schema-builder.ts b/src/schema/schema-builder.ts index 04d1de63..f6e21306 100644 --- a/src/schema/schema-builder.ts +++ b/src/schema/schema-builder.ts @@ -1,19 +1,19 @@ -import { buildASTSchema, DocumentNode, GraphQLSchema, parse, print } from 'graphql'; -import { SchemaGenerator } from '../schema-generation/schema-generator'; -import { - ASTTransformationContext, executePostMergeTransformationPipeline, executePreMergeTransformationPipeline, - executeSchemaTransformationPipeline, SchemaTransformationContext -} from './preparation/transformation-pipeline'; -import { validatePostMerge, validateSource } from './preparation/ast-validator'; -import { SchemaConfig, SchemaPartConfig } from '../config/schema-config'; +import { DocumentNode, GraphQLSchema, parse } from 'graphql'; +import { load as loadYaml } from 'js-yaml'; import { globalContext } from '../config/global'; +import { SchemaConfig, SchemaPartConfig } from '../config/schema-config'; +import { DatabaseAdapter } from '../database/database-adapter'; import { createModel, Model, ValidationMessage, ValidationResult } from '../model'; +import { createPermissionMap } from '../model/implementation/permission-profile'; import { Project } from '../project/project'; -import { DatabaseAdapter } from '../database/database-adapter'; import { SourceType } from '../project/source'; -import { load as loadYaml } from 'js-yaml'; +import { SchemaGenerator } from '../schema-generation/schema-generator'; import { flatMap } from '../utils/utils'; -import { createPermissionMap } from '../model/implementation/permission-profile'; +import { validatePostMerge, validateSource } from './preparation/ast-validator'; +import { + ASTTransformationContext, executePreMergeTransformationPipeline, executeSchemaTransformationPipeline, + SchemaTransformationContext +} from './preparation/transformation-pipeline'; /** * Validates a project and thus determines whether createSchema() would succeed @@ -29,7 +29,7 @@ export function validateSchema(project: Project): ValidationResult { function validateAndPrepareSchema(project: Project): - { validationResult: ValidationResult, schemaConfig: SchemaConfig, mergedSchema: DocumentNode, rootContext: ASTTransformationContext, model: Model } { + { validationResult: ValidationResult, rootContext: ASTTransformationContext, model: Model } { const messages: ValidationMessage[] = []; const sources = flatMap(project.sources, source => { @@ -53,11 +53,11 @@ function validateAndPrepareSchema(project: Project): const mergedSchema: DocumentNode = mergeSchemaDefinition(schemaConfig); const result = validatePostMerge(mergedSchema, rootContext, model); - //messages.push(...result.messages); + messages.push(...result.messages); messages.push(...model.validate().messages); const validationResult = new ValidationResult(messages); - return { validationResult, schemaConfig, mergedSchema, rootContext, model }; + return { validationResult, rootContext, model }; } /** @@ -71,7 +71,7 @@ export function createSchema(project: Project, databaseAdapter: DatabaseAdapter) try { const logger = globalContext.loggerProvider.getLogger('schema-builder'); - const { validationResult, schemaConfig, mergedSchema, rootContext, model } = validateAndPrepareSchema(project); + const { validationResult, rootContext, model } = validateAndPrepareSchema(project); if (validationResult.hasErrors()) { throw new Error('Project has errors:\n' + validationResult.toString()) } diff --git a/src/schema/schema-utils.ts b/src/schema/schema-utils.ts index f034969c..b322d09d 100644 --- a/src/schema/schema-utils.ts +++ b/src/schema/schema-utils.ts @@ -1,59 +1,16 @@ import { - DirectiveNode, - DocumentNode, - EnumTypeDefinitionNode, - EnumValueDefinitionNode, - FieldDefinitionNode, - GraphQLEnumValue, - GraphQLField, - GraphQLInputField, - GraphQLObjectType, - GraphQLType, - InputObjectTypeDefinitionNode, - InputValueDefinitionNode, - Location, NamedTypeNode, - NameNode, - ObjectTypeDefinitionNode, - ScalarTypeDefinitionNode, - TypeNode, - ValueNode + DirectiveNode, DocumentNode, EnumTypeDefinitionNode, EnumValueDefinitionNode, FieldDefinitionNode, + InputObjectTypeDefinitionNode, InputValueDefinitionNode, NamedTypeNode, NameNode, ObjectTypeDefinitionNode, + ScalarTypeDefinitionNode, TypeNode } from 'graphql'; import { - DIRECTIVE, - ENUM_TYPE_DEFINITION, - FIELD_DEFINITION, - INPUT_OBJECT_TYPE_DEFINITION, - LIST_TYPE, - NAME, - NAMED_TYPE, - NON_NULL_TYPE, - OBJECT_TYPE_DEFINITION, - SCALAR_TYPE_DEFINITION + ENUM_TYPE_DEFINITION, INPUT_OBJECT_TYPE_DEFINITION, LIST_TYPE, NAME, NAMED_TYPE, NON_NULL_TYPE, + OBJECT_TYPE_DEFINITION, SCALAR_TYPE_DEFINITION } from '../graphql/kinds'; -import { - CALC_MUTATIONS_DIRECTIVE, - CALC_MUTATIONS_OPERATORS, - CALC_MUTATIONS_OPERATORS_ARG, - CalcMutationOperator, - CHILD_ENTITY_DIRECTIVE, - ENTITY_CREATED_AT, - ENTITY_EXTENSION_DIRECTIVE, - ID_FIELD, - KEY_FIELD_DIRECTIVE, - NAMESPACE_FIELD_PATH_DIRECTIVE, - REFERENCE_DIRECTIVE, - RELATION_DIRECTIVE, - ROLES_DIRECTIVE, - ROLES_READ_ARG, - ROLES_READ_WRITE_ARG, - ROOT_ENTITY_DIRECTIVE, - VALUE_OBJECT_DIRECTIVE -} from './schema-defaults'; -import {compact, flatMap, objectValues} from '../utils/utils'; -import {namespacedType} from '../graphql/names'; -import {isEqual} from 'lodash'; import { CORE_SCALARS } from './graphql-base'; - +import { + CHILD_ENTITY_DIRECTIVE, ENTITY_EXTENSION_DIRECTIVE, ROOT_ENTITY_DIRECTIVE, VALUE_OBJECT_DIRECTIVE +} from './constants'; /** * Get all @link ObjectTypeDefinitionNode a model. @@ -124,66 +81,7 @@ export function getValueObjectTypes(model: DocumentNode): ObjectTypeDefinitionNo ); } -/** - * Get all @link FieldDefinitionNode in all @link ObjectTypeDefinition of a model (ast). - * @param {DocumentNode} model (ast) - * @returns {FieldDefinitionNode[]} - */ -export function getFieldDefinitionNodes(model: DocumentNode): FieldDefinitionNode[] { - return flatMap( model.definitions.filter(def => def.kind === OBJECT_TYPE_DEFINITION), def => def.fields); -} - -/** - * Create a @link FieldDefinitionNode using the following arguments. - * @param {string} name - * @param {string} type - * @param {Location} loc - * @param {InputValueDefinitionNode[]} args - * @returns {FieldDefinitionNode} - */ -export function createFieldDefinitionNode(name: string, type: string, loc?: Location, args?: InputValueDefinitionNode[]): FieldDefinitionNode { - return { - kind: FIELD_DEFINITION, - name: { kind: NAME, value: name, loc: loc }, - type: { - kind: NAMED_TYPE, - name: { - kind: NAME, - value: type, - loc: loc - } - }, - arguments: args || [] - }; - -} - -/** - * Check if a @link FieldDefinitionNode of a name and type exists. - * @param {{fields: FieldDefinitionNode[]}} moType - * @param {string} name - * @returns {boolean} - */ -export function fieldDefinitionNodeByNameExists(moType: { fields: FieldDefinitionNode[] }, name: string) { - return moType.fields.some(f => f.name.value === name); -} - -/** - * Wrap a type into a NonNullType unless it already is a NonNullType - * @param {TypeNode} type - * @returns {TypeNode} - */ -export function nonNullifyType(type: TypeNode): TypeNode { - if (type.kind === NON_NULL_TYPE) { - return type; - } - return { - kind: NON_NULL_TYPE, - type: type - }; -} - -export function getScalarFieldsOfObjectDefinition(ast: DocumentNode, objectDefinition: ObjectTypeDefinitionNode): FieldDefinitionNode[] { +function getScalarFieldsOfObjectDefinition(ast: DocumentNode, objectDefinition: ObjectTypeDefinitionNode): FieldDefinitionNode[] { return objectDefinition.fields.filter(field => { switch (field.type.kind) { case NAMED_TYPE: @@ -199,7 +97,7 @@ export function getScalarFieldsOfObjectDefinition(ast: DocumentNode, objectDefin }); } -export function getNamedTypeDefinitionASTIfExists(ast: DocumentNode, name: string): ObjectTypeDefinitionNode|ScalarTypeDefinitionNode|EnumTypeDefinitionNode|InputObjectTypeDefinitionNode|undefined { +function getNamedTypeDefinitionASTIfExists(ast: DocumentNode, name: string): ObjectTypeDefinitionNode|ScalarTypeDefinitionNode|EnumTypeDefinitionNode|InputObjectTypeDefinitionNode|undefined { const scalar = CORE_SCALARS.definitions.find(def => def.kind == SCALAR_TYPE_DEFINITION && def.name.value == name); if (scalar) { return scalar as ScalarTypeDefinitionNode; @@ -274,251 +172,9 @@ export function hasDirectiveWithName(typeOrField: ObjectTypeDefinitionNode|Field return !!findDirectiveWithName(typeOrField, directiveName); } -function getTypeDefinitionNode(type: GraphQLType): ObjectTypeDefinitionNode|undefined { - if (!(type instanceof GraphQLObjectType)) { - return undefined; - } - return (type as any).astNode; -} - -function getFieldDefinitionNode(field: GraphQLField): FieldDefinitionNode { - const astNode = field.astNode; - if (!astNode) { - throw new Error(`astNode on field ${field.name} expected but missing`); - } - return astNode; -} - -function getInputFieldDefinitionNode(field: GraphQLInputField): InputValueDefinitionNode { - const astNode = field.astNode; - if (!astNode) { - throw new Error(`astNode on input field ${field.name} expected but missing`); - } - return astNode; -} - -function getASTNodeWithDirectives(field: GraphQLInputField|GraphQLField|GraphQLEnumValue): InputValueDefinitionNode|FieldDefinitionNode|EnumValueDefinitionNode { - const astNode = field.astNode; - if (!astNode) { - throw new Error(`astNode on field ${field.name} expected but missing`); - } - return astNode; -} - -export function isTypeWithIdentity(type: GraphQLType) { - const astNode = getTypeDefinitionNode(type); - if (!astNode) { - return false; - } - return hasDirectiveWithName(astNode, ROOT_ENTITY_DIRECTIVE) || - hasDirectiveWithName(astNode, CHILD_ENTITY_DIRECTIVE); -} - -export function isRootEntityType(type: GraphQLType) { - const astNode = getTypeDefinitionNode(type); - if (!astNode) { - return false; - } - return hasDirectiveWithName(astNode, ROOT_ENTITY_DIRECTIVE); -} - -export function isEntityExtensionType(type: GraphQLType) { - const astNode = getTypeDefinitionNode(type); - if (!astNode) { - return false; - } - return hasDirectiveWithName(astNode, ENTITY_EXTENSION_DIRECTIVE); -} - -export function isChildEntityType(type: GraphQLType) { - const astNode = getTypeDefinitionNode(type); - if (!astNode) { - return false; - } - return hasDirectiveWithName(astNode, CHILD_ENTITY_DIRECTIVE); -} - -export function isRelationField(field: GraphQLField) { - const astNode = getFieldDefinitionNode(field); - return hasDirectiveWithName(astNode, RELATION_DIRECTIVE); -} - -export function isReferenceField(field: GraphQLField) { - const astNode = getFieldDefinitionNode(field); - return hasDirectiveWithName(astNode, REFERENCE_DIRECTIVE); -} - -export function isKeyField(field: GraphQLField) { - const astNode = getFieldDefinitionNode(field); - return hasDirectiveWithName(astNode, KEY_FIELD_DIRECTIVE); -} - -export function isCalcMutationField(field: GraphQLField) { - const astNode = getFieldDefinitionNode(field); - return hasDirectiveWithName(astNode, CALC_MUTATIONS_DIRECTIVE); -} - -export function getStringListValues(value: ValueNode): string[] { - if (value.kind == 'ListValue') { - return value.values.map(value => { - if (value.kind == 'StringValue' || value.kind == 'EnumValue') { - return value.value; - } - throw new Error(`Expected string or enum, got ${value.kind}`); - }); - } - if (value.kind == 'StringValue' || value.kind == 'EnumValue') { - return [ value.value ]; - } - throw new Error(`Expected string/enum or list of string/enum, got ${value.kind}`); -} - -export function getAllowedReadRoles(field: GraphQLField|GraphQLInputField|GraphQLEnumValue): string[]|undefined { - const readRoles = getAllowedRoles(field, ROLES_READ_ARG); - const readWriteRoles = getAllowedRoles(field, ROLES_READ_WRITE_ARG); - if (readRoles && readWriteRoles) { - return Array.from(new Set([...readRoles, ...readWriteRoles])); - } - return readRoles || readWriteRoles; -} - -export function getAllowedWriteRoles(field: GraphQLField|GraphQLInputField|GraphQLEnumValue): string[]|undefined { - return getAllowedRoles(field, ROLES_READ_WRITE_ARG); -} - -export function getRoleListFromDirective(directive: DirectiveNode, argName: string): string[] { - const arg = (directive.arguments || []).find(arg => arg.name.value == argName); - if (arg) { - return getStringListValues(arg.value); - } - - // if the directive is specified but an arg is missing, this default to [] (default to secure option) - return []; -} - -/** - * Determines whether two @roles directives are specifying exactly the same roles - */ -export function areRolesDirectivesEqual(lhs: DirectiveNode, rhs: DirectiveNode) { - function forArg(argName: string) { - const lhsRoles = getRoleListFromDirective(lhs, argName); - const rhsRoles = getRoleListFromDirective(lhs, argName); - return isEqual(lhsRoles.sort(), rhsRoles.sort()); - } - - return forArg(ROLES_READ_ARG) && forArg(ROLES_READ_WRITE_ARG); -} - -function getAllowedRoles(field: GraphQLField|GraphQLInputField|GraphQLEnumValue, argName: string): string[]|undefined { - const astNode = getASTNodeWithDirectives(field); - const directive = findDirectiveWithName(astNode, ROLES_DIRECTIVE); - if (!directive) { - // directive missing, so no restriction - return undefined; - } - return getRoleListFromDirective(directive, argName); -} - - -export function getCalcMutationOperatorsFromDirective(directive: DirectiveNode): CalcMutationOperator[] { - const arg = (directive.arguments || []).find(arg => arg.name.value == CALC_MUTATIONS_OPERATORS_ARG); - if (arg) { - return compact(getStringListValues(arg.value).map(operatorName => - CALC_MUTATIONS_OPERATORS.find(operator => operator.name === operatorName))); - } - - // if the directive is specified but an arg is missing, this default to [] (default to secure option) - return []; -} - -/** - * Determines whether a field is controlled by the system and can not be written directly by the user - * @param {GraphQLField} field - * @returns {boolean} - */ -export function isWriteProtectedSystemField(field: GraphQLField, parentType: GraphQLObjectType) { - if (!isTypeWithIdentity(parentType)) { - // value objects and extensions do not have system fields - return false; - } - // No need to protect ENTITY_UPDATED_AT because because it will be set - // to the correct value by the update logic anyway. - return field.name == ID_FIELD || field.name == ENTITY_CREATED_AT; -} - -export function getSingleKeyField(type: GraphQLObjectType): GraphQLField|undefined { - const keyFields = objectValues(type.getFields()).filter(field => isKeyField(field)); - if (keyFields.length != 1) { - return undefined; - } - return keyFields[0]; -} - -export function getReferenceKeyField(objectTypeWithKeyField: ObjectTypeDefinitionNode): string { - const field = objectTypeWithKeyField.fields.find(field => field.directives != undefined && field.directives.some(directive => directive.name.value === KEY_FIELD_DIRECTIVE)); - if (!field) { - throw new Error(`Missing @key directive on ${objectTypeWithKeyField.name.value}`); - } - return getTypeNameIgnoringNonNullAndList(field.type); -} - export function getNodeByName(listOfNodes: T[]|undefined, name: string): T|undefined { if (!listOfNodes) { return undefined; } return listOfNodes.find(node => node.name.value === name); } - -export function createObjectTypeNode(name: string): ObjectTypeDefinitionNode { - return { - kind: OBJECT_TYPE_DEFINITION, - name: buildNameNode(name), - fields: [] - }; -} - -export function createFieldWithDirective(name: string, typeName: string, directiveName: string): FieldDefinitionNode { - return { - kind: FIELD_DEFINITION, - name: buildNameNode(name), - type: { - kind: NAMED_TYPE, - name: buildNameNode(typeName) - }, - arguments: [], - directives: [ - { - kind: DIRECTIVE, - name: buildNameNode(directiveName), - arguments: [] - } - ] - }; -} - -/** - * walks one namespace part along the namespace path. Creates missing fields and types and returns the current node. - * - * @param ast - * @param {ObjectTypeDefinitionNode} currentNode - the starting node - * @param {string} namespacePart - one step of the namespace to walk - * @param baseOperationType "Query" or "Mutation" - * @returns {ObjectTypeDefinitionNode} - */ -export function enterOrCreateNextNamespacePart(ast: DocumentNode, currentNode: ObjectTypeDefinitionNode, namespacePart: string, baseOperationType: string): ObjectTypeDefinitionNode { - let namespacePartField = getNodeByName(currentNode.fields, namespacePart); - if (namespacePartField) { - // Hey, were done, this one already exists. Just search and return the corresponding type. - const arrivingNode = getNodeByName(getObjectTypes(ast), namespacedType(namespacePart, baseOperationType)); - if (!arrivingNode) { - throw Error(`Found path field ${namespacePart} but not corresponding type. That seems a bit strange...`); - } - return arrivingNode; - } else { - const arrivingNode = createObjectTypeNode(namespacedType(namespacePart, baseOperationType)); - ast.definitions.push(arrivingNode); - currentNode.fields.push(createFieldWithDirective(namespacePart, namespacedType(namespacePart, baseOperationType), NAMESPACE_FIELD_PATH_DIRECTIVE)); - return arrivingNode; - } -} - diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 4a683068..1539a5af 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -93,31 +93,6 @@ export function takeRandomSample(arr: ReadonlyArray): T|undefined { return arr[Math.floor(Math.random() * arr.length)]; } -/** - * Takes {@code count} samples randomly of the given array. The same sample may occur mulitple times - * @param arr the source population - * @param count the number of samples - * @returns the samples, or undefined if the input array is empty - */ -export function takeRandomSamples(arr: ReadonlyArray, count: number): T[]|undefined { - if (arr.length == 0) { - return undefined; - } - return range(count).map(() => takeRandomSample(arr)!); -} - -export function removeDuplicates(list: ReadonlyArray, keyFn: (item: T) => U): T[] { - const existingKeys = new Set(); - return list.filter(item => { - const key = keyFn(item); - if (existingKeys.has(key)) { - return false; - } - existingKeys.add(key); - return true; - }); -} - export function arrayToObject(array: ReadonlyArray, keyFn: (item: TValue, index: number) => string): { [name: string]: TValue } { const result: { [name: string]: TValue } = {}; for (let i = 0; i < array.length; i++) {