From 9b1c402fe584790b4d1504967640326c2f5233ff Mon Sep 17 00:00:00 2001 From: Michael Webb <28074382+mjfwebb@users.noreply.github.com> Date: Wed, 19 Jul 2023 13:06:11 +0200 Subject: [PATCH 01/22] Add directive annotations (#3615) * feat: add alias directive annotation * feat: add queryOptions directive annotation * feat: parse Kind.INT and Kind.Float into JS numbers * feat: add default directive annotation * feat: add coalesce directive annotation * feat: add customResolver annotation * feat: add ID directive annotation * refactor: use makeDirectiveNode in key-annotation tests * feat: add mutation directive annotation * feat: add plural directive annotation * feat: add filterable directive annotation * feat: add fulltext directive annotation * feat: add node directive annotation * feat: add populatedBy directive annotation * feat: add query directive annotation * feat: add private directive annotation * feat: add relationshipProperties annotation * feat: add selectable directive annotation * feat: add settable directive annotation * feat: add timestamp directive annotation * feat: add unique directive annotation * feat: add subscription directive annotation * feat: add jwt-claim directive annotation * feat: add jwt-payload directive annotation * feat: add directives to Annotation --------- Co-authored-by: MacondoExpress --- .../annotation/AliasAnnotation.test.ts | 29 +++ .../annotation/AliasAnnotation.ts | 26 +++ .../src/schema-model/annotation/Annotation.ts | 112 ++++++++- .../annotation/CoalesceAnnotation.ts | 28 +++ .../annotation/CustomResolverAnnotation.ts | 26 +++ .../annotation/DefaultAnnotation.ts | 28 +++ .../annotation/FilterableAnnotation.ts | 28 +++ .../annotation/FullTextAnnotation.ts | 33 +++ .../schema-model/annotation/IDAnnotation.ts | 30 +++ .../annotation/JWTClaimAnnotation.ts | 26 +++ .../annotation/JWTPayloadAnnotation.ts | 20 ++ .../annotation/MutationAnnotation.ts | 26 +++ .../schema-model/annotation/NodeAnnotation.ts | 28 +++ .../annotation/PluralAnnotation.ts | 26 +++ .../annotation/PopulatedByAnnotation.ts | 28 +++ .../annotation/PrivateAnnotation.ts | 20 ++ .../annotation/QueryAnnotation.ts | 28 +++ .../annotation/QueryOptionsAnnotation.ts | 31 +++ .../RelationshipPropertiesAnnotation.ts | 20 ++ .../annotation/SelectableAnnotation.ts | 28 +++ .../annotation/SettableAnnotation.ts | 28 +++ .../annotation/SubscriptionAnnotation.ts | 26 +++ .../annotation/TimestampAnnotation.ts | 26 +++ .../annotation/UniqueAnnotation.ts | 26 +++ .../parser/alias-annotation.test.ts | 29 +++ .../schema-model/parser/alias-annotation.ts | 38 +++ .../parser/coalesce-annotation.test.ts | 81 +++++++ .../parser/coalesce-annotation.ts | 49 ++++ .../parser/custom-resolver-annotation.test.ts | 30 +++ .../parser/custom-resolver-annotation.ts | 32 +++ .../parser/default-annotation.test.ts | 81 +++++++ .../schema-model/parser/default-annotation.ts | 49 ++++ .../parser/filterable-annotation.test.ts | 37 +++ .../parser/filterable-annotation.ts | 30 +++ .../parser/full-text-annotation.test.ts | 42 ++++ .../parser/full-text-annotation.ts | 30 +++ .../schema-model/parser/id-annotation.test.ts | 62 +++++ .../src/schema-model/parser/id-annotation.ts | 36 +++ .../parser/jwt-claim-annotation.test.ts | 31 +++ .../parser/jwt-claim-annotation.ts | 29 +++ .../parser/jwt-payload-annoatation.ts | 25 ++ .../parser/key-annotation.test.ts | 221 ++++-------------- .../parser/mutation-annotation.test.ts | 65 ++++++ .../parser/mutation-annotation.ts | 32 +++ .../parser/node-annotation.test.ts | 34 +++ .../schema-model/parser/node-annotation.ts | 30 +++ .../parser/plural-annotation.test.ts | 30 +++ .../schema-model/parser/plural-annotation.ts | 28 +++ .../parser/populated-by-annotation.test.ts | 34 +++ .../parser/populated-by-annotation.ts | 30 +++ .../schema-model/parser/private-annotation.ts | 25 ++ .../parser/query-annotation.test.ts | 56 +++++ .../schema-model/parser/query-annotation.ts | 31 +++ .../parser/query-options-annotation.test.ts | 76 ++++++ .../parser/query-options-annotation.ts | 50 ++++ .../relationship-properties-annotation.ts | 26 +++ .../parser/selectable-annotation.test.ts | 67 ++++++ .../parser/selectable-annotation.ts | 30 +++ .../parser/settable-annotation.test.ts | 67 ++++++ .../parser/settable-annotation.ts | 30 +++ .../parser/subscription-annotation.test.ts | 51 ++++ .../parser/subscription-annotation.ts | 29 +++ .../parser/timestamp-annotation.test.ts | 51 ++++ .../parser/timestamp-annotation.ts | 29 +++ .../parser/unique-annotation.test.ts | 31 +++ .../schema-model/parser/unique-annotation.ts | 29 +++ 66 files changed, 2441 insertions(+), 179 deletions(-) create mode 100644 packages/graphql/src/schema-model/annotation/AliasAnnotation.test.ts create mode 100644 packages/graphql/src/schema-model/annotation/AliasAnnotation.ts create mode 100644 packages/graphql/src/schema-model/annotation/CoalesceAnnotation.ts create mode 100644 packages/graphql/src/schema-model/annotation/CustomResolverAnnotation.ts create mode 100644 packages/graphql/src/schema-model/annotation/DefaultAnnotation.ts create mode 100644 packages/graphql/src/schema-model/annotation/FilterableAnnotation.ts create mode 100644 packages/graphql/src/schema-model/annotation/FullTextAnnotation.ts create mode 100644 packages/graphql/src/schema-model/annotation/IDAnnotation.ts create mode 100644 packages/graphql/src/schema-model/annotation/JWTClaimAnnotation.ts create mode 100644 packages/graphql/src/schema-model/annotation/JWTPayloadAnnotation.ts create mode 100644 packages/graphql/src/schema-model/annotation/MutationAnnotation.ts create mode 100644 packages/graphql/src/schema-model/annotation/NodeAnnotation.ts create mode 100644 packages/graphql/src/schema-model/annotation/PluralAnnotation.ts create mode 100644 packages/graphql/src/schema-model/annotation/PopulatedByAnnotation.ts create mode 100644 packages/graphql/src/schema-model/annotation/PrivateAnnotation.ts create mode 100644 packages/graphql/src/schema-model/annotation/QueryAnnotation.ts create mode 100644 packages/graphql/src/schema-model/annotation/QueryOptionsAnnotation.ts create mode 100644 packages/graphql/src/schema-model/annotation/RelationshipPropertiesAnnotation.ts create mode 100644 packages/graphql/src/schema-model/annotation/SelectableAnnotation.ts create mode 100644 packages/graphql/src/schema-model/annotation/SettableAnnotation.ts create mode 100644 packages/graphql/src/schema-model/annotation/SubscriptionAnnotation.ts create mode 100644 packages/graphql/src/schema-model/annotation/TimestampAnnotation.ts create mode 100644 packages/graphql/src/schema-model/annotation/UniqueAnnotation.ts create mode 100644 packages/graphql/src/schema-model/parser/alias-annotation.test.ts create mode 100644 packages/graphql/src/schema-model/parser/alias-annotation.ts create mode 100644 packages/graphql/src/schema-model/parser/coalesce-annotation.test.ts create mode 100644 packages/graphql/src/schema-model/parser/coalesce-annotation.ts create mode 100644 packages/graphql/src/schema-model/parser/custom-resolver-annotation.test.ts create mode 100644 packages/graphql/src/schema-model/parser/custom-resolver-annotation.ts create mode 100644 packages/graphql/src/schema-model/parser/default-annotation.test.ts create mode 100644 packages/graphql/src/schema-model/parser/default-annotation.ts create mode 100644 packages/graphql/src/schema-model/parser/filterable-annotation.test.ts create mode 100644 packages/graphql/src/schema-model/parser/filterable-annotation.ts create mode 100644 packages/graphql/src/schema-model/parser/full-text-annotation.test.ts create mode 100644 packages/graphql/src/schema-model/parser/full-text-annotation.ts create mode 100644 packages/graphql/src/schema-model/parser/id-annotation.test.ts create mode 100644 packages/graphql/src/schema-model/parser/id-annotation.ts create mode 100644 packages/graphql/src/schema-model/parser/jwt-claim-annotation.test.ts create mode 100644 packages/graphql/src/schema-model/parser/jwt-claim-annotation.ts create mode 100644 packages/graphql/src/schema-model/parser/jwt-payload-annoatation.ts create mode 100644 packages/graphql/src/schema-model/parser/mutation-annotation.test.ts create mode 100644 packages/graphql/src/schema-model/parser/mutation-annotation.ts create mode 100644 packages/graphql/src/schema-model/parser/node-annotation.test.ts create mode 100644 packages/graphql/src/schema-model/parser/node-annotation.ts create mode 100644 packages/graphql/src/schema-model/parser/plural-annotation.test.ts create mode 100644 packages/graphql/src/schema-model/parser/plural-annotation.ts create mode 100644 packages/graphql/src/schema-model/parser/populated-by-annotation.test.ts create mode 100644 packages/graphql/src/schema-model/parser/populated-by-annotation.ts create mode 100644 packages/graphql/src/schema-model/parser/private-annotation.ts create mode 100644 packages/graphql/src/schema-model/parser/query-annotation.test.ts create mode 100644 packages/graphql/src/schema-model/parser/query-annotation.ts create mode 100644 packages/graphql/src/schema-model/parser/query-options-annotation.test.ts create mode 100644 packages/graphql/src/schema-model/parser/query-options-annotation.ts create mode 100644 packages/graphql/src/schema-model/parser/relationship-properties-annotation.ts create mode 100644 packages/graphql/src/schema-model/parser/selectable-annotation.test.ts create mode 100644 packages/graphql/src/schema-model/parser/selectable-annotation.ts create mode 100644 packages/graphql/src/schema-model/parser/settable-annotation.test.ts create mode 100644 packages/graphql/src/schema-model/parser/settable-annotation.ts create mode 100644 packages/graphql/src/schema-model/parser/subscription-annotation.test.ts create mode 100644 packages/graphql/src/schema-model/parser/subscription-annotation.ts create mode 100644 packages/graphql/src/schema-model/parser/timestamp-annotation.test.ts create mode 100644 packages/graphql/src/schema-model/parser/timestamp-annotation.ts create mode 100644 packages/graphql/src/schema-model/parser/unique-annotation.test.ts create mode 100644 packages/graphql/src/schema-model/parser/unique-annotation.ts diff --git a/packages/graphql/src/schema-model/annotation/AliasAnnotation.test.ts b/packages/graphql/src/schema-model/annotation/AliasAnnotation.test.ts new file mode 100644 index 0000000000..0d2796a434 --- /dev/null +++ b/packages/graphql/src/schema-model/annotation/AliasAnnotation.test.ts @@ -0,0 +1,29 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AliasAnnotation } from "./AliasAnnotation"; + +describe("AliasAnnotation", () => { + it("initialize class correctly when property param is set", () => { + const aliasAnnotation = new AliasAnnotation({ + property: "test", + }); + expect(aliasAnnotation.property).toBe("test"); + }); +}); diff --git a/packages/graphql/src/schema-model/annotation/AliasAnnotation.ts b/packages/graphql/src/schema-model/annotation/AliasAnnotation.ts new file mode 100644 index 0000000000..ed65256827 --- /dev/null +++ b/packages/graphql/src/schema-model/annotation/AliasAnnotation.ts @@ -0,0 +1,26 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class AliasAnnotation { + public readonly property: string; + + constructor({ property }: { property: string }) { + this.property = property; + } +} diff --git a/packages/graphql/src/schema-model/annotation/Annotation.ts b/packages/graphql/src/schema-model/annotation/Annotation.ts index f54263cb1a..27ce2192f2 100644 --- a/packages/graphql/src/schema-model/annotation/Annotation.ts +++ b/packages/graphql/src/schema-model/annotation/Annotation.ts @@ -17,18 +17,62 @@ * limitations under the License. */ +import { AliasAnnotation } from "./AliasAnnotation"; import { AuthenticationAnnotation } from "./AuthenticationAnnotation"; import { AuthorizationAnnotation } from "./AuthorizationAnnotation"; +import { CoalesceAnnotation } from "./CoalesceAnnotation"; +import { CustomResolverAnnotation } from "./CustomResolverAnnotation"; import { CypherAnnotation } from "./CypherAnnotation"; +import { DefaultAnnotation } from "./DefaultAnnotation"; +import { FilterableAnnotation } from "./FilterableAnnotation"; +import { FullTextAnnotation } from "./FullTextAnnotation"; +import { IDAnnotation } from "./IDAnnotation"; +import { JWTClaimAnnotation } from "./JWTClaimAnnotation"; +import { JWTPayloadAnnotation } from "./JWTPayloadAnnotation"; import { KeyAnnotation } from "./KeyAnnotation"; +import { MutationAnnotation } from "./MutationAnnotation"; +import { NodeAnnotation } from "./NodeAnnotation"; +import { PluralAnnotation } from "./PluralAnnotation"; +import { PopulatedByAnnotation } from "./PopulatedByAnnotation"; +import { PrivateAnnotation } from "./PrivateAnnotation"; +import { QueryAnnotation } from "./QueryAnnotation"; +import { QueryOptionsAnnotation } from "./QueryOptionsAnnotation"; +import { RelationshipPropertiesAnnotation } from "./RelationshipPropertiesAnnotation"; +import { SelectableAnnotation } from "./SelectableAnnotation"; +import { SettableAnnotation } from "./SettableAnnotation"; +import { SubscriptionAnnotation } from "./SubscriptionAnnotation"; import { SubscriptionsAuthorizationAnnotation } from "./SubscriptionsAuthorizationAnnotation"; +import { TimestampAnnotation } from "./TimestampAnnotation"; +import { UniqueAnnotation } from "./UniqueAnnotation"; export type Annotation = | CypherAnnotation | AuthorizationAnnotation | AuthenticationAnnotation | KeyAnnotation - | SubscriptionsAuthorizationAnnotation; + | SubscriptionsAuthorizationAnnotation + | AliasAnnotation + | QueryOptionsAnnotation + | DefaultAnnotation + | CoalesceAnnotation + | CustomResolverAnnotation + | IDAnnotation + | MutationAnnotation + | PluralAnnotation + | FilterableAnnotation + | FullTextAnnotation + | NodeAnnotation + | PopulatedByAnnotation + | QueryAnnotation + | PrivateAnnotation + | RelationshipPropertiesAnnotation + | SelectableAnnotation + | SettableAnnotation + | TimestampAnnotation + | UniqueAnnotation + | SubscriptionAnnotation + | JWTClaimAnnotation + | JWTPayloadAnnotation; export enum AnnotationsKey { cypher = "cypher", @@ -36,6 +80,28 @@ export enum AnnotationsKey { authentication = "authentication", key = "key", subscriptionsAuthorization = "subscriptionsAuthorization", + alias = "alias", + queryOptions = "queryOptions", + default = "default", + coalesce = "coalesce", + customResolver = "customResolver", + id = "id", + mutation = "mutation", + plural = "plural", + filterable = "filterable", + fulltext = "fulltext", + node = "node", + populatedBy = "populatedBy", + query = "query", + private = "private", + relationshipProperties = "relationshipProperties", + selectable = "selectable", + settable = "settable", + timestamp = "timestamp", + unique = "unique", + subscription = "subscription", + jwtClaim = "jwtClaim", + jwtPayload = "jwtPayload", } export type Annotations = { @@ -44,6 +110,28 @@ export type Annotations = { [AnnotationsKey.authentication]: AuthenticationAnnotation; [AnnotationsKey.key]: KeyAnnotation; [AnnotationsKey.subscriptionsAuthorization]: SubscriptionsAuthorizationAnnotation; + [AnnotationsKey.alias]: AliasAnnotation; + [AnnotationsKey.queryOptions]: QueryOptionsAnnotation; + [AnnotationsKey.default]: DefaultAnnotation; + [AnnotationsKey.coalesce]: CoalesceAnnotation; + [AnnotationsKey.customResolver]: CustomResolverAnnotation; + [AnnotationsKey.id]: IDAnnotation; + [AnnotationsKey.mutation]: MutationAnnotation; + [AnnotationsKey.plural]: PluralAnnotation; + [AnnotationsKey.filterable]: FilterableAnnotation; + [AnnotationsKey.fulltext]: FullTextAnnotation; + [AnnotationsKey.node]: NodeAnnotation; + [AnnotationsKey.populatedBy]: PopulatedByAnnotation; + [AnnotationsKey.query]: QueryAnnotation; + [AnnotationsKey.private]: PrivateAnnotation; + [AnnotationsKey.relationshipProperties]: RelationshipPropertiesAnnotation; + [AnnotationsKey.selectable]: SelectableAnnotation; + [AnnotationsKey.settable]: SettableAnnotation; + [AnnotationsKey.timestamp]: TimestampAnnotation; + [AnnotationsKey.unique]: UniqueAnnotation; + [AnnotationsKey.subscription]: SubscriptionAnnotation; + [AnnotationsKey.jwtClaim]: JWTClaimAnnotation; + [AnnotationsKey.jwtPayload]: JWTPayloadAnnotation; }; export function annotationToKey(ann: Annotation): keyof Annotations { @@ -52,5 +140,27 @@ export function annotationToKey(ann: Annotation): keyof Annotations { if (ann instanceof AuthenticationAnnotation) return AnnotationsKey.authentication; if (ann instanceof KeyAnnotation) return AnnotationsKey.key; if (ann instanceof SubscriptionsAuthorizationAnnotation) return AnnotationsKey.subscriptionsAuthorization; + if (ann instanceof AliasAnnotation) return AnnotationsKey.alias; + if (ann instanceof QueryOptionsAnnotation) return AnnotationsKey.queryOptions; + if (ann instanceof DefaultAnnotation) return AnnotationsKey.default; + if (ann instanceof CoalesceAnnotation) return AnnotationsKey.coalesce; + if (ann instanceof CustomResolverAnnotation) return AnnotationsKey.customResolver; + if (ann instanceof IDAnnotation) return AnnotationsKey.id; + if (ann instanceof MutationAnnotation) return AnnotationsKey.mutation; + if (ann instanceof PluralAnnotation) return AnnotationsKey.plural; + if (ann instanceof FilterableAnnotation) return AnnotationsKey.filterable; + if (ann instanceof FullTextAnnotation) return AnnotationsKey.fulltext; + if (ann instanceof NodeAnnotation) return AnnotationsKey.node; + if (ann instanceof PopulatedByAnnotation) return AnnotationsKey.populatedBy; + if (ann instanceof QueryAnnotation) return AnnotationsKey.query; + if (ann instanceof PrivateAnnotation) return AnnotationsKey.private; + if (ann instanceof RelationshipPropertiesAnnotation) return AnnotationsKey.relationshipProperties; + if (ann instanceof SelectableAnnotation) return AnnotationsKey.selectable; + if (ann instanceof SettableAnnotation) return AnnotationsKey.settable; + if (ann instanceof TimestampAnnotation) return AnnotationsKey.timestamp; + if (ann instanceof UniqueAnnotation) return AnnotationsKey.unique; + if (ann instanceof SubscriptionAnnotation) return AnnotationsKey.subscription; + if (ann instanceof JWTClaimAnnotation) return AnnotationsKey.jwtClaim; + if (ann instanceof JWTPayloadAnnotation) return AnnotationsKey.jwtPayload; throw new Error("annotation not known"); } diff --git a/packages/graphql/src/schema-model/annotation/CoalesceAnnotation.ts b/packages/graphql/src/schema-model/annotation/CoalesceAnnotation.ts new file mode 100644 index 0000000000..0a7879fece --- /dev/null +++ b/packages/graphql/src/schema-model/annotation/CoalesceAnnotation.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type CoalesceAnnotationValue = string | number | boolean; + +export class CoalesceAnnotation { + public readonly value: CoalesceAnnotationValue; + + constructor({ value }: { value: CoalesceAnnotationValue }) { + this.value = value; + } +} diff --git a/packages/graphql/src/schema-model/annotation/CustomResolverAnnotation.ts b/packages/graphql/src/schema-model/annotation/CustomResolverAnnotation.ts new file mode 100644 index 0000000000..4f5a71b2cf --- /dev/null +++ b/packages/graphql/src/schema-model/annotation/CustomResolverAnnotation.ts @@ -0,0 +1,26 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class CustomResolverAnnotation { + public readonly requires: string[]; + + constructor({ requires }: { requires: string[] }) { + this.requires = requires; + } +} diff --git a/packages/graphql/src/schema-model/annotation/DefaultAnnotation.ts b/packages/graphql/src/schema-model/annotation/DefaultAnnotation.ts new file mode 100644 index 0000000000..502c893e22 --- /dev/null +++ b/packages/graphql/src/schema-model/annotation/DefaultAnnotation.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type DefaultAnnotationValue = string | number | boolean; + +export class DefaultAnnotation { + public readonly value: DefaultAnnotationValue; + + constructor({ value }: { value: DefaultAnnotationValue }) { + this.value = value; + } +} diff --git a/packages/graphql/src/schema-model/annotation/FilterableAnnotation.ts b/packages/graphql/src/schema-model/annotation/FilterableAnnotation.ts new file mode 100644 index 0000000000..a5617b0148 --- /dev/null +++ b/packages/graphql/src/schema-model/annotation/FilterableAnnotation.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class FilterableAnnotation { + public readonly byValue: boolean; + public readonly byAnnotation: boolean; + + constructor({ byValue, byAnnotation }: { byValue: boolean; byAnnotation: boolean }) { + this.byValue = byValue; + this.byAnnotation = byAnnotation; + } +} diff --git a/packages/graphql/src/schema-model/annotation/FullTextAnnotation.ts b/packages/graphql/src/schema-model/annotation/FullTextAnnotation.ts new file mode 100644 index 0000000000..02e92b7ea6 --- /dev/null +++ b/packages/graphql/src/schema-model/annotation/FullTextAnnotation.ts @@ -0,0 +1,33 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type FullTextFields = { + name: string; + fields: string[]; + queryName: string; + indexName: string; +}; + +export class FullTextAnnotation { + public readonly fields: FullTextFields; + + constructor({ fields }: { fields: FullTextFields }) { + this.fields = fields; + } +} diff --git a/packages/graphql/src/schema-model/annotation/IDAnnotation.ts b/packages/graphql/src/schema-model/annotation/IDAnnotation.ts new file mode 100644 index 0000000000..0f058831a6 --- /dev/null +++ b/packages/graphql/src/schema-model/annotation/IDAnnotation.ts @@ -0,0 +1,30 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class IDAnnotation { + public readonly autogenerate: boolean; + public readonly unique: boolean; + public readonly global: boolean; + + constructor({ autogenerate, unique, global }: { autogenerate: boolean; unique: boolean; global: boolean }) { + this.autogenerate = autogenerate; + this.unique = unique; + this.global = global; + } +} diff --git a/packages/graphql/src/schema-model/annotation/JWTClaimAnnotation.ts b/packages/graphql/src/schema-model/annotation/JWTClaimAnnotation.ts new file mode 100644 index 0000000000..fd751d4bcb --- /dev/null +++ b/packages/graphql/src/schema-model/annotation/JWTClaimAnnotation.ts @@ -0,0 +1,26 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class JWTClaimAnnotation { + public readonly path: string; + + constructor({ path }: { path: string }) { + this.path = path; + } +} diff --git a/packages/graphql/src/schema-model/annotation/JWTPayloadAnnotation.ts b/packages/graphql/src/schema-model/annotation/JWTPayloadAnnotation.ts new file mode 100644 index 0000000000..62590de76e --- /dev/null +++ b/packages/graphql/src/schema-model/annotation/JWTPayloadAnnotation.ts @@ -0,0 +1,20 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class JWTPayloadAnnotation {} diff --git a/packages/graphql/src/schema-model/annotation/MutationAnnotation.ts b/packages/graphql/src/schema-model/annotation/MutationAnnotation.ts new file mode 100644 index 0000000000..4683bc9727 --- /dev/null +++ b/packages/graphql/src/schema-model/annotation/MutationAnnotation.ts @@ -0,0 +1,26 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class MutationAnnotation { + public readonly operations: string[]; + + constructor({ operations }: { operations: string[] }) { + this.operations = operations; + } +} diff --git a/packages/graphql/src/schema-model/annotation/NodeAnnotation.ts b/packages/graphql/src/schema-model/annotation/NodeAnnotation.ts new file mode 100644 index 0000000000..37e9888a53 --- /dev/null +++ b/packages/graphql/src/schema-model/annotation/NodeAnnotation.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class NodeAnnotation { + public readonly labels: string[]; + public readonly label: string; + + constructor({ labels, label }: { labels: string[]; label: string }) { + this.labels = labels; + this.label = label; + } +} diff --git a/packages/graphql/src/schema-model/annotation/PluralAnnotation.ts b/packages/graphql/src/schema-model/annotation/PluralAnnotation.ts new file mode 100644 index 0000000000..92101c33af --- /dev/null +++ b/packages/graphql/src/schema-model/annotation/PluralAnnotation.ts @@ -0,0 +1,26 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class PluralAnnotation { + public readonly value: string; + + constructor({ value }: { value: string }) { + this.value = value; + } +} diff --git a/packages/graphql/src/schema-model/annotation/PopulatedByAnnotation.ts b/packages/graphql/src/schema-model/annotation/PopulatedByAnnotation.ts new file mode 100644 index 0000000000..46b9419495 --- /dev/null +++ b/packages/graphql/src/schema-model/annotation/PopulatedByAnnotation.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class PopulatedByAnnotation { + public readonly callback: string; + public readonly operations: string[]; + + constructor({ callback, operations }: { callback: string; operations: string[] }) { + this.callback = callback; + this.operations = operations; + } +} diff --git a/packages/graphql/src/schema-model/annotation/PrivateAnnotation.ts b/packages/graphql/src/schema-model/annotation/PrivateAnnotation.ts new file mode 100644 index 0000000000..aee68d9f11 --- /dev/null +++ b/packages/graphql/src/schema-model/annotation/PrivateAnnotation.ts @@ -0,0 +1,20 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class PrivateAnnotation {} diff --git a/packages/graphql/src/schema-model/annotation/QueryAnnotation.ts b/packages/graphql/src/schema-model/annotation/QueryAnnotation.ts new file mode 100644 index 0000000000..046fd7ae3d --- /dev/null +++ b/packages/graphql/src/schema-model/annotation/QueryAnnotation.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class QueryAnnotation { + public readonly read: boolean; + public readonly aggregate: boolean; + + constructor({ read, aggregate }: { read: boolean; aggregate: boolean }) { + this.read = read; + this.aggregate = aggregate; + } +} diff --git a/packages/graphql/src/schema-model/annotation/QueryOptionsAnnotation.ts b/packages/graphql/src/schema-model/annotation/QueryOptionsAnnotation.ts new file mode 100644 index 0000000000..840a3deb08 --- /dev/null +++ b/packages/graphql/src/schema-model/annotation/QueryOptionsAnnotation.ts @@ -0,0 +1,31 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +type QueryOptionsLimit = { + default?: number; + max?: number; +}; + +export class QueryOptionsAnnotation { + public readonly limit: QueryOptionsLimit; + + constructor({ limit }: { limit: QueryOptionsLimit }) { + this.limit = limit; + } +} diff --git a/packages/graphql/src/schema-model/annotation/RelationshipPropertiesAnnotation.ts b/packages/graphql/src/schema-model/annotation/RelationshipPropertiesAnnotation.ts new file mode 100644 index 0000000000..79bacffef2 --- /dev/null +++ b/packages/graphql/src/schema-model/annotation/RelationshipPropertiesAnnotation.ts @@ -0,0 +1,20 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class RelationshipPropertiesAnnotation {} diff --git a/packages/graphql/src/schema-model/annotation/SelectableAnnotation.ts b/packages/graphql/src/schema-model/annotation/SelectableAnnotation.ts new file mode 100644 index 0000000000..25bab6f83e --- /dev/null +++ b/packages/graphql/src/schema-model/annotation/SelectableAnnotation.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class SelectableAnnotation { + public readonly onRead: boolean; + public readonly onAggregate: boolean; + + constructor({ onRead, onAggregate }: { onRead: boolean; onAggregate: boolean }) { + this.onRead = onRead; + this.onAggregate = onAggregate; + } +} diff --git a/packages/graphql/src/schema-model/annotation/SettableAnnotation.ts b/packages/graphql/src/schema-model/annotation/SettableAnnotation.ts new file mode 100644 index 0000000000..4c04cb169c --- /dev/null +++ b/packages/graphql/src/schema-model/annotation/SettableAnnotation.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class SettableAnnotation { + public readonly onCreate: boolean; + public readonly onUpdate: boolean; + + constructor({ onCreate, onUpdate }: { onCreate: boolean; onUpdate: boolean }) { + this.onCreate = onCreate; + this.onUpdate = onUpdate; + } +} diff --git a/packages/graphql/src/schema-model/annotation/SubscriptionAnnotation.ts b/packages/graphql/src/schema-model/annotation/SubscriptionAnnotation.ts new file mode 100644 index 0000000000..e400df4fa0 --- /dev/null +++ b/packages/graphql/src/schema-model/annotation/SubscriptionAnnotation.ts @@ -0,0 +1,26 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class SubscriptionAnnotation { + public readonly operations: string[]; + + constructor({ operations }: { operations: string[] }) { + this.operations = operations; + } +} diff --git a/packages/graphql/src/schema-model/annotation/TimestampAnnotation.ts b/packages/graphql/src/schema-model/annotation/TimestampAnnotation.ts new file mode 100644 index 0000000000..54c53512cd --- /dev/null +++ b/packages/graphql/src/schema-model/annotation/TimestampAnnotation.ts @@ -0,0 +1,26 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class TimestampAnnotation { + public readonly operations: string[]; + + constructor({ operations }: { operations: string[] }) { + this.operations = operations; + } +} diff --git a/packages/graphql/src/schema-model/annotation/UniqueAnnotation.ts b/packages/graphql/src/schema-model/annotation/UniqueAnnotation.ts new file mode 100644 index 0000000000..8fb475a2a7 --- /dev/null +++ b/packages/graphql/src/schema-model/annotation/UniqueAnnotation.ts @@ -0,0 +1,26 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class UniqueAnnotation { + public readonly constraintName: string; + + constructor({ constraintName }: { constraintName: string }) { + this.constraintName = constraintName; + } +} diff --git a/packages/graphql/src/schema-model/parser/alias-annotation.test.ts b/packages/graphql/src/schema-model/parser/alias-annotation.test.ts new file mode 100644 index 0000000000..ecc499420e --- /dev/null +++ b/packages/graphql/src/schema-model/parser/alias-annotation.test.ts @@ -0,0 +1,29 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { makeDirectiveNode } from "@graphql-tools/utils"; +import { parseAliasAnnotation } from "./alias-annotation"; + +describe("parseAliasAnnotation", () => { + it("should parse correctly", () => { + const directive = makeDirectiveNode("alias", { property: "dbId" }); + const aliasAnnotation = parseAliasAnnotation(directive); + expect(aliasAnnotation.property).toBe("dbId"); + }); +}); diff --git a/packages/graphql/src/schema-model/parser/alias-annotation.ts b/packages/graphql/src/schema-model/parser/alias-annotation.ts new file mode 100644 index 0000000000..2b8048a81a --- /dev/null +++ b/packages/graphql/src/schema-model/parser/alias-annotation.ts @@ -0,0 +1,38 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { DirectiveNode } from "graphql"; +import { Neo4jGraphQLSchemaValidationError } from "../../classes"; +import { AliasAnnotation } from "../annotation/AliasAnnotation"; +import { parseArguments } from "./utils"; + +export function parseAliasAnnotation(directive: DirectiveNode): AliasAnnotation { + const { property, ...unrecognizedArguments } = parseArguments(directive) as { + property: string; + }; + + if (Object.keys(unrecognizedArguments).length) { + throw new Neo4jGraphQLSchemaValidationError( + `@alias unrecognized arguments: ${Object.keys(unrecognizedArguments).join(", ")}` + ); + } + + return new AliasAnnotation({ + property, + }); +} diff --git a/packages/graphql/src/schema-model/parser/coalesce-annotation.test.ts b/packages/graphql/src/schema-model/parser/coalesce-annotation.test.ts new file mode 100644 index 0000000000..0ca137158a --- /dev/null +++ b/packages/graphql/src/schema-model/parser/coalesce-annotation.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { makeDirectiveNode } from "@graphql-tools/utils"; +import type { DirectiveNode } from "graphql"; +import { Kind } from "graphql"; +import { parseCoalesceAnnotation } from "./coalesce-annotation"; + +describe("parseCoalesceAnnotation", () => { + it("should parse correctly with string coalesce value", () => { + const directive = makeDirectiveNode("coalesce", { value: "myCoalesceValue" }); + const coalesceAnnotation = parseCoalesceAnnotation(directive); + expect(coalesceAnnotation.value).toBe("myCoalesceValue"); + }); + it("should parse correctly with int coalesce value", () => { + const directive = makeDirectiveNode("coalesce", { value: 25 }); + const coalesceAnnotation = parseCoalesceAnnotation(directive); + expect(coalesceAnnotation.value).toBe(25); + }); + it("should parse correctly with float coalesce value", () => { + const directive = makeDirectiveNode("coalesce", { value: 25.5 }); + const coalesceAnnotation = parseCoalesceAnnotation(directive); + expect(coalesceAnnotation.value).toBe(25.5); + }); + it("should parse correctly with boolean coalesce value", () => { + const directive = makeDirectiveNode("coalesce", { value: true }); + const coalesceAnnotation = parseCoalesceAnnotation(directive); + expect(coalesceAnnotation.value).toBe(true); + }); + it("should parse correctly with enum coalesce value", () => { + const directive: DirectiveNode = { + kind: Kind.DIRECTIVE, + name: { + kind: Kind.NAME, + value: "coalesce", + }, + arguments: [ + { + kind: Kind.ARGUMENT, + name: { + kind: Kind.NAME, + value: "value", + }, + value: { + kind: Kind.ENUM, + value: "myEnumValue", + }, + }, + ], + }; + const coalesceAnnotation = parseCoalesceAnnotation(directive); + expect(coalesceAnnotation.value).toBe("myEnumValue"); + }); + it("should throw error if no value is provided", () => { + const directive: DirectiveNode = { + kind: Kind.DIRECTIVE, + name: { + kind: Kind.NAME, + value: "coalesce", + }, + arguments: [], + }; + expect(() => parseCoalesceAnnotation(directive)).toThrow("@coalesce directive must have a value"); + }); +}); diff --git a/packages/graphql/src/schema-model/parser/coalesce-annotation.ts b/packages/graphql/src/schema-model/parser/coalesce-annotation.ts new file mode 100644 index 0000000000..57a9d3c61d --- /dev/null +++ b/packages/graphql/src/schema-model/parser/coalesce-annotation.ts @@ -0,0 +1,49 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Kind, type DirectiveNode } from "graphql"; +import { Neo4jGraphQLSchemaValidationError } from "../../classes"; +import type { CoalesceAnnotationValue } from "../annotation/CoalesceAnnotation"; +import { CoalesceAnnotation } from "../annotation/CoalesceAnnotation"; +import { getArgumentValueByType } from "./utils"; + +export function parseCoalesceAnnotation(directive: DirectiveNode): CoalesceAnnotation { + if (!directive.arguments || !directive.arguments[0] || !directive.arguments[0].value.kind) { + throw new Error("@coalesce directive must have a value"); + } + + let value: CoalesceAnnotationValue; + switch (directive.arguments[0].value.kind) { + case Kind.ENUM: + case Kind.STRING: + case Kind.BOOLEAN: + case Kind.INT: + case Kind.FLOAT: + value = getArgumentValueByType(directive.arguments[0].value) as CoalesceAnnotationValue; + break; + default: + throw new Neo4jGraphQLSchemaValidationError( + "@coalesce directive can only be used on types: Int | Float | String | Boolean | ID | DateTime | Enum" + ); + } + + return new CoalesceAnnotation({ + value, + }); +} diff --git a/packages/graphql/src/schema-model/parser/custom-resolver-annotation.test.ts b/packages/graphql/src/schema-model/parser/custom-resolver-annotation.test.ts new file mode 100644 index 0000000000..d0ebb8220b --- /dev/null +++ b/packages/graphql/src/schema-model/parser/custom-resolver-annotation.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { makeDirectiveNode } from "@graphql-tools/utils"; +import type { DirectiveNode } from "graphql"; +import { parseCustomResolverAnnotation } from "./custom-resolver-annotation"; + +describe("parseCustomResolverAnnotation", () => { + it("should parse correctly", () => { + const directive: DirectiveNode = makeDirectiveNode("customResolver", { requires: ["firstName", "lastName"] }); + const customResolverAnnotation = parseCustomResolverAnnotation(directive); + expect(customResolverAnnotation.requires).toEqual(["firstName", "lastName"]); + }); +}); diff --git a/packages/graphql/src/schema-model/parser/custom-resolver-annotation.ts b/packages/graphql/src/schema-model/parser/custom-resolver-annotation.ts new file mode 100644 index 0000000000..5f545f0d9a --- /dev/null +++ b/packages/graphql/src/schema-model/parser/custom-resolver-annotation.ts @@ -0,0 +1,32 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { DirectiveNode } from "graphql"; +import { Neo4jGraphQLSchemaValidationError } from "../../classes"; +import { CustomResolverAnnotation } from "../annotation/CustomResolverAnnotation"; +import { parseArguments } from "./utils"; + +export function parseCustomResolverAnnotation(directive: DirectiveNode): CustomResolverAnnotation { + const { requires } = parseArguments(directive); + if (!Array.isArray(requires)) { + throw new Neo4jGraphQLSchemaValidationError("@customResolver requires must be an array"); + } + return new CustomResolverAnnotation({ + requires, + }); +} diff --git a/packages/graphql/src/schema-model/parser/default-annotation.test.ts b/packages/graphql/src/schema-model/parser/default-annotation.test.ts new file mode 100644 index 0000000000..b3fd66774a --- /dev/null +++ b/packages/graphql/src/schema-model/parser/default-annotation.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { makeDirectiveNode } from "@graphql-tools/utils"; +import type { DirectiveNode } from "graphql"; +import { Kind } from "graphql"; +import { parseDefaultAnnotation } from "./default-annotation"; + +describe("parseDefaultAnnotation", () => { + it("should parse correctly with string default value", () => { + const directive = makeDirectiveNode("default", { value: "myDefaultValue" }); + const defaultAnnotation = parseDefaultAnnotation(directive); + expect(defaultAnnotation.value).toBe("myDefaultValue"); + }); + it("should parse correctly with int default value", () => { + const directive = makeDirectiveNode("default", { value: 25 }); + const defaultAnnotation = parseDefaultAnnotation(directive); + expect(defaultAnnotation.value).toBe(25); + }); + it("should parse correctly with float default value", () => { + const directive = makeDirectiveNode("default", { value: 25.5 }); + const defaultAnnotation = parseDefaultAnnotation(directive); + expect(defaultAnnotation.value).toBe(25.5); + }); + it("should parse correctly with boolean default value", () => { + const directive = makeDirectiveNode("default", { value: true }); + const defaultAnnotation = parseDefaultAnnotation(directive); + expect(defaultAnnotation.value).toBe(true); + }); + it("should parse correctly with enum default value", () => { + const directive: DirectiveNode = { + kind: Kind.DIRECTIVE, + name: { + kind: Kind.NAME, + value: "default", + }, + arguments: [ + { + kind: Kind.ARGUMENT, + name: { + kind: Kind.NAME, + value: "value", + }, + value: { + kind: Kind.ENUM, + value: "myEnumValue", + }, + }, + ], + }; + const defaultAnnotation = parseDefaultAnnotation(directive); + expect(defaultAnnotation.value).toBe("myEnumValue"); + }); + it("should throw error if no value is provided", () => { + const directive: DirectiveNode = { + kind: Kind.DIRECTIVE, + name: { + kind: Kind.NAME, + value: "default", + }, + arguments: [], + }; + expect(() => parseDefaultAnnotation(directive)).toThrow("@default directive must have a value"); + }); +}); diff --git a/packages/graphql/src/schema-model/parser/default-annotation.ts b/packages/graphql/src/schema-model/parser/default-annotation.ts new file mode 100644 index 0000000000..ca4a4a6929 --- /dev/null +++ b/packages/graphql/src/schema-model/parser/default-annotation.ts @@ -0,0 +1,49 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Kind, type DirectiveNode } from "graphql"; +import { Neo4jGraphQLSchemaValidationError } from "../../classes"; +import type { DefaultAnnotationValue } from "../annotation/DefaultAnnotation"; +import { DefaultAnnotation } from "../annotation/DefaultAnnotation"; +import { getArgumentValueByType } from "./utils"; + +export function parseDefaultAnnotation(directive: DirectiveNode): DefaultAnnotation { + if (!directive.arguments || !directive.arguments[0] || !directive.arguments[0].value.kind) { + throw new Error("@default directive must have a value"); + } + + let value: DefaultAnnotationValue; + switch (directive.arguments[0].value.kind) { + case Kind.ENUM: + case Kind.STRING: + case Kind.BOOLEAN: + case Kind.INT: + case Kind.FLOAT: + value = getArgumentValueByType(directive.arguments[0].value) as DefaultAnnotationValue; + break; + default: + throw new Neo4jGraphQLSchemaValidationError( + "@default directive can only be used on types: Int | Float | String | Boolean | ID | DateTime | Enum" + ); + } + + return new DefaultAnnotation({ + value, + }); +} diff --git a/packages/graphql/src/schema-model/parser/filterable-annotation.test.ts b/packages/graphql/src/schema-model/parser/filterable-annotation.test.ts new file mode 100644 index 0000000000..c875180f9c --- /dev/null +++ b/packages/graphql/src/schema-model/parser/filterable-annotation.test.ts @@ -0,0 +1,37 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { makeDirectiveNode } from "@graphql-tools/utils"; +import type { DirectiveNode } from "graphql"; +import { parseFilterableAnnotation } from "./filterable-annotation"; + +describe("parseFilterableAnnotation", () => { + it("should parse correctly when byValue is set to true and byAnnotation is set to false", () => { + const directive: DirectiveNode = makeDirectiveNode("filterable", { byValue: true, byAnnotation: false }); + const filterableAnnotation = parseFilterableAnnotation(directive); + expect(filterableAnnotation.byValue).toBe(true); + expect(filterableAnnotation.byAnnotation).toBe(false); + }); + it("should parse correctly when byValue is set to false and byAnnotation is set to true", () => { + const directive: DirectiveNode = makeDirectiveNode("filterable", { byValue: false, byAnnotation: true }); + const filterableAnnotation = parseFilterableAnnotation(directive); + expect(filterableAnnotation.byValue).toBe(false); + expect(filterableAnnotation.byAnnotation).toBe(true); + }); +}); diff --git a/packages/graphql/src/schema-model/parser/filterable-annotation.ts b/packages/graphql/src/schema-model/parser/filterable-annotation.ts new file mode 100644 index 0000000000..1c8b4032e6 --- /dev/null +++ b/packages/graphql/src/schema-model/parser/filterable-annotation.ts @@ -0,0 +1,30 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { DirectiveNode } from "graphql"; +import { FilterableAnnotation } from "../annotation/FilterableAnnotation"; +import { parseArguments } from "./utils"; + +export function parseFilterableAnnotation(directive: DirectiveNode): FilterableAnnotation { + const { byValue, byAnnotation } = parseArguments(directive) as { byValue: boolean; byAnnotation: boolean }; + + return new FilterableAnnotation({ + byAnnotation, + byValue, + }); +} diff --git a/packages/graphql/src/schema-model/parser/full-text-annotation.test.ts b/packages/graphql/src/schema-model/parser/full-text-annotation.test.ts new file mode 100644 index 0000000000..283f2a53e4 --- /dev/null +++ b/packages/graphql/src/schema-model/parser/full-text-annotation.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { makeDirectiveNode } from "@graphql-tools/utils"; +import type { DirectiveNode } from "graphql"; +import { parseFullTextAnnotation } from "./full-text-annotation"; + +describe("parseFullTextAnnotation", () => { + it("should parse correctly", () => { + const directive: DirectiveNode = makeDirectiveNode("fullText", { + fields: { + name: "myName", + fields: ["firstField", "secondField"], + queryName: "myQueryName", + indexName: "myIndexName", + }, + }); + const fullTextAnnotation = parseFullTextAnnotation(directive); + expect(fullTextAnnotation.fields).toEqual({ + name: "myName", + fields: ["firstField", "secondField"], + queryName: "myQueryName", + indexName: "myIndexName", + }); + }); +}); diff --git a/packages/graphql/src/schema-model/parser/full-text-annotation.ts b/packages/graphql/src/schema-model/parser/full-text-annotation.ts new file mode 100644 index 0000000000..c6e2c7ee70 --- /dev/null +++ b/packages/graphql/src/schema-model/parser/full-text-annotation.ts @@ -0,0 +1,30 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { DirectiveNode } from "graphql"; +import type { FullTextFields } from "../annotation/FullTextAnnotation"; +import { FullTextAnnotation } from "../annotation/FullTextAnnotation"; +import { parseArguments } from "./utils"; + +export function parseFullTextAnnotation(directive: DirectiveNode): FullTextAnnotation { + const { fields } = parseArguments(directive) as { fields: FullTextFields }; + + return new FullTextAnnotation({ + fields, + }); +} diff --git a/packages/graphql/src/schema-model/parser/id-annotation.test.ts b/packages/graphql/src/schema-model/parser/id-annotation.test.ts new file mode 100644 index 0000000000..7a38a3da52 --- /dev/null +++ b/packages/graphql/src/schema-model/parser/id-annotation.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { makeDirectiveNode } from "@graphql-tools/utils"; +import { parseIDAnnotation } from "./id-annotation"; + +const tests = [ + { + name: "should parse correctly with all properties set to true", + directive: makeDirectiveNode("id", { autogenerate: true, unique: true, global: true }), + expected: { + autogenerate: true, + unique: true, + global: true, + }, + }, + { + name: "should parse correctly with all properties set to false", + directive: makeDirectiveNode("id", { autogenerate: false, unique: false, global: false }), + expected: { + autogenerate: false, + unique: false, + global: false, + }, + }, + { + name: "should parse correctly with autogenerate set to false, unique and global set to true", + directive: makeDirectiveNode("id", { autogenerate: false, unique: true, global: true }), + expected: { + autogenerate: false, + unique: true, + global: true, + }, + }, +]; + +describe("parseIDAnnotation", () => { + tests.forEach((test) => { + it(`${test.name}`, () => { + const idAnnotation = parseIDAnnotation(test.directive); + expect(idAnnotation.autogenerate).toBe(test.expected.autogenerate); + expect(idAnnotation.unique).toBe(test.expected.unique); + expect(idAnnotation.global).toBe(test.expected.global); + }); + }); +}); diff --git a/packages/graphql/src/schema-model/parser/id-annotation.ts b/packages/graphql/src/schema-model/parser/id-annotation.ts new file mode 100644 index 0000000000..55837a40f7 --- /dev/null +++ b/packages/graphql/src/schema-model/parser/id-annotation.ts @@ -0,0 +1,36 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { type DirectiveNode } from "graphql"; +import { IDAnnotation } from "../annotation/IDAnnotation"; +import { parseArguments } from "./utils"; + +export function parseIDAnnotation(directive: DirectiveNode): IDAnnotation { + const { autogenerate, unique, global } = parseArguments(directive) as { + autogenerate: boolean; + unique: boolean; + global: boolean; + }; + + return new IDAnnotation({ + autogenerate, + unique, + global, + }); +} diff --git a/packages/graphql/src/schema-model/parser/jwt-claim-annotation.test.ts b/packages/graphql/src/schema-model/parser/jwt-claim-annotation.test.ts new file mode 100644 index 0000000000..6282bd6a2e --- /dev/null +++ b/packages/graphql/src/schema-model/parser/jwt-claim-annotation.test.ts @@ -0,0 +1,31 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { makeDirectiveNode } from "@graphql-tools/utils"; +import type { DirectiveNode } from "graphql"; +import { parseJWTClaimAnnotation } from "./jwt-claim-annotation"; + +describe("parseJWTClaimAnnotation", () => { + test("should correctly parse jwtClaim path", () => { + const directive: DirectiveNode = makeDirectiveNode("jwtClaim", { path: "jwtClaimPath" }); + const jwtClaimAnnotation = parseJWTClaimAnnotation(directive); + + expect(jwtClaimAnnotation.path).toBe("jwtClaimPath"); + }); +}); diff --git a/packages/graphql/src/schema-model/parser/jwt-claim-annotation.ts b/packages/graphql/src/schema-model/parser/jwt-claim-annotation.ts new file mode 100644 index 0000000000..0ededd766d --- /dev/null +++ b/packages/graphql/src/schema-model/parser/jwt-claim-annotation.ts @@ -0,0 +1,29 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { DirectiveNode } from "graphql"; +import { JWTClaimAnnotation } from "../annotation/JWTClaimAnnotation"; +import { parseArguments } from "./utils"; + +export function parseJWTClaimAnnotation(directive: DirectiveNode): JWTClaimAnnotation { + const { path } = parseArguments(directive) as { path: string }; + + return new JWTClaimAnnotation({ + path, + }); +} diff --git a/packages/graphql/src/schema-model/parser/jwt-payload-annoatation.ts b/packages/graphql/src/schema-model/parser/jwt-payload-annoatation.ts new file mode 100644 index 0000000000..e5b30b67f6 --- /dev/null +++ b/packages/graphql/src/schema-model/parser/jwt-payload-annoatation.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { DirectiveNode } from "graphql"; +import { JWTPayloadAnnotation } from "../annotation/JWTPayloadAnnotation"; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function parseJWTPayloadAnnotation(_directive: DirectiveNode): JWTPayloadAnnotation { + return new JWTPayloadAnnotation(); +} diff --git a/packages/graphql/src/schema-model/parser/key-annotation.test.ts b/packages/graphql/src/schema-model/parser/key-annotation.test.ts index ab1c4f4b9c..5b00737350 100644 --- a/packages/graphql/src/schema-model/parser/key-annotation.test.ts +++ b/packages/graphql/src/schema-model/parser/key-annotation.test.ts @@ -17,186 +17,51 @@ * limitations under the License. */ -import type { DirectiveNode } from "graphql"; -import { Kind } from "graphql"; +import { makeDirectiveNode } from "@graphql-tools/utils"; import { parseKeyAnnotation } from "./key-annotation"; +const tests = [ + { + name: "should parse when there is only one directive", + directives: [makeDirectiveNode("key", { fields: "sku variation { id }" })], + expected: { + resolvable: true, + }, + }, + { + name: "should parse when there are two directives", + directives: [ + makeDirectiveNode("key", { fields: "sku variation { id }" }), + makeDirectiveNode("key", { fields: "sku variation { id }" }), + ], + expected: { + resolvable: true, + }, + }, + { + name: "should parse resolvable when there is only one directive", + directives: [makeDirectiveNode("key", { fields: "sku variation { id }", resolvable: true })], + expected: { + resolvable: true, + }, + }, + { + name: "should parse resolvable when there are two directives", + directives: [ + makeDirectiveNode("key", { fields: "sku variation { id }", resolvable: true }), + makeDirectiveNode("key", { fields: "sku variation { id }" }), + ], + expected: { + resolvable: true, + }, + }, +]; + describe("parseKeyAnnotation", () => { - it("should parse when there is only one directive", () => { - const directives: readonly DirectiveNode[] = [ - { - kind: Kind.DIRECTIVE, - name: { - kind: Kind.NAME, - value: "key", - }, - arguments: [ - { - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: "fields", - }, - value: { - kind: Kind.STRING, - value: "sku variation { id }", - }, - }, - ], - }, - ]; - const keyAnnotation = parseKeyAnnotation(directives); - expect(keyAnnotation.resolvable).toBe(true); - }); - it("should parse when there are two directives", () => { - const directives: readonly DirectiveNode[] = [ - { - kind: Kind.DIRECTIVE, - name: { - kind: Kind.NAME, - value: "key", - }, - arguments: [ - { - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: "fields", - }, - value: { - kind: Kind.STRING, - value: "id", - }, - }, - ], - }, - { - kind: Kind.DIRECTIVE, - name: { - kind: Kind.NAME, - value: "key", - }, - arguments: [ - { - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: "fields", - }, - value: { - kind: Kind.STRING, - value: "sku variation { id }", - }, - }, - ], - }, - ]; - const keyAnnotation = parseKeyAnnotation(directives); - expect(keyAnnotation.resolvable).toBe(true); - }); - it("should parse resolvable when there is only one directive", () => { - const directives: readonly DirectiveNode[] = [ - { - kind: Kind.DIRECTIVE, - name: { - kind: Kind.NAME, - value: "key", - }, - arguments: [ - { - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: "fields", - }, - value: { - kind: Kind.STRING, - value: "sku variation { id }", - }, - }, - { - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: "resolvable", - }, - value: { - kind: Kind.BOOLEAN, - value: false, - }, - }, - ], - }, - ]; - const keyAnnotation = parseKeyAnnotation(directives); - expect(keyAnnotation.resolvable).toBe(false); - }); - it("should parse resolvable when there are two directives", () => { - const directives: readonly DirectiveNode[] = [ - { - kind: Kind.DIRECTIVE, - name: { - kind: Kind.NAME, - value: "key", - }, - arguments: [ - { - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: "fields", - }, - value: { - kind: Kind.STRING, - value: "sku variation { id }", - }, - }, - { - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: "resolvable", - }, - value: { - kind: Kind.BOOLEAN, - value: false, - }, - }, - ], - }, - { - kind: Kind.DIRECTIVE, - name: { - kind: Kind.NAME, - value: "key", - }, - arguments: [ - { - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: "fields", - }, - value: { - kind: Kind.STRING, - value: "id", - }, - }, - { - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: "resolvable", - }, - value: { - kind: Kind.BOOLEAN, - value: true, - }, - }, - ], - }, - ]; - const keyAnnotation = parseKeyAnnotation(directives); - expect(keyAnnotation.resolvable).toBe(true); + tests.forEach((test) => { + it(`${test.name}`, () => { + const keyAnnotation = parseKeyAnnotation(test.directives); + expect(keyAnnotation.resolvable).toBe(test.expected.resolvable); + }); }); }); diff --git a/packages/graphql/src/schema-model/parser/mutation-annotation.test.ts b/packages/graphql/src/schema-model/parser/mutation-annotation.test.ts new file mode 100644 index 0000000000..c85ed84ff4 --- /dev/null +++ b/packages/graphql/src/schema-model/parser/mutation-annotation.test.ts @@ -0,0 +1,65 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { makeDirectiveNode } from "@graphql-tools/utils"; +import { MutationOperations } from "../../graphql/directives/mutation"; +import { parseMutationAnnotation } from "./mutation-annotation"; + +const tests = [ + { + name: "should parse correctly with a CREATE operation set", + directive: makeDirectiveNode("mutation", { operations: [MutationOperations.CREATE] }), + expected: { operations: [MutationOperations.CREATE] }, + }, + { + name: "should parse correctly with an UPDATE operation set", + directive: makeDirectiveNode("mutation", { operations: [MutationOperations.UPDATE] }), + expected: { operations: [MutationOperations.UPDATE] }, + }, + { + name: "should parse correctly with a DELETE operation set", + directive: makeDirectiveNode("mutation", { operations: [MutationOperations.DELETE] }), + expected: { operations: [MutationOperations.DELETE] }, + }, + { + name: "should parse correctly with a CREATE and UPDATE operation set", + directive: makeDirectiveNode("mutation", { + operations: [MutationOperations.CREATE, MutationOperations.UPDATE], + }), + expected: { operations: [MutationOperations.CREATE, MutationOperations.UPDATE] }, + }, + { + name: "should parse correctly with a CREATE, UPDATE and DELETE operation set", + directive: makeDirectiveNode("mutation", { + operations: [MutationOperations.CREATE, MutationOperations.UPDATE, MutationOperations.DELETE], + }), + expected: { + operations: [MutationOperations.CREATE, MutationOperations.UPDATE, MutationOperations.DELETE], + }, + }, +]; + +describe("parseMutationAnnotation", () => { + tests.forEach((test) => { + it(`${test.name}`, () => { + const mutationAnnotation = parseMutationAnnotation(test.directive); + expect(mutationAnnotation.operations).toEqual(test.expected.operations); + }); + }); +}); diff --git a/packages/graphql/src/schema-model/parser/mutation-annotation.ts b/packages/graphql/src/schema-model/parser/mutation-annotation.ts new file mode 100644 index 0000000000..ae78c0b656 --- /dev/null +++ b/packages/graphql/src/schema-model/parser/mutation-annotation.ts @@ -0,0 +1,32 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { DirectiveNode } from "graphql"; +import { Neo4jGraphQLSchemaValidationError } from "../../classes"; +import { MutationAnnotation } from "../annotation/MutationAnnotation"; +import { parseArguments } from "./utils"; + +export function parseMutationAnnotation(directive: DirectiveNode): MutationAnnotation { + const { operations } = parseArguments(directive); + if (!Array.isArray(operations)) { + throw new Neo4jGraphQLSchemaValidationError("@mutation operations must be an array"); + } + return new MutationAnnotation({ + operations, + }); +} diff --git a/packages/graphql/src/schema-model/parser/node-annotation.test.ts b/packages/graphql/src/schema-model/parser/node-annotation.test.ts new file mode 100644 index 0000000000..8d570aec50 --- /dev/null +++ b/packages/graphql/src/schema-model/parser/node-annotation.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { makeDirectiveNode } from "@graphql-tools/utils"; +import type { DirectiveNode } from "graphql"; +import { parseNodeAnnotation } from "./node-annotation"; + +describe("parseNodeAnnotation", () => { + it("should parse correctly", () => { + const directive: DirectiveNode = makeDirectiveNode("node", { + label: "Movie", + labels: ["Movie", "Person"], + }); + const nodeAnnotation = parseNodeAnnotation(directive); + expect(nodeAnnotation.label).toBe("Movie"); + expect(nodeAnnotation.labels).toEqual(["Movie", "Person"]); + }); +}); diff --git a/packages/graphql/src/schema-model/parser/node-annotation.ts b/packages/graphql/src/schema-model/parser/node-annotation.ts new file mode 100644 index 0000000000..081dfca7ee --- /dev/null +++ b/packages/graphql/src/schema-model/parser/node-annotation.ts @@ -0,0 +1,30 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { DirectiveNode } from "graphql"; +import { NodeAnnotation } from "../annotation/NodeAnnotation"; +import { parseArguments } from "./utils"; + +export function parseNodeAnnotation(directive: DirectiveNode): NodeAnnotation { + const { label, labels } = parseArguments(directive) as { label: string; labels: string[] }; + + return new NodeAnnotation({ + label, + labels, + }); +} diff --git a/packages/graphql/src/schema-model/parser/plural-annotation.test.ts b/packages/graphql/src/schema-model/parser/plural-annotation.test.ts new file mode 100644 index 0000000000..0d87346646 --- /dev/null +++ b/packages/graphql/src/schema-model/parser/plural-annotation.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { makeDirectiveNode } from "@graphql-tools/utils"; +import type { DirectiveNode } from "graphql"; +import { parsePluralAnnotation } from "./plural-annotation"; + +describe("parsePluralAnnotation", () => { + it("should parse correctly", () => { + const directive: DirectiveNode = makeDirectiveNode("Plural", { value: "myPluralString" }); + const pluralAnnotation = parsePluralAnnotation(directive); + expect(pluralAnnotation.value).toBe("myPluralString"); + }); +}); diff --git a/packages/graphql/src/schema-model/parser/plural-annotation.ts b/packages/graphql/src/schema-model/parser/plural-annotation.ts new file mode 100644 index 0000000000..1398199bbc --- /dev/null +++ b/packages/graphql/src/schema-model/parser/plural-annotation.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { DirectiveNode } from "graphql"; +import { PluralAnnotation } from "../annotation/PluralAnnotation"; +import { parseArguments } from "./utils"; + +export function parsePluralAnnotation(directive: DirectiveNode): PluralAnnotation { + const { value } = parseArguments(directive) as { value: string }; + return new PluralAnnotation({ + value, + }); +} diff --git a/packages/graphql/src/schema-model/parser/populated-by-annotation.test.ts b/packages/graphql/src/schema-model/parser/populated-by-annotation.test.ts new file mode 100644 index 0000000000..ec6cef02bd --- /dev/null +++ b/packages/graphql/src/schema-model/parser/populated-by-annotation.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { makeDirectiveNode } from "@graphql-tools/utils"; +import type { DirectiveNode } from "graphql"; +import { parsePopulatedByAnnotation } from "./populated-by-annotation"; + +describe("parsePopulatedByAnnotation", () => { + it("should parse correctly", () => { + const directive: DirectiveNode = makeDirectiveNode("populatedBy", { + callback: "callback", + operations: ["create", "update"], + }); + const populatedByAnnotation = parsePopulatedByAnnotation(directive); + expect(populatedByAnnotation.callback).toBe("callback"); + expect(populatedByAnnotation.operations).toEqual(["create", "update"]); + }); +}); diff --git a/packages/graphql/src/schema-model/parser/populated-by-annotation.ts b/packages/graphql/src/schema-model/parser/populated-by-annotation.ts new file mode 100644 index 0000000000..5d6503bfdd --- /dev/null +++ b/packages/graphql/src/schema-model/parser/populated-by-annotation.ts @@ -0,0 +1,30 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { DirectiveNode } from "graphql"; +import { PopulatedByAnnotation } from "../annotation/PopulatedByAnnotation"; +import { parseArguments } from "./utils"; + +export function parsePopulatedByAnnotation(directive: DirectiveNode): PopulatedByAnnotation { + const { callback, operations } = parseArguments(directive) as { callback: string; operations: string[] }; + + return new PopulatedByAnnotation({ + callback, + operations, + }); +} diff --git a/packages/graphql/src/schema-model/parser/private-annotation.ts b/packages/graphql/src/schema-model/parser/private-annotation.ts new file mode 100644 index 0000000000..7da1971ebc --- /dev/null +++ b/packages/graphql/src/schema-model/parser/private-annotation.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { DirectiveNode } from "graphql"; +import { PrivateAnnotation } from "../annotation/PrivateAnnotation"; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function parsePrivateAnnotation(_directive: DirectiveNode): PrivateAnnotation { + return new PrivateAnnotation(); +} diff --git a/packages/graphql/src/schema-model/parser/query-annotation.test.ts b/packages/graphql/src/schema-model/parser/query-annotation.test.ts new file mode 100644 index 0000000000..4730b075b2 --- /dev/null +++ b/packages/graphql/src/schema-model/parser/query-annotation.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { makeDirectiveNode } from "@graphql-tools/utils"; +import { parseQueryAnnotation } from "./query-annotation"; + +const tests = [ + { + name: "should parse correctly when read is true and aggregate is false", + directive: makeDirectiveNode("query", { + read: true, + aggregate: false, + }), + expected: { + read: true, + aggregate: false, + }, + }, + { + name: "should parse correctly when read is false and aggregate is true", + directive: makeDirectiveNode("query", { + read: false, + aggregate: true, + }), + expected: { + read: false, + aggregate: true, + }, + }, +]; + +describe("parseQueryAnnotation", () => { + tests.forEach((test) => { + it(`${test.name}`, () => { + const queryAnnotation = parseQueryAnnotation(test.directive); + expect(queryAnnotation.read).toBe(test.expected.read); + expect(queryAnnotation.aggregate).toBe(test.expected.aggregate); + }); + }); +}); diff --git a/packages/graphql/src/schema-model/parser/query-annotation.ts b/packages/graphql/src/schema-model/parser/query-annotation.ts new file mode 100644 index 0000000000..8abbe28f6c --- /dev/null +++ b/packages/graphql/src/schema-model/parser/query-annotation.ts @@ -0,0 +1,31 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { DirectiveNode } from "graphql"; +import { QueryAnnotation } from "../annotation/QueryAnnotation"; +import { parseArguments } from "./utils"; + +export function parseQueryAnnotation(directive: DirectiveNode): QueryAnnotation { + const { read, aggregate } = parseArguments(directive) as { read: boolean; aggregate: boolean }; + + return new QueryAnnotation({ + read, + aggregate, + }); +} diff --git a/packages/graphql/src/schema-model/parser/query-options-annotation.test.ts b/packages/graphql/src/schema-model/parser/query-options-annotation.test.ts new file mode 100644 index 0000000000..9619f6cb83 --- /dev/null +++ b/packages/graphql/src/schema-model/parser/query-options-annotation.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { makeDirectiveNode } from "@graphql-tools/utils"; +import { parseQueryOptionsAnnotation } from "./query-options-annotation"; + +const tests = [ + { + name: "should parse correctly with both limit arguments", + directive: makeDirectiveNode("queryOptions", { + limit: { + default: 25, + max: 100, + }, + }), + expected: { + limit: { + default: 25, + max: 100, + }, + }, + }, + { + name: "should parse correctly with only default limit argument", + directive: makeDirectiveNode("queryOptions", { + limit: { + default: 25, + }, + }), + expected: { + limit: { + default: 25, + max: undefined, + }, + }, + }, + { + name: "should parse correctly with only max limit argument", + directive: makeDirectiveNode("queryOptions", { + limit: { + max: 100, + }, + }), + expected: { + limit: { + default: undefined, + max: 100, + }, + }, + }, +]; + +describe("parseQueryOptionsAnnotation", () => { + tests.forEach((test) => { + it(`${test.name}`, () => { + const queryOptionsAnnotation = parseQueryOptionsAnnotation(test.directive); + expect(queryOptionsAnnotation.limit).toEqual(test.expected.limit); + }); + }); +}); diff --git a/packages/graphql/src/schema-model/parser/query-options-annotation.ts b/packages/graphql/src/schema-model/parser/query-options-annotation.ts new file mode 100644 index 0000000000..090f3ee204 --- /dev/null +++ b/packages/graphql/src/schema-model/parser/query-options-annotation.ts @@ -0,0 +1,50 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { DirectiveNode } from "graphql"; +import { Neo4jGraphQLSchemaValidationError } from "../../classes"; +import { QueryOptionsAnnotation } from "../annotation/QueryOptionsAnnotation"; +import { parseArguments } from "./utils"; + +export function parseQueryOptionsAnnotation(directive: DirectiveNode): QueryOptionsAnnotation { + const { limit } = parseArguments(directive) as { + limit: { + default?: string; + max?: string; + }; + resolvable: boolean; + }; + + const parsedLimit = { + default: limit.default ? parseInt(limit.default) : undefined, + max: limit.max ? parseInt(limit.max) : undefined, + }; + + if (parsedLimit.default && typeof parsedLimit.default !== "number") { + throw new Neo4jGraphQLSchemaValidationError(`@queryOptions limit.default must be a number`); + } + + if (parsedLimit.max && typeof parsedLimit.max !== "number") { + throw new Neo4jGraphQLSchemaValidationError(`@queryOptions limit.max must be a number`); + } + + return new QueryOptionsAnnotation({ + limit: parsedLimit, + }); +} diff --git a/packages/graphql/src/schema-model/parser/relationship-properties-annotation.ts b/packages/graphql/src/schema-model/parser/relationship-properties-annotation.ts new file mode 100644 index 0000000000..8f86e3e9c0 --- /dev/null +++ b/packages/graphql/src/schema-model/parser/relationship-properties-annotation.ts @@ -0,0 +1,26 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { DirectiveNode } from "graphql"; +import { RelationshipPropertiesAnnotation } from "../annotation/RelationshipPropertiesAnnotation"; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function parseRelationshipPropertiesAnnotation(_directive: DirectiveNode): RelationshipPropertiesAnnotation { + return new RelationshipPropertiesAnnotation(); +} diff --git a/packages/graphql/src/schema-model/parser/selectable-annotation.test.ts b/packages/graphql/src/schema-model/parser/selectable-annotation.test.ts new file mode 100644 index 0000000000..a8ea8550ef --- /dev/null +++ b/packages/graphql/src/schema-model/parser/selectable-annotation.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { makeDirectiveNode } from "@graphql-tools/utils"; +import { parseSelectableAnnotation } from "./selectable-annotation"; + +const tests = [ + { + name: "should parse correctly when onRead is true and onAggregate is true", + directive: makeDirectiveNode("selectable", { + onRead: true, + onAggregate: true, + }), + expected: { + onRead: true, + onAggregate: true, + }, + }, + { + name: "should parse correctly when onRead is true and onAggregate is false", + directive: makeDirectiveNode("selectable", { + onRead: true, + onAggregate: false, + }), + expected: { + onRead: true, + onAggregate: false, + }, + }, + { + name: "should parse correctly when onRead is false and onAggregate is true", + directive: makeDirectiveNode("selectable", { + onRead: false, + onAggregate: true, + }), + expected: { + onRead: false, + onAggregate: true, + }, + }, +]; + +describe("parseSelectableAnnotation", () => { + tests.forEach((test) => { + it(`${test.name}`, () => { + const selectableAnnotation = parseSelectableAnnotation(test.directive); + expect(selectableAnnotation.onRead).toBe(test.expected.onRead); + expect(selectableAnnotation.onAggregate).toBe(test.expected.onAggregate); + }); + }); +}); diff --git a/packages/graphql/src/schema-model/parser/selectable-annotation.ts b/packages/graphql/src/schema-model/parser/selectable-annotation.ts new file mode 100644 index 0000000000..e20e5b5cc2 --- /dev/null +++ b/packages/graphql/src/schema-model/parser/selectable-annotation.ts @@ -0,0 +1,30 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { DirectiveNode } from "graphql"; +import { SelectableAnnotation } from "../annotation/SelectableAnnotation"; +import { parseArguments } from "./utils"; + +export function parseSelectableAnnotation(directive: DirectiveNode): SelectableAnnotation { + const { onRead, onAggregate } = parseArguments(directive) as { onRead: boolean; onAggregate: boolean }; + + return new SelectableAnnotation({ + onRead, + onAggregate, + }); +} diff --git a/packages/graphql/src/schema-model/parser/settable-annotation.test.ts b/packages/graphql/src/schema-model/parser/settable-annotation.test.ts new file mode 100644 index 0000000000..cd145f2dfe --- /dev/null +++ b/packages/graphql/src/schema-model/parser/settable-annotation.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { makeDirectiveNode } from "@graphql-tools/utils"; +import { parseSettableAnnotation } from "./settable-annotation"; + +const tests = [ + { + name: "should parse correctly when onCreate is true and onUpdate is true", + directive: makeDirectiveNode("settable", { + onCreate: true, + onUpdate: true, + }), + expected: { + onCreate: true, + onUpdate: true, + }, + }, + { + name: "should parse correctly when onCreate is true and onUpdate is false", + directive: makeDirectiveNode("settable", { + onCreate: true, + onUpdate: false, + }), + expected: { + onCreate: true, + onUpdate: false, + }, + }, + { + name: "should parse correctly when onCreate is false and onUpdate is true", + directive: makeDirectiveNode("settable", { + onCreate: false, + onUpdate: true, + }), + expected: { + onCreate: false, + onUpdate: true, + }, + }, +]; + +describe("parseSettableAnnotation", () => { + tests.forEach((test) => { + it(`${test.name}`, () => { + const settableAnnotation = parseSettableAnnotation(test.directive); + expect(settableAnnotation.onCreate).toBe(test.expected.onCreate); + expect(settableAnnotation.onUpdate).toBe(test.expected.onUpdate); + }); + }); +}); diff --git a/packages/graphql/src/schema-model/parser/settable-annotation.ts b/packages/graphql/src/schema-model/parser/settable-annotation.ts new file mode 100644 index 0000000000..d2a81cbe86 --- /dev/null +++ b/packages/graphql/src/schema-model/parser/settable-annotation.ts @@ -0,0 +1,30 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { DirectiveNode } from "graphql"; +import { SettableAnnotation } from "../annotation/SettableAnnotation"; +import { parseArguments } from "./utils"; + +export function parseSettableAnnotation(directive: DirectiveNode): SettableAnnotation { + const { onCreate, onUpdate } = parseArguments(directive) as { onCreate: boolean; onUpdate: boolean }; + + return new SettableAnnotation({ + onCreate, + onUpdate, + }); +} diff --git a/packages/graphql/src/schema-model/parser/subscription-annotation.test.ts b/packages/graphql/src/schema-model/parser/subscription-annotation.test.ts new file mode 100644 index 0000000000..e4fd5d1db0 --- /dev/null +++ b/packages/graphql/src/schema-model/parser/subscription-annotation.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { makeDirectiveNode } from "@graphql-tools/utils"; +import { parseSubscriptionAnnotation } from "./subscription-annotation"; + +const tests = [ + { + name: "should parse correctly when CREATE operation is passed", + directive: makeDirectiveNode("subscription", { operations: ["CREATE"] }), + operations: ["CREATE"], + expected: { operations: ["CREATE"] }, + }, + { + name: "should parse correctly when UPDATE operation is passed", + directive: makeDirectiveNode("subscription", { operations: ["UPDATE"] }), + operations: ["UPDATE"], + expected: { operations: ["UPDATE"] }, + }, + { + name: "should parse correctly when CREATE and UPDATE operations are passed", + directive: makeDirectiveNode("subscription", { operations: ["CREATE", "UPDATE"] }), + operations: ["CREATE", "UPDATE"], + expected: { operations: ["CREATE", "UPDATE"] }, + }, +]; + +describe("parseSubscriptionAnnotation", () => { + tests.forEach((test) => { + it(`${test.name}`, () => { + const subscriptionAnnotation = parseSubscriptionAnnotation(test.directive); + expect(subscriptionAnnotation.operations).toEqual(test.operations); + }); + }); +}); diff --git a/packages/graphql/src/schema-model/parser/subscription-annotation.ts b/packages/graphql/src/schema-model/parser/subscription-annotation.ts new file mode 100644 index 0000000000..4e3202d963 --- /dev/null +++ b/packages/graphql/src/schema-model/parser/subscription-annotation.ts @@ -0,0 +1,29 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { DirectiveNode } from "graphql"; +import { SubscriptionAnnotation } from "../annotation/SubscriptionAnnotation"; +import { parseArguments } from "./utils"; + +export function parseSubscriptionAnnotation(directive: DirectiveNode): SubscriptionAnnotation { + const { operations } = parseArguments(directive) as { operations: string[] }; + + return new SubscriptionAnnotation({ + operations, + }); +} diff --git a/packages/graphql/src/schema-model/parser/timestamp-annotation.test.ts b/packages/graphql/src/schema-model/parser/timestamp-annotation.test.ts new file mode 100644 index 0000000000..689110e89b --- /dev/null +++ b/packages/graphql/src/schema-model/parser/timestamp-annotation.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { makeDirectiveNode } from "@graphql-tools/utils"; +import { parseTimestampAnnotation } from "./timestamp-annotation"; + +const tests = [ + { + name: "should parse correctly when CREATE operation is passed", + directive: makeDirectiveNode("timestamp", { operations: ["CREATE"] }), + operations: ["CREATE"], + expected: { operations: ["CREATE"] }, + }, + { + name: "should parse correctly when UPDATE operation is passed", + directive: makeDirectiveNode("timestamp", { operations: ["UPDATE"] }), + operations: ["UPDATE"], + expected: { operations: ["UPDATE"] }, + }, + { + name: "should parse correctly when CREATE and UPDATE operations are passed", + directive: makeDirectiveNode("timestamp", { operations: ["CREATE", "UPDATE"] }), + operations: ["CREATE", "UPDATE"], + expected: { operations: ["CREATE", "UPDATE"] }, + }, +]; + +describe("parseTimestampAnnotation", () => { + tests.forEach((test) => { + it(`${test.name}`, () => { + const timestampAnnotation = parseTimestampAnnotation(test.directive); + expect(timestampAnnotation.operations).toEqual(test.operations); + }); + }); +}); diff --git a/packages/graphql/src/schema-model/parser/timestamp-annotation.ts b/packages/graphql/src/schema-model/parser/timestamp-annotation.ts new file mode 100644 index 0000000000..23abd21f77 --- /dev/null +++ b/packages/graphql/src/schema-model/parser/timestamp-annotation.ts @@ -0,0 +1,29 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { DirectiveNode } from "graphql"; +import { TimestampAnnotation } from "../annotation/TimestampAnnotation"; +import { parseArguments } from "./utils"; + +export function parseTimestampAnnotation(directive: DirectiveNode): TimestampAnnotation { + const { operations } = parseArguments(directive) as { operations: string[] }; + + return new TimestampAnnotation({ + operations, + }); +} diff --git a/packages/graphql/src/schema-model/parser/unique-annotation.test.ts b/packages/graphql/src/schema-model/parser/unique-annotation.test.ts new file mode 100644 index 0000000000..81d85c3426 --- /dev/null +++ b/packages/graphql/src/schema-model/parser/unique-annotation.test.ts @@ -0,0 +1,31 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { makeDirectiveNode } from "@graphql-tools/utils"; +import type { DirectiveNode } from "graphql"; +import { parseUniqueAnnotation } from "./unique-annotation"; + +describe("parseUniqueAnnotation", () => { + test("should correctly parse unique constraint name", () => { + const directive: DirectiveNode = makeDirectiveNode("unique", { constraintName: "uniqueConstraintName" }); + const uniqueAnnotation = parseUniqueAnnotation(directive); + + expect(uniqueAnnotation.constraintName).toBe("uniqueConstraintName"); + }); +}); diff --git a/packages/graphql/src/schema-model/parser/unique-annotation.ts b/packages/graphql/src/schema-model/parser/unique-annotation.ts new file mode 100644 index 0000000000..e814f87816 --- /dev/null +++ b/packages/graphql/src/schema-model/parser/unique-annotation.ts @@ -0,0 +1,29 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { DirectiveNode } from "graphql"; +import { UniqueAnnotation } from "../annotation/UniqueAnnotation"; +import { parseArguments } from "./utils"; + +export function parseUniqueAnnotation(directive: DirectiveNode): UniqueAnnotation { + const { constraintName } = parseArguments(directive) as { constraintName: string }; + + return new UniqueAnnotation({ + constraintName, + }); +} From c54a10bd6df566f152fc5462ce4df852de0ed3f1 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Wed, 19 Jul 2023 12:21:12 +0100 Subject: [PATCH 02/22] Schema model re-design (#3596) * initial refactor of the schema model * add unit tests for schema model Attributes * include initial design for the GraphQL models * add Attribute tests, improve Attribute models --- packages/graphql/src/classes/Node.ts | 6 +- packages/graphql/src/constants.ts | 2 +- .../schema-model/Neo4jGraphQLSchemaModel.ts | 2 +- .../attribute/AbstractAttribute.ts | 334 +++++++++++++++ .../schema-model/attribute/Attribute.test.ts | 399 ++++++++++++++++++ .../src/schema-model/attribute/Attribute.ts | 60 +-- .../graphql-models/AggregationModel.ts | 68 +++ .../graphql-models/AttributeModel.ts | 107 +++++ .../attribute/graphql-models/ListModel.ts | 53 +++ .../attribute/graphql-models/MathModel.ts | 53 +++ .../schema-model/entity/CompositeEntity.ts | 2 + .../src/schema-model/entity/ConcreteEntity.ts | 8 +- .../graphql/src/schema-model/entity/Entity.ts | 14 + .../graphql-models/ConcreteEntityModel.ts | 105 +++++ .../src/schema-model/generate-model.test.ts | 173 ++++++++ .../src/schema-model/generate-model.ts | 237 +++++++---- .../parser/definition-collection.ts | 103 +++++ .../graphql/src/schema-model/parser/utils.ts | 7 + .../schema-model/relationship/Relationship.ts | 4 +- .../src/schema-model/utils/get-from-map.ts | 31 ++ .../schema-model/utils/string-manipulation.ts | 38 ++ 21 files changed, 1657 insertions(+), 149 deletions(-) create mode 100644 packages/graphql/src/schema-model/attribute/AbstractAttribute.ts create mode 100644 packages/graphql/src/schema-model/attribute/Attribute.test.ts create mode 100644 packages/graphql/src/schema-model/attribute/graphql-models/AggregationModel.ts create mode 100644 packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.ts create mode 100644 packages/graphql/src/schema-model/attribute/graphql-models/ListModel.ts create mode 100644 packages/graphql/src/schema-model/attribute/graphql-models/MathModel.ts create mode 100644 packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.ts create mode 100644 packages/graphql/src/schema-model/parser/definition-collection.ts create mode 100644 packages/graphql/src/schema-model/utils/get-from-map.ts create mode 100644 packages/graphql/src/schema-model/utils/string-manipulation.ts diff --git a/packages/graphql/src/classes/Node.ts b/packages/graphql/src/classes/Node.ts index 29af016888..4e8ca1f7d7 100644 --- a/packages/graphql/src/classes/Node.ts +++ b/packages/graphql/src/classes/Node.ts @@ -169,8 +169,8 @@ class Node extends GraphElement { ...this.temporalFields, ...this.enumFields, ...this.objectFields, - ...this.scalarFields, - ...this.primitiveFields, + ...this.scalarFields, // this are just custom scalars + ...this.primitiveFields, // this are instead built-in scalars, confirmed By Alexandra ...this.interfaceFields, ...this.objectFields, ...this.unionFields, @@ -179,6 +179,7 @@ class Node extends GraphElement { } /** Fields you can apply auth allow and bind to */ + // Maybe we can remove this as they may not be used anymore in the new auth system public get authableFields(): AuthableField[] { return [ ...this.primitiveFields, @@ -233,6 +234,7 @@ class Node extends GraphElement { }; } + public get fulltextTypeNames(): FulltextTypeNames { return { result: `${this.pascalCaseSingular}FulltextResult`, diff --git a/packages/graphql/src/constants.ts b/packages/graphql/src/constants.ts index 900f327ec2..1a7f5448ed 100644 --- a/packages/graphql/src/constants.ts +++ b/packages/graphql/src/constants.ts @@ -72,7 +72,7 @@ export const NODE_OR_EDGE_KEYS = ["node", "edge"]; export const LOGICAL_OPERATORS = ["AND", "OR", "NOT"] as const; // aggregation -export const AGGREGATION_COMPARISON_OPERATORS = ["EQUAL", "GT", "GTE", "LT", "LTE"]; +export const AGGREGATION_COMPARISON_OPERATORS = ["EQUAL", "GT", "GTE", "LT", "LTE"] as const; export const AGGREGATION_AGGREGATE_COUNT_OPERATORS = ["count", "count_LT", "count_LTE", "count_GT", "count_GTE"]; export const WHERE_AGGREGATION_TYPES = [ diff --git a/packages/graphql/src/schema-model/Neo4jGraphQLSchemaModel.ts b/packages/graphql/src/schema-model/Neo4jGraphQLSchemaModel.ts index 62a9e5f5fb..cc5636d616 100644 --- a/packages/graphql/src/schema-model/Neo4jGraphQLSchemaModel.ts +++ b/packages/graphql/src/schema-model/Neo4jGraphQLSchemaModel.ts @@ -22,7 +22,7 @@ import type { Operation } from "./Operation"; import type { Annotations, Annotation } from "./annotation/Annotation"; import { annotationToKey } from "./annotation/Annotation"; import { CompositeEntity } from "./entity/CompositeEntity"; -import { ConcreteEntity } from "./entity/ConcreteEntity"; +import { ConcreteEntity } from "./entity/ConcreteEntity"; import type { Entity } from "./entity/Entity"; export type Operations = { Query?: Operation; diff --git a/packages/graphql/src/schema-model/attribute/AbstractAttribute.ts b/packages/graphql/src/schema-model/attribute/AbstractAttribute.ts new file mode 100644 index 0000000000..6278e9fdc5 --- /dev/null +++ b/packages/graphql/src/schema-model/attribute/AbstractAttribute.ts @@ -0,0 +1,334 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Neo4jGraphQLSchemaValidationError } from "../../classes/Error"; +import { annotationToKey } from "../annotation/Annotation"; +import type { Annotation, Annotations } from "../annotation/Annotation"; + +export enum GraphQLBuiltInScalarType { + Int = "Int", + Float = "Float", + String = "String", + Boolean = "Boolean", + ID = "ID", +} + +export enum Neo4jGraphQLSpatialType { + CartesianPoint = "CartesianPoint", + Point = "Point", +} + +export enum Neo4jGraphQLNumberType { + BigInt = "BigInt", +} + +export enum Neo4jGraphQLTemporalType { + DateTime = "DateTime", + LocalDateTime = "LocalDateTime", + Time = "Time", + LocalTime = "LocalTime", + Date = "Date", + Duration = "Duration", +} + +export enum ScalarTypeCategory { + Neo4jGraphQLTemporalType = "Neo4jGraphQLTemporalType", + Neo4jGraphQLNumberType = "Neo4jGraphQLNumberType", + Neo4jGraphQLSpatialType = "Neo4jGraphQLSpatialType", + GraphQLBuiltInScalarType = "GraphQLBuiltInScalarType", +} + +export type Neo4jGraphQLScalarType = Neo4jGraphQLTemporalType | Neo4jGraphQLNumberType | Neo4jGraphQLSpatialType; + +// The ScalarType class is not used to represent user defined scalar types, see UserScalarType for that. +export class ScalarType { + public readonly name: GraphQLBuiltInScalarType | Neo4jGraphQLScalarType; + public readonly isRequired: boolean; + public readonly category: ScalarTypeCategory; + constructor(name: GraphQLBuiltInScalarType | Neo4jGraphQLScalarType, isRequired: boolean) { + this.name = name; + this.isRequired = isRequired; + switch (name) { + case GraphQLBuiltInScalarType.String: + case GraphQLBuiltInScalarType.Boolean: + case GraphQLBuiltInScalarType.ID: + case GraphQLBuiltInScalarType.Int: + case GraphQLBuiltInScalarType.Float: + this.category = ScalarTypeCategory.GraphQLBuiltInScalarType; + break; + case Neo4jGraphQLSpatialType.CartesianPoint: + case Neo4jGraphQLSpatialType.Point: + this.category = ScalarTypeCategory.Neo4jGraphQLSpatialType; + break; + case Neo4jGraphQLTemporalType.DateTime: + case Neo4jGraphQLTemporalType.LocalDateTime: + case Neo4jGraphQLTemporalType.Time: + case Neo4jGraphQLTemporalType.LocalTime: + case Neo4jGraphQLTemporalType.Date: + case Neo4jGraphQLTemporalType.Duration: + this.category = ScalarTypeCategory.Neo4jGraphQLTemporalType; + break; + case Neo4jGraphQLNumberType.BigInt: + this.category = ScalarTypeCategory.Neo4jGraphQLNumberType; + } + } +} + +export class UserScalarType { + public readonly name: string; + public readonly isRequired: boolean; + constructor(name: string, isRequired: boolean) { + this.name = name; + this.isRequired = isRequired; + } +} + +export class ObjectType { + public readonly name: string; + public readonly isRequired: boolean; + constructor(name: string, isRequired: boolean) { + this.name = name; + this.isRequired = isRequired; + } +} + +export class ListType { + public ofType: Exclude; + public isRequired: boolean; + constructor(ofType: AttributeType, isRequired: boolean) { + if (ofType instanceof ListType) { + throw new Neo4jGraphQLSchemaValidationError("two-dimensional lists are not supported"); + } + this.ofType = ofType; + this.isRequired = isRequired; + } +} + +export class EnumType { + public name: string; + public isRequired: boolean; + + constructor(name: string, isRequired: boolean) { + this.name = name; + this.isRequired = isRequired; + } +} + +export class UnionType { + public name: string; + public isRequired: boolean; + + constructor(name: string, isRequired: boolean) { + this.name = name; + this.isRequired = isRequired; + } +} + +export class InterfaceType { + public name: string; + public isRequired: boolean; + + constructor(name: string, isRequired: boolean) { + this.name = name; + this.isRequired = isRequired; + } +} + +export type AttributeType = ScalarType | UserScalarType | ObjectType | ListType | EnumType | UnionType | InterfaceType; + +export abstract class AbstractAttribute { + public name: string; + public type: AttributeType; + public annotations: Partial = {}; + + constructor({ + name, + type, + annotations, + }: { + name: string; + type: AttributeType; + annotations: Annotation[] | Partial; + }) { + this.name = name; + this.type = type; + if (Array.isArray(annotations)) { + for (const annotation of annotations) { + this.addAnnotation(annotation); + } + } else { + this.annotations = annotations; + } + } + + isBoolean(): boolean { + return this.type instanceof ScalarType && this.type.name === GraphQLBuiltInScalarType.Boolean; + } + + isID(): boolean { + return this.type instanceof ScalarType && this.type.name === GraphQLBuiltInScalarType.ID; + } + + isInt(): boolean { + return this.type instanceof ScalarType && this.type.name === GraphQLBuiltInScalarType.Int; + } + + isFloat(): boolean { + return this.type instanceof ScalarType && this.type.name === GraphQLBuiltInScalarType.Float; + } + + isString(): boolean { + return this.type instanceof ScalarType && this.type.name === GraphQLBuiltInScalarType.String; + } + + isCartesianPoint(): boolean { + return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLSpatialType.CartesianPoint; + } + + isPoint(): boolean { + return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLSpatialType.Point; + } + + isBigInt(): boolean { + return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLNumberType.BigInt; + } + + isDate(): boolean { + return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLTemporalType.Date; + } + + isDateTime(): boolean { + return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLTemporalType.DateTime; + } + + isLocalDateTime(): boolean { + return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLTemporalType.LocalDateTime; + } + + isTime(): boolean { + return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLTemporalType.Time; + } + + isLocalTime(): boolean { + return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLTemporalType.LocalTime; + } + + isDuration(): boolean { + return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLTemporalType.Duration; + } + + isList(): boolean { + return this.type instanceof ListType; + } + + // nested list of type are not supported yet, change this method when they are + isListOf(elementType: Exclude | GraphQLBuiltInScalarType | Neo4jGraphQLScalarType | string): boolean { + if (!(this.type instanceof ListType)) { + return false; + } + if (typeof elementType === "string") { + return this.type.ofType.name === elementType; + } + + return this.type.ofType.name === elementType.name; + } + + isListElementRequired(): boolean { + if (!(this.type instanceof ListType)) { + return false; + } + return this.type.ofType.isRequired; + } + + isObject(): boolean { + return this.type instanceof ObjectType; + } + + isEnum(): boolean { + return this.type instanceof EnumType; + } + + isRequired(): boolean { + return this.type.isRequired; + } + + isInterface(): boolean { + return this.type instanceof InterfaceType; + } + + isUnion(): boolean { + return this.type instanceof UnionType; + } + + isUserScalar(): boolean { + return this.type instanceof UserScalarType; + } + + /** + * START of category assertions + */ + isGraphQLBuiltInScalar(): boolean { + return this.type instanceof ScalarType && this.type.category === ScalarTypeCategory.GraphQLBuiltInScalarType; + } + + isSpatial(): boolean { + return this.type instanceof ScalarType && this.type.category === ScalarTypeCategory.Neo4jGraphQLSpatialType; + } + + isTemporal(): boolean { + return this.type instanceof ScalarType && this.type.category === ScalarTypeCategory.Neo4jGraphQLTemporalType; + } + + isAbstract(): boolean { + return this.isInterface() || this.isUnion(); + } + /** + * END of category assertions + */ + + /** + * START of Refactoring methods, these methods are just adapters to the new methods + * to help the transition from the old Node/Relationship/BaseField classes + * */ + + // TODO: remove this method and use isGraphQLBuiltInScalar instead + isPrimitive(): boolean { + return this.isGraphQLBuiltInScalar(); + } + + // TODO: remove this and use isUserScalar instead + isScalar(): boolean { + return this.isUserScalar(); + } + + /** + * END of refactoring methods + */ + + private addAnnotation(annotation: Annotation): void { + const annotationKey = annotationToKey(annotation); + if (this.annotations[annotationKey]) { + throw new Neo4jGraphQLSchemaValidationError(`Annotation ${annotationKey} already exists in ${this.name}`); + } + + // We cast to any because we aren't narrowing the Annotation type here. + // There's no reason to narrow either, since we care more about performance. + this.annotations[annotationKey] = annotation as any; + } +} diff --git a/packages/graphql/src/schema-model/attribute/Attribute.test.ts b/packages/graphql/src/schema-model/attribute/Attribute.test.ts new file mode 100644 index 0000000000..617c6af5be --- /dev/null +++ b/packages/graphql/src/schema-model/attribute/Attribute.test.ts @@ -0,0 +1,399 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + EnumType, + GraphQLBuiltInScalarType, + InterfaceType, + ListType, + Neo4jGraphQLNumberType, + Neo4jGraphQLSpatialType, + Neo4jGraphQLTemporalType, + ObjectType, + ScalarType, + UnionType, + UserScalarType, +} from "./AbstractAttribute"; +import { Attribute } from "./Attribute"; + +describe("Attribute", () => { + test("should clone attribute", () => { + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.String, true), + }); + const clone = attribute.clone(); + expect(attribute).toStrictEqual(clone); + }); + + describe("type assertions", () => { + test("isID", () => { + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.ID, true), + }); + + expect(attribute.isID()).toBe(true); + }); + + test("isBoolean", () => { + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.Boolean, true), + }); + + expect(attribute.isBoolean()).toBe(true); + }); + + test("isInt", () => { + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.Int, true), + }); + + expect(attribute.isInt()).toBe(true); + }); + + test("isFloat", () => { + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.Float, true), + }); + expect(attribute.isFloat()).toBe(true); + }); + + test("isString", () => { + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.String, true), + }); + expect(attribute.isString()).toBe(true); + }); + + test("isCartesianPoint", () => { + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(Neo4jGraphQLSpatialType.CartesianPoint, true), + }); + + expect(attribute.isCartesianPoint()).toBe(true); + }); + + test("isPoint", () => { + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(Neo4jGraphQLSpatialType.Point, true), + }); + + expect(attribute.isPoint()).toBe(true); + }); + + test("isBigInt", () => { + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(Neo4jGraphQLNumberType.BigInt, true), + }); + + expect(attribute.isBigInt()).toBe(true); + }); + + test("isDate", () => { + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(Neo4jGraphQLTemporalType.Date, true), + }); + + expect(attribute.isDate()).toBe(true); + }); + + test("isDateTime", () => { + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(Neo4jGraphQLTemporalType.DateTime, true), + }); + + expect(attribute.isDateTime()).toBe(true); + }); + + test("isLocalDateTime", () => { + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(Neo4jGraphQLTemporalType.LocalDateTime, true), + }); + + expect(attribute.isLocalDateTime()).toBe(true); + }); + + test("isTime", () => { + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(Neo4jGraphQLTemporalType.Time, true), + }); + + expect(attribute.isTime()).toBe(true); + }); + + test("isLocalTime", () => { + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(Neo4jGraphQLTemporalType.LocalTime, true), + }); + + expect(attribute.isLocalTime()).toBe(true); + }); + + test("isDuration", () => { + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(Neo4jGraphQLTemporalType.Duration, true), + }); + + expect(attribute.isDuration()).toBe(true); + }); + + test("isObject", () => { + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new ObjectType("testType", true), + }); + + expect(attribute.isObject()).toBe(true); + }); + + test("isEnum", () => { + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new EnumType("testType", true), + }); + + expect(attribute.isEnum()).toBe(true); + }); + + test("isUserScalar", () => { + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new UserScalarType("testType", true), + }); + + expect(attribute.isUserScalar()).toBe(true); + }); + + test("isInterface", () => { + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new InterfaceType("Tool", true), + }); + expect(attribute.isInterface()).toBe(true); + }); + + test("isUnion", () => { + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new UnionType("Tool", true), + }); + expect(attribute.isUnion()).toBe(true); + }); + + describe("List", () => { + test("isList", () => { + const stringType = new ScalarType(GraphQLBuiltInScalarType.String, true); + + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new ListType(stringType, true), + }); + + expect(attribute.isList()).toBe(true); + }); + + test("isListOf, should return false if attribute it's not a list", () => { + const stringType = new ScalarType(GraphQLBuiltInScalarType.String, true); + + const attribute = new Attribute({ + name: "test", + annotations: [], + type: stringType, + }); + + expect(attribute.isListOf(stringType)).toBe(false); + }); + + test("isListOf(Attribute), should return false if it's a list of a different type", () => { + const stringType = new ScalarType(GraphQLBuiltInScalarType.String, true); + + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new ListType(stringType, true), + }); + const intType = new ScalarType(GraphQLBuiltInScalarType.Int, true); + expect(attribute.isListOf(intType)).toBe(false); + }); + + test("isListOf(Attribute), should return true if it's a list of a the same type.", () => { + const stringType = new ScalarType(GraphQLBuiltInScalarType.String, true); + + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new ListType(stringType, true), + }); + const stringType2 = new ScalarType(GraphQLBuiltInScalarType.String, true); + expect(attribute.isListOf(stringType2)).toBe(true); + }); + + test("isListOf(string), should return false if it's a list of a different type", () => { + const stringType = new ScalarType(GraphQLBuiltInScalarType.String, true); + + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new ListType(stringType, true), + }); + expect(attribute.isListOf(GraphQLBuiltInScalarType.Int)).toBe(false); + }); + + test("isListOf(string), should return true if it's a list of a the same type.", () => { + const stringType = new ScalarType(GraphQLBuiltInScalarType.String, true); + + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new ListType(stringType, true), + }); + expect(attribute.isListOf(GraphQLBuiltInScalarType.String)).toBe(true); + }); + }); + }); + + describe("category assertions", () => { + test("isGraphQLBuiltInScalar", () => { + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.String, true), + }); + + expect(attribute.isGraphQLBuiltInScalar()).toBe(true); + }); + + test("isSpatial", () => { + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(Neo4jGraphQLSpatialType.CartesianPoint, true), + }); + + expect(attribute.isSpatial()).toBe(true); + }); + + test("isTemporal", () => { + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(Neo4jGraphQLTemporalType.Date, true), + }); + + expect(attribute.isTemporal()).toBe(true); + }); + + + test("isAbstract", () => { + const attribute = new Attribute({ + name: "test", + annotations: [], + type: new UnionType("Tool", true), + }); + + expect(attribute.isAbstract()).toBe(true); + }); + }); + + test("isRequired", () => { + const attributeRequired = new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.String, true), + }); + + const attributeNotRequired = new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.String, false), + }); + + expect(attributeRequired.isRequired()).toBe(true); + expect(attributeNotRequired.isRequired()).toBe(false); + }); + + test("isRequired - List", () => { + const attributeRequired = new Attribute({ + name: "test", + annotations: [], + type: new ListType(new ScalarType(GraphQLBuiltInScalarType.String, true), true), + }); + + const attributeNotRequired = new Attribute({ + name: "test", + annotations: [], + type: new ListType(new ScalarType(GraphQLBuiltInScalarType.String, true), false), + }); + + expect(attributeRequired.isRequired()).toBe(true); + expect(attributeNotRequired.isRequired()).toBe(false); + }); + + test("isListElementRequired", () => { + const listElementRequired = new Attribute({ + name: "test", + annotations: [], + type: new ListType(new ScalarType(GraphQLBuiltInScalarType.String, true), false), + }); + + const listElementNotRequired = new Attribute({ + name: "test", + annotations: [], + type: new ListType(new ScalarType(GraphQLBuiltInScalarType.String, false), true), + }); + + expect(listElementRequired.isListElementRequired()).toBe(true); + expect(listElementNotRequired.isListElementRequired()).toBe(false); + }); +}); diff --git a/packages/graphql/src/schema-model/attribute/Attribute.ts b/packages/graphql/src/schema-model/attribute/Attribute.ts index 552a5a45a1..198ff15253 100644 --- a/packages/graphql/src/schema-model/attribute/Attribute.ts +++ b/packages/graphql/src/schema-model/attribute/Attribute.ts @@ -17,51 +17,15 @@ * limitations under the License. */ -import { Neo4jGraphQLSchemaValidationError } from "../../classes/Error"; import type { Annotation, Annotations } from "../annotation/Annotation"; -import { annotationToKey } from "../annotation/Annotation"; +import type { AttributeType } from "./AbstractAttribute"; +import { AbstractAttribute } from "./AbstractAttribute"; -export enum AttributeType { - Boolean = "Boolean", - ID = "ID", - String = "String", - Int = "Int", - BigInt = "BigInt", - Float = "Float", - DateTime = "DateTime", - LocalDateTime = "LocalDateTime", - Time = "Time", - LocalTime = "LocalTime", - Date = "Date", - Duration = "Duration", - Point = "Point", - ObjectType = "ObjectType", -} - -export class Attribute { - public readonly name: string; - public readonly annotations: Partial = {}; - public readonly type: AttributeType; - public readonly isArray: boolean; - - constructor({ - name, - annotations, - type, - isArray, - }: { - name: string; - annotations: Annotation[]; - type: AttributeType; - isArray: boolean; - }) { - this.name = name; - this.type = type; - this.isArray = isArray; +// At this moment Attribute is a dummy class, most of the logic is shared logic between Attribute and AttributeModels defined in the AbstractAttribute class +export class Attribute extends AbstractAttribute { - for (const annotation of annotations) { - this.addAnnotation(annotation); - } + constructor({ name, annotations = [], type }: { name: string; annotations: Annotation[]; type: AttributeType }) { + super({ name, type, annotations }); } public clone(): Attribute { @@ -69,18 +33,6 @@ export class Attribute { name: this.name, annotations: Object.values(this.annotations), type: this.type, - isArray: this.isArray, }); } - - private addAnnotation(annotation: Annotation): void { - const annotationKey = annotationToKey(annotation); - if (this.annotations[annotationKey]) { - throw new Neo4jGraphQLSchemaValidationError(`Annotation ${annotationKey} already exists in ${this.name}`); - } - - // We cast to any because we aren't narrowing the Annotation type here. - // There's no reason to narrow either, since we care more about performance. - this.annotations[annotationKey] = annotation as any; - } } diff --git a/packages/graphql/src/schema-model/attribute/graphql-models/AggregationModel.ts b/packages/graphql/src/schema-model/attribute/graphql-models/AggregationModel.ts new file mode 100644 index 0000000000..6a8e1b43c9 --- /dev/null +++ b/packages/graphql/src/schema-model/attribute/graphql-models/AggregationModel.ts @@ -0,0 +1,68 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { AttributeModel } from "./AttributeModel"; + +import { AGGREGATION_COMPARISON_OPERATORS } from "../../../constants"; + +type ComparisonOperator = typeof AGGREGATION_COMPARISON_OPERATORS[number]; + +export class AggregationModel { + readonly attributeModel: AttributeModel; + constructor(attributeModel: AttributeModel) { + if (!attributeModel.isScalar()) { + throw new Error("Attribute is not a scalar"); + } + this.attributeModel = attributeModel; + } + + getAggregationComparators(): string[] { + return AGGREGATION_COMPARISON_OPERATORS.map((comparator) => [ + this.getAverageComparator(comparator), + this.getMinComparator(comparator), + this.getMaxComparator(comparator), + this.getSumComparator(comparator), + ]).flat(); + } + + getAverageComparator(comparator: ComparisonOperator): string { + return this.attributeModel.isString() + ? `${this.attributeModel.name}_AVERAGE_LENGTH_${comparator}` + : `${this.attributeModel.name}_AVERAGE_${comparator}`; + } + + getMinComparator(comparator: ComparisonOperator): string { + return `${this.attributeModel.name}_MIN_${comparator}`; + } + + getMaxComparator(comparator: ComparisonOperator): string { + return `${this.attributeModel.name}_MAX_${comparator}`; + } + + getSumComparator(comparator: ComparisonOperator): string { + return `${this.attributeModel.name}_SUM_${comparator}`; + } + + /** + * Given the GraphQL field name, returns the semantic information about the aggregation it tries to perform + **/ + getAggregationMetadata(graphQLField: string): { fieldName: string; operator: string; comparator: string } { + throw new Error("Not implemented"); + } +} diff --git a/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.ts b/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.ts new file mode 100644 index 0000000000..fa27a5b962 --- /dev/null +++ b/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.ts @@ -0,0 +1,107 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { MathModel } from "./MathModel"; +import { AggregationModel } from "./AggregationModel"; +import { ListModel } from "./ListModel"; +import type { Attribute } from "../Attribute"; +import type { Annotations } from "../../annotation/Annotation"; +import type { AttributeType } from "../AbstractAttribute"; +import { AbstractAttribute } from "../AbstractAttribute"; + +export class AttributeModel extends AbstractAttribute { + private _listModel: ListModel | undefined; + private _mathModel: MathModel | undefined; + private _aggregationModel: AggregationModel | undefined; + public name: string; + public annotations: Partial; + public type: AttributeType; + + constructor(attribute: Attribute) { + super({ name: attribute.name, type: attribute.type, annotations: attribute.annotations }); + this.name = attribute.name; + this.annotations = attribute.annotations; + this.type = attribute.type; + } + + /** + * Previously defined as: + * [ + ...this.temporalFields, + ...this.enumFields, + ...this.objectFields, + ...this.scalarFields, + ...this.primitiveFields, + ...this.interfaceFields, + ...this.objectFields, + ...this.unionFields, + ...this.pointFields, + ]; + */ + isMutable(): boolean { + return this.isTemporal() || this.isEnum() || this.isObject() || this.isScalar() || + this.isPrimitive() || this.isInterface() || this.isUnion() || this.isPoint(); + } + + isUnique(): boolean { + // TODO: add it when the annotations are merged + // return this.attribute.annotations.unique ? true : false; + return false; + } + + /** + * Previously defined as: + * [...this.primitiveFields, + ...this.scalarFields, + ...this.enumFields, + ...this.temporalFields, + ...this.pointFields,] + */ + isConstrainable(): boolean { + return this.isPrimitive() || this.isScalar() || this.isEnum() || this.isTemporal() || this.isPoint(); + } + // TODO: remember to figure out constrainableFields + + /** + * @throws {Error} if the attribute is not a list + */ + get listModel(): ListModel { + if (!this._listModel) { + this._listModel = new ListModel(this); + } + return this._listModel; + } + + /** + * @throws {Error} if the attribute is not a scalar + */ + get mathModel(): MathModel { + if (!this._mathModel) { + this._mathModel = new MathModel(this); + } + return this._mathModel; + } + + get aggregationModel(): AggregationModel { + if (!this._aggregationModel) { + this._aggregationModel = new AggregationModel(this); + } + return this._aggregationModel; + } +} diff --git a/packages/graphql/src/schema-model/attribute/graphql-models/ListModel.ts b/packages/graphql/src/schema-model/attribute/graphql-models/ListModel.ts new file mode 100644 index 0000000000..bece713c09 --- /dev/null +++ b/packages/graphql/src/schema-model/attribute/graphql-models/ListModel.ts @@ -0,0 +1,53 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { AttributeModel } from "./AttributeModel"; + +export class ListModel { + readonly attributeModel: AttributeModel; + + constructor(attributeModel: AttributeModel) { + if (!attributeModel.isList()) { + throw new Error("Attribute is not a list"); + } + this.attributeModel = attributeModel; + } + + getPush(): string { + return `${this.attributeModel.name}_PUSH`; + } + + getPop(): string { + return `${this.attributeModel.name}_POP`; + } + + getIncludes(): string { + return `${this.attributeModel.name}_INCLUDES`; + } + + getNotIncludes(): string { + return `${this.attributeModel.name}_NOT_INCLUDES`; + } + /** + * Given the GraphQL field name, returns the semantic information about the list operation it tries to perform + **/ + getListMetadata(graphQLField: string): { fieldName: string; operator: string; comparator?: string } { + throw new Error("Not implemented"); + } +} diff --git a/packages/graphql/src/schema-model/attribute/graphql-models/MathModel.ts b/packages/graphql/src/schema-model/attribute/graphql-models/MathModel.ts new file mode 100644 index 0000000000..4c31ff3e6a --- /dev/null +++ b/packages/graphql/src/schema-model/attribute/graphql-models/MathModel.ts @@ -0,0 +1,53 @@ + +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { AttributeModel } from "./AttributeModel"; + +export class MathModel { + readonly attributeModel: AttributeModel; + constructor(attributeModel: AttributeModel) { + if (!attributeModel.isScalar()) { + throw new Error("Attribute is not a scalar"); + } + this.attributeModel = attributeModel; + } + + getMathOperations(): string[] { + return [this.getAdd(), this.getSubtract(), this.getMultiply(), this.getDecrement()]; + } + + getAdd(): string { + return this.attributeModel.isInt() || this.attributeModel.isBigInt() + ? `${this.attributeModel.name}_INCREMENT` + : `${this.attributeModel.name}_ADD`; + } + + getSubtract(): string { + return `${this.attributeModel.name}_SUBTRACT`; + } + + getMultiply(): string { + return `${this.attributeModel.name}_MULTIPLY`; + } + + getDecrement(): string { + return `${this.attributeModel.name}_DECREMENT`; + } +} diff --git a/packages/graphql/src/schema-model/entity/CompositeEntity.ts b/packages/graphql/src/schema-model/entity/CompositeEntity.ts index c573df79ef..b600b7abe0 100644 --- a/packages/graphql/src/schema-model/entity/CompositeEntity.ts +++ b/packages/graphql/src/schema-model/entity/CompositeEntity.ts @@ -24,6 +24,8 @@ import type { Entity } from "./Entity"; export class CompositeEntity implements Entity { public readonly name: string; public concreteEntities: ConcreteEntity[]; + // TODO: add type interface or union, and for interface add fields + // TODO: add annotations constructor({ name, concreteEntities }: { name: string; concreteEntities: ConcreteEntity[] }) { this.name = name; diff --git a/packages/graphql/src/schema-model/entity/ConcreteEntity.ts b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts index 5b3520508f..d59a71a752 100644 --- a/packages/graphql/src/schema-model/entity/ConcreteEntity.ts +++ b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts @@ -23,11 +23,10 @@ import type { Annotation, Annotations } from "../annotation/Annotation"; import { annotationToKey } from "../annotation/Annotation"; import type { Attribute } from "../attribute/Attribute"; import type { Relationship } from "../relationship/Relationship"; +import { AbstractConcreteEntity } from "./Entity"; import type { Entity } from "./Entity"; -export class ConcreteEntity implements Entity { - public readonly name: string; - public readonly labels: Set; +export class ConcreteEntity extends AbstractConcreteEntity implements Entity { public readonly attributes: Map = new Map(); public readonly relationships: Map = new Map(); public readonly annotations: Partial = {}; @@ -45,8 +44,7 @@ export class ConcreteEntity implements Entity { annotations?: Annotation[]; relationships?: Relationship[]; }) { - this.name = name; - this.labels = new Set(labels); + super({ name, labels }); for (const attribute of attributes) { this.addAttribute(attribute); diff --git a/packages/graphql/src/schema-model/entity/Entity.ts b/packages/graphql/src/schema-model/entity/Entity.ts index 78d4f4c668..cfcbea3585 100644 --- a/packages/graphql/src/schema-model/entity/Entity.ts +++ b/packages/graphql/src/schema-model/entity/Entity.ts @@ -17,6 +17,7 @@ * limitations under the License. */ + export interface Entity { readonly name: string; @@ -24,3 +25,16 @@ export interface Entity { // relationships // annotations } + +export abstract class AbstractConcreteEntity { +/* protected readonly listAttributes: Attribute[] = []; + protected readonly listRelationships: Attribute[] = []; */ + + public readonly name: string; + public readonly labels: Set; + + constructor({ name, labels }: { name: string; labels: string[] }) { + this.name = name; + this.labels = new Set(labels); + } +} diff --git a/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.ts b/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.ts new file mode 100644 index 0000000000..e8db9a1f73 --- /dev/null +++ b/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.ts @@ -0,0 +1,105 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AttributeModel } from "../../attribute/graphql-models/AttributeModel"; +import type { Relationship } from "../../relationship/Relationship"; +import { getFromMap } from "../../utils/get-from-map"; +import type { Entity } from "../Entity"; +import { AbstractConcreteEntity } from "../Entity"; +import { singular, plural } from "../../utils/string-manipulation"; +import type { ConcreteEntity } from "../ConcreteEntity"; +import type { Attribute } from "../../attribute/Attribute"; + +export class ConcreteEntityModel extends AbstractConcreteEntity { + public readonly attributes: Map = new Map(); + // TODO: change Relationship to RelationshipModel + public readonly relationships: Map = new Map(); + + // These keys allow to store the keys of the map in memory and avoid keep iterating over the map. + private mutableFieldsKeys: string[] = []; + private uniqueFieldsKeys: string[] = []; + private constrainableFieldsKeys: string[] = []; + + // TODO: remove this just added to help the migration. + private readonly listAttributes: Attribute[] = []; + + // typesNames + private _singular: string | undefined; + private _plural: string | undefined; + + constructor(entity: ConcreteEntity) { + super({ name: entity.name, labels: [...entity.labels] }); + this.initAttributes(); + } + + private initAttributes() { + this.listAttributes.forEach((attribute) => { + const attributeModel = new AttributeModel(attribute); + if (attributeModel.isMutable()) { + this.mutableFieldsKeys.push(attribute.name); + } + if (attributeModel.isUnique()) { + this.uniqueFieldsKeys.push(attribute.name); + } + if (attributeModel.isConstrainable()) { + this.constrainableFieldsKeys.push(attribute.name); + } + + this.attributes.set(attribute.name, attributeModel); + }); + } + + public get mutableFields(): AttributeModel[] { + return this.mutableFieldsKeys.map((key) => getFromMap(this.attributes, key)); + } + + public get uniqueFields(): AttributeModel[] { + return this.uniqueFieldsKeys.map((key) => getFromMap(this.attributes, key)); + } + + public get constrainableFields(): AttributeModel[] { + return this.constrainableFieldsKeys.map((key) => getFromMap(this.attributes, key)); + } + + public get relationshipAttributesName(): string[] { + return [...this.relationships.keys()]; + } + + public getRelatedEntities(): Entity[] { + return [...this.relationships.values()].map((relationship) => relationship.target); + } + + public getAllLabels(): string[] { + throw new Error("Method not implemented."); + } + + public get singular(): string { + if (!this._singular) { + this._singular = singular(this.name); + } + return this._singular; + } + + public get plural(): string { + if (!this._plural) { + this._plural = plural(this.name); + } + return this._plural; + } +} diff --git a/packages/graphql/src/schema-model/generate-model.test.ts b/packages/graphql/src/schema-model/generate-model.test.ts index e236428f1a..59b7839319 100644 --- a/packages/graphql/src/schema-model/generate-model.test.ts +++ b/packages/graphql/src/schema-model/generate-model.test.ts @@ -26,9 +26,14 @@ import { } from "./annotation/AuthorizationAnnotation"; import { generateModel } from "./generate-model"; import type { Neo4jGraphQLSchemaModel } from "./Neo4jGraphQLSchemaModel"; + +import type { ConcreteEntity } from "./entity/ConcreteEntity"; +import type { Attribute } from "./attribute/Attribute"; +import type { Relationship } from "./relationship/Relationship"; import { SubscriptionsAuthorizationFilterEventRule } from "./annotation/SubscriptionsAuthorizationAnnotation"; import { AuthenticationAnnotation } from "./annotation/AuthenticationAnnotation"; + describe("Schema model generation", () => { test("parses @authentication directive with no arguments", () => { const typeDefs = gql` @@ -287,3 +292,171 @@ describe("ComposeEntity generation", () => { expect(humanEntities?.concreteEntities).toHaveLength(1); // User }); }); + +describe("Attribute generation", () => { + let schemaModel: Neo4jGraphQLSchemaModel; + // entities + let userEntity: ConcreteEntity; + let accountEntity: ConcreteEntity; + + // relationships + let userAccounts: Relationship; + + // user attributes + let id: Attribute; + let name: Attribute; + let createdAt: Attribute; + let releaseDate: Attribute; + let runningTime: Attribute; + let accountSize: Attribute; + let favoriteColors: Attribute; + let password: Attribute; + + // hasAccount relationship attributes + let creationTime: Attribute; + + // account attributes + let status: Attribute; + let aOrB: Attribute; + + beforeAll(() => { + const typeDefs = gql` + type User { + id: ID! + name: String! + createdAt: DateTime + releaseDate: Date! + runningTime: Time + accountSize: BigInt + favoriteColors: [String!]! + accounts: [Account!]! @relationship(type: "HAS_ACCOUNT", properties: "hasAccount", direction: OUT) + } + + interface hasAccount @relationshipProperties { + creationTime: DateTime! + } + + type A { + id: ID + } + + type B { + age: Int + } + + union AorB = A | B + + enum Status { + ACTIVATED + DISABLED + } + + type Account { + status: Status + aOrB: AorB + } + + extend type User { + password: String! + } + `; + + const document = mergeTypeDefs(typeDefs); + schemaModel = generateModel(document); + + // entities + userEntity = schemaModel.entities.get("User") as ConcreteEntity; + userAccounts = userEntity.relationships.get("accounts") as Relationship; + accountEntity = schemaModel.entities.get("Account") as ConcreteEntity; + + // user attributes + id = userEntity?.attributes.get("id") as Attribute; + name = userEntity?.attributes.get("name") as Attribute; + createdAt = userEntity?.attributes.get("createdAt") as Attribute; + releaseDate = userEntity?.attributes.get("releaseDate") as Attribute; + runningTime = userEntity?.attributes.get("runningTime") as Attribute; + accountSize = userEntity?.attributes.get("accountSize") as Attribute; + favoriteColors = userEntity?.attributes.get("favoriteColors") as Attribute; + + // extended attributes + password = userEntity?.attributes.get("password") as Attribute; + + // hasAccount relationship attributes + creationTime = userAccounts?.attributes.get("creationTime") as Attribute; + + // account attributes + status = accountEntity?.attributes.get("status") as Attribute; + aOrB = accountEntity?.attributes.get("aOrB") as Attribute; + }); + + describe("attribute types", () => { + test("ID", () => { + expect(id.isID()).toBe(true); + expect(id.isGraphQLBuiltInScalar()).toBe(true); + }); + + test("String", () => { + expect(name.isString()).toBe(true); + expect(id.isGraphQLBuiltInScalar()).toBe(true); + }); + + test("DateTime", () => { + expect(createdAt.isDateTime()).toBe(true); + expect(createdAt.isGraphQLBuiltInScalar()).toBe(false); + expect(createdAt.isTemporal()).toBe(true); + expect(creationTime.isDateTime()).toBe(true); + expect(creationTime.isGraphQLBuiltInScalar()).toBe(false); + expect(creationTime.isTemporal()).toBe(true); + + }); + + test("Date", () => { + expect(releaseDate.isDate()).toBe(true); + expect(releaseDate.isGraphQLBuiltInScalar()).toBe(false); + expect(releaseDate.isTemporal()).toBe(true); + }); + + test("Time", () => { + expect(runningTime.isTime()).toBe(true); + expect(runningTime.isGraphQLBuiltInScalar()).toBe(false); + expect(runningTime.isTemporal()).toBe(true); + }); + + test("BigInt", () => { + expect(accountSize.isBigInt()).toBe(true); + expect(accountSize.isGraphQLBuiltInScalar()).toBe(false); + }); + + test("Enum", () => { + expect(status.isEnum()).toBe(true); + expect(status.isGraphQLBuiltInScalar()).toBe(false); + }) + + test("Union", () => { + expect(aOrB.isUnion()).toBe(true); + expect(aOrB.isGraphQLBuiltInScalar()).toBe(false); + expect(aOrB.isAbstract()).toBe(true); + }) + + test("List", () => { + expect(favoriteColors.isList()).toBe(true); + expect(favoriteColors.isString()).toBe(false); + }); + + test("on extended entity", () => { + expect(password.isString()).toBe(true); + expect(password.isGraphQLBuiltInScalar()).toBe(true); + }); + + test("isRequired", () => { + expect(name.isRequired()).toBe(true); + expect(id.isRequired()).toBe(true); + expect(createdAt.isRequired()).toBe(false); + expect(releaseDate.isRequired()).toBe(true); + expect(runningTime.isRequired()).toBe(false); + expect(accountSize.isRequired()).toBe(false); + expect(favoriteColors.isRequired()).toBe(true); + expect(password.isRequired()).toBe(true); + }); + }); +}); diff --git a/packages/graphql/src/schema-model/generate-model.ts b/packages/graphql/src/schema-model/generate-model.ts index f56491589e..49e8e1feb1 100644 --- a/packages/graphql/src/schema-model/generate-model.ts +++ b/packages/graphql/src/schema-model/generate-model.ts @@ -17,37 +17,55 @@ * limitations under the License. */ import type { + TypeNode, DirectiveNode, DocumentNode, FieldDefinitionNode, - InterfaceTypeDefinitionNode, ObjectTypeDefinitionNode, SchemaExtensionNode, } from "graphql"; +import { Kind } from "graphql"; import { Neo4jGraphQLSchemaValidationError } from "../classes"; -import type { DefinitionNodes } from "../schema/get-definition-nodes"; -import { getDefinitionNodes } from "../schema/get-definition-nodes"; import getFieldTypeMeta from "../schema/get-field-type-meta"; import { filterTruthy } from "../utils/utils"; import { Neo4jGraphQLSchemaModel } from "./Neo4jGraphQLSchemaModel"; import type { Operations } from "./Neo4jGraphQLSchemaModel"; import type { Annotation } from "./annotation/Annotation"; -import { Attribute, AttributeType } from "./attribute/Attribute"; +import { + Neo4jGraphQLNumberType, + GraphQLBuiltInScalarType, + ListType, + Neo4jGraphQLSpatialType, + ScalarType, + Neo4jGraphQLTemporalType, + EnumType, + UserScalarType, + ObjectType, + UnionType, + InterfaceType, +} from "./attribute/AbstractAttribute"; +import type { Neo4jGraphQLScalarType, AttributeType } from "./attribute/AbstractAttribute"; +import { Attribute } from "./attribute/Attribute"; import { CompositeEntity } from "./entity/CompositeEntity"; import { ConcreteEntity } from "./entity/ConcreteEntity"; import { parseAuthorizationAnnotation } from "./parser/authorization-annotation"; import { parseCypherAnnotation } from "./parser/cypher-annotation"; import { parseKeyAnnotation } from "./parser/key-annotation"; -import { parseArguments } from "./parser/utils"; +import { parseArguments, findDirective } from "./parser/utils"; import type { RelationshipDirection } from "./relationship/Relationship"; import { Relationship } from "./relationship/Relationship"; + +import { getDefinitionCollection } from "./parser/definition-collection"; +import type { DefinitionCollection } from "./parser/definition-collection"; + import { parseAuthenticationAnnotation } from "./parser/authentication-annotation"; import { Operation } from "./Operation"; import { Field } from "./attribute/Field"; import { parseSubscriptionsAuthorizationAnnotation } from "./parser/subscriptions-authorization-annotation"; + export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { - const definitionNodes = getDefinitionNodes(document); + const definitionCollection: DefinitionCollection = getDefinitionCollection(document); const operations: Operations = definitionNodes.operations.reduce((acc, definition): Operations => { acc[definition.name.value] = generateOperation(definition); @@ -55,11 +73,14 @@ export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { }, {}); // init interface to typeNames map - const interfaceToImplementingTypeNamesMap = initInterfacesToTypeNamesMap(definitionNodes); // hydrate interface to typeNames map - hydrateInterfacesToTypeNamesMap(definitionNodes, interfaceToImplementingTypeNamesMap); + hydrateInterfacesToTypeNamesMap(definitionCollection); - const concreteEntities = definitionNodes.objectTypes.map(generateConcreteEntity); + const concreteEntities = Array.from(definitionCollection.nodes.values()).map((node) => + generateConcreteEntity(node, definitionCollection) + ); + + // TODO: this error could happen directly in getDefinitionCollection instead of here, as because we moved to Map structure it will never be the case that we have duplicate nodes. const concreteEntitiesMap = concreteEntities.reduce((acc, entity) => { if (acc.has(entity.name)) { throw new Neo4jGraphQLSchemaValidationError(`Duplicate node ${entity.name}`); @@ -68,15 +89,15 @@ export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { return acc; }, new Map()); - const interfaceEntities = Array.from(interfaceToImplementingTypeNamesMap.entries()).map( + const interfaceEntities = Array.from(definitionCollection.interfaceToImplementingTypeNamesMap.entries()).map( ([name, concreteEntities]) => { return generateCompositeEntity(name, concreteEntities, concreteEntitiesMap); } ); - const unionEntities = definitionNodes.unionTypes.map((entity) => { + const unionEntities = Array.from(definitionCollection.unionTypes).map(([unionName, unionDefinition]) => { return generateCompositeEntity( - entity.name.value, - entity.types?.map((t) => t.name.value) || [], + unionName, + unionDefinition.types?.map((t) => t.name.value) || [], concreteEntitiesMap ); }); @@ -100,36 +121,28 @@ export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { annotations, }); - definitionNodes.objectTypes.map((def) => hydrateRelationships(def, schema, definitionNodes)); return schema; } -function initInterfacesToTypeNamesMap(definitionNodes: DefinitionNodes) { - return definitionNodes.interfaceTypes.reduce((acc, entity) => { - const interfaceTypeName = entity.name.value; - acc.set(interfaceTypeName, []); - return acc; - }, new Map()); -} - -function hydrateInterfacesToTypeNamesMap( - definitionNodes: DefinitionNodes, - interfaceToImplementingTypeNamesMap: Map -) { - return definitionNodes.objectTypes.forEach((el) => { - if (!el.interfaces) { +function hydrateInterfacesToTypeNamesMap(definitionCollection: DefinitionCollection) { + return definitionCollection.nodes.forEach((node) => { + if (!node.interfaces) { return; } - const objectTypeName = el.name.value; - el.interfaces?.forEach((i) => { + const objectTypeName = node.name.value; + node.interfaces?.forEach((i) => { const interfaceTypeName = i.name.value; - const concreteEntities = interfaceToImplementingTypeNamesMap.get(interfaceTypeName); + const concreteEntities = definitionCollection.interfaceToImplementingTypeNamesMap.get(interfaceTypeName); if (!concreteEntities) { throw new Neo4jGraphQLSchemaValidationError( `Could not find composite entity with name ${interfaceTypeName}` ); } - interfaceToImplementingTypeNamesMap.set(interfaceTypeName, concreteEntities.concat(objectTypeName)); + // TODO: modify the existing array instead of creating a new one + definitionCollection.interfaceToImplementingTypeNamesMap.set( + interfaceTypeName, + concreteEntities.concat(objectTypeName) + ); }); }); } @@ -146,13 +159,11 @@ function generateCompositeEntity( } return concreteEntity; }); - - // TODO: fix for interfaces annotated with @relationshipFields - which will never have concrete entities - // if (!compositeFields.length) { - // throw new Neo4jGraphQLSchemaValidationError( - // `Composite entity ${entityDefinitionName} has no concrete entities` - // ); - // } + if (!compositeFields.length) { + throw new Neo4jGraphQLSchemaValidationError( + `Composite entity ${entityDefinitionName} has no concrete entities` + ); + } // TODO: add annotations return new CompositeEntity({ name: entityDefinitionName, @@ -163,7 +174,7 @@ function generateCompositeEntity( function hydrateRelationships( definition: ObjectTypeDefinitionNode, schema: Neo4jGraphQLSchemaModel, - definitionNodes: DefinitionNodes + definitionCollection: DefinitionCollection ): void { const name = definition.name.value; const entity = schema.getEntity(name); @@ -171,12 +182,8 @@ function hydrateRelationships( if (!schema.isConcreteEntity(entity)) { throw new Error(`Cannot add relationship to non-concrete entity ${name}`); } - - const relationshipPropertyInterfaces = getRelationshipPropertiesInterfaces(definitionNodes); - const relationshipFields = (definition.fields || []).map((fieldDefinition) => { - // TODO: use same relationship for 2 different entities if possible - return generateRelationshipField(fieldDefinition, schema, entity, relationshipPropertyInterfaces); + return generateRelationshipField(fieldDefinition, schema, entity, definitionCollection); }); for (const relationship of filterTruthy(relationshipFields)) { @@ -184,28 +191,11 @@ function hydrateRelationships( } } -function getRelationshipPropertiesInterfaces( - definitionNodes: DefinitionNodes -): Map { - return ( - definitionNodes.interfaceTypes - // Uncomment this to enforce @relationshipProperties in 4.0 - // .filter((interfaceDef: InterfaceTypeDefinitionNode) => { - // const relDirective = findDirective(interfaceDef.directives || [], "relationshipProperties"); - // return Boolean(relDirective); - // }) - .reduce((acc, interfaceDef) => { - acc.set(interfaceDef.name.value, interfaceDef); - return acc; - }, new Map()) - ); -} - function generateRelationshipField( field: FieldDefinitionNode, schema: Neo4jGraphQLSchemaModel, source: ConcreteEntity, - propertyInterfaces: Map + definitionCollection: DefinitionCollection ): Relationship | undefined { const fieldTypeMeta = getFieldTypeMeta(field.type); const relationshipDirective = findDirective(field.directives || [], "relationship"); @@ -220,12 +210,14 @@ function generateRelationshipField( let attributes: Attribute[] = []; if (properties && typeof properties === "string") { - const propertyInterface = propertyInterfaces.get(properties); + const propertyInterface = definitionCollection.relationshipProperties.get(properties); if (!propertyInterface) throw new Error( `There is no matching interface defined with @relationshipProperties for properties "${properties}"` ); - const fields = (propertyInterface.fields || []).map((field) => generateAttribute(field)); + + const fields = (propertyInterface.fields || []).map((field) => generateAttribute(field, definitionCollection)); + attributes = filterTruthy(fields); } return new Relationship({ @@ -238,8 +230,13 @@ function generateRelationshipField( }); } -function generateConcreteEntity(definition: ObjectTypeDefinitionNode): ConcreteEntity { - const fields = (definition.fields || []).map((fieldDefinition) => generateAttribute(fieldDefinition)); +function generateConcreteEntity( + definition: ObjectTypeDefinitionNode, + definitionCollection: DefinitionCollection +): ConcreteEntity { + const fields = (definition.fields || []).map((fieldDefinition) => + generateAttribute(fieldDefinition, definitionCollection) + ); const directives = (definition.directives || []).reduce((acc, directive) => { acc.set(directive.name.value, parseArguments(directive)); @@ -263,35 +260,107 @@ function getLabels(definition: ObjectTypeDefinitionNode, nodeDirectiveArguments: return [definition.name.value]; } -function generateAttribute(field: FieldDefinitionNode): Attribute | undefined { - const typeMeta = getFieldTypeMeta(field.type); // TODO: without originalType - const attributeType = isAttributeType(typeMeta.name) ? typeMeta.name : AttributeType.ObjectType; +function parseTypeNode( + definitionCollection: DefinitionCollection, + typeNode: TypeNode, + isRequired = false +): AttributeType { + switch (typeNode.kind) { + case Kind.NAMED_TYPE: { + if (isScalarType(typeNode.name.value)) { + return new ScalarType(typeNode.name.value, isRequired); + } else if (isEnum(definitionCollection, typeNode.name.value)) { + return new EnumType(typeNode.name.value, isRequired); + } else if (isUserScalar(definitionCollection, typeNode.name.value)) { + return new UserScalarType(typeNode.name.value, isRequired); + } else if (isObject(definitionCollection, typeNode.name.value)) { + return new ObjectType(typeNode.name.value, isRequired); + } else if (isUnion(definitionCollection, typeNode.name.value)) { + return new UnionType(typeNode.name.value, isRequired); + } else if (isInterface(definitionCollection, typeNode.name.value)) { + return new InterfaceType(typeNode.name.value, isRequired); + } else { + throw new Error(`Error while parsing Attribute with name: ${typeNode.name.value}`); + } + } + + case Kind.LIST_TYPE: { + const innerType = parseTypeNode(definitionCollection, typeNode.type); + return new ListType(innerType, isRequired); + } + case Kind.NON_NULL_TYPE: + return parseTypeNode(definitionCollection, typeNode.type, true); + } +} + +function generateAttribute(field: FieldDefinitionNode, definitionCollection: DefinitionCollection): Attribute { + const name = field.name.value; + const type = parseTypeNode(definitionCollection, field.type); const annotations = createFieldAnnotations(field.directives || []); return new Attribute({ - name: field.name.value, + name, annotations, - type: attributeType, - isArray: Boolean(typeMeta.array), + type, }); } -function generateField(field: FieldDefinitionNode): Field | undefined { - const annotations = createFieldAnnotations(field.directives || []); - return new Field({ - name: field.name.value, - annotations, - }); + +function isInterface(definitionCollection: DefinitionCollection, name: string): boolean { + return definitionCollection.interfaceTypes.has(name); +} + +function isUnion(definitionCollection: DefinitionCollection, name: string): boolean { + return definitionCollection.unionTypes.has(name); +} + +function isEnum(definitionCollection: DefinitionCollection, name: string): boolean { + return definitionCollection.enumTypes.has(name); +} + +function isUserScalar(definitionCollection: DefinitionCollection, name: string) { + return definitionCollection.scalarTypes.has(name); +} + +function isObject(definitionCollection, name: string) { + return definitionCollection.nodes.has(name); +} + +function isScalarType(value: string): value is GraphQLBuiltInScalarType | Neo4jGraphQLScalarType { + return ( + isGraphQLBuiltInScalar(value) || + isNeo4jGraphQLSpatialType(value) || + isNeo4jGraphQLNumberType(value) || + isNeo4jGraphQLTemporalType(value) + ); +} + +function isGraphQLBuiltInScalar(value: string): value is GraphQLBuiltInScalarType { + return Object.values(GraphQLBuiltInScalarType).includes(value); +} + +function isNeo4jGraphQLSpatialType(value: string): value is Neo4jGraphQLSpatialType { + return Object.values(Neo4jGraphQLSpatialType).includes(value); } -function isAttributeType(typeName: string): typeName is AttributeType { - return Object.values(AttributeType).includes(typeName as any); +function isNeo4jGraphQLNumberType(value: string): value is Neo4jGraphQLNumberType { + return Object.values(Neo4jGraphQLNumberType).includes(value); } -function findDirective(directives: readonly DirectiveNode[], name: string): DirectiveNode | undefined { - return directives.find((d) => { - return d.name.value === name; +function isNeo4jGraphQLTemporalType(value: string): value is Neo4jGraphQLTemporalType { + return Object.values(Neo4jGraphQLTemporalType).includes(value); +} + +function generateField(field: FieldDefinitionNode, definitionCollection: DefinitionCollection): Attribute { + const name = field.name.value; + const type = parseTypeNode(definitionCollection, field.type); + const annotations = createFieldAnnotations(field.directives || []); + return new Attribute({ + name, + annotations, + type, }); } + function createFieldAnnotations(directives: readonly DirectiveNode[]): Annotation[] { return filterTruthy( directives.map((directive) => { diff --git a/packages/graphql/src/schema-model/parser/definition-collection.ts b/packages/graphql/src/schema-model/parser/definition-collection.ts new file mode 100644 index 0000000000..4525fd1e66 --- /dev/null +++ b/packages/graphql/src/schema-model/parser/definition-collection.ts @@ -0,0 +1,103 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { + DirectiveDefinitionNode, + DocumentNode, + EnumTypeDefinitionNode, + InterfaceTypeDefinitionNode, + ObjectTypeDefinitionNode, + ScalarTypeDefinitionNode, + SchemaExtensionNode, + UnionTypeDefinitionNode, +} from "graphql"; +import { Kind } from "graphql"; +import { jwtPayload, relationshipPropertiesDirective } from "../../graphql/directives"; +import { isRootType } from "../../utils/is-root-type"; +import { findDirective } from "./utils"; + +export type DefinitionCollection = { + nodes: Map; // this does not include @jwtPayload type. + scalarTypes: Map; + enumTypes: Map; + interfaceTypes: Map; // this does not include @relationshipProperties interfaces. + unionTypes: Map; + directives: Map; + relationshipProperties: Map; + schemaExtension: SchemaExtensionNode | undefined; + jwtPayload: ObjectTypeDefinitionNode | undefined; + interfaceToImplementingTypeNamesMap: Map; // TODO: change this logic, this was the logic contained in initInterfacesToTypeNamesMap but potentially can be simplified now. +}; + +export function getDefinitionCollection(document: DocumentNode): DefinitionCollection { + return document.definitions.reduce( + (definitionCollection, definition) => { + switch (definition.kind) { + case Kind.SCALAR_TYPE_DEFINITION: + definitionCollection.scalarTypes.set(definition.name.value, definition); + break; + case Kind.OBJECT_TYPE_DEFINITION: + if (definition.directives && findDirective(definition.directives, jwtPayload.name)) { + definitionCollection.jwtPayload = definition; + } else if (!isRootType(definition)) { + definitionCollection.nodes.set(definition.name.value, definition); + } + break; + case Kind.ENUM_TYPE_DEFINITION: + definitionCollection.enumTypes.set(definition.name.value, definition); + break; + case Kind.INTERFACE_TYPE_DEFINITION: + if ( + definition.directives && + findDirective(definition.directives, relationshipPropertiesDirective.name) + ) { + definitionCollection.relationshipProperties.set(definition.name.value, definition); + } else { + definitionCollection.interfaceTypes.set(definition.name.value, definition); + definitionCollection.interfaceToImplementingTypeNamesMap.set(definition.name.value, []); // previous initInterfacesToTypeNamesMap logic. + } + break; + case Kind.DIRECTIVE_DEFINITION: + definitionCollection.directives.set(definition.name.value, definition); + break; + case Kind.UNION_TYPE_DEFINITION: + definitionCollection.unionTypes.set(definition.name.value, definition); + break; + case Kind.SCHEMA_EXTENSION: + // This is based on the assumption that mergeTypeDefs is used and therefore there is only one schema extension (merged), this assumption is currently used as well for object extensions. + definitionCollection.schemaExtension = definition; + break; + } + + return definitionCollection; + }, + { + nodes: new Map(), + enumTypes: new Map(), + scalarTypes: new Map(), + interfaceTypes: new Map(), + directives: new Map(), + unionTypes: new Map(), + relationshipProperties: new Map(), + schemaExtension: undefined, + jwtPayload: undefined, + interfaceToImplementingTypeNamesMap: new Map(), + } + ); +} diff --git a/packages/graphql/src/schema-model/parser/utils.ts b/packages/graphql/src/schema-model/parser/utils.ts index fbc8fe8a3e..f005bb3473 100644 --- a/packages/graphql/src/schema-model/parser/utils.ts +++ b/packages/graphql/src/schema-model/parser/utils.ts @@ -25,3 +25,10 @@ export function parseArguments(directive: DirectiveNode): Record { + return d.name.value === name; + }); +} + diff --git a/packages/graphql/src/schema-model/relationship/Relationship.ts b/packages/graphql/src/schema-model/relationship/Relationship.ts index e9acd889b2..06c88c4d12 100644 --- a/packages/graphql/src/schema-model/relationship/Relationship.ts +++ b/packages/graphql/src/schema-model/relationship/Relationship.ts @@ -26,8 +26,8 @@ import type { Entity } from "../entity/Entity"; export type RelationshipDirection = "IN" | "OUT"; export class Relationship { - public readonly name: string; - public readonly type: string; + public readonly name: string; //Movie.genres + public readonly type: string; // "HAS_GENRE" public readonly attributes: Map = new Map(); public readonly source: ConcreteEntity; // Origin field of relationship public readonly target: Entity; diff --git a/packages/graphql/src/schema-model/utils/get-from-map.ts b/packages/graphql/src/schema-model/utils/get-from-map.ts new file mode 100644 index 0000000000..ff1fff0e89 --- /dev/null +++ b/packages/graphql/src/schema-model/utils/get-from-map.ts @@ -0,0 +1,31 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Utility function to ensure that the key exists in the map and avoid unnecessary type casting. + * Get the value from a map, if the key does not exist throw an error. + * + * */ +export function getFromMap(map: Map, key: K): V { + const item = map.get(key); + if (item === undefined) { + throw new Error(`Key "${String(key)}" does not exist in the map.`); + } + return item; +} diff --git a/packages/graphql/src/schema-model/utils/string-manipulation.ts b/packages/graphql/src/schema-model/utils/string-manipulation.ts new file mode 100644 index 0000000000..9e3124f87a --- /dev/null +++ b/packages/graphql/src/schema-model/utils/string-manipulation.ts @@ -0,0 +1,38 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import camelcase from "camelcase"; +import pluralize from "pluralize"; + +export function singular(name: string): string { + const singular = camelcase(name); + return `${leadingUnderscores(name)}${singular}`; +} + +// TODO this has to be tested as is different from Node.generatePlural +export function plural(name: string): string { + const plural = pluralize(camelcase(name)); + return `${leadingUnderscores(name)}${plural}`; +} + +export function leadingUnderscores(name: string): string { + const re = /^(_+).+/; + const match = re.exec(name); + return match?.[1] || ""; +} \ No newline at end of file From eabd22ea21203bb7cc7f5310b69b768f76c6fd80 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Wed, 19 Jul 2023 12:50:05 +0100 Subject: [PATCH 03/22] merge schema model changes with authorization changes --- .../src/schema-model/generate-model.ts | 34 +++++++++---------- .../parser/coalesce-annotation.ts | 4 +-- .../schema-model/parser/default-annotation.ts | 5 +-- .../parser/definition-collection.ts | 12 +++++-- 4 files changed, 31 insertions(+), 24 deletions(-) diff --git a/packages/graphql/src/schema-model/generate-model.ts b/packages/graphql/src/schema-model/generate-model.ts index 49e8e1feb1..04878db9a7 100644 --- a/packages/graphql/src/schema-model/generate-model.ts +++ b/packages/graphql/src/schema-model/generate-model.ts @@ -60,15 +60,15 @@ import type { DefinitionCollection } from "./parser/definition-collection"; import { parseAuthenticationAnnotation } from "./parser/authentication-annotation"; import { Operation } from "./Operation"; -import { Field } from "./attribute/Field"; import { parseSubscriptionsAuthorizationAnnotation } from "./parser/subscriptions-authorization-annotation"; +import { Field } from "./attribute/Field"; export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { const definitionCollection: DefinitionCollection = getDefinitionCollection(document); - const operations: Operations = definitionNodes.operations.reduce((acc, definition): Operations => { - acc[definition.name.value] = generateOperation(definition); + const operations: Operations = definitionCollection.operations.reduce((acc, definition): Operations => { + acc[definition.name.value] = generateOperation(definition, definitionCollection); return acc; }, {}); @@ -102,15 +102,7 @@ export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { ); }); - const schemaDirectives = definitionNodes.schemaExtensions.reduce( - (directives: DirectiveNode[], schemaExtension: SchemaExtensionNode) => { - if (schemaExtension.directives) { - directives.push(...schemaExtension.directives); - } - return directives; - }, - [] - ); + const schemaDirectives = definitionCollection.schemaDirectives; const annotations = createSchemaModelAnnotations(schemaDirectives); @@ -218,7 +210,7 @@ function generateRelationshipField( const fields = (propertyInterface.fields || []).map((field) => generateAttribute(field, definitionCollection)); - attributes = filterTruthy(fields); + attributes = filterTruthy(fields) as Attribute[]; } return new Relationship({ name: fieldName, @@ -248,7 +240,7 @@ function generateConcreteEntity( return new ConcreteEntity({ name: definition.name.value, labels, - attributes: filterTruthy(fields), + attributes: filterTruthy(fields) as Attribute[], annotations: createEntityAnnotations(definition.directives || []), }); } @@ -292,11 +284,17 @@ function parseTypeNode( return parseTypeNode(definitionCollection, typeNode.type, true); } } - -function generateAttribute(field: FieldDefinitionNode, definitionCollection: DefinitionCollection): Attribute { +// TODO: figure out difference between field and attribute +function generateAttribute(field: FieldDefinitionNode, definitionCollection: DefinitionCollection, retField = false): Attribute | Field { const name = field.name.value; const type = parseTypeNode(definitionCollection, field.type); const annotations = createFieldAnnotations(field.directives || []); + if (retField) { + return new Field({ + name, + annotations, + }) + } return new Attribute({ name, annotations, @@ -424,8 +422,8 @@ function createSchemaModelAnnotations(directives: readonly DirectiveNode[]): Ann return schemaModelAnnotations.concat(annotations); } -function generateOperation(definition: ObjectTypeDefinitionNode): Operation { - const fields = (definition.fields || []).map((fieldDefinition) => generateField(fieldDefinition)); +function generateOperation(definition: ObjectTypeDefinitionNode, definitionCollection: DefinitionCollection): Operation { + const fields = (definition.fields || []).map((fieldDefinition) => generateAttribute(fieldDefinition, definitionCollection, true)) as Field[]; return new Operation({ name: definition.name.value, diff --git a/packages/graphql/src/schema-model/parser/coalesce-annotation.ts b/packages/graphql/src/schema-model/parser/coalesce-annotation.ts index 57a9d3c61d..3728dc4913 100644 --- a/packages/graphql/src/schema-model/parser/coalesce-annotation.ts +++ b/packages/graphql/src/schema-model/parser/coalesce-annotation.ts @@ -21,7 +21,7 @@ import { Kind, type DirectiveNode } from "graphql"; import { Neo4jGraphQLSchemaValidationError } from "../../classes"; import type { CoalesceAnnotationValue } from "../annotation/CoalesceAnnotation"; import { CoalesceAnnotation } from "../annotation/CoalesceAnnotation"; -import { getArgumentValueByType } from "./utils"; +import parseValueNode from "./parse-value-node"; export function parseCoalesceAnnotation(directive: DirectiveNode): CoalesceAnnotation { if (!directive.arguments || !directive.arguments[0] || !directive.arguments[0].value.kind) { @@ -35,7 +35,7 @@ export function parseCoalesceAnnotation(directive: DirectiveNode): CoalesceAnnot case Kind.BOOLEAN: case Kind.INT: case Kind.FLOAT: - value = getArgumentValueByType(directive.arguments[0].value) as CoalesceAnnotationValue; + value = parseValueNode(directive.arguments[0].value) as CoalesceAnnotationValue; break; default: throw new Neo4jGraphQLSchemaValidationError( diff --git a/packages/graphql/src/schema-model/parser/default-annotation.ts b/packages/graphql/src/schema-model/parser/default-annotation.ts index ca4a4a6929..1589b79ebb 100644 --- a/packages/graphql/src/schema-model/parser/default-annotation.ts +++ b/packages/graphql/src/schema-model/parser/default-annotation.ts @@ -21,7 +21,8 @@ import { Kind, type DirectiveNode } from "graphql"; import { Neo4jGraphQLSchemaValidationError } from "../../classes"; import type { DefaultAnnotationValue } from "../annotation/DefaultAnnotation"; import { DefaultAnnotation } from "../annotation/DefaultAnnotation"; -import { getArgumentValueByType } from "./utils"; +import parseValueNode from "./parse-value-node"; + export function parseDefaultAnnotation(directive: DirectiveNode): DefaultAnnotation { if (!directive.arguments || !directive.arguments[0] || !directive.arguments[0].value.kind) { @@ -35,7 +36,7 @@ export function parseDefaultAnnotation(directive: DirectiveNode): DefaultAnnotat case Kind.BOOLEAN: case Kind.INT: case Kind.FLOAT: - value = getArgumentValueByType(directive.arguments[0].value) as DefaultAnnotationValue; + value = parseValueNode(directive.arguments[0].value) as DefaultAnnotationValue; break; default: throw new Neo4jGraphQLSchemaValidationError( diff --git a/packages/graphql/src/schema-model/parser/definition-collection.ts b/packages/graphql/src/schema-model/parser/definition-collection.ts index 4525fd1e66..fc05b68106 100644 --- a/packages/graphql/src/schema-model/parser/definition-collection.ts +++ b/packages/graphql/src/schema-model/parser/definition-collection.ts @@ -19,6 +19,7 @@ import type { DirectiveDefinitionNode, + DirectiveNode, DocumentNode, EnumTypeDefinitionNode, InterfaceTypeDefinitionNode, @@ -28,7 +29,7 @@ import type { UnionTypeDefinitionNode, } from "graphql"; import { Kind } from "graphql"; -import { jwtPayload, relationshipPropertiesDirective } from "../../graphql/directives"; +import { jwt, relationshipPropertiesDirective } from "../../graphql/directives"; import { isRootType } from "../../utils/is-root-type"; import { findDirective } from "./utils"; @@ -43,6 +44,8 @@ export type DefinitionCollection = { schemaExtension: SchemaExtensionNode | undefined; jwtPayload: ObjectTypeDefinitionNode | undefined; interfaceToImplementingTypeNamesMap: Map; // TODO: change this logic, this was the logic contained in initInterfacesToTypeNamesMap but potentially can be simplified now. + operations: ObjectTypeDefinitionNode[]; + schemaDirectives: DirectiveNode[]; }; export function getDefinitionCollection(document: DocumentNode): DefinitionCollection { @@ -53,10 +56,12 @@ export function getDefinitionCollection(document: DocumentNode): DefinitionColle definitionCollection.scalarTypes.set(definition.name.value, definition); break; case Kind.OBJECT_TYPE_DEFINITION: - if (definition.directives && findDirective(definition.directives, jwtPayload.name)) { + if (definition.directives && findDirective(definition.directives, jwt.name)) { definitionCollection.jwtPayload = definition; } else if (!isRootType(definition)) { definitionCollection.nodes.set(definition.name.value, definition); + } else { + definitionCollection.operations.push(definition); } break; case Kind.ENUM_TYPE_DEFINITION: @@ -82,6 +87,7 @@ export function getDefinitionCollection(document: DocumentNode): DefinitionColle case Kind.SCHEMA_EXTENSION: // This is based on the assumption that mergeTypeDefs is used and therefore there is only one schema extension (merged), this assumption is currently used as well for object extensions. definitionCollection.schemaExtension = definition; + definitionCollection.schemaDirectives = definition.directives ? Array.from(definition.directives) : []; break; } @@ -98,6 +104,8 @@ export function getDefinitionCollection(document: DocumentNode): DefinitionColle schemaExtension: undefined, jwtPayload: undefined, interfaceToImplementingTypeNamesMap: new Map(), + operations: [], + schemaDirectives: [], } ); } From 705d28763e32d08e2e938fa889d54012fbca5af0 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Wed, 19 Jul 2023 14:09:40 +0100 Subject: [PATCH 04/22] fix relationship-field parsing on schema model --- packages/graphql/src/schema-model/generate-model.ts | 7 ++----- .../graphql/src/schema-model/utils/string-manipulation.ts | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/graphql/src/schema-model/generate-model.ts b/packages/graphql/src/schema-model/generate-model.ts index 04878db9a7..d550c4e958 100644 --- a/packages/graphql/src/schema-model/generate-model.ts +++ b/packages/graphql/src/schema-model/generate-model.ts @@ -72,7 +72,6 @@ export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { return acc; }, {}); - // init interface to typeNames map // hydrate interface to typeNames map hydrateInterfacesToTypeNamesMap(definitionCollection); @@ -102,9 +101,7 @@ export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { ); }); - const schemaDirectives = definitionCollection.schemaDirectives; - - const annotations = createSchemaModelAnnotations(schemaDirectives); + const annotations = createSchemaModelAnnotations(definitionCollection.schemaDirectives); const schema = new Neo4jGraphQLSchemaModel({ compositeEntities: [...unionEntities, ...interfaceEntities], @@ -112,7 +109,7 @@ export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { operations, annotations, }); - + definitionCollection.nodes.forEach((def) => hydrateRelationships(def, schema, definitionCollection)); return schema; } diff --git a/packages/graphql/src/schema-model/utils/string-manipulation.ts b/packages/graphql/src/schema-model/utils/string-manipulation.ts index 9e3124f87a..387e97ec6c 100644 --- a/packages/graphql/src/schema-model/utils/string-manipulation.ts +++ b/packages/graphql/src/schema-model/utils/string-manipulation.ts @@ -35,4 +35,4 @@ export function leadingUnderscores(name: string): string { const re = /^(_+).+/; const match = re.exec(name); return match?.[1] || ""; -} \ No newline at end of file +} From b6c113a7e489a97428bded5fb3523dd6b30f346e Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Wed, 19 Jul 2023 14:46:40 +0100 Subject: [PATCH 05/22] organise parser folder --- .../src/schema-model/generate-model.ts | 184 ++---------------- .../alias-annotation.test.ts | 0 .../alias-annotation.ts | 6 +- .../authentication-annotation.ts | 8 +- .../authorization-annotation.ts | 8 +- .../coalesce-annotation.test.ts | 0 .../coalesce-annotation.ts | 8 +- .../custom-resolver-annotation.test.ts | 0 .../custom-resolver-annotation.ts | 6 +- .../cypher-annotation.ts | 6 +- .../default-annotation.test.ts | 0 .../default-annotation.ts | 9 +- .../filterable-annotation.test.ts | 0 .../filterable-annotation.ts | 4 +- .../full-text-annotation.test.ts | 0 .../full-text-annotation.ts | 6 +- .../id-annotation.test.ts | 0 .../{ => annotations-parser}/id-annotation.ts | 4 +- .../jwt-claim-annotation.test.ts | 0 .../jwt-claim-annotation.ts | 4 +- .../jwt-payload-annoatation.ts | 2 +- .../key-annotation.test.ts | 0 .../key-annotation.ts | 7 +- .../mutation-annotation.test.ts | 2 +- .../mutation-annotation.ts | 6 +- .../node-annotation.test.ts | 0 .../node-annotation.ts | 4 +- .../plural-annotation.test.ts | 0 .../plural-annotation.ts | 4 +- .../populated-by-annotation.test.ts | 0 .../populated-by-annotation.ts | 4 +- .../private-annotation.ts | 2 +- .../query-annotation.test.ts | 0 .../query-annotation.ts | 4 +- .../query-options-annotation.test.ts | 0 .../query-options-annotation.ts | 6 +- .../relationship-properties-annotation.ts | 2 +- .../selectable-annotation.test.ts | 0 .../selectable-annotation.ts | 4 +- .../settable-annotation.test.ts | 0 .../settable-annotation.ts | 4 +- .../subscription-annotation.test.ts | 0 .../subscription-annotation.ts | 4 +- .../subscriptions-authorization-annotation.ts | 8 +- .../timestamp-annotation.test.ts | 0 .../timestamp-annotation.ts | 4 +- .../unique-annotation.test.ts | 0 .../unique-annotation.ts | 4 +- .../schema-model/parser/parse-attribute.ts | 166 ++++++++++++++++ .../parser/parse-value-node.test.ts | 2 +- .../schema-model/parser/parse-value-node.ts | 3 +- .../graphql/src/schema-model/parser/utils.ts | 3 +- .../graphql/src/schema/get-obj-field-meta.ts | 2 +- .../schema/parse/parse-fulltext-directive.ts | 2 +- .../parse/parse-query-options-directive.ts | 4 +- packages/graphql/src/schema/to-compose.ts | 2 +- 56 files changed, 259 insertions(+), 249 deletions(-) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/alias-annotation.test.ts (100%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/alias-annotation.ts (86%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/authentication-annotation.ts (83%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/authorization-annotation.ts (90%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/coalesce-annotation.test.ts (100%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/coalesce-annotation.ts (84%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/custom-resolver-annotation.test.ts (100%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/custom-resolver-annotation.ts (84%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/cypher-annotation.ts (85%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/default-annotation.test.ts (100%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/default-annotation.ts (84%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/filterable-annotation.test.ts (100%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/filterable-annotation.ts (89%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/full-text-annotation.test.ts (100%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/full-text-annotation.ts (83%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/id-annotation.test.ts (100%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/id-annotation.ts (90%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/jwt-claim-annotation.test.ts (100%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/jwt-claim-annotation.ts (88%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/jwt-payload-annoatation.ts (92%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/key-annotation.test.ts (100%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/key-annotation.ts (86%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/mutation-annotation.test.ts (97%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/mutation-annotation.ts (84%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/node-annotation.test.ts (100%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/node-annotation.ts (89%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/plural-annotation.test.ts (100%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/plural-annotation.ts (89%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/populated-by-annotation.test.ts (100%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/populated-by-annotation.ts (89%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/private-annotation.ts (92%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/query-annotation.test.ts (100%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/query-annotation.ts (89%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/query-options-annotation.test.ts (100%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/query-options-annotation.ts (88%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/relationship-properties-annotation.ts (90%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/selectable-annotation.test.ts (100%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/selectable-annotation.ts (89%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/settable-annotation.test.ts (100%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/settable-annotation.ts (89%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/subscription-annotation.test.ts (100%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/subscription-annotation.ts (88%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/subscriptions-authorization-annotation.ts (88%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/timestamp-annotation.test.ts (100%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/timestamp-annotation.ts (88%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/unique-annotation.test.ts (100%) rename packages/graphql/src/schema-model/parser/{ => annotations-parser}/unique-annotation.ts (89%) create mode 100644 packages/graphql/src/schema-model/parser/parse-attribute.ts diff --git a/packages/graphql/src/schema-model/generate-model.ts b/packages/graphql/src/schema-model/generate-model.ts index d550c4e958..a66629e2ac 100644 --- a/packages/graphql/src/schema-model/generate-model.ts +++ b/packages/graphql/src/schema-model/generate-model.ts @@ -16,59 +16,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type { - TypeNode, - DirectiveNode, - DocumentNode, - FieldDefinitionNode, - ObjectTypeDefinitionNode, - SchemaExtensionNode, -} from "graphql"; -import { Kind } from "graphql"; +import type { DirectiveNode, DocumentNode, FieldDefinitionNode, ObjectTypeDefinitionNode } from "graphql"; import { Neo4jGraphQLSchemaValidationError } from "../classes"; import getFieldTypeMeta from "../schema/get-field-type-meta"; import { filterTruthy } from "../utils/utils"; import { Neo4jGraphQLSchemaModel } from "./Neo4jGraphQLSchemaModel"; import type { Operations } from "./Neo4jGraphQLSchemaModel"; import type { Annotation } from "./annotation/Annotation"; -import { - Neo4jGraphQLNumberType, - GraphQLBuiltInScalarType, - ListType, - Neo4jGraphQLSpatialType, - ScalarType, - Neo4jGraphQLTemporalType, - EnumType, - UserScalarType, - ObjectType, - UnionType, - InterfaceType, -} from "./attribute/AbstractAttribute"; -import type { Neo4jGraphQLScalarType, AttributeType } from "./attribute/AbstractAttribute"; -import { Attribute } from "./attribute/Attribute"; +import type { Attribute } from "./attribute/Attribute"; import { CompositeEntity } from "./entity/CompositeEntity"; import { ConcreteEntity } from "./entity/ConcreteEntity"; -import { parseAuthorizationAnnotation } from "./parser/authorization-annotation"; -import { parseCypherAnnotation } from "./parser/cypher-annotation"; -import { parseKeyAnnotation } from "./parser/key-annotation"; +import { parseAuthorizationAnnotation } from "./parser/annotations-parser/authorization-annotation"; +import { parseKeyAnnotation } from "./parser/annotations-parser/key-annotation"; import { parseArguments, findDirective } from "./parser/utils"; import type { RelationshipDirection } from "./relationship/Relationship"; import { Relationship } from "./relationship/Relationship"; - -import { getDefinitionCollection } from "./parser/definition-collection"; import type { DefinitionCollection } from "./parser/definition-collection"; - -import { parseAuthenticationAnnotation } from "./parser/authentication-annotation"; +import { getDefinitionCollection } from "./parser/definition-collection"; +import { parseAuthenticationAnnotation } from "./parser/annotations-parser/authentication-annotation"; import { Operation } from "./Operation"; -import { parseSubscriptionsAuthorizationAnnotation } from "./parser/subscriptions-authorization-annotation"; -import { Field } from "./attribute/Field"; - +import { parseSubscriptionsAuthorizationAnnotation } from "./parser/annotations-parser/subscriptions-authorization-annotation"; +import { parseAttribute, parseField } from "./parser/parse-attribute"; export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { const definitionCollection: DefinitionCollection = getDefinitionCollection(document); const operations: Operations = definitionCollection.operations.reduce((acc, definition): Operations => { - acc[definition.name.value] = generateOperation(definition, definitionCollection); + acc[definition.name.value] = generateOperation(definition); return acc; }, {}); @@ -149,9 +123,9 @@ function generateCompositeEntity( return concreteEntity; }); if (!compositeFields.length) { - throw new Neo4jGraphQLSchemaValidationError( + throw new Neo4jGraphQLSchemaValidationError( `Composite entity ${entityDefinitionName} has no concrete entities` - ); + ); } // TODO: add annotations return new CompositeEntity({ @@ -204,8 +178,8 @@ function generateRelationshipField( throw new Error( `There is no matching interface defined with @relationshipProperties for properties "${properties}"` ); - - const fields = (propertyInterface.fields || []).map((field) => generateAttribute(field, definitionCollection)); + + const fields = (propertyInterface.fields || []).map((field) => parseAttribute(field, definitionCollection)); attributes = filterTruthy(fields) as Attribute[]; } @@ -224,7 +198,7 @@ function generateConcreteEntity( definitionCollection: DefinitionCollection ): ConcreteEntity { const fields = (definition.fields || []).map((fieldDefinition) => - generateAttribute(fieldDefinition, definitionCollection) + parseAttribute(fieldDefinition, definitionCollection) ); const directives = (definition.directives || []).reduce((acc, directive) => { @@ -249,132 +223,6 @@ function getLabels(definition: ObjectTypeDefinitionNode, nodeDirectiveArguments: return [definition.name.value]; } -function parseTypeNode( - definitionCollection: DefinitionCollection, - typeNode: TypeNode, - isRequired = false -): AttributeType { - switch (typeNode.kind) { - case Kind.NAMED_TYPE: { - if (isScalarType(typeNode.name.value)) { - return new ScalarType(typeNode.name.value, isRequired); - } else if (isEnum(definitionCollection, typeNode.name.value)) { - return new EnumType(typeNode.name.value, isRequired); - } else if (isUserScalar(definitionCollection, typeNode.name.value)) { - return new UserScalarType(typeNode.name.value, isRequired); - } else if (isObject(definitionCollection, typeNode.name.value)) { - return new ObjectType(typeNode.name.value, isRequired); - } else if (isUnion(definitionCollection, typeNode.name.value)) { - return new UnionType(typeNode.name.value, isRequired); - } else if (isInterface(definitionCollection, typeNode.name.value)) { - return new InterfaceType(typeNode.name.value, isRequired); - } else { - throw new Error(`Error while parsing Attribute with name: ${typeNode.name.value}`); - } - } - - case Kind.LIST_TYPE: { - const innerType = parseTypeNode(definitionCollection, typeNode.type); - return new ListType(innerType, isRequired); - } - case Kind.NON_NULL_TYPE: - return parseTypeNode(definitionCollection, typeNode.type, true); - } -} -// TODO: figure out difference between field and attribute -function generateAttribute(field: FieldDefinitionNode, definitionCollection: DefinitionCollection, retField = false): Attribute | Field { - const name = field.name.value; - const type = parseTypeNode(definitionCollection, field.type); - const annotations = createFieldAnnotations(field.directives || []); - if (retField) { - return new Field({ - name, - annotations, - }) - } - return new Attribute({ - name, - annotations, - type, - }); -} - -function isInterface(definitionCollection: DefinitionCollection, name: string): boolean { - return definitionCollection.interfaceTypes.has(name); -} - -function isUnion(definitionCollection: DefinitionCollection, name: string): boolean { - return definitionCollection.unionTypes.has(name); -} - -function isEnum(definitionCollection: DefinitionCollection, name: string): boolean { - return definitionCollection.enumTypes.has(name); -} - -function isUserScalar(definitionCollection: DefinitionCollection, name: string) { - return definitionCollection.scalarTypes.has(name); -} - -function isObject(definitionCollection, name: string) { - return definitionCollection.nodes.has(name); -} - -function isScalarType(value: string): value is GraphQLBuiltInScalarType | Neo4jGraphQLScalarType { - return ( - isGraphQLBuiltInScalar(value) || - isNeo4jGraphQLSpatialType(value) || - isNeo4jGraphQLNumberType(value) || - isNeo4jGraphQLTemporalType(value) - ); -} - -function isGraphQLBuiltInScalar(value: string): value is GraphQLBuiltInScalarType { - return Object.values(GraphQLBuiltInScalarType).includes(value); -} - -function isNeo4jGraphQLSpatialType(value: string): value is Neo4jGraphQLSpatialType { - return Object.values(Neo4jGraphQLSpatialType).includes(value); -} - -function isNeo4jGraphQLNumberType(value: string): value is Neo4jGraphQLNumberType { - return Object.values(Neo4jGraphQLNumberType).includes(value); -} - -function isNeo4jGraphQLTemporalType(value: string): value is Neo4jGraphQLTemporalType { - return Object.values(Neo4jGraphQLTemporalType).includes(value); -} - -function generateField(field: FieldDefinitionNode, definitionCollection: DefinitionCollection): Attribute { - const name = field.name.value; - const type = parseTypeNode(definitionCollection, field.type); - const annotations = createFieldAnnotations(field.directives || []); - return new Attribute({ - name, - annotations, - type, - }); -} - - -function createFieldAnnotations(directives: readonly DirectiveNode[]): Annotation[] { - return filterTruthy( - directives.map((directive) => { - switch (directive.name.value) { - case "cypher": - return parseCypherAnnotation(directive); - case "authorization": - return parseAuthorizationAnnotation(directive); - case "authentication": - return parseAuthenticationAnnotation(directive); - case "subscriptionsAuthorization": - return parseSubscriptionsAuthorizationAnnotation(directive); - default: - return undefined; - } - }) - ); -} - function createEntityAnnotations(directives: readonly DirectiveNode[]): Annotation[] { const entityAnnotations: Annotation[] = []; @@ -419,8 +267,8 @@ function createSchemaModelAnnotations(directives: readonly DirectiveNode[]): Ann return schemaModelAnnotations.concat(annotations); } -function generateOperation(definition: ObjectTypeDefinitionNode, definitionCollection: DefinitionCollection): Operation { - const fields = (definition.fields || []).map((fieldDefinition) => generateAttribute(fieldDefinition, definitionCollection, true)) as Field[]; +function generateOperation(definition: ObjectTypeDefinitionNode): Operation { + const fields = (definition.fields || []).map((fieldDefinition) => parseField(fieldDefinition)); return new Operation({ name: definition.name.value, diff --git a/packages/graphql/src/schema-model/parser/alias-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/alias-annotation.test.ts similarity index 100% rename from packages/graphql/src/schema-model/parser/alias-annotation.test.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/alias-annotation.test.ts diff --git a/packages/graphql/src/schema-model/parser/alias-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/alias-annotation.ts similarity index 86% rename from packages/graphql/src/schema-model/parser/alias-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/alias-annotation.ts index 2b8048a81a..698cdd2a0b 100644 --- a/packages/graphql/src/schema-model/parser/alias-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/alias-annotation.ts @@ -17,9 +17,9 @@ * limitations under the License. */ import type { DirectiveNode } from "graphql"; -import { Neo4jGraphQLSchemaValidationError } from "../../classes"; -import { AliasAnnotation } from "../annotation/AliasAnnotation"; -import { parseArguments } from "./utils"; +import { Neo4jGraphQLSchemaValidationError } from "../../../classes"; +import { AliasAnnotation } from "../../annotation/AliasAnnotation"; +import { parseArguments } from "../utils"; export function parseAliasAnnotation(directive: DirectiveNode): AliasAnnotation { const { property, ...unrecognizedArguments } = parseArguments(directive) as { diff --git a/packages/graphql/src/schema-model/parser/authentication-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/authentication-annotation.ts similarity index 83% rename from packages/graphql/src/schema-model/parser/authentication-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/authentication-annotation.ts index feb6c50b4c..0b957c7b84 100644 --- a/packages/graphql/src/schema-model/parser/authentication-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/authentication-annotation.ts @@ -17,11 +17,11 @@ * limitations under the License. */ import type { DirectiveNode } from "graphql"; -import type { GraphQLWhereArg } from "../../types"; -import type { AuthenticationOperation } from "../annotation/AuthenticationAnnotation"; -import { AuthenticationAnnotation } from "../annotation/AuthenticationAnnotation"; +import type { GraphQLWhereArg } from "../../../types"; +import type { AuthenticationOperation } from "../../annotation/AuthenticationAnnotation"; +import { AuthenticationAnnotation } from "../../annotation/AuthenticationAnnotation"; -import { parseArguments } from "./utils"; +import { parseArguments } from "../utils"; const authenticationDefaultOperations: AuthenticationOperation[] = [ "READ", "AGGREGATE", diff --git a/packages/graphql/src/schema-model/parser/authorization-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/authorization-annotation.ts similarity index 90% rename from packages/graphql/src/schema-model/parser/authorization-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/authorization-annotation.ts index ae647fc5d1..0044cbd13f 100644 --- a/packages/graphql/src/schema-model/parser/authorization-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/authorization-annotation.ts @@ -17,18 +17,18 @@ * limitations under the License. */ import type { DirectiveNode } from "graphql"; -import { Neo4jGraphQLSchemaValidationError } from "../../classes"; +import { Neo4jGraphQLSchemaValidationError } from "../../../classes"; import type { AuthorizationFilterRuleConstructor, AuthorizationValidateRuleConstructor, -} from "../annotation/AuthorizationAnnotation"; +} from "../../annotation/AuthorizationAnnotation"; import { AuthorizationAnnotation, AuthorizationAnnotationArguments, AuthorizationFilterRule, AuthorizationValidateRule, -} from "../annotation/AuthorizationAnnotation"; -import { parseArguments } from "./utils"; +} from "../../annotation/AuthorizationAnnotation"; +import { parseArguments } from "../utils"; export function parseAuthorizationAnnotation(directive: DirectiveNode): AuthorizationAnnotation { const { filter, validate, ...unrecognizedArguments } = parseArguments(directive) as { diff --git a/packages/graphql/src/schema-model/parser/coalesce-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/coalesce-annotation.test.ts similarity index 100% rename from packages/graphql/src/schema-model/parser/coalesce-annotation.test.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/coalesce-annotation.test.ts diff --git a/packages/graphql/src/schema-model/parser/coalesce-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/coalesce-annotation.ts similarity index 84% rename from packages/graphql/src/schema-model/parser/coalesce-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/coalesce-annotation.ts index 3728dc4913..9ad4c2f19d 100644 --- a/packages/graphql/src/schema-model/parser/coalesce-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/coalesce-annotation.ts @@ -18,10 +18,10 @@ */ import { Kind, type DirectiveNode } from "graphql"; -import { Neo4jGraphQLSchemaValidationError } from "../../classes"; -import type { CoalesceAnnotationValue } from "../annotation/CoalesceAnnotation"; -import { CoalesceAnnotation } from "../annotation/CoalesceAnnotation"; -import parseValueNode from "./parse-value-node"; +import { Neo4jGraphQLSchemaValidationError } from "../../../classes"; +import type { CoalesceAnnotationValue } from "../../annotation/CoalesceAnnotation"; +import { CoalesceAnnotation } from "../../annotation/CoalesceAnnotation"; +import { parseValueNode } from "../parse-value-node"; export function parseCoalesceAnnotation(directive: DirectiveNode): CoalesceAnnotation { if (!directive.arguments || !directive.arguments[0] || !directive.arguments[0].value.kind) { diff --git a/packages/graphql/src/schema-model/parser/custom-resolver-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/custom-resolver-annotation.test.ts similarity index 100% rename from packages/graphql/src/schema-model/parser/custom-resolver-annotation.test.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/custom-resolver-annotation.test.ts diff --git a/packages/graphql/src/schema-model/parser/custom-resolver-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/custom-resolver-annotation.ts similarity index 84% rename from packages/graphql/src/schema-model/parser/custom-resolver-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/custom-resolver-annotation.ts index 5f545f0d9a..6b1125bf2c 100644 --- a/packages/graphql/src/schema-model/parser/custom-resolver-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/custom-resolver-annotation.ts @@ -17,9 +17,9 @@ * limitations under the License. */ import type { DirectiveNode } from "graphql"; -import { Neo4jGraphQLSchemaValidationError } from "../../classes"; -import { CustomResolverAnnotation } from "../annotation/CustomResolverAnnotation"; -import { parseArguments } from "./utils"; +import { Neo4jGraphQLSchemaValidationError } from "../../../classes"; +import { CustomResolverAnnotation } from "../../annotation/CustomResolverAnnotation"; +import { parseArguments } from "../utils"; export function parseCustomResolverAnnotation(directive: DirectiveNode): CustomResolverAnnotation { const { requires } = parseArguments(directive); diff --git a/packages/graphql/src/schema-model/parser/cypher-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/cypher-annotation.ts similarity index 85% rename from packages/graphql/src/schema-model/parser/cypher-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/cypher-annotation.ts index 521a396bc8..c587920b3d 100644 --- a/packages/graphql/src/schema-model/parser/cypher-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/cypher-annotation.ts @@ -17,9 +17,9 @@ * limitations under the License. */ import type { DirectiveNode } from "graphql"; -import { Neo4jGraphQLSchemaValidationError } from "../../classes"; -import { CypherAnnotation } from "../annotation/CypherAnnotation"; -import { parseArguments } from "./utils"; +import { Neo4jGraphQLSchemaValidationError } from "../../../classes"; +import { CypherAnnotation } from "../../annotation/CypherAnnotation"; +import { parseArguments } from "../utils"; export function parseCypherAnnotation(directive: DirectiveNode): CypherAnnotation { const { statement } = parseArguments(directive); diff --git a/packages/graphql/src/schema-model/parser/default-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/default-annotation.test.ts similarity index 100% rename from packages/graphql/src/schema-model/parser/default-annotation.test.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/default-annotation.test.ts diff --git a/packages/graphql/src/schema-model/parser/default-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/default-annotation.ts similarity index 84% rename from packages/graphql/src/schema-model/parser/default-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/default-annotation.ts index 1589b79ebb..7c1a7543c2 100644 --- a/packages/graphql/src/schema-model/parser/default-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/default-annotation.ts @@ -18,11 +18,10 @@ */ import { Kind, type DirectiveNode } from "graphql"; -import { Neo4jGraphQLSchemaValidationError } from "../../classes"; -import type { DefaultAnnotationValue } from "../annotation/DefaultAnnotation"; -import { DefaultAnnotation } from "../annotation/DefaultAnnotation"; -import parseValueNode from "./parse-value-node"; - +import { Neo4jGraphQLSchemaValidationError } from "../../../classes"; +import type { DefaultAnnotationValue } from "../../annotation/DefaultAnnotation"; +import { DefaultAnnotation } from "../../annotation/DefaultAnnotation"; +import { parseValueNode } from "../parse-value-node"; export function parseDefaultAnnotation(directive: DirectiveNode): DefaultAnnotation { if (!directive.arguments || !directive.arguments[0] || !directive.arguments[0].value.kind) { diff --git a/packages/graphql/src/schema-model/parser/filterable-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/filterable-annotation.test.ts similarity index 100% rename from packages/graphql/src/schema-model/parser/filterable-annotation.test.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/filterable-annotation.test.ts diff --git a/packages/graphql/src/schema-model/parser/filterable-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/filterable-annotation.ts similarity index 89% rename from packages/graphql/src/schema-model/parser/filterable-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/filterable-annotation.ts index 1c8b4032e6..8b6ab4700d 100644 --- a/packages/graphql/src/schema-model/parser/filterable-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/filterable-annotation.ts @@ -17,8 +17,8 @@ * limitations under the License. */ import type { DirectiveNode } from "graphql"; -import { FilterableAnnotation } from "../annotation/FilterableAnnotation"; -import { parseArguments } from "./utils"; +import { FilterableAnnotation } from "../../annotation/FilterableAnnotation"; +import { parseArguments } from "../utils"; export function parseFilterableAnnotation(directive: DirectiveNode): FilterableAnnotation { const { byValue, byAnnotation } = parseArguments(directive) as { byValue: boolean; byAnnotation: boolean }; diff --git a/packages/graphql/src/schema-model/parser/full-text-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/full-text-annotation.test.ts similarity index 100% rename from packages/graphql/src/schema-model/parser/full-text-annotation.test.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/full-text-annotation.test.ts diff --git a/packages/graphql/src/schema-model/parser/full-text-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/full-text-annotation.ts similarity index 83% rename from packages/graphql/src/schema-model/parser/full-text-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/full-text-annotation.ts index c6e2c7ee70..cf4384088d 100644 --- a/packages/graphql/src/schema-model/parser/full-text-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/full-text-annotation.ts @@ -17,9 +17,9 @@ * limitations under the License. */ import type { DirectiveNode } from "graphql"; -import type { FullTextFields } from "../annotation/FullTextAnnotation"; -import { FullTextAnnotation } from "../annotation/FullTextAnnotation"; -import { parseArguments } from "./utils"; +import type { FullTextFields } from "../../annotation/FullTextAnnotation"; +import { FullTextAnnotation } from "../../annotation/FullTextAnnotation"; +import { parseArguments } from "../utils"; export function parseFullTextAnnotation(directive: DirectiveNode): FullTextAnnotation { const { fields } = parseArguments(directive) as { fields: FullTextFields }; diff --git a/packages/graphql/src/schema-model/parser/id-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/id-annotation.test.ts similarity index 100% rename from packages/graphql/src/schema-model/parser/id-annotation.test.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/id-annotation.test.ts diff --git a/packages/graphql/src/schema-model/parser/id-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/id-annotation.ts similarity index 90% rename from packages/graphql/src/schema-model/parser/id-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/id-annotation.ts index 55837a40f7..397ece6ea0 100644 --- a/packages/graphql/src/schema-model/parser/id-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/id-annotation.ts @@ -18,8 +18,8 @@ */ import { type DirectiveNode } from "graphql"; -import { IDAnnotation } from "../annotation/IDAnnotation"; -import { parseArguments } from "./utils"; +import { IDAnnotation } from "../../annotation/IDAnnotation"; +import { parseArguments } from "../utils"; export function parseIDAnnotation(directive: DirectiveNode): IDAnnotation { const { autogenerate, unique, global } = parseArguments(directive) as { diff --git a/packages/graphql/src/schema-model/parser/jwt-claim-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/jwt-claim-annotation.test.ts similarity index 100% rename from packages/graphql/src/schema-model/parser/jwt-claim-annotation.test.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/jwt-claim-annotation.test.ts diff --git a/packages/graphql/src/schema-model/parser/jwt-claim-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/jwt-claim-annotation.ts similarity index 88% rename from packages/graphql/src/schema-model/parser/jwt-claim-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/jwt-claim-annotation.ts index 0ededd766d..9903f14b78 100644 --- a/packages/graphql/src/schema-model/parser/jwt-claim-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/jwt-claim-annotation.ts @@ -17,8 +17,8 @@ * limitations under the License. */ import type { DirectiveNode } from "graphql"; -import { JWTClaimAnnotation } from "../annotation/JWTClaimAnnotation"; -import { parseArguments } from "./utils"; +import { JWTClaimAnnotation } from "../../annotation/JWTClaimAnnotation"; +import { parseArguments } from "../utils"; export function parseJWTClaimAnnotation(directive: DirectiveNode): JWTClaimAnnotation { const { path } = parseArguments(directive) as { path: string }; diff --git a/packages/graphql/src/schema-model/parser/jwt-payload-annoatation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/jwt-payload-annoatation.ts similarity index 92% rename from packages/graphql/src/schema-model/parser/jwt-payload-annoatation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/jwt-payload-annoatation.ts index e5b30b67f6..5f97893024 100644 --- a/packages/graphql/src/schema-model/parser/jwt-payload-annoatation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/jwt-payload-annoatation.ts @@ -17,7 +17,7 @@ * limitations under the License. */ import type { DirectiveNode } from "graphql"; -import { JWTPayloadAnnotation } from "../annotation/JWTPayloadAnnotation"; +import { JWTPayloadAnnotation } from "../../annotation/JWTPayloadAnnotation"; // eslint-disable-next-line @typescript-eslint/no-unused-vars export function parseJWTPayloadAnnotation(_directive: DirectiveNode): JWTPayloadAnnotation { diff --git a/packages/graphql/src/schema-model/parser/key-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/key-annotation.test.ts similarity index 100% rename from packages/graphql/src/schema-model/parser/key-annotation.test.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/key-annotation.test.ts diff --git a/packages/graphql/src/schema-model/parser/key-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/key-annotation.ts similarity index 86% rename from packages/graphql/src/schema-model/parser/key-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/key-annotation.ts index df10216c89..bb1a8275b5 100644 --- a/packages/graphql/src/schema-model/parser/key-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/key-annotation.ts @@ -17,16 +17,15 @@ * limitations under the License. */ import type { DirectiveNode } from "graphql"; -import { Neo4jGraphQLSchemaValidationError } from "../../classes"; -import { KeyAnnotation } from "../annotation/KeyAnnotation"; -import { parseArguments } from "./utils"; +import { Neo4jGraphQLSchemaValidationError } from "../../../classes"; +import { KeyAnnotation } from "../../annotation/KeyAnnotation"; +import { parseArguments } from "../utils"; export function parseKeyAnnotation(directives: readonly DirectiveNode[]): KeyAnnotation { let isResolvable = false; directives.forEach((directive) => { // fields is a recognized argument but we don't use it, hence we ignore the non-usage of the variable. - // eslint-disable-next-line @typescript-eslint/no-unused-vars const { fields, resolvable, ...unrecognizedArguments } = parseArguments(directive) as { fields: string; resolvable: boolean; diff --git a/packages/graphql/src/schema-model/parser/mutation-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/mutation-annotation.test.ts similarity index 97% rename from packages/graphql/src/schema-model/parser/mutation-annotation.test.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/mutation-annotation.test.ts index c85ed84ff4..127532e218 100644 --- a/packages/graphql/src/schema-model/parser/mutation-annotation.test.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/mutation-annotation.test.ts @@ -18,7 +18,7 @@ */ import { makeDirectiveNode } from "@graphql-tools/utils"; -import { MutationOperations } from "../../graphql/directives/mutation"; +import { MutationOperations } from "../../../graphql/directives/mutation"; import { parseMutationAnnotation } from "./mutation-annotation"; const tests = [ diff --git a/packages/graphql/src/schema-model/parser/mutation-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/mutation-annotation.ts similarity index 84% rename from packages/graphql/src/schema-model/parser/mutation-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/mutation-annotation.ts index ae78c0b656..961b3f118f 100644 --- a/packages/graphql/src/schema-model/parser/mutation-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/mutation-annotation.ts @@ -17,9 +17,9 @@ * limitations under the License. */ import type { DirectiveNode } from "graphql"; -import { Neo4jGraphQLSchemaValidationError } from "../../classes"; -import { MutationAnnotation } from "../annotation/MutationAnnotation"; -import { parseArguments } from "./utils"; +import { Neo4jGraphQLSchemaValidationError } from "../../../classes"; +import { MutationAnnotation } from "../../annotation/MutationAnnotation"; +import { parseArguments } from "../utils"; export function parseMutationAnnotation(directive: DirectiveNode): MutationAnnotation { const { operations } = parseArguments(directive); diff --git a/packages/graphql/src/schema-model/parser/node-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/node-annotation.test.ts similarity index 100% rename from packages/graphql/src/schema-model/parser/node-annotation.test.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/node-annotation.test.ts diff --git a/packages/graphql/src/schema-model/parser/node-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/node-annotation.ts similarity index 89% rename from packages/graphql/src/schema-model/parser/node-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/node-annotation.ts index 081dfca7ee..126505040c 100644 --- a/packages/graphql/src/schema-model/parser/node-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/node-annotation.ts @@ -17,8 +17,8 @@ * limitations under the License. */ import type { DirectiveNode } from "graphql"; -import { NodeAnnotation } from "../annotation/NodeAnnotation"; -import { parseArguments } from "./utils"; +import { NodeAnnotation } from "../../annotation/NodeAnnotation"; +import { parseArguments } from "../utils"; export function parseNodeAnnotation(directive: DirectiveNode): NodeAnnotation { const { label, labels } = parseArguments(directive) as { label: string; labels: string[] }; diff --git a/packages/graphql/src/schema-model/parser/plural-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/plural-annotation.test.ts similarity index 100% rename from packages/graphql/src/schema-model/parser/plural-annotation.test.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/plural-annotation.test.ts diff --git a/packages/graphql/src/schema-model/parser/plural-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/plural-annotation.ts similarity index 89% rename from packages/graphql/src/schema-model/parser/plural-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/plural-annotation.ts index 1398199bbc..a4c39af0bf 100644 --- a/packages/graphql/src/schema-model/parser/plural-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/plural-annotation.ts @@ -17,8 +17,8 @@ * limitations under the License. */ import type { DirectiveNode } from "graphql"; -import { PluralAnnotation } from "../annotation/PluralAnnotation"; -import { parseArguments } from "./utils"; +import { PluralAnnotation } from "../../annotation/PluralAnnotation"; +import { parseArguments } from "../utils"; export function parsePluralAnnotation(directive: DirectiveNode): PluralAnnotation { const { value } = parseArguments(directive) as { value: string }; diff --git a/packages/graphql/src/schema-model/parser/populated-by-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/populated-by-annotation.test.ts similarity index 100% rename from packages/graphql/src/schema-model/parser/populated-by-annotation.test.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/populated-by-annotation.test.ts diff --git a/packages/graphql/src/schema-model/parser/populated-by-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/populated-by-annotation.ts similarity index 89% rename from packages/graphql/src/schema-model/parser/populated-by-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/populated-by-annotation.ts index 5d6503bfdd..58f11722c8 100644 --- a/packages/graphql/src/schema-model/parser/populated-by-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/populated-by-annotation.ts @@ -17,8 +17,8 @@ * limitations under the License. */ import type { DirectiveNode } from "graphql"; -import { PopulatedByAnnotation } from "../annotation/PopulatedByAnnotation"; -import { parseArguments } from "./utils"; +import { PopulatedByAnnotation } from "../../annotation/PopulatedByAnnotation"; +import { parseArguments } from "../utils"; export function parsePopulatedByAnnotation(directive: DirectiveNode): PopulatedByAnnotation { const { callback, operations } = parseArguments(directive) as { callback: string; operations: string[] }; diff --git a/packages/graphql/src/schema-model/parser/private-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/private-annotation.ts similarity index 92% rename from packages/graphql/src/schema-model/parser/private-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/private-annotation.ts index 7da1971ebc..a9223010d6 100644 --- a/packages/graphql/src/schema-model/parser/private-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/private-annotation.ts @@ -17,7 +17,7 @@ * limitations under the License. */ import type { DirectiveNode } from "graphql"; -import { PrivateAnnotation } from "../annotation/PrivateAnnotation"; +import { PrivateAnnotation } from "../../annotation/PrivateAnnotation"; // eslint-disable-next-line @typescript-eslint/no-unused-vars export function parsePrivateAnnotation(_directive: DirectiveNode): PrivateAnnotation { diff --git a/packages/graphql/src/schema-model/parser/query-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/query-annotation.test.ts similarity index 100% rename from packages/graphql/src/schema-model/parser/query-annotation.test.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/query-annotation.test.ts diff --git a/packages/graphql/src/schema-model/parser/query-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/query-annotation.ts similarity index 89% rename from packages/graphql/src/schema-model/parser/query-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/query-annotation.ts index 8abbe28f6c..0412d1d085 100644 --- a/packages/graphql/src/schema-model/parser/query-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/query-annotation.ts @@ -18,8 +18,8 @@ */ import type { DirectiveNode } from "graphql"; -import { QueryAnnotation } from "../annotation/QueryAnnotation"; -import { parseArguments } from "./utils"; +import { QueryAnnotation } from "../../annotation/QueryAnnotation"; +import { parseArguments } from "../utils"; export function parseQueryAnnotation(directive: DirectiveNode): QueryAnnotation { const { read, aggregate } = parseArguments(directive) as { read: boolean; aggregate: boolean }; diff --git a/packages/graphql/src/schema-model/parser/query-options-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/query-options-annotation.test.ts similarity index 100% rename from packages/graphql/src/schema-model/parser/query-options-annotation.test.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/query-options-annotation.test.ts diff --git a/packages/graphql/src/schema-model/parser/query-options-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/query-options-annotation.ts similarity index 88% rename from packages/graphql/src/schema-model/parser/query-options-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/query-options-annotation.ts index 090f3ee204..9e2f7ce0ec 100644 --- a/packages/graphql/src/schema-model/parser/query-options-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/query-options-annotation.ts @@ -18,9 +18,9 @@ */ import type { DirectiveNode } from "graphql"; -import { Neo4jGraphQLSchemaValidationError } from "../../classes"; -import { QueryOptionsAnnotation } from "../annotation/QueryOptionsAnnotation"; -import { parseArguments } from "./utils"; +import { Neo4jGraphQLSchemaValidationError } from "../../../classes"; +import { QueryOptionsAnnotation } from "../../annotation/QueryOptionsAnnotation"; +import { parseArguments } from "../utils"; export function parseQueryOptionsAnnotation(directive: DirectiveNode): QueryOptionsAnnotation { const { limit } = parseArguments(directive) as { diff --git a/packages/graphql/src/schema-model/parser/relationship-properties-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/relationship-properties-annotation.ts similarity index 90% rename from packages/graphql/src/schema-model/parser/relationship-properties-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/relationship-properties-annotation.ts index 8f86e3e9c0..8b07fc78a1 100644 --- a/packages/graphql/src/schema-model/parser/relationship-properties-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/relationship-properties-annotation.ts @@ -18,7 +18,7 @@ */ import type { DirectiveNode } from "graphql"; -import { RelationshipPropertiesAnnotation } from "../annotation/RelationshipPropertiesAnnotation"; +import { RelationshipPropertiesAnnotation } from "../../annotation/RelationshipPropertiesAnnotation"; // eslint-disable-next-line @typescript-eslint/no-unused-vars export function parseRelationshipPropertiesAnnotation(_directive: DirectiveNode): RelationshipPropertiesAnnotation { diff --git a/packages/graphql/src/schema-model/parser/selectable-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/selectable-annotation.test.ts similarity index 100% rename from packages/graphql/src/schema-model/parser/selectable-annotation.test.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/selectable-annotation.test.ts diff --git a/packages/graphql/src/schema-model/parser/selectable-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/selectable-annotation.ts similarity index 89% rename from packages/graphql/src/schema-model/parser/selectable-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/selectable-annotation.ts index e20e5b5cc2..c6c8c8efdb 100644 --- a/packages/graphql/src/schema-model/parser/selectable-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/selectable-annotation.ts @@ -17,8 +17,8 @@ * limitations under the License. */ import type { DirectiveNode } from "graphql"; -import { SelectableAnnotation } from "../annotation/SelectableAnnotation"; -import { parseArguments } from "./utils"; +import { SelectableAnnotation } from "../../annotation/SelectableAnnotation"; +import { parseArguments } from "../utils"; export function parseSelectableAnnotation(directive: DirectiveNode): SelectableAnnotation { const { onRead, onAggregate } = parseArguments(directive) as { onRead: boolean; onAggregate: boolean }; diff --git a/packages/graphql/src/schema-model/parser/settable-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/settable-annotation.test.ts similarity index 100% rename from packages/graphql/src/schema-model/parser/settable-annotation.test.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/settable-annotation.test.ts diff --git a/packages/graphql/src/schema-model/parser/settable-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/settable-annotation.ts similarity index 89% rename from packages/graphql/src/schema-model/parser/settable-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/settable-annotation.ts index d2a81cbe86..06c1b6eada 100644 --- a/packages/graphql/src/schema-model/parser/settable-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/settable-annotation.ts @@ -17,8 +17,8 @@ * limitations under the License. */ import type { DirectiveNode } from "graphql"; -import { SettableAnnotation } from "../annotation/SettableAnnotation"; -import { parseArguments } from "./utils"; +import { SettableAnnotation } from "../../annotation/SettableAnnotation"; +import { parseArguments } from "../utils"; export function parseSettableAnnotation(directive: DirectiveNode): SettableAnnotation { const { onCreate, onUpdate } = parseArguments(directive) as { onCreate: boolean; onUpdate: boolean }; diff --git a/packages/graphql/src/schema-model/parser/subscription-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/subscription-annotation.test.ts similarity index 100% rename from packages/graphql/src/schema-model/parser/subscription-annotation.test.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/subscription-annotation.test.ts diff --git a/packages/graphql/src/schema-model/parser/subscription-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/subscription-annotation.ts similarity index 88% rename from packages/graphql/src/schema-model/parser/subscription-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/subscription-annotation.ts index 4e3202d963..9d8e654b01 100644 --- a/packages/graphql/src/schema-model/parser/subscription-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/subscription-annotation.ts @@ -17,8 +17,8 @@ * limitations under the License. */ import type { DirectiveNode } from "graphql"; -import { SubscriptionAnnotation } from "../annotation/SubscriptionAnnotation"; -import { parseArguments } from "./utils"; +import { SubscriptionAnnotation } from "../../annotation/SubscriptionAnnotation"; +import { parseArguments } from "../utils"; export function parseSubscriptionAnnotation(directive: DirectiveNode): SubscriptionAnnotation { const { operations } = parseArguments(directive) as { operations: string[] }; diff --git a/packages/graphql/src/schema-model/parser/subscriptions-authorization-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/subscriptions-authorization-annotation.ts similarity index 88% rename from packages/graphql/src/schema-model/parser/subscriptions-authorization-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/subscriptions-authorization-annotation.ts index b383430e06..8ab0e5df55 100644 --- a/packages/graphql/src/schema-model/parser/subscriptions-authorization-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/subscriptions-authorization-annotation.ts @@ -17,14 +17,14 @@ * limitations under the License. */ import type { DirectiveNode } from "graphql"; -import { Neo4jGraphQLSchemaValidationError } from "../../classes"; -import { parseArguments } from "./utils"; -import type { SubscriptionsAuthorizationFilterRuleConstructor } from "../annotation/SubscriptionsAuthorizationAnnotation"; +import { Neo4jGraphQLSchemaValidationError } from "../../../classes"; +import { parseArguments } from "../utils"; +import type { SubscriptionsAuthorizationFilterRuleConstructor } from "../../annotation/SubscriptionsAuthorizationAnnotation"; import { SubscriptionsAuthorizationAnnotation, SubscriptionsAuthorizationAnnotationArguments, SubscriptionsAuthorizationFilterRule, -} from "../annotation/SubscriptionsAuthorizationAnnotation"; +} from "../../annotation/SubscriptionsAuthorizationAnnotation"; export function parseSubscriptionsAuthorizationAnnotation( directive: DirectiveNode diff --git a/packages/graphql/src/schema-model/parser/timestamp-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/timestamp-annotation.test.ts similarity index 100% rename from packages/graphql/src/schema-model/parser/timestamp-annotation.test.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/timestamp-annotation.test.ts diff --git a/packages/graphql/src/schema-model/parser/timestamp-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/timestamp-annotation.ts similarity index 88% rename from packages/graphql/src/schema-model/parser/timestamp-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/timestamp-annotation.ts index 23abd21f77..86569c468f 100644 --- a/packages/graphql/src/schema-model/parser/timestamp-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/timestamp-annotation.ts @@ -17,8 +17,8 @@ * limitations under the License. */ import type { DirectiveNode } from "graphql"; -import { TimestampAnnotation } from "../annotation/TimestampAnnotation"; -import { parseArguments } from "./utils"; +import { TimestampAnnotation } from "../../annotation/TimestampAnnotation"; +import { parseArguments } from "../utils"; export function parseTimestampAnnotation(directive: DirectiveNode): TimestampAnnotation { const { operations } = parseArguments(directive) as { operations: string[] }; diff --git a/packages/graphql/src/schema-model/parser/unique-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/unique-annotation.test.ts similarity index 100% rename from packages/graphql/src/schema-model/parser/unique-annotation.test.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/unique-annotation.test.ts diff --git a/packages/graphql/src/schema-model/parser/unique-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/unique-annotation.ts similarity index 89% rename from packages/graphql/src/schema-model/parser/unique-annotation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/unique-annotation.ts index e814f87816..87b9b74a63 100644 --- a/packages/graphql/src/schema-model/parser/unique-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/unique-annotation.ts @@ -17,8 +17,8 @@ * limitations under the License. */ import type { DirectiveNode } from "graphql"; -import { UniqueAnnotation } from "../annotation/UniqueAnnotation"; -import { parseArguments } from "./utils"; +import { UniqueAnnotation } from "../../annotation/UniqueAnnotation"; +import { parseArguments } from "../utils"; export function parseUniqueAnnotation(directive: DirectiveNode): UniqueAnnotation { const { constraintName } = parseArguments(directive) as { constraintName: string }; diff --git a/packages/graphql/src/schema-model/parser/parse-attribute.ts b/packages/graphql/src/schema-model/parser/parse-attribute.ts new file mode 100644 index 0000000000..64eb953603 --- /dev/null +++ b/packages/graphql/src/schema-model/parser/parse-attribute.ts @@ -0,0 +1,166 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { FieldDefinitionNode, TypeNode, DirectiveNode } from "graphql"; +import { Kind } from "graphql"; +import { filterTruthy } from "../../utils/utils"; +import type { Annotation } from "../annotation/Annotation"; +import type { AttributeType, Neo4jGraphQLScalarType } from "../attribute/AbstractAttribute"; +import { + ScalarType, + EnumType, + UserScalarType, + ObjectType, + UnionType, + InterfaceType, + ListType, + GraphQLBuiltInScalarType, + Neo4jGraphQLSpatialType, + Neo4jGraphQLNumberType, + Neo4jGraphQLTemporalType, +} from "../attribute/AbstractAttribute"; +import { Attribute } from "../attribute/Attribute"; +import { Field } from "../attribute/Field"; +import { parseAuthenticationAnnotation } from "./annotations-parser/authentication-annotation"; +import { parseAuthorizationAnnotation } from "./annotations-parser/authorization-annotation"; +import { parseCypherAnnotation } from "./annotations-parser/cypher-annotation"; +import type { DefinitionCollection } from "./definition-collection"; +import { parseSubscriptionsAuthorizationAnnotation } from "./annotations-parser/subscriptions-authorization-annotation"; + +// TODO: figure out difference between field and attribute +export function parseAttribute( + field: FieldDefinitionNode, + definitionCollection: DefinitionCollection +): Attribute | Field { + const name = field.name.value; + const type = parseTypeNode(definitionCollection, field.type); + const annotations = createFieldAnnotations(field.directives || []); + + return new Attribute({ + name, + annotations, + type, + }); +} + +export function parseField(field: FieldDefinitionNode): Field { + const name = field.name.value; + const annotations = createFieldAnnotations(field.directives || []); + return new Field({ + name, + annotations, + }); +} + +function parseTypeNode( + definitionCollection: DefinitionCollection, + typeNode: TypeNode, + isRequired = false +): AttributeType { + switch (typeNode.kind) { + case Kind.NAMED_TYPE: { + if (isScalarType(typeNode.name.value)) { + return new ScalarType(typeNode.name.value, isRequired); + } else if (isEnum(definitionCollection, typeNode.name.value)) { + return new EnumType(typeNode.name.value, isRequired); + } else if (isUserScalar(definitionCollection, typeNode.name.value)) { + return new UserScalarType(typeNode.name.value, isRequired); + } else if (isObject(definitionCollection, typeNode.name.value)) { + return new ObjectType(typeNode.name.value, isRequired); + } else if (isUnion(definitionCollection, typeNode.name.value)) { + return new UnionType(typeNode.name.value, isRequired); + } else if (isInterface(definitionCollection, typeNode.name.value)) { + return new InterfaceType(typeNode.name.value, isRequired); + } else { + throw new Error(`Error while parsing Attribute with name: ${typeNode.name.value}`); + } + } + + case Kind.LIST_TYPE: { + const innerType = parseTypeNode(definitionCollection, typeNode.type); + return new ListType(innerType, isRequired); + } + case Kind.NON_NULL_TYPE: + return parseTypeNode(definitionCollection, typeNode.type, true); + } +} + +function isInterface(definitionCollection: DefinitionCollection, name: string): boolean { + return definitionCollection.interfaceTypes.has(name); +} + +function isUnion(definitionCollection: DefinitionCollection, name: string): boolean { + return definitionCollection.unionTypes.has(name); +} + +function isEnum(definitionCollection: DefinitionCollection, name: string): boolean { + return definitionCollection.enumTypes.has(name); +} + +function isUserScalar(definitionCollection: DefinitionCollection, name: string) { + return definitionCollection.scalarTypes.has(name); +} + +function isObject(definitionCollection, name: string) { + return definitionCollection.nodes.has(name); +} + +function isScalarType(value: string): value is GraphQLBuiltInScalarType | Neo4jGraphQLScalarType { + return ( + isGraphQLBuiltInScalar(value) || + isNeo4jGraphQLSpatialType(value) || + isNeo4jGraphQLNumberType(value) || + isNeo4jGraphQLTemporalType(value) + ); +} + +function isGraphQLBuiltInScalar(value: string): value is GraphQLBuiltInScalarType { + return Object.values(GraphQLBuiltInScalarType).includes(value); +} + +function isNeo4jGraphQLSpatialType(value: string): value is Neo4jGraphQLSpatialType { + return Object.values(Neo4jGraphQLSpatialType).includes(value); +} + +function isNeo4jGraphQLNumberType(value: string): value is Neo4jGraphQLNumberType { + return Object.values(Neo4jGraphQLNumberType).includes(value); +} + +function isNeo4jGraphQLTemporalType(value: string): value is Neo4jGraphQLTemporalType { + return Object.values(Neo4jGraphQLTemporalType).includes(value); +} + +function createFieldAnnotations(directives: readonly DirectiveNode[]): Annotation[] { + return filterTruthy( + directives.map((directive) => { + switch (directive.name.value) { + case "cypher": + return parseCypherAnnotation(directive); + case "authorization": + return parseAuthorizationAnnotation(directive); + case "authentication": + return parseAuthenticationAnnotation(directive); + case "subscriptionsAuthorization": + return parseSubscriptionsAuthorizationAnnotation(directive); + default: + return undefined; + } + }) + ); +} diff --git a/packages/graphql/src/schema-model/parser/parse-value-node.test.ts b/packages/graphql/src/schema-model/parser/parse-value-node.test.ts index cb37cc3c24..947671c90c 100644 --- a/packages/graphql/src/schema-model/parser/parse-value-node.test.ts +++ b/packages/graphql/src/schema-model/parser/parse-value-node.test.ts @@ -19,7 +19,7 @@ import type { ValueNode } from "graphql"; import { parse } from "graphql"; -import parseValueNode from "./parse-value-node"; +import { parseValueNode } from "./parse-value-node"; describe("parseValueNode", () => { test("should return a correct nested object", () => { diff --git a/packages/graphql/src/schema-model/parser/parse-value-node.ts b/packages/graphql/src/schema-model/parser/parse-value-node.ts index 43e0f571d8..9d04b07dfd 100644 --- a/packages/graphql/src/schema-model/parser/parse-value-node.ts +++ b/packages/graphql/src/schema-model/parser/parse-value-node.ts @@ -20,7 +20,7 @@ import type { ValueNode } from "graphql/language/ast"; import { Kind } from "graphql/language"; -function parseValueNode(ast: ValueNode): any { +export function parseValueNode(ast: ValueNode): any { switch (ast.kind) { case Kind.ENUM: case Kind.STRING: @@ -43,4 +43,3 @@ function parseValueNode(ast: ValueNode): any { } } -export default parseValueNode; diff --git a/packages/graphql/src/schema-model/parser/utils.ts b/packages/graphql/src/schema-model/parser/utils.ts index f005bb3473..de13c998f5 100644 --- a/packages/graphql/src/schema-model/parser/utils.ts +++ b/packages/graphql/src/schema-model/parser/utils.ts @@ -17,7 +17,7 @@ * limitations under the License. */ import type { DirectiveNode } from "graphql"; -import parseValueNode from "./parse-value-node"; +import { parseValueNode } from "./parse-value-node"; export function parseArguments(directive: DirectiveNode): Record { return (directive.arguments || [])?.reduce((acc, argument) => { @@ -31,4 +31,3 @@ export function findDirective(directives: readonly DirectiveNode[], name: string return d.name.value === name; }); } - diff --git a/packages/graphql/src/schema/get-obj-field-meta.ts b/packages/graphql/src/schema/get-obj-field-meta.ts index b750caced3..108864954b 100644 --- a/packages/graphql/src/schema/get-obj-field-meta.ts +++ b/packages/graphql/src/schema/get-obj-field-meta.ts @@ -60,7 +60,7 @@ import type { SettableOptions, FilterableOptions, } from "../types"; -import parseValueNode from "../schema-model/parser/parse-value-node"; +import { parseValueNode } from "../schema-model/parser/parse-value-node"; import checkDirectiveCombinations from "./check-directive-combinations"; import { upperFirst } from "../utils/upper-first"; import { getPopulatedByMeta } from "./get-populated-by-meta"; diff --git a/packages/graphql/src/schema/parse/parse-fulltext-directive.ts b/packages/graphql/src/schema/parse/parse-fulltext-directive.ts index fa5a8bf984..51030d43e8 100644 --- a/packages/graphql/src/schema/parse/parse-fulltext-directive.ts +++ b/packages/graphql/src/schema/parse/parse-fulltext-directive.ts @@ -20,7 +20,7 @@ import type { ArgumentNode, DirectiveNode, ObjectTypeDefinitionNode } from "graphql"; import type { FullText, FulltextIndex } from "../../types"; import type { ObjectFields } from "../get-obj-field-meta"; -import parseValueNode from "../../schema-model/parser/parse-value-node"; +import { parseValueNode } from "../../schema-model/parser/parse-value-node"; const deprecationWarning = "The @fulltext name argument has been deprecated and will be removed in 4.0.0. " + diff --git a/packages/graphql/src/schema/parse/parse-query-options-directive.ts b/packages/graphql/src/schema/parse/parse-query-options-directive.ts index 6c1e05057e..e53268df10 100644 --- a/packages/graphql/src/schema/parse/parse-query-options-directive.ts +++ b/packages/graphql/src/schema/parse/parse-query-options-directive.ts @@ -21,7 +21,7 @@ import type { DirectiveNode, ObjectFieldNode, ObjectTypeDefinitionNode, ObjectVa import * as neo4j from "neo4j-driver"; import { QueryOptionsDirective } from "../../classes/QueryOptionsDirective"; import { Neo4jGraphQLError } from "../../classes/Error"; -import parseValueNode from "../../schema-model/parser/parse-value-node"; +import { parseValueNode } from "../../schema-model/parser/parse-value-node"; export function parseQueryOptionsDirective({ directive, @@ -30,7 +30,7 @@ export function parseQueryOptionsDirective({ directive: DirectiveNode; definition: ObjectTypeDefinitionNode; }): QueryOptionsDirective { - const limitArgument = directive.arguments?.find((direc) => direc.name.value === "limit"); + const limitArgument = directive.arguments?.find((argument) => argument.name.value === "limit"); const limitValue = limitArgument?.value as ObjectValueNode | undefined; const defaultLimitArgument = limitValue?.fields.find((field) => field.name.value === "default"); const maxLimitArgument = limitValue?.fields.find((field) => field.name.value === "max"); diff --git a/packages/graphql/src/schema/to-compose.ts b/packages/graphql/src/schema/to-compose.ts index 112a4cc7db..0e9a69203d 100644 --- a/packages/graphql/src/schema/to-compose.ts +++ b/packages/graphql/src/schema/to-compose.ts @@ -22,7 +22,7 @@ import type { Directive, DirectiveArgs, ObjectTypeComposerFieldConfigAsObjectDef import type { BaseField, InputField, PrimitiveField, TemporalField } from "../types"; import { DEPRECATE_NOT } from "./constants"; import getFieldTypeMeta from "./get-field-type-meta"; -import parseValueNode from "../schema-model/parser/parse-value-node"; +import { parseValueNode } from "../schema-model/parser/parse-value-node"; import { idResolver } from "./resolvers/field/id"; import { numericalResolver } from "./resolvers/field/numerical"; From 9ba748922418f4ef28b6fb41a42a4717cd3f8766 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Wed, 19 Jul 2023 21:20:37 +0100 Subject: [PATCH 06/22] Add missing GraphQL models to the Schema Models --- .../schema-model/Neo4jGraphQLSchemaModel.ts | 10 +- .../annotation/CypherAnnotation.ts | 5 +- .../attribute/AbstractAttribute.ts | 334 ------------ .../schema-model/attribute/Attribute.test.ts | 368 +------------ .../src/schema-model/attribute/Attribute.ts | 31 +- .../schema-model/attribute/AttributeType.ts | 157 ++++++ .../graphql-models/AggregationModel.ts | 7 - .../graphql-models/AttributeModel.test.ts | 488 ++++++++++++++++++ .../graphql-models/AttributeModel.ts | 190 ++++++- .../src/schema-model/entity/ConcreteEntity.ts | 9 +- .../graphql/src/schema-model/entity/Entity.ts | 13 - .../graphql-models/CompositeEntityModel.ts | 33 ++ .../ConcreteEntityModel.test.ts | 88 ++++ .../graphql-models/ConcreteEntityModel.ts | 39 +- .../src/schema-model/generate-model.test.ts | 85 ++- .../annotations-parser/cypher-annotation.ts | 6 +- .../schema-model/parser/parse-attribute.ts | 4 +- .../schema-model/relationship/Relationship.ts | 9 +- .../graphql-model/RelationshipModel.ts | 91 ++++ 19 files changed, 1155 insertions(+), 812 deletions(-) delete mode 100644 packages/graphql/src/schema-model/attribute/AbstractAttribute.ts create mode 100644 packages/graphql/src/schema-model/attribute/AttributeType.ts create mode 100644 packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.test.ts create mode 100644 packages/graphql/src/schema-model/entity/graphql-models/CompositeEntityModel.ts create mode 100644 packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.test.ts create mode 100644 packages/graphql/src/schema-model/relationship/graphql-model/RelationshipModel.ts diff --git a/packages/graphql/src/schema-model/Neo4jGraphQLSchemaModel.ts b/packages/graphql/src/schema-model/Neo4jGraphQLSchemaModel.ts index cc5636d616..2943ec82f8 100644 --- a/packages/graphql/src/schema-model/Neo4jGraphQLSchemaModel.ts +++ b/packages/graphql/src/schema-model/Neo4jGraphQLSchemaModel.ts @@ -24,6 +24,7 @@ import { annotationToKey } from "./annotation/Annotation"; import { CompositeEntity } from "./entity/CompositeEntity"; import { ConcreteEntity } from "./entity/ConcreteEntity"; import type { Entity } from "./entity/Entity"; +import { ConcreteEntityModel } from "./entity/graphql-models/ConcreteEntityModel"; export type Operations = { Query?: Operation; Mutation?: Operation; @@ -52,6 +53,8 @@ export class Neo4jGraphQLSchemaModel { acc.set(entity.name, entity); return acc; }, new Map()); + + this.concreteEntities = concreteEntities; this.compositeEntities = compositeEntities; this.operations = operations; @@ -60,11 +63,16 @@ export class Neo4jGraphQLSchemaModel { this.addAnnotation(annotation); } } - + public getEntity(name: string): Entity | undefined { return this.entities.get(name); } + public getConcreteEntityModel(name: string): ConcreteEntityModel | undefined { + const concreteEntityModel = this.concreteEntities.find((entity) => entity.name === name); + return concreteEntityModel ? new ConcreteEntityModel(concreteEntityModel) : undefined; + } + public getEntitiesByLabels(labels: string[]): ConcreteEntity[] { return this.concreteEntities.filter((entity) => entity.matchLabels(labels)); } diff --git a/packages/graphql/src/schema-model/annotation/CypherAnnotation.ts b/packages/graphql/src/schema-model/annotation/CypherAnnotation.ts index 6f5ac8e56e..008d0d790f 100644 --- a/packages/graphql/src/schema-model/annotation/CypherAnnotation.ts +++ b/packages/graphql/src/schema-model/annotation/CypherAnnotation.ts @@ -17,11 +17,12 @@ * limitations under the License. */ - export class CypherAnnotation { public statement: string; + public columnName: string; - constructor({ statement }: { statement: string }) { + constructor({ statement, columnName }: { statement: string, columnName: string }) { this.statement = statement; + this.columnName = columnName; } } diff --git a/packages/graphql/src/schema-model/attribute/AbstractAttribute.ts b/packages/graphql/src/schema-model/attribute/AbstractAttribute.ts deleted file mode 100644 index 6278e9fdc5..0000000000 --- a/packages/graphql/src/schema-model/attribute/AbstractAttribute.ts +++ /dev/null @@ -1,334 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Neo4jGraphQLSchemaValidationError } from "../../classes/Error"; -import { annotationToKey } from "../annotation/Annotation"; -import type { Annotation, Annotations } from "../annotation/Annotation"; - -export enum GraphQLBuiltInScalarType { - Int = "Int", - Float = "Float", - String = "String", - Boolean = "Boolean", - ID = "ID", -} - -export enum Neo4jGraphQLSpatialType { - CartesianPoint = "CartesianPoint", - Point = "Point", -} - -export enum Neo4jGraphQLNumberType { - BigInt = "BigInt", -} - -export enum Neo4jGraphQLTemporalType { - DateTime = "DateTime", - LocalDateTime = "LocalDateTime", - Time = "Time", - LocalTime = "LocalTime", - Date = "Date", - Duration = "Duration", -} - -export enum ScalarTypeCategory { - Neo4jGraphQLTemporalType = "Neo4jGraphQLTemporalType", - Neo4jGraphQLNumberType = "Neo4jGraphQLNumberType", - Neo4jGraphQLSpatialType = "Neo4jGraphQLSpatialType", - GraphQLBuiltInScalarType = "GraphQLBuiltInScalarType", -} - -export type Neo4jGraphQLScalarType = Neo4jGraphQLTemporalType | Neo4jGraphQLNumberType | Neo4jGraphQLSpatialType; - -// The ScalarType class is not used to represent user defined scalar types, see UserScalarType for that. -export class ScalarType { - public readonly name: GraphQLBuiltInScalarType | Neo4jGraphQLScalarType; - public readonly isRequired: boolean; - public readonly category: ScalarTypeCategory; - constructor(name: GraphQLBuiltInScalarType | Neo4jGraphQLScalarType, isRequired: boolean) { - this.name = name; - this.isRequired = isRequired; - switch (name) { - case GraphQLBuiltInScalarType.String: - case GraphQLBuiltInScalarType.Boolean: - case GraphQLBuiltInScalarType.ID: - case GraphQLBuiltInScalarType.Int: - case GraphQLBuiltInScalarType.Float: - this.category = ScalarTypeCategory.GraphQLBuiltInScalarType; - break; - case Neo4jGraphQLSpatialType.CartesianPoint: - case Neo4jGraphQLSpatialType.Point: - this.category = ScalarTypeCategory.Neo4jGraphQLSpatialType; - break; - case Neo4jGraphQLTemporalType.DateTime: - case Neo4jGraphQLTemporalType.LocalDateTime: - case Neo4jGraphQLTemporalType.Time: - case Neo4jGraphQLTemporalType.LocalTime: - case Neo4jGraphQLTemporalType.Date: - case Neo4jGraphQLTemporalType.Duration: - this.category = ScalarTypeCategory.Neo4jGraphQLTemporalType; - break; - case Neo4jGraphQLNumberType.BigInt: - this.category = ScalarTypeCategory.Neo4jGraphQLNumberType; - } - } -} - -export class UserScalarType { - public readonly name: string; - public readonly isRequired: boolean; - constructor(name: string, isRequired: boolean) { - this.name = name; - this.isRequired = isRequired; - } -} - -export class ObjectType { - public readonly name: string; - public readonly isRequired: boolean; - constructor(name: string, isRequired: boolean) { - this.name = name; - this.isRequired = isRequired; - } -} - -export class ListType { - public ofType: Exclude; - public isRequired: boolean; - constructor(ofType: AttributeType, isRequired: boolean) { - if (ofType instanceof ListType) { - throw new Neo4jGraphQLSchemaValidationError("two-dimensional lists are not supported"); - } - this.ofType = ofType; - this.isRequired = isRequired; - } -} - -export class EnumType { - public name: string; - public isRequired: boolean; - - constructor(name: string, isRequired: boolean) { - this.name = name; - this.isRequired = isRequired; - } -} - -export class UnionType { - public name: string; - public isRequired: boolean; - - constructor(name: string, isRequired: boolean) { - this.name = name; - this.isRequired = isRequired; - } -} - -export class InterfaceType { - public name: string; - public isRequired: boolean; - - constructor(name: string, isRequired: boolean) { - this.name = name; - this.isRequired = isRequired; - } -} - -export type AttributeType = ScalarType | UserScalarType | ObjectType | ListType | EnumType | UnionType | InterfaceType; - -export abstract class AbstractAttribute { - public name: string; - public type: AttributeType; - public annotations: Partial = {}; - - constructor({ - name, - type, - annotations, - }: { - name: string; - type: AttributeType; - annotations: Annotation[] | Partial; - }) { - this.name = name; - this.type = type; - if (Array.isArray(annotations)) { - for (const annotation of annotations) { - this.addAnnotation(annotation); - } - } else { - this.annotations = annotations; - } - } - - isBoolean(): boolean { - return this.type instanceof ScalarType && this.type.name === GraphQLBuiltInScalarType.Boolean; - } - - isID(): boolean { - return this.type instanceof ScalarType && this.type.name === GraphQLBuiltInScalarType.ID; - } - - isInt(): boolean { - return this.type instanceof ScalarType && this.type.name === GraphQLBuiltInScalarType.Int; - } - - isFloat(): boolean { - return this.type instanceof ScalarType && this.type.name === GraphQLBuiltInScalarType.Float; - } - - isString(): boolean { - return this.type instanceof ScalarType && this.type.name === GraphQLBuiltInScalarType.String; - } - - isCartesianPoint(): boolean { - return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLSpatialType.CartesianPoint; - } - - isPoint(): boolean { - return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLSpatialType.Point; - } - - isBigInt(): boolean { - return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLNumberType.BigInt; - } - - isDate(): boolean { - return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLTemporalType.Date; - } - - isDateTime(): boolean { - return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLTemporalType.DateTime; - } - - isLocalDateTime(): boolean { - return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLTemporalType.LocalDateTime; - } - - isTime(): boolean { - return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLTemporalType.Time; - } - - isLocalTime(): boolean { - return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLTemporalType.LocalTime; - } - - isDuration(): boolean { - return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLTemporalType.Duration; - } - - isList(): boolean { - return this.type instanceof ListType; - } - - // nested list of type are not supported yet, change this method when they are - isListOf(elementType: Exclude | GraphQLBuiltInScalarType | Neo4jGraphQLScalarType | string): boolean { - if (!(this.type instanceof ListType)) { - return false; - } - if (typeof elementType === "string") { - return this.type.ofType.name === elementType; - } - - return this.type.ofType.name === elementType.name; - } - - isListElementRequired(): boolean { - if (!(this.type instanceof ListType)) { - return false; - } - return this.type.ofType.isRequired; - } - - isObject(): boolean { - return this.type instanceof ObjectType; - } - - isEnum(): boolean { - return this.type instanceof EnumType; - } - - isRequired(): boolean { - return this.type.isRequired; - } - - isInterface(): boolean { - return this.type instanceof InterfaceType; - } - - isUnion(): boolean { - return this.type instanceof UnionType; - } - - isUserScalar(): boolean { - return this.type instanceof UserScalarType; - } - - /** - * START of category assertions - */ - isGraphQLBuiltInScalar(): boolean { - return this.type instanceof ScalarType && this.type.category === ScalarTypeCategory.GraphQLBuiltInScalarType; - } - - isSpatial(): boolean { - return this.type instanceof ScalarType && this.type.category === ScalarTypeCategory.Neo4jGraphQLSpatialType; - } - - isTemporal(): boolean { - return this.type instanceof ScalarType && this.type.category === ScalarTypeCategory.Neo4jGraphQLTemporalType; - } - - isAbstract(): boolean { - return this.isInterface() || this.isUnion(); - } - /** - * END of category assertions - */ - - /** - * START of Refactoring methods, these methods are just adapters to the new methods - * to help the transition from the old Node/Relationship/BaseField classes - * */ - - // TODO: remove this method and use isGraphQLBuiltInScalar instead - isPrimitive(): boolean { - return this.isGraphQLBuiltInScalar(); - } - - // TODO: remove this and use isUserScalar instead - isScalar(): boolean { - return this.isUserScalar(); - } - - /** - * END of refactoring methods - */ - - private addAnnotation(annotation: Annotation): void { - const annotationKey = annotationToKey(annotation); - if (this.annotations[annotationKey]) { - throw new Neo4jGraphQLSchemaValidationError(`Annotation ${annotationKey} already exists in ${this.name}`); - } - - // We cast to any because we aren't narrowing the Annotation type here. - // There's no reason to narrow either, since we care more about performance. - this.annotations[annotationKey] = annotation as any; - } -} diff --git a/packages/graphql/src/schema-model/attribute/Attribute.test.ts b/packages/graphql/src/schema-model/attribute/Attribute.test.ts index 617c6af5be..e4b61aacad 100644 --- a/packages/graphql/src/schema-model/attribute/Attribute.test.ts +++ b/packages/graphql/src/schema-model/attribute/Attribute.test.ts @@ -17,20 +17,8 @@ * limitations under the License. */ -import { - EnumType, - GraphQLBuiltInScalarType, - InterfaceType, - ListType, - Neo4jGraphQLNumberType, - Neo4jGraphQLSpatialType, - Neo4jGraphQLTemporalType, - ObjectType, - ScalarType, - UnionType, - UserScalarType, -} from "./AbstractAttribute"; import { Attribute } from "./Attribute"; +import { ScalarType, GraphQLBuiltInScalarType } from "./AttributeType"; describe("Attribute", () => { test("should clone attribute", () => { @@ -42,358 +30,4 @@ describe("Attribute", () => { const clone = attribute.clone(); expect(attribute).toStrictEqual(clone); }); - - describe("type assertions", () => { - test("isID", () => { - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new ScalarType(GraphQLBuiltInScalarType.ID, true), - }); - - expect(attribute.isID()).toBe(true); - }); - - test("isBoolean", () => { - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new ScalarType(GraphQLBuiltInScalarType.Boolean, true), - }); - - expect(attribute.isBoolean()).toBe(true); - }); - - test("isInt", () => { - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new ScalarType(GraphQLBuiltInScalarType.Int, true), - }); - - expect(attribute.isInt()).toBe(true); - }); - - test("isFloat", () => { - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new ScalarType(GraphQLBuiltInScalarType.Float, true), - }); - expect(attribute.isFloat()).toBe(true); - }); - - test("isString", () => { - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new ScalarType(GraphQLBuiltInScalarType.String, true), - }); - expect(attribute.isString()).toBe(true); - }); - - test("isCartesianPoint", () => { - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new ScalarType(Neo4jGraphQLSpatialType.CartesianPoint, true), - }); - - expect(attribute.isCartesianPoint()).toBe(true); - }); - - test("isPoint", () => { - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new ScalarType(Neo4jGraphQLSpatialType.Point, true), - }); - - expect(attribute.isPoint()).toBe(true); - }); - - test("isBigInt", () => { - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new ScalarType(Neo4jGraphQLNumberType.BigInt, true), - }); - - expect(attribute.isBigInt()).toBe(true); - }); - - test("isDate", () => { - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new ScalarType(Neo4jGraphQLTemporalType.Date, true), - }); - - expect(attribute.isDate()).toBe(true); - }); - - test("isDateTime", () => { - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new ScalarType(Neo4jGraphQLTemporalType.DateTime, true), - }); - - expect(attribute.isDateTime()).toBe(true); - }); - - test("isLocalDateTime", () => { - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new ScalarType(Neo4jGraphQLTemporalType.LocalDateTime, true), - }); - - expect(attribute.isLocalDateTime()).toBe(true); - }); - - test("isTime", () => { - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new ScalarType(Neo4jGraphQLTemporalType.Time, true), - }); - - expect(attribute.isTime()).toBe(true); - }); - - test("isLocalTime", () => { - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new ScalarType(Neo4jGraphQLTemporalType.LocalTime, true), - }); - - expect(attribute.isLocalTime()).toBe(true); - }); - - test("isDuration", () => { - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new ScalarType(Neo4jGraphQLTemporalType.Duration, true), - }); - - expect(attribute.isDuration()).toBe(true); - }); - - test("isObject", () => { - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new ObjectType("testType", true), - }); - - expect(attribute.isObject()).toBe(true); - }); - - test("isEnum", () => { - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new EnumType("testType", true), - }); - - expect(attribute.isEnum()).toBe(true); - }); - - test("isUserScalar", () => { - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new UserScalarType("testType", true), - }); - - expect(attribute.isUserScalar()).toBe(true); - }); - - test("isInterface", () => { - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new InterfaceType("Tool", true), - }); - expect(attribute.isInterface()).toBe(true); - }); - - test("isUnion", () => { - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new UnionType("Tool", true), - }); - expect(attribute.isUnion()).toBe(true); - }); - - describe("List", () => { - test("isList", () => { - const stringType = new ScalarType(GraphQLBuiltInScalarType.String, true); - - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new ListType(stringType, true), - }); - - expect(attribute.isList()).toBe(true); - }); - - test("isListOf, should return false if attribute it's not a list", () => { - const stringType = new ScalarType(GraphQLBuiltInScalarType.String, true); - - const attribute = new Attribute({ - name: "test", - annotations: [], - type: stringType, - }); - - expect(attribute.isListOf(stringType)).toBe(false); - }); - - test("isListOf(Attribute), should return false if it's a list of a different type", () => { - const stringType = new ScalarType(GraphQLBuiltInScalarType.String, true); - - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new ListType(stringType, true), - }); - const intType = new ScalarType(GraphQLBuiltInScalarType.Int, true); - expect(attribute.isListOf(intType)).toBe(false); - }); - - test("isListOf(Attribute), should return true if it's a list of a the same type.", () => { - const stringType = new ScalarType(GraphQLBuiltInScalarType.String, true); - - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new ListType(stringType, true), - }); - const stringType2 = new ScalarType(GraphQLBuiltInScalarType.String, true); - expect(attribute.isListOf(stringType2)).toBe(true); - }); - - test("isListOf(string), should return false if it's a list of a different type", () => { - const stringType = new ScalarType(GraphQLBuiltInScalarType.String, true); - - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new ListType(stringType, true), - }); - expect(attribute.isListOf(GraphQLBuiltInScalarType.Int)).toBe(false); - }); - - test("isListOf(string), should return true if it's a list of a the same type.", () => { - const stringType = new ScalarType(GraphQLBuiltInScalarType.String, true); - - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new ListType(stringType, true), - }); - expect(attribute.isListOf(GraphQLBuiltInScalarType.String)).toBe(true); - }); - }); - }); - - describe("category assertions", () => { - test("isGraphQLBuiltInScalar", () => { - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new ScalarType(GraphQLBuiltInScalarType.String, true), - }); - - expect(attribute.isGraphQLBuiltInScalar()).toBe(true); - }); - - test("isSpatial", () => { - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new ScalarType(Neo4jGraphQLSpatialType.CartesianPoint, true), - }); - - expect(attribute.isSpatial()).toBe(true); - }); - - test("isTemporal", () => { - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new ScalarType(Neo4jGraphQLTemporalType.Date, true), - }); - - expect(attribute.isTemporal()).toBe(true); - }); - - - test("isAbstract", () => { - const attribute = new Attribute({ - name: "test", - annotations: [], - type: new UnionType("Tool", true), - }); - - expect(attribute.isAbstract()).toBe(true); - }); - }); - - test("isRequired", () => { - const attributeRequired = new Attribute({ - name: "test", - annotations: [], - type: new ScalarType(GraphQLBuiltInScalarType.String, true), - }); - - const attributeNotRequired = new Attribute({ - name: "test", - annotations: [], - type: new ScalarType(GraphQLBuiltInScalarType.String, false), - }); - - expect(attributeRequired.isRequired()).toBe(true); - expect(attributeNotRequired.isRequired()).toBe(false); - }); - - test("isRequired - List", () => { - const attributeRequired = new Attribute({ - name: "test", - annotations: [], - type: new ListType(new ScalarType(GraphQLBuiltInScalarType.String, true), true), - }); - - const attributeNotRequired = new Attribute({ - name: "test", - annotations: [], - type: new ListType(new ScalarType(GraphQLBuiltInScalarType.String, true), false), - }); - - expect(attributeRequired.isRequired()).toBe(true); - expect(attributeNotRequired.isRequired()).toBe(false); - }); - - test("isListElementRequired", () => { - const listElementRequired = new Attribute({ - name: "test", - annotations: [], - type: new ListType(new ScalarType(GraphQLBuiltInScalarType.String, true), false), - }); - - const listElementNotRequired = new Attribute({ - name: "test", - annotations: [], - type: new ListType(new ScalarType(GraphQLBuiltInScalarType.String, false), true), - }); - - expect(listElementRequired.isListElementRequired()).toBe(true); - expect(listElementNotRequired.isListElementRequired()).toBe(false); - }); }); diff --git a/packages/graphql/src/schema-model/attribute/Attribute.ts b/packages/graphql/src/schema-model/attribute/Attribute.ts index 198ff15253..05c481832d 100644 --- a/packages/graphql/src/schema-model/attribute/Attribute.ts +++ b/packages/graphql/src/schema-model/attribute/Attribute.ts @@ -17,16 +17,22 @@ * limitations under the License. */ -import type { Annotation, Annotations } from "../annotation/Annotation"; -import type { AttributeType } from "./AbstractAttribute"; -import { AbstractAttribute } from "./AbstractAttribute"; +import { Neo4jGraphQLSchemaValidationError } from "../../classes/Error"; +import { annotationToKey, type Annotation, type Annotations } from "../annotation/Annotation"; +import type { AttributeType } from "./AttributeType"; -// At this moment Attribute is a dummy class, most of the logic is shared logic between Attribute and AttributeModels defined in the AbstractAttribute class -export class Attribute extends AbstractAttribute { +export class Attribute { + public readonly name: string; + public readonly annotations: Partial = {}; + public readonly type: AttributeType; constructor({ name, annotations = [], type }: { name: string; annotations: Annotation[]; type: AttributeType }) { - super({ name, type, annotations }); - } + this.name = name; + this.type = type; + for (const annotation of annotations) { + this.addAnnotation(annotation); + } + } public clone(): Attribute { return new Attribute({ @@ -35,4 +41,15 @@ export class Attribute extends AbstractAttribute { type: this.type, }); } + + private addAnnotation(annotation: Annotation): void { + const annotationKey = annotationToKey(annotation); + if (this.annotations[annotationKey]) { + throw new Neo4jGraphQLSchemaValidationError(`Annotation ${annotationKey} already exists in ${this.name}`); + } + + // We cast to any because we aren't narrowing the Annotation type here. + // There's no reason to narrow either, since we care more about performance. + this.annotations[annotationKey] = annotation as any; + } } diff --git a/packages/graphql/src/schema-model/attribute/AttributeType.ts b/packages/graphql/src/schema-model/attribute/AttributeType.ts new file mode 100644 index 0000000000..62eea88956 --- /dev/null +++ b/packages/graphql/src/schema-model/attribute/AttributeType.ts @@ -0,0 +1,157 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Neo4jGraphQLSchemaValidationError } from "../../classes/Error"; + + +export enum GraphQLBuiltInScalarType { + Int = "Int", + Float = "Float", + String = "String", + Boolean = "Boolean", + ID = "ID", +} + +export enum Neo4jGraphQLSpatialType { + CartesianPoint = "CartesianPoint", + Point = "Point", +} + +export enum Neo4jGraphQLNumberType { + BigInt = "BigInt", +} + +export enum Neo4jGraphQLTemporalType { + DateTime = "DateTime", + LocalDateTime = "LocalDateTime", + Time = "Time", + LocalTime = "LocalTime", + Date = "Date", + Duration = "Duration", +} + +export enum ScalarTypeCategory { + Neo4jGraphQLTemporalType = "Neo4jGraphQLTemporalType", + Neo4jGraphQLNumberType = "Neo4jGraphQLNumberType", + Neo4jGraphQLSpatialType = "Neo4jGraphQLSpatialType", + GraphQLBuiltInScalarType = "GraphQLBuiltInScalarType", +} + +export type Neo4jGraphQLScalarType = Neo4jGraphQLTemporalType | Neo4jGraphQLNumberType | Neo4jGraphQLSpatialType; + +// The ScalarType class is not used to represent user defined scalar types, see UserScalarType for that. +export class ScalarType { + public readonly name: GraphQLBuiltInScalarType | Neo4jGraphQLScalarType; + public readonly isRequired: boolean; + public readonly category: ScalarTypeCategory; + constructor(name: GraphQLBuiltInScalarType | Neo4jGraphQLScalarType, isRequired: boolean) { + this.name = name; + this.isRequired = isRequired; + switch (name) { + case GraphQLBuiltInScalarType.String: + case GraphQLBuiltInScalarType.Boolean: + case GraphQLBuiltInScalarType.ID: + case GraphQLBuiltInScalarType.Int: + case GraphQLBuiltInScalarType.Float: + this.category = ScalarTypeCategory.GraphQLBuiltInScalarType; + break; + case Neo4jGraphQLSpatialType.CartesianPoint: + case Neo4jGraphQLSpatialType.Point: + this.category = ScalarTypeCategory.Neo4jGraphQLSpatialType; + break; + case Neo4jGraphQLTemporalType.DateTime: + case Neo4jGraphQLTemporalType.LocalDateTime: + case Neo4jGraphQLTemporalType.Time: + case Neo4jGraphQLTemporalType.LocalTime: + case Neo4jGraphQLTemporalType.Date: + case Neo4jGraphQLTemporalType.Duration: + this.category = ScalarTypeCategory.Neo4jGraphQLTemporalType; + break; + case Neo4jGraphQLNumberType.BigInt: + this.category = ScalarTypeCategory.Neo4jGraphQLNumberType; + } + } +} + +export class UserScalarType { + public readonly name: string; + public readonly isRequired: boolean; + constructor(name: string, isRequired: boolean) { + this.name = name; + this.isRequired = isRequired; + } +} + +export class ObjectType { + public readonly name: string; + public readonly isRequired: boolean; + // TODO: add fields + + constructor(name: string, isRequired: boolean) { + this.name = name; + this.isRequired = isRequired; + } +} + +export class ListType { + public ofType: Exclude; + public isRequired: boolean; + constructor(ofType: AttributeType, isRequired: boolean) { + if (ofType instanceof ListType) { + throw new Neo4jGraphQLSchemaValidationError("two-dimensional lists are not supported"); + } + this.ofType = ofType; + this.isRequired = isRequired; + } +} + +export class EnumType { + public name: string; + public isRequired: boolean; + // TODO: add enum values + + constructor(name: string, isRequired: boolean) { + this.name = name; + this.isRequired = isRequired; + } +} + +export class UnionType { + public name: string; + public isRequired: boolean; + // TODO: add implementing types + + constructor(name: string, isRequired: boolean) { + this.name = name; + this.isRequired = isRequired; + } +} + +export class InterfaceType { + public name: string; + public isRequired: boolean; + // TODO: add shared fields + + constructor(name: string, isRequired: boolean) { + this.name = name; + this.isRequired = isRequired; + } +} + +export type AttributeType = ScalarType | UserScalarType | ObjectType | ListType | EnumType | UnionType | InterfaceType; diff --git a/packages/graphql/src/schema-model/attribute/graphql-models/AggregationModel.ts b/packages/graphql/src/schema-model/attribute/graphql-models/AggregationModel.ts index 6a8e1b43c9..cab82a29f0 100644 --- a/packages/graphql/src/schema-model/attribute/graphql-models/AggregationModel.ts +++ b/packages/graphql/src/schema-model/attribute/graphql-models/AggregationModel.ts @@ -58,11 +58,4 @@ export class AggregationModel { getSumComparator(comparator: ComparisonOperator): string { return `${this.attributeModel.name}_SUM_${comparator}`; } - - /** - * Given the GraphQL field name, returns the semantic information about the aggregation it tries to perform - **/ - getAggregationMetadata(graphQLField: string): { fieldName: string; operator: string; comparator: string } { - throw new Error("Not implemented"); - } } diff --git a/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.test.ts b/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.test.ts new file mode 100644 index 0000000000..70b28bde48 --- /dev/null +++ b/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.test.ts @@ -0,0 +1,488 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + EnumType, + GraphQLBuiltInScalarType, + InterfaceType, + ListType, + Neo4jGraphQLNumberType, + Neo4jGraphQLSpatialType, + Neo4jGraphQLTemporalType, + ObjectType, + ScalarType, + UnionType, + UserScalarType, +} from "../AttributeType"; +import { Attribute } from "../Attribute"; +import { AttributeModel } from "./AttributeModel"; +import { UniqueAnnotation } from "../../annotation/UniqueAnnotation"; +import { CypherAnnotation } from "../../annotation/CypherAnnotation"; + +describe("Attribute", () => { + describe("type assertions", () => { + test("isID", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.ID, true), + }) + ); + + expect(attribute.isID()).toBe(true); + }); + + test("isBoolean", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.Boolean, true), + }) + ); + + expect(attribute.isBoolean()).toBe(true); + }); + + test("isInt", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.Int, true), + }) + ); + + expect(attribute.isInt()).toBe(true); + }); + + test("isFloat", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.Float, true), + }) + ); + expect(attribute.isFloat()).toBe(true); + }); + + test("isString", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.String, true), + }) + ); + expect(attribute.isString()).toBe(true); + }); + + test("isCartesianPoint", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(Neo4jGraphQLSpatialType.CartesianPoint, true), + }) + ); + + expect(attribute.isCartesianPoint()).toBe(true); + }); + + test("isPoint", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(Neo4jGraphQLSpatialType.Point, true), + }) + ); + + expect(attribute.isPoint()).toBe(true); + }); + + test("isBigInt", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(Neo4jGraphQLNumberType.BigInt, true), + }) + ); + + expect(attribute.isBigInt()).toBe(true); + }); + + test("isDate", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(Neo4jGraphQLTemporalType.Date, true), + }) + ); + + expect(attribute.isDate()).toBe(true); + }); + + test("isDateTime", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(Neo4jGraphQLTemporalType.DateTime, true), + }) + ); + + expect(attribute.isDateTime()).toBe(true); + }); + + test("isLocalDateTime", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(Neo4jGraphQLTemporalType.LocalDateTime, true), + }) + ); + + expect(attribute.isLocalDateTime()).toBe(true); + }); + + test("isTime", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(Neo4jGraphQLTemporalType.Time, true), + }) + ); + + expect(attribute.isTime()).toBe(true); + }); + + test("isLocalTime", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(Neo4jGraphQLTemporalType.LocalTime, true), + }) + ); + + expect(attribute.isLocalTime()).toBe(true); + }); + + test("isDuration", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(Neo4jGraphQLTemporalType.Duration, true), + }) + ); + + expect(attribute.isDuration()).toBe(true); + }); + + test("isObject", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ObjectType("testType", true), + }) + ); + + expect(attribute.isObject()).toBe(true); + }); + + test("isEnum", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new EnumType("testType", true), + }) + ); + + expect(attribute.isEnum()).toBe(true); + }); + + test("isUserScalar", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new UserScalarType("testType", true), + }) + ); + + expect(attribute.isUserScalar()).toBe(true); + }); + + test("isInterface", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new InterfaceType("Tool", true), + }) + ); + expect(attribute.isInterface()).toBe(true); + }); + + test("isUnion", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new UnionType("Tool", true), + }) + ); + expect(attribute.isUnion()).toBe(true); + }); + + describe("List", () => { + test("isList", () => { + const stringType = new ScalarType(GraphQLBuiltInScalarType.String, true); + + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ListType(stringType, true), + }) + ); + + expect(attribute.isList()).toBe(true); + }); + + test("isListOf, should return false if attribute it's not a list", () => { + const stringType = new ScalarType(GraphQLBuiltInScalarType.String, true); + + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: stringType, + }) + ); + + expect(attribute.isListOf(stringType)).toBe(false); + }); + + test("isListOf(Attribute), should return false if it's a list of a different type", () => { + const stringType = new ScalarType(GraphQLBuiltInScalarType.String, true); + + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ListType(stringType, true), + }) + ); + const intType = new ScalarType(GraphQLBuiltInScalarType.Int, true); + expect(attribute.isListOf(intType)).toBe(false); + }); + + test("isListOf(Attribute), should return true if it's a list of a the same type.", () => { + const stringType = new ScalarType(GraphQLBuiltInScalarType.String, true); + + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ListType(stringType, true), + }) + ); + const stringType2 = new ScalarType(GraphQLBuiltInScalarType.String, true); + expect(attribute.isListOf(stringType2)).toBe(true); + }); + + test("isListOf(string), should return false if it's a list of a different type", () => { + const stringType = new ScalarType(GraphQLBuiltInScalarType.String, true); + + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ListType(stringType, true), + }) + ); + expect(attribute.isListOf(GraphQLBuiltInScalarType.Int)).toBe(false); + }); + + test("isListOf(string), should return true if it's a list of a the same type.", () => { + const stringType = new ScalarType(GraphQLBuiltInScalarType.String, true); + + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ListType(stringType, true), + }) + ); + expect(attribute.isListOf(GraphQLBuiltInScalarType.String)).toBe(true); + }); + }); + }); + + describe("category assertions", () => { + test("isGraphQLBuiltInScalar", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.String, true), + }) + ); + + expect(attribute.isGraphQLBuiltInScalar()).toBe(true); + }); + + test("isSpatial", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(Neo4jGraphQLSpatialType.CartesianPoint, true), + }) + ); + + expect(attribute.isSpatial()).toBe(true); + }); + + test("isTemporal", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(Neo4jGraphQLTemporalType.Date, true), + }) + ); + + expect(attribute.isTemporal()).toBe(true); + }); + + test("isAbstract", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new UnionType("Tool", true), + }) + ); + + expect(attribute.isAbstract()).toBe(true); + }); + }); + + test("isRequired", () => { + const attributeRequired = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.String, true), + }) + ); + + const attributeNotRequired = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.String, false), + }) + ); + + expect(attributeRequired.isRequired()).toBe(true); + expect(attributeNotRequired.isRequired()).toBe(false); + }); + + test("isRequired - List", () => { + const attributeRequired = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ListType(new ScalarType(GraphQLBuiltInScalarType.String, true), true), + }) + ); + + const attributeNotRequired = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ListType(new ScalarType(GraphQLBuiltInScalarType.String, true), false), + }) + ); + + expect(attributeRequired.isRequired()).toBe(true); + expect(attributeNotRequired.isRequired()).toBe(false); + }); + + test("isListElementRequired", () => { + const listElementRequired = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ListType(new ScalarType(GraphQLBuiltInScalarType.String, true), false), + }) + ); + + const listElementNotRequired = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ListType(new ScalarType(GraphQLBuiltInScalarType.String, false), true), + }) + ); + + expect(listElementRequired.isListElementRequired()).toBe(true); + expect(listElementNotRequired.isListElementRequired()).toBe(false); + }); + + describe("annotation assertions", () => { + test("isUnique", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [new UniqueAnnotation({ constraintName: "test" })], + type: new ScalarType(GraphQLBuiltInScalarType.ID, true), + }) + ); + expect(attribute.isUnique()).toBe(true); + }); + + test("isCypher", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [new CypherAnnotation({ + statement: "MATCH (this)-[:FRIENDS_WITH]->(closestUser:User) RETURN closestUser", + columnName: "closestUser", + })], + type: new ScalarType(GraphQLBuiltInScalarType.ID, true), + }) + ); + expect(attribute.isCypher()).toBe(true); + }); + }); +}); diff --git a/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.ts b/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.ts index fa27a5b962..5706bbb4e5 100644 --- a/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.ts +++ b/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.ts @@ -22,10 +22,23 @@ import { AggregationModel } from "./AggregationModel"; import { ListModel } from "./ListModel"; import type { Attribute } from "../Attribute"; import type { Annotations } from "../../annotation/Annotation"; -import type { AttributeType } from "../AbstractAttribute"; -import { AbstractAttribute } from "../AbstractAttribute"; +import { + EnumType, + GraphQLBuiltInScalarType, + InterfaceType, + ListType, + Neo4jGraphQLNumberType, + Neo4jGraphQLSpatialType, + Neo4jGraphQLTemporalType, + ObjectType, + ScalarType, + ScalarTypeCategory, + UnionType, + UserScalarType, +} from "../AttributeType"; +import type { Neo4jGraphQLScalarType, AttributeType } from "../AttributeType"; -export class AttributeModel extends AbstractAttribute { +export class AttributeModel { private _listModel: ListModel | undefined; private _mathModel: MathModel | undefined; private _aggregationModel: AggregationModel | undefined; @@ -34,10 +47,9 @@ export class AttributeModel extends AbstractAttribute { public type: AttributeType; constructor(attribute: Attribute) { - super({ name: attribute.name, type: attribute.type, annotations: attribute.annotations }); this.name = attribute.name; - this.annotations = attribute.annotations; this.type = attribute.type; + this.annotations = attribute.annotations; } /** @@ -55,16 +67,23 @@ export class AttributeModel extends AbstractAttribute { ]; */ isMutable(): boolean { - return this.isTemporal() || this.isEnum() || this.isObject() || this.isScalar() || - this.isPrimitive() || this.isInterface() || this.isUnion() || this.isPoint(); + return ( + (this.isTemporal() || + this.isEnum() || + this.isObject() || + this.isScalar() || + this.isPrimitive() || + this.isInterface() || + this.isUnion() || + this.isPoint()) && + !this.isCypher() + ); } isUnique(): boolean { - // TODO: add it when the annotations are merged - // return this.attribute.annotations.unique ? true : false; - return false; + return this.annotations.unique ? true : false; } - + /** * Previously defined as: * [...this.primitiveFields, @@ -104,4 +123,153 @@ export class AttributeModel extends AbstractAttribute { } return this._aggregationModel; } + + isBoolean(): boolean { + return this.type instanceof ScalarType && this.type.name === GraphQLBuiltInScalarType.Boolean; + } + + isID(): boolean { + return this.type instanceof ScalarType && this.type.name === GraphQLBuiltInScalarType.ID; + } + + isInt(): boolean { + return this.type instanceof ScalarType && this.type.name === GraphQLBuiltInScalarType.Int; + } + + isFloat(): boolean { + return this.type instanceof ScalarType && this.type.name === GraphQLBuiltInScalarType.Float; + } + + isString(): boolean { + return this.type instanceof ScalarType && this.type.name === GraphQLBuiltInScalarType.String; + } + + isCartesianPoint(): boolean { + return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLSpatialType.CartesianPoint; + } + + isPoint(): boolean { + return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLSpatialType.Point; + } + + isBigInt(): boolean { + return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLNumberType.BigInt; + } + + isDate(): boolean { + return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLTemporalType.Date; + } + + isDateTime(): boolean { + return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLTemporalType.DateTime; + } + + isLocalDateTime(): boolean { + return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLTemporalType.LocalDateTime; + } + + isTime(): boolean { + return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLTemporalType.Time; + } + + isLocalTime(): boolean { + return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLTemporalType.LocalTime; + } + + isDuration(): boolean { + return this.type instanceof ScalarType && this.type.name === Neo4jGraphQLTemporalType.Duration; + } + + isList(): boolean { + return this.type instanceof ListType; + } + + isListOf( + elementType: Exclude | GraphQLBuiltInScalarType | Neo4jGraphQLScalarType | string + ): boolean { + if (!(this.type instanceof ListType)) { + return false; + } + if (typeof elementType === "string") { + return this.type.ofType.name === elementType; + } + + return this.type.ofType.name === elementType.name; + } + + isListElementRequired(): boolean { + if (!(this.type instanceof ListType)) { + return false; + } + return this.type.ofType.isRequired; + } + + isObject(): boolean { + return this.type instanceof ObjectType; + } + + isEnum(): boolean { + return this.type instanceof EnumType; + } + + isRequired(): boolean { + return this.type.isRequired; + } + + isInterface(): boolean { + return this.type instanceof InterfaceType; + } + + isUnion(): boolean { + return this.type instanceof UnionType; + } + + isUserScalar(): boolean { + return this.type instanceof UserScalarType; + } + + /** + * START of category assertions + */ + isGraphQLBuiltInScalar(): boolean { + return this.type instanceof ScalarType && this.type.category === ScalarTypeCategory.GraphQLBuiltInScalarType; + } + + isSpatial(): boolean { + return this.type instanceof ScalarType && this.type.category === ScalarTypeCategory.Neo4jGraphQLSpatialType; + } + + isTemporal(): boolean { + return this.type instanceof ScalarType && this.type.category === ScalarTypeCategory.Neo4jGraphQLTemporalType; + } + + isAbstract(): boolean { + return this.isInterface() || this.isUnion(); + } + /** + * END of category assertions + */ + + isCypher(): boolean { + return this.annotations.cypher ? true : false; + } + + /** + * START of Refactoring methods, these methods are just adapters to the new methods + * to help the transition from the old Node/Relationship/BaseField classes + * */ + + // TODO: remove this method and use isGraphQLBuiltInScalar instead + isPrimitive(): boolean { + return this.isGraphQLBuiltInScalar(); + } + + // TODO: remove this and use isUserScalar instead + isScalar(): boolean { + return this.isUserScalar(); + } + + /** + * END of refactoring methods + */ } diff --git a/packages/graphql/src/schema-model/entity/ConcreteEntity.ts b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts index d59a71a752..c4e25c61bb 100644 --- a/packages/graphql/src/schema-model/entity/ConcreteEntity.ts +++ b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts @@ -23,10 +23,11 @@ import type { Annotation, Annotations } from "../annotation/Annotation"; import { annotationToKey } from "../annotation/Annotation"; import type { Attribute } from "../attribute/Attribute"; import type { Relationship } from "../relationship/Relationship"; -import { AbstractConcreteEntity } from "./Entity"; import type { Entity } from "./Entity"; -export class ConcreteEntity extends AbstractConcreteEntity implements Entity { +export class ConcreteEntity implements Entity { + public readonly name: string; + public readonly labels: Set; public readonly attributes: Map = new Map(); public readonly relationships: Map = new Map(); public readonly annotations: Partial = {}; @@ -44,8 +45,8 @@ export class ConcreteEntity extends AbstractConcreteEntity implements Entity { annotations?: Annotation[]; relationships?: Relationship[]; }) { - super({ name, labels }); - + this.name = name; + this.labels = new Set(labels); for (const attribute of attributes) { this.addAttribute(attribute); } diff --git a/packages/graphql/src/schema-model/entity/Entity.ts b/packages/graphql/src/schema-model/entity/Entity.ts index cfcbea3585..ba59a90a21 100644 --- a/packages/graphql/src/schema-model/entity/Entity.ts +++ b/packages/graphql/src/schema-model/entity/Entity.ts @@ -25,16 +25,3 @@ export interface Entity { // relationships // annotations } - -export abstract class AbstractConcreteEntity { -/* protected readonly listAttributes: Attribute[] = []; - protected readonly listRelationships: Attribute[] = []; */ - - public readonly name: string; - public readonly labels: Set; - - constructor({ name, labels }: { name: string; labels: string[] }) { - this.name = name; - this.labels = new Set(labels); - } -} diff --git a/packages/graphql/src/schema-model/entity/graphql-models/CompositeEntityModel.ts b/packages/graphql/src/schema-model/entity/graphql-models/CompositeEntityModel.ts new file mode 100644 index 0000000000..fcf251bf6a --- /dev/null +++ b/packages/graphql/src/schema-model/entity/graphql-models/CompositeEntityModel.ts @@ -0,0 +1,33 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ConcreteEntityModel } from "./ConcreteEntityModel"; + +// As the composite entity is not yet implemented, this is a placeholder +export class CompositeEntityModel { + public readonly name: string; + public concreteEntities: ConcreteEntityModel[]; + // TODO: add type interface or union, and for interface add fields + // TODO: add annotations + + constructor({ name, concreteEntities }: { name: string; concreteEntities: ConcreteEntityModel[] }) { + this.name = name; + this.concreteEntities = concreteEntities; + } +} \ No newline at end of file diff --git a/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.test.ts b/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.test.ts new file mode 100644 index 0000000000..98d3d8bcc2 --- /dev/null +++ b/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.test.ts @@ -0,0 +1,88 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConcreteEntity } from "../ConcreteEntity"; +import { Attribute } from "../../attribute/Attribute"; +import { GraphQLBuiltInScalarType, ScalarType } from "../../attribute/AttributeType"; +import { ConcreteEntityModel } from "./ConcreteEntityModel"; +import { AttributeModel } from "../../attribute/graphql-models/AttributeModel"; +import { CypherAnnotation } from "../../annotation/CypherAnnotation"; + +describe("ConcreteEntityModel", () => { + let userModel: ConcreteEntityModel; + let userId: AttributeModel; + let userName: AttributeModel; + let closestUser: AttributeModel; + + beforeAll(() => { + const idAttribute = new Attribute({ + name: "id", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.ID, true), + }); + + const nameAttribute = new Attribute({ + name: "name", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.String, true), + }); + + const closestUserAttribute = new Attribute({ + name: "closestUser", + annotations: [ + new CypherAnnotation({ + statement: "MATCH (this)-[:FRIENDS_WITH]->(closestUser:User) RETURN closestUser", + columnName: "closestUser", + }), + ], + type: new ScalarType(GraphQLBuiltInScalarType.String, true), + }); + + const userEntity = new ConcreteEntity({ + name: "User", + labels: ["User"], + attributes: [idAttribute, nameAttribute, closestUserAttribute], + }); + + userModel = new ConcreteEntityModel(userEntity); + userId = userModel.attributes.get("id") as AttributeModel; + userName = userModel.attributes.get("name") as AttributeModel; + closestUser = userModel.attributes.get("closestUser") as AttributeModel; + }); + + test("should generate a valid GraphQL model", () => { + expect(userModel).toBeDefined(); + expect(userModel).toBeInstanceOf(ConcreteEntityModel); + expect(userModel.name).toBe("User"); + expect(userModel.labels).toEqual(new Set(["User"])); + expect(userModel.attributes.size).toBe(3); + expect(userModel.relationships.size).toBe(0); + expect(userId).toBeDefined(); + expect(userId).toBeInstanceOf(AttributeModel); + expect(userName).toBeDefined(); + expect(userName).toBeInstanceOf(AttributeModel); + expect(closestUser).toBeDefined(); + expect(closestUser).toBeInstanceOf(AttributeModel); + }); + + test("should return the correct mutable fields, (Cypher fields are removed)", () => { + expect(userModel.mutableFields).toHaveLength(2); + expect(userModel.mutableFields).toEqual([userId, userName]); + }); +}); diff --git a/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.ts b/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.ts index e8db9a1f73..6bb29dc2eb 100644 --- a/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.ts +++ b/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.ts @@ -21,36 +21,37 @@ import { AttributeModel } from "../../attribute/graphql-models/AttributeModel"; import type { Relationship } from "../../relationship/Relationship"; import { getFromMap } from "../../utils/get-from-map"; import type { Entity } from "../Entity"; -import { AbstractConcreteEntity } from "../Entity"; import { singular, plural } from "../../utils/string-manipulation"; import type { ConcreteEntity } from "../ConcreteEntity"; import type { Attribute } from "../../attribute/Attribute"; +import { RelationshipModel } from "../../relationship/graphql-model/RelationshipModel"; -export class ConcreteEntityModel extends AbstractConcreteEntity { +export class ConcreteEntityModel { + public readonly name: string; + public readonly labels: Set; public readonly attributes: Map = new Map(); - // TODO: change Relationship to RelationshipModel - public readonly relationships: Map = new Map(); - + public readonly relationships: Map = new Map(); + // These keys allow to store the keys of the map in memory and avoid keep iterating over the map. private mutableFieldsKeys: string[] = []; private uniqueFieldsKeys: string[] = []; private constrainableFieldsKeys: string[] = []; - // TODO: remove this just added to help the migration. - private readonly listAttributes: Attribute[] = []; - // typesNames private _singular: string | undefined; private _plural: string | undefined; constructor(entity: ConcreteEntity) { - super({ name: entity.name, labels: [...entity.labels] }); - this.initAttributes(); + this.name = entity.name; + this.labels = entity.labels; + this.initAttributes(entity.attributes); + this.initRelationships(entity.relationships); } - private initAttributes() { - this.listAttributes.forEach((attribute) => { + private initAttributes(attributes: Map) { + for (const [attributeName, attribute] of attributes.entries()) { const attributeModel = new AttributeModel(attribute); + this.attributes.set(attributeName, attributeModel); if (attributeModel.isMutable()) { this.mutableFieldsKeys.push(attribute.name); } @@ -60,9 +61,14 @@ export class ConcreteEntityModel extends AbstractConcreteEntity { if (attributeModel.isConstrainable()) { this.constrainableFieldsKeys.push(attribute.name); } + } + } - this.attributes.set(attribute.name, attributeModel); - }); + private initRelationships(relationships: Map) { + for (const [relationshipName, relationship] of relationships.entries()) { + const {name, type, direction, target, attributes } = relationship; + this.relationships.set(relationshipName, new RelationshipModel({name, type, direction, source: this, target, attributes })); + } } public get mutableFields(): AttributeModel[] { @@ -86,9 +92,10 @@ export class ConcreteEntityModel extends AbstractConcreteEntity { } public getAllLabels(): string[] { - throw new Error("Method not implemented."); + return this.labels ? [...this.labels] : [this.name]; } + public get singular(): string { if (!this._singular) { this._singular = singular(this.name); @@ -98,7 +105,9 @@ export class ConcreteEntityModel extends AbstractConcreteEntity { public get plural(): string { if (!this._plural) { + // TODO: consider case when the plural is defined with the plural annotation this._plural = plural(this.name); + } return this._plural; } diff --git a/packages/graphql/src/schema-model/generate-model.test.ts b/packages/graphql/src/schema-model/generate-model.test.ts index 59b7839319..2fd78fc64c 100644 --- a/packages/graphql/src/schema-model/generate-model.test.ts +++ b/packages/graphql/src/schema-model/generate-model.test.ts @@ -26,13 +26,11 @@ import { } from "./annotation/AuthorizationAnnotation"; import { generateModel } from "./generate-model"; import type { Neo4jGraphQLSchemaModel } from "./Neo4jGraphQLSchemaModel"; - -import type { ConcreteEntity } from "./entity/ConcreteEntity"; -import type { Attribute } from "./attribute/Attribute"; -import type { Relationship } from "./relationship/Relationship"; import { SubscriptionsAuthorizationFilterEventRule } from "./annotation/SubscriptionsAuthorizationAnnotation"; import { AuthenticationAnnotation } from "./annotation/AuthenticationAnnotation"; - +import type { AttributeModel } from "./attribute/graphql-models/AttributeModel"; +import type { ConcreteEntityModel } from "./entity/graphql-models/ConcreteEntityModel"; +import type { RelationshipModel } from "./relationship/graphql-model/RelationshipModel"; describe("Schema model generation", () => { test("parses @authentication directive with no arguments", () => { @@ -293,31 +291,31 @@ describe("ComposeEntity generation", () => { }); }); -describe("Attribute generation", () => { +describe("GraphQL models", () => { let schemaModel: Neo4jGraphQLSchemaModel; // entities - let userEntity: ConcreteEntity; - let accountEntity: ConcreteEntity; + let userEntity: ConcreteEntityModel; + let accountEntity: ConcreteEntityModel; - // relationships - let userAccounts: Relationship; + // relationships + let userAccounts: RelationshipModel; // user attributes - let id: Attribute; - let name: Attribute; - let createdAt: Attribute; - let releaseDate: Attribute; - let runningTime: Attribute; - let accountSize: Attribute; - let favoriteColors: Attribute; - let password: Attribute; + let id: AttributeModel; + let name: AttributeModel; + let createdAt: AttributeModel; + let releaseDate: AttributeModel; + let runningTime: AttributeModel; + let accountSize: AttributeModel; + let favoriteColors: AttributeModel; + let password: AttributeModel; // hasAccount relationship attributes - let creationTime: Attribute; + let creationTime: AttributeModel; // account attributes - let status: Attribute; - let aOrB: Attribute; + let status: AttributeModel; + let aOrB: AttributeModel; beforeAll(() => { const typeDefs = gql` @@ -331,7 +329,7 @@ describe("Attribute generation", () => { favoriteColors: [String!]! accounts: [Account!]! @relationship(type: "HAS_ACCOUNT", properties: "hasAccount", direction: OUT) } - + interface hasAccount @relationshipProperties { creationTime: DateTime! } @@ -345,7 +343,7 @@ describe("Attribute generation", () => { } union AorB = A | B - + enum Status { ACTIVATED DISABLED @@ -363,32 +361,32 @@ describe("Attribute generation", () => { const document = mergeTypeDefs(typeDefs); schemaModel = generateModel(document); - + // entities - userEntity = schemaModel.entities.get("User") as ConcreteEntity; - userAccounts = userEntity.relationships.get("accounts") as Relationship; - accountEntity = schemaModel.entities.get("Account") as ConcreteEntity; - + userEntity = schemaModel.getConcreteEntityModel("User") as ConcreteEntityModel; + userAccounts = userEntity.relationships.get("accounts") as RelationshipModel; + accountEntity = schemaModel.getConcreteEntityModel("Account") as ConcreteEntityModel; + // user attributes - id = userEntity?.attributes.get("id") as Attribute; - name = userEntity?.attributes.get("name") as Attribute; - createdAt = userEntity?.attributes.get("createdAt") as Attribute; - releaseDate = userEntity?.attributes.get("releaseDate") as Attribute; - runningTime = userEntity?.attributes.get("runningTime") as Attribute; - accountSize = userEntity?.attributes.get("accountSize") as Attribute; - favoriteColors = userEntity?.attributes.get("favoriteColors") as Attribute; - + id = userEntity?.attributes.get("id") as AttributeModel; + name = userEntity?.attributes.get("name") as AttributeModel; + createdAt = userEntity?.attributes.get("createdAt") as AttributeModel; + releaseDate = userEntity?.attributes.get("releaseDate") as AttributeModel; + runningTime = userEntity?.attributes.get("runningTime") as AttributeModel; + accountSize = userEntity?.attributes.get("accountSize") as AttributeModel; + favoriteColors = userEntity?.attributes.get("favoriteColors") as AttributeModel; + // extended attributes - password = userEntity?.attributes.get("password") as Attribute; + password = userEntity?.attributes.get("password") as AttributeModel; // hasAccount relationship attributes - creationTime = userAccounts?.attributes.get("creationTime") as Attribute; + creationTime = userAccounts?.attributes.get("creationTime") as AttributeModel; // account attributes - status = accountEntity?.attributes.get("status") as Attribute; - aOrB = accountEntity?.attributes.get("aOrB") as Attribute; + status = accountEntity?.attributes.get("status") as AttributeModel; + aOrB = accountEntity?.attributes.get("aOrB") as AttributeModel; }); - + describe("attribute types", () => { test("ID", () => { expect(id.isID()).toBe(true); @@ -407,7 +405,6 @@ describe("Attribute generation", () => { expect(creationTime.isDateTime()).toBe(true); expect(creationTime.isGraphQLBuiltInScalar()).toBe(false); expect(creationTime.isTemporal()).toBe(true); - }); test("Date", () => { @@ -430,13 +427,13 @@ describe("Attribute generation", () => { test("Enum", () => { expect(status.isEnum()).toBe(true); expect(status.isGraphQLBuiltInScalar()).toBe(false); - }) + }); test("Union", () => { expect(aOrB.isUnion()).toBe(true); expect(aOrB.isGraphQLBuiltInScalar()).toBe(false); expect(aOrB.isAbstract()).toBe(true); - }) + }); test("List", () => { expect(favoriteColors.isList()).toBe(true); diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/cypher-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/cypher-annotation.ts index c587920b3d..fc86d9543a 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/cypher-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/cypher-annotation.ts @@ -22,11 +22,15 @@ import { CypherAnnotation } from "../../annotation/CypherAnnotation"; import { parseArguments } from "../utils"; export function parseCypherAnnotation(directive: DirectiveNode): CypherAnnotation { - const { statement } = parseArguments(directive); + const { statement, columnName } = parseArguments(directive); if (!statement || typeof statement !== "string") { throw new Neo4jGraphQLSchemaValidationError("@cypher statement required"); } + if (!columnName || typeof columnName !== "string") { + throw new Neo4jGraphQLSchemaValidationError("@cypher columnName required"); + } return new CypherAnnotation({ statement: statement, + columnName: columnName, }); } diff --git a/packages/graphql/src/schema-model/parser/parse-attribute.ts b/packages/graphql/src/schema-model/parser/parse-attribute.ts index 64eb953603..999be2985b 100644 --- a/packages/graphql/src/schema-model/parser/parse-attribute.ts +++ b/packages/graphql/src/schema-model/parser/parse-attribute.ts @@ -21,7 +21,7 @@ import type { FieldDefinitionNode, TypeNode, DirectiveNode } from "graphql"; import { Kind } from "graphql"; import { filterTruthy } from "../../utils/utils"; import type { Annotation } from "../annotation/Annotation"; -import type { AttributeType, Neo4jGraphQLScalarType } from "../attribute/AbstractAttribute"; +import type { AttributeType, Neo4jGraphQLScalarType } from "../attribute/AttributeType"; import { ScalarType, EnumType, @@ -34,7 +34,7 @@ import { Neo4jGraphQLSpatialType, Neo4jGraphQLNumberType, Neo4jGraphQLTemporalType, -} from "../attribute/AbstractAttribute"; +} from "../attribute/AttributeType"; import { Attribute } from "../attribute/Attribute"; import { Field } from "../attribute/Field"; import { parseAuthenticationAnnotation } from "./annotations-parser/authentication-annotation"; diff --git a/packages/graphql/src/schema-model/relationship/Relationship.ts b/packages/graphql/src/schema-model/relationship/Relationship.ts index 06c88c4d12..09b5fb8911 100644 --- a/packages/graphql/src/schema-model/relationship/Relationship.ts +++ b/packages/graphql/src/schema-model/relationship/Relationship.ts @@ -26,13 +26,14 @@ import type { Entity } from "../entity/Entity"; export type RelationshipDirection = "IN" | "OUT"; export class Relationship { - public readonly name: string; //Movie.genres - public readonly type: string; // "HAS_GENRE" + public readonly name: string; // name of the relationship field, e.g. friends + public readonly type: string; // name of the relationship type, e.g. "IS_FRIENDS_WITH" public readonly attributes: Map = new Map(); - public readonly source: ConcreteEntity; // Origin field of relationship + public readonly source: ConcreteEntity; public readonly target: Entity; public readonly direction: RelationshipDirection; - + + // TODO: Delegate to the RelationshipModel the following properties /**Note: Required for now to infer the types without ResolveTree */ public get connectionFieldTypename(): string { return `${this.source.name}${upperFirst(this.name)}Connection`; diff --git a/packages/graphql/src/schema-model/relationship/graphql-model/RelationshipModel.ts b/packages/graphql/src/schema-model/relationship/graphql-model/RelationshipModel.ts new file mode 100644 index 0000000000..037d0f322f --- /dev/null +++ b/packages/graphql/src/schema-model/relationship/graphql-model/RelationshipModel.ts @@ -0,0 +1,91 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { upperFirst } from "graphql-compose"; +import type { Entity } from "../../entity/Entity"; +import { ConcreteEntityModel } from "../../entity/graphql-models/ConcreteEntityModel"; +import type { RelationshipDirection } from "../Relationship"; +import { AttributeModel } from "../../attribute/graphql-models/AttributeModel"; +import type { Attribute } from "../../attribute/Attribute"; +import { ConcreteEntity } from "../../entity/ConcreteEntity"; +import { CompositeEntity } from "../../entity/CompositeEntity"; + +export class RelationshipModel { + public readonly name: string; + public readonly type: string; + public readonly attributes: Map = new Map(); + public readonly source: ConcreteEntityModel; + private rawEntity: Entity; + private _target: Entity | undefined; + public readonly direction: RelationshipDirection; + + /**Note: Required for now to infer the types without ResolveTree */ + public get connectionFieldTypename(): string { + return `${this.source.name}${upperFirst(this.name)}Connection`; + } + + /**Note: Required for now to infer the types without ResolveTree */ + public get relationshipFieldTypename(): string { + return `${this.source.name}${upperFirst(this.name)}Relationship`; + } + + constructor({ + name, + type, + attributes = new Map(), + source, + target, + direction, + }: { + name: string; + type: string; + attributes?: Map; + source: ConcreteEntityModel; + target: Entity; + direction: RelationshipDirection; + }) { + this.name = name; + this.type = type; + this.source = source; + this.direction = direction; + this.rawEntity = target; + this.initAttributes(attributes); + } + + private initAttributes(attributes: Map) { + for (const [attributeName, attribute] of attributes.entries()) { + const attributeModel = new AttributeModel(attribute); + this.attributes.set(attributeName, attributeModel); + } + } + + // construct the target entity only when requested + get target(): Entity { + if (!this._target) { + if (this.rawEntity instanceof ConcreteEntity) { + this._target = new ConcreteEntityModel(this.rawEntity); + } else if (this.rawEntity instanceof CompositeEntity) { + this._target = new CompositeEntity(this.rawEntity); + } else { + throw new Error("invalid target entity type"); + } + } + return this._target; + } +} From bdb4da8c0c64dfcdd48fb59edd1a6c617865ca8d Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Wed, 19 Jul 2023 21:32:22 +0100 Subject: [PATCH 07/22] change Schema Model test to showcase how to dynamically get EntityModels from relationships --- packages/graphql/src/schema-model/generate-model.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphql/src/schema-model/generate-model.test.ts b/packages/graphql/src/schema-model/generate-model.test.ts index 2fd78fc64c..e9baba00ec 100644 --- a/packages/graphql/src/schema-model/generate-model.test.ts +++ b/packages/graphql/src/schema-model/generate-model.test.ts @@ -365,7 +365,7 @@ describe("GraphQL models", () => { // entities userEntity = schemaModel.getConcreteEntityModel("User") as ConcreteEntityModel; userAccounts = userEntity.relationships.get("accounts") as RelationshipModel; - accountEntity = schemaModel.getConcreteEntityModel("Account") as ConcreteEntityModel; + accountEntity = userAccounts.target as ConcreteEntityModel; // user attributes id = userEntity?.attributes.get("id") as AttributeModel; From 2a0621cb48fc53f68cbf1eb269840226e7373821 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Thu, 20 Jul 2023 10:36:47 +0100 Subject: [PATCH 08/22] improve coverage on the Attribute Models --- .../graphql-models/AggregationModel.ts | 29 +++-- .../graphql-models/AttributeModel.test.ts | 115 +++++++++++++++++- .../graphql-models/AttributeModel.ts | 31 ++--- .../attribute/graphql-models/ListModel.ts | 7 +- .../attribute/graphql-models/MathModel.ts | 15 ++- 5 files changed, 151 insertions(+), 46 deletions(-) diff --git a/packages/graphql/src/schema-model/attribute/graphql-models/AggregationModel.ts b/packages/graphql/src/schema-model/attribute/graphql-models/AggregationModel.ts index cab82a29f0..d188f34386 100644 --- a/packages/graphql/src/schema-model/attribute/graphql-models/AggregationModel.ts +++ b/packages/graphql/src/schema-model/attribute/graphql-models/AggregationModel.ts @@ -21,24 +21,26 @@ import type { AttributeModel } from "./AttributeModel"; import { AGGREGATION_COMPARISON_OPERATORS } from "../../../constants"; -type ComparisonOperator = typeof AGGREGATION_COMPARISON_OPERATORS[number]; +type ComparisonOperator = (typeof AGGREGATION_COMPARISON_OPERATORS)[number]; export class AggregationModel { readonly attributeModel: AttributeModel; constructor(attributeModel: AttributeModel) { if (!attributeModel.isScalar()) { - throw new Error("Attribute is not a scalar"); + throw new Error("Aggregation model available only for scalar attributes"); } this.attributeModel = attributeModel; } getAggregationComparators(): string[] { - return AGGREGATION_COMPARISON_OPERATORS.map((comparator) => [ - this.getAverageComparator(comparator), - this.getMinComparator(comparator), - this.getMaxComparator(comparator), - this.getSumComparator(comparator), - ]).flat(); + return AGGREGATION_COMPARISON_OPERATORS.map((comparator) => { + const aggregationList: string[] = []; + aggregationList.push(this.getAverageComparator(comparator)); + aggregationList.push(this.getMinComparator(comparator)); + aggregationList.push(this.getMaxComparator(comparator)); + aggregationList.push(this.getSumComparator(comparator)); + return aggregationList; + }).flat(); } getAverageComparator(comparator: ComparisonOperator): string { @@ -48,14 +50,21 @@ export class AggregationModel { } getMinComparator(comparator: ComparisonOperator): string { - return `${this.attributeModel.name}_MIN_${comparator}`; + return this.attributeModel.isString() + ? `${this.attributeModel.name}_SHORTEST_LENGTH_${comparator}` + : `${this.attributeModel.name}_MIN_${comparator}`; } getMaxComparator(comparator: ComparisonOperator): string { - return `${this.attributeModel.name}_MAX_${comparator}`; + return this.attributeModel.isString() + ? `${this.attributeModel.name}_LONGEST_LENGTH_${comparator}` + : `${this.attributeModel.name}_MAX_${comparator}`; } getSumComparator(comparator: ComparisonOperator): string { + if (!this.attributeModel.isNumeric()) { + throw new Error("Sum aggregation is available only for numeric attributes"); + } return `${this.attributeModel.name}_SUM_${comparator}`; } } diff --git a/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.test.ts b/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.test.ts index 70b28bde48..ed28d17579 100644 --- a/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.test.ts +++ b/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.test.ts @@ -34,6 +34,7 @@ import { Attribute } from "../Attribute"; import { AttributeModel } from "./AttributeModel"; import { UniqueAnnotation } from "../../annotation/UniqueAnnotation"; import { CypherAnnotation } from "../../annotation/CypherAnnotation"; +import { e } from "@neo4j/cypher-builder"; describe("Attribute", () => { describe("type assertions", () => { @@ -475,14 +476,120 @@ describe("Attribute", () => { const attribute = new AttributeModel( new Attribute({ name: "test", - annotations: [new CypherAnnotation({ - statement: "MATCH (this)-[:FRIENDS_WITH]->(closestUser:User) RETURN closestUser", - columnName: "closestUser", - })], + annotations: [ + new CypherAnnotation({ + statement: "MATCH (this)-[:FRIENDS_WITH]->(closestUser:User) RETURN closestUser", + columnName: "closestUser", + }), + ], type: new ScalarType(GraphQLBuiltInScalarType.ID, true), }) ); expect(attribute.isCypher()).toBe(true); }); }); + + describe("specialized models", () => { + test("List Model", () => { + const listElementAttribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ListType(new ScalarType(GraphQLBuiltInScalarType.String, true), false), + }) + ); + + expect(listElementAttribute).toBeInstanceOf(AttributeModel); + expect(listElementAttribute.listModel).toBeDefined(); + expect(listElementAttribute.listModel.getIncludes()).toMatchInlineSnapshot(`"test_INCLUDES"`); + expect(listElementAttribute.listModel.getNotIncludes()).toMatchInlineSnapshot(`"test_NOT_INCLUDES"`); + expect(listElementAttribute.listModel.getPop()).toMatchInlineSnapshot(`"test_POP"`); + expect(listElementAttribute.listModel.getPush()).toMatchInlineSnapshot(`"test_PUSH"`); + }); + + test("Aggregation Model", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.Int, true), + }) + ); + // TODO: test it with String as well. + + expect(attribute).toBeInstanceOf(AttributeModel); + expect(attribute.aggregationModel).toBeDefined(); + expect(attribute.aggregationModel.getAggregationComparators()).toEqual( + expect.arrayContaining([ + "test_AVERAGE_EQUAL", + "test_MIN_EQUAL", + "test_MAX_EQUAL", + "test_SUM_EQUAL", + "test_AVERAGE_GT", + "test_MIN_GT", + "test_MAX_GT", + "test_SUM_GT", + "test_AVERAGE_GTE", + "test_MIN_GTE", + "test_MAX_GTE", + "test_SUM_GTE", + "test_AVERAGE_LT", + "test_MIN_LT", + "test_MAX_LT", + "test_SUM_LT", + "test_AVERAGE_LTE", + "test_MIN_LTE", + "test_MAX_LTE", + "test_SUM_LTE", + ]) + ); + // Average + expect(attribute.aggregationModel.getAverageComparator("EQUAL")).toMatchInlineSnapshot( + `"test_AVERAGE_EQUAL"` + ); + expect(attribute.aggregationModel.getAverageComparator("GT")).toMatchInlineSnapshot(`"test_AVERAGE_GT"`); + expect(attribute.aggregationModel.getAverageComparator("GTE")).toMatchInlineSnapshot(`"test_AVERAGE_GTE"`); + expect(attribute.aggregationModel.getAverageComparator("LT")).toMatchInlineSnapshot(`"test_AVERAGE_LT"`); + expect(attribute.aggregationModel.getAverageComparator("LTE")).toMatchInlineSnapshot(`"test_AVERAGE_LTE"`); + // Max + expect(attribute.aggregationModel.getMaxComparator("EQUAL")).toMatchInlineSnapshot(`"test_MAX_EQUAL"`); + expect(attribute.aggregationModel.getMaxComparator("GT")).toMatchInlineSnapshot(`"test_MAX_GT"`); + expect(attribute.aggregationModel.getMaxComparator("GTE")).toMatchInlineSnapshot(`"test_MAX_GTE"`); + expect(attribute.aggregationModel.getMaxComparator("LT")).toMatchInlineSnapshot(`"test_MAX_LT"`); + expect(attribute.aggregationModel.getMaxComparator("LTE")).toMatchInlineSnapshot(`"test_MAX_LTE"`); + // Min + expect(attribute.aggregationModel.getMinComparator("EQUAL")).toMatchInlineSnapshot(`"test_MIN_EQUAL"`); + expect(attribute.aggregationModel.getMinComparator("GT")).toMatchInlineSnapshot(`"test_MIN_GT"`); + expect(attribute.aggregationModel.getMinComparator("GTE")).toMatchInlineSnapshot(`"test_MIN_GTE"`); + expect(attribute.aggregationModel.getMinComparator("LT")).toMatchInlineSnapshot(`"test_MIN_LT"`); + expect(attribute.aggregationModel.getMinComparator("LTE")).toMatchInlineSnapshot(`"test_MIN_LTE"`); + // Sum + expect(attribute.aggregationModel.getSumComparator("EQUAL")).toMatchInlineSnapshot(`"test_SUM_EQUAL"`); + expect(attribute.aggregationModel.getSumComparator("GT")).toMatchInlineSnapshot(`"test_SUM_GT"`); + expect(attribute.aggregationModel.getSumComparator("GTE")).toMatchInlineSnapshot(`"test_SUM_GTE"`); + expect(attribute.aggregationModel.getSumComparator("LT")).toMatchInlineSnapshot(`"test_SUM_LT"`); + expect(attribute.aggregationModel.getSumComparator("LTE")).toMatchInlineSnapshot(`"test_SUM_LTE"`); + }); + + test("Math Model", () => { + const attribute = new AttributeModel( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.Int, true), + }) + ); + // TODO: test it with float as well. + expect(attribute).toBeInstanceOf(AttributeModel); + expect(attribute.mathModel).toBeDefined(); + expect(attribute.mathModel.getMathOperations()).toEqual( + expect.arrayContaining(["test_INCREMENT", "test_DECREMENT", "test_MULTIPLY", "test_DIVIDE"]) + ); + + expect(attribute.mathModel.getAdd()).toMatchInlineSnapshot(`"test_INCREMENT"`); + expect(attribute.mathModel.getSubtract()).toMatchInlineSnapshot(`"test_DECREMENT"`); + expect(attribute.mathModel.getMultiply()).toMatchInlineSnapshot(`"test_MULTIPLY"`); + expect(attribute.mathModel.getDivide()).toMatchInlineSnapshot(`"test_DIVIDE"`); + }); + }); }); diff --git a/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.ts b/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.ts index 5706bbb4e5..95f9ea0995 100644 --- a/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.ts +++ b/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.ts @@ -72,7 +72,7 @@ export class AttributeModel { this.isEnum() || this.isObject() || this.isScalar() || - this.isPrimitive() || + this.isGraphQLBuiltInScalar() || this.isInterface() || this.isUnion() || this.isPoint()) && @@ -93,7 +93,7 @@ export class AttributeModel { ...this.pointFields,] */ isConstrainable(): boolean { - return this.isPrimitive() || this.isScalar() || this.isEnum() || this.isTemporal() || this.isPoint(); + return this.isGraphQLBuiltInScalar() || this.isScalar() || this.isEnum() || this.isTemporal() || this.isPoint(); } // TODO: remember to figure out constrainableFields @@ -246,6 +246,15 @@ export class AttributeModel { isAbstract(): boolean { return this.isInterface() || this.isUnion(); } + + isScalar(): boolean { + return this.isGraphQLBuiltInScalar() || this.isUserScalar() || this.isSpatial() || this.isTemporal() || this.isBigInt(); + } + + isNumeric(): boolean { + return this.isBigInt() || this.isFloat() || this.isInt(); + } + /** * END of category assertions */ @@ -254,22 +263,4 @@ export class AttributeModel { return this.annotations.cypher ? true : false; } - /** - * START of Refactoring methods, these methods are just adapters to the new methods - * to help the transition from the old Node/Relationship/BaseField classes - * */ - - // TODO: remove this method and use isGraphQLBuiltInScalar instead - isPrimitive(): boolean { - return this.isGraphQLBuiltInScalar(); - } - - // TODO: remove this and use isUserScalar instead - isScalar(): boolean { - return this.isUserScalar(); - } - - /** - * END of refactoring methods - */ } diff --git a/packages/graphql/src/schema-model/attribute/graphql-models/ListModel.ts b/packages/graphql/src/schema-model/attribute/graphql-models/ListModel.ts index bece713c09..c036060dce 100644 --- a/packages/graphql/src/schema-model/attribute/graphql-models/ListModel.ts +++ b/packages/graphql/src/schema-model/attribute/graphql-models/ListModel.ts @@ -44,10 +44,5 @@ export class ListModel { getNotIncludes(): string { return `${this.attributeModel.name}_NOT_INCLUDES`; } - /** - * Given the GraphQL field name, returns the semantic information about the list operation it tries to perform - **/ - getListMetadata(graphQLField: string): { fieldName: string; operator: string; comparator?: string } { - throw new Error("Not implemented"); - } + } diff --git a/packages/graphql/src/schema-model/attribute/graphql-models/MathModel.ts b/packages/graphql/src/schema-model/attribute/graphql-models/MathModel.ts index 4c31ff3e6a..0e2c0433b7 100644 --- a/packages/graphql/src/schema-model/attribute/graphql-models/MathModel.ts +++ b/packages/graphql/src/schema-model/attribute/graphql-models/MathModel.ts @@ -22,15 +22,16 @@ import type { AttributeModel } from "./AttributeModel"; export class MathModel { readonly attributeModel: AttributeModel; + constructor(attributeModel: AttributeModel) { - if (!attributeModel.isScalar()) { - throw new Error("Attribute is not a scalar"); + if (!attributeModel.isNumeric()) { + throw new Error("Math model available only for numeric attributes"); } this.attributeModel = attributeModel; } getMathOperations(): string[] { - return [this.getAdd(), this.getSubtract(), this.getMultiply(), this.getDecrement()]; + return [this.getAdd(), this.getSubtract(), this.getMultiply(), this.getDivide()]; } getAdd(): string { @@ -40,14 +41,16 @@ export class MathModel { } getSubtract(): string { - return `${this.attributeModel.name}_SUBTRACT`; + return this.attributeModel.isInt() || this.attributeModel.isBigInt() ? + `${this.attributeModel.name}_DECREMENT` :`${this.attributeModel.name}_SUBTRACT`; } getMultiply(): string { return `${this.attributeModel.name}_MULTIPLY`; } - getDecrement(): string { - return `${this.attributeModel.name}_DECREMENT`; + getDivide(): string { + return `${this.attributeModel.name}_DIVIDE`; } + } From 48fddb600e1d1078c9f06fa9300aa6199470fa6f Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Thu, 20 Jul 2023 16:05:49 +0100 Subject: [PATCH 09/22] add more tests to the ConcreteEntityModel --- .../src/schema-model/attribute/Attribute.ts | 4 +- .../schema-model/attribute/AttributeType.ts | 1 - .../graphql-models/AggregationModel.ts | 4 +- .../graphql-models/AttributeModel.test.ts | 1 - .../graphql-models/AttributeModel.ts | 1 - .../attribute/graphql-models/ListModel.ts | 2 +- .../ConcreteEntityModel.test.ts | 48 +++++++++- .../graphql-models/ConcreteEntityModel.ts | 62 ++++++++---- .../ConcreteEntityOperations.ts | 94 +++++++++++++++++++ .../src/schema-model/generate-model.test.ts | 4 +- .../src/schema-model/generate-model.ts | 13 ++- .../schema-model/utils/string-manipulation.ts | 2 +- 12 files changed, 203 insertions(+), 33 deletions(-) create mode 100644 packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityOperations.ts diff --git a/packages/graphql/src/schema-model/attribute/Attribute.ts b/packages/graphql/src/schema-model/attribute/Attribute.ts index 05c481832d..d04e602bc4 100644 --- a/packages/graphql/src/schema-model/attribute/Attribute.ts +++ b/packages/graphql/src/schema-model/attribute/Attribute.ts @@ -21,11 +21,11 @@ import { Neo4jGraphQLSchemaValidationError } from "../../classes/Error"; import { annotationToKey, type Annotation, type Annotations } from "../annotation/Annotation"; import type { AttributeType } from "./AttributeType"; -export class Attribute { +export class Attribute { public readonly name: string; public readonly annotations: Partial = {}; public readonly type: AttributeType; - + constructor({ name, annotations = [], type }: { name: string; annotations: Annotation[]; type: AttributeType }) { this.name = name; this.type = type; diff --git a/packages/graphql/src/schema-model/attribute/AttributeType.ts b/packages/graphql/src/schema-model/attribute/AttributeType.ts index 62eea88956..7630dd88fe 100644 --- a/packages/graphql/src/schema-model/attribute/AttributeType.ts +++ b/packages/graphql/src/schema-model/attribute/AttributeType.ts @@ -19,7 +19,6 @@ import { Neo4jGraphQLSchemaValidationError } from "../../classes/Error"; - export enum GraphQLBuiltInScalarType { Int = "Int", Float = "Float", diff --git a/packages/graphql/src/schema-model/attribute/graphql-models/AggregationModel.ts b/packages/graphql/src/schema-model/attribute/graphql-models/AggregationModel.ts index d188f34386..13593fd86b 100644 --- a/packages/graphql/src/schema-model/attribute/graphql-models/AggregationModel.ts +++ b/packages/graphql/src/schema-model/attribute/graphql-models/AggregationModel.ts @@ -38,7 +38,9 @@ export class AggregationModel { aggregationList.push(this.getAverageComparator(comparator)); aggregationList.push(this.getMinComparator(comparator)); aggregationList.push(this.getMaxComparator(comparator)); - aggregationList.push(this.getSumComparator(comparator)); + if (this.attributeModel.isNumeric()) { + aggregationList.push(this.getSumComparator(comparator)); + } return aggregationList; }).flat(); } diff --git a/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.test.ts b/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.test.ts index ed28d17579..180e6afa98 100644 --- a/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.test.ts +++ b/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.test.ts @@ -34,7 +34,6 @@ import { Attribute } from "../Attribute"; import { AttributeModel } from "./AttributeModel"; import { UniqueAnnotation } from "../../annotation/UniqueAnnotation"; import { CypherAnnotation } from "../../annotation/CypherAnnotation"; -import { e } from "@neo4j/cypher-builder"; describe("Attribute", () => { describe("type assertions", () => { diff --git a/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.ts b/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.ts index 95f9ea0995..ee00cc6e06 100644 --- a/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.ts +++ b/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.ts @@ -95,7 +95,6 @@ export class AttributeModel { isConstrainable(): boolean { return this.isGraphQLBuiltInScalar() || this.isScalar() || this.isEnum() || this.isTemporal() || this.isPoint(); } - // TODO: remember to figure out constrainableFields /** * @throws {Error} if the attribute is not a list diff --git a/packages/graphql/src/schema-model/attribute/graphql-models/ListModel.ts b/packages/graphql/src/schema-model/attribute/graphql-models/ListModel.ts index c036060dce..fb2836b6c1 100644 --- a/packages/graphql/src/schema-model/attribute/graphql-models/ListModel.ts +++ b/packages/graphql/src/schema-model/attribute/graphql-models/ListModel.ts @@ -44,5 +44,5 @@ export class ListModel { getNotIncludes(): string { return `${this.attributeModel.name}_NOT_INCLUDES`; } - + } diff --git a/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.test.ts b/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.test.ts index 98d3d8bcc2..fce08c1574 100644 --- a/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.test.ts +++ b/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.test.ts @@ -23,6 +23,7 @@ import { GraphQLBuiltInScalarType, ScalarType } from "../../attribute/AttributeT import { ConcreteEntityModel } from "./ConcreteEntityModel"; import { AttributeModel } from "../../attribute/graphql-models/AttributeModel"; import { CypherAnnotation } from "../../annotation/CypherAnnotation"; +import { UniqueAnnotation } from "../../annotation/UniqueAnnotation"; describe("ConcreteEntityModel", () => { let userModel: ConcreteEntityModel; @@ -33,7 +34,7 @@ describe("ConcreteEntityModel", () => { beforeAll(() => { const idAttribute = new Attribute({ name: "id", - annotations: [], + annotations: [new UniqueAnnotation( { constraintName: "User_id_unique" })], type: new ScalarType(GraphQLBuiltInScalarType.ID, true), }); @@ -66,7 +67,7 @@ describe("ConcreteEntityModel", () => { closestUser = userModel.attributes.get("closestUser") as AttributeModel; }); - test("should generate a valid GraphQL model", () => { + test("should generate a valid ConcreteEntityModel model", () => { expect(userModel).toBeDefined(); expect(userModel).toBeInstanceOf(ConcreteEntityModel); expect(userModel.name).toBe("User"); @@ -85,4 +86,47 @@ describe("ConcreteEntityModel", () => { expect(userModel.mutableFields).toHaveLength(2); expect(userModel.mutableFields).toEqual([userId, userName]); }); + + test("should return the correct labels", () => { + expect(userModel.getAllLabels()).toStrictEqual(["User"]); + expect(userModel.getMainLabel()).toBe("User"); + }); + + test("should return the correct unique fields", () => { + expect(userModel.uniqueFields).toHaveLength(1); + expect(userModel.uniqueFields).toStrictEqual([userId]); + }); + + test("should return the correct singular name", () => { + expect(userModel.singular).toBe("user"); + }); + + test("should return the correct plural name", () => { + expect(userModel.plural).toBe("users"); + }); + + describe("ConcreteEntityOperations", () => { + test("should construct a valid ConcreteEntityOperations", () => { + expect(userModel.operations).toBeDefined(); + }); + + test("should return the correct rootTypeFieldNames", () => { + expect(userModel.operations.rootTypeFieldNames).toStrictEqual({ + aggregate: "usersAggregate", + create: "createUsers", + delete: "deleteUsers", + read: "users", + subscribe: { + created: "userCreated", + deleted: "userDeleted", + relationship_created: "userRelationshipCreated", + relationship_deleted: "userRelationshipDeleted", + updated: "userUpdated", + }, + update: "updateUsers", + }); + }); + + // TODO: add tests for all the other operations if we keep them + }); }); diff --git a/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.ts b/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.ts index 6bb29dc2eb..137485ab69 100644 --- a/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.ts +++ b/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.ts @@ -25,25 +25,33 @@ import { singular, plural } from "../../utils/string-manipulation"; import type { ConcreteEntity } from "../ConcreteEntity"; import type { Attribute } from "../../attribute/Attribute"; import { RelationshipModel } from "../../relationship/graphql-model/RelationshipModel"; +import type { Annotations } from "../../annotation/Annotation"; +import { ConcreteEntityOperations } from "./ConcreteEntityOperations"; export class ConcreteEntityModel { public readonly name: string; public readonly labels: Set; public readonly attributes: Map = new Map(); public readonly relationships: Map = new Map(); + public readonly annotations: Partial; // These keys allow to store the keys of the map in memory and avoid keep iterating over the map. private mutableFieldsKeys: string[] = []; private uniqueFieldsKeys: string[] = []; private constrainableFieldsKeys: string[] = []; - // typesNames + private _relatedEntities: Entity[] | undefined; + private _singular: string | undefined; private _plural: string | undefined; + // specialize models + private _operations: ConcreteEntityOperations | undefined; + constructor(entity: ConcreteEntity) { this.name = entity.name; this.labels = entity.labels; + this.annotations = entity.annotations; this.initAttributes(entity.attributes); this.initRelationships(entity.relationships); } @@ -55,19 +63,23 @@ export class ConcreteEntityModel { if (attributeModel.isMutable()) { this.mutableFieldsKeys.push(attribute.name); } - if (attributeModel.isUnique()) { - this.uniqueFieldsKeys.push(attribute.name); - } + if (attributeModel.isConstrainable()) { this.constrainableFieldsKeys.push(attribute.name); + if (attributeModel.isUnique()) { + this.uniqueFieldsKeys.push(attribute.name); + } } } } private initRelationships(relationships: Map) { for (const [relationshipName, relationship] of relationships.entries()) { - const {name, type, direction, target, attributes } = relationship; - this.relationships.set(relationshipName, new RelationshipModel({name, type, direction, source: this, target, attributes })); + const { name, type, direction, target, attributes } = relationship; + this.relationships.set( + relationshipName, + new RelationshipModel({ name, type, direction, source: this, target, attributes }) + ); } } @@ -83,19 +95,26 @@ export class ConcreteEntityModel { return this.constrainableFieldsKeys.map((key) => getFromMap(this.attributes, key)); } - public get relationshipAttributesName(): string[] { - return [...this.relationships.keys()]; - } - - public getRelatedEntities(): Entity[] { - return [...this.relationships.values()].map((relationship) => relationship.target); + public get relatedEntities(): Entity[] { + if (!this._relatedEntities) { + this._relatedEntities = [...this.relationships.values()].map((relationship) => relationship.target); + } + return this._relatedEntities; } + // TODO: identify usage of old Node.[getLabels | getLabelsString] and migrate them if needed public getAllLabels(): string[] { - return this.labels ? [...this.labels] : [this.name]; + if (this.annotations.node?.labels) { + return this.annotations.node.labels; + } + return [this.name]; } + public getMainLabel(): string { + return this.getAllLabels()[0] as string; // getAllLabels always returns at least one label + } + public get singular(): string { if (!this._singular) { this._singular = singular(this.name); @@ -105,10 +124,21 @@ export class ConcreteEntityModel { public get plural(): string { if (!this._plural) { - // TODO: consider case when the plural is defined with the plural annotation - this._plural = plural(this.name); - + if (this.annotations.plural) { + this._plural = plural(this.annotations.plural.value); + } else { + this._plural = plural(this.name); + } } return this._plural; } + + get operations(): ConcreteEntityOperations { + if (!this._operations) { + return new ConcreteEntityOperations(this); + } + return this._operations; + } + + // TODO: Implement the Globals methods toGlobalId and fromGlobalId, getGlobalId etc... } diff --git a/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityOperations.ts b/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityOperations.ts new file mode 100644 index 0000000000..64e8a54aee --- /dev/null +++ b/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityOperations.ts @@ -0,0 +1,94 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { upperFirst } from "../../../utils/upper-first"; +import type{ ConcreteEntityModel } from "./ConcreteEntityModel"; +import type { AggregateTypeNames, FulltextTypeNames, MutationResponseTypeNames, RootTypeFieldNames, SubscriptionEvents } from "../../../classes/Node"; + +export class ConcreteEntityOperations { + private readonly concreteEntityModel: ConcreteEntityModel; + private readonly pascalCasePlural: string; + private readonly pascalCaseSingular: string; + + constructor(concreteEntityModel: ConcreteEntityModel) { + this.concreteEntityModel = concreteEntityModel; + this.pascalCasePlural = upperFirst(this.concreteEntityModel.plural); + this.pascalCaseSingular = upperFirst(this.concreteEntityModel.singular); + } + + public get rootTypeFieldNames(): RootTypeFieldNames { + return { + create: `create${this.pascalCasePlural}`, + read: this.concreteEntityModel.plural, + update: `update${this.pascalCasePlural}`, + delete: `delete${this.pascalCasePlural}`, + aggregate: `${this.concreteEntityModel.plural}Aggregate`, + subscribe: { + created: `${this.concreteEntityModel.singular}Created`, + updated: `${this.concreteEntityModel.singular}Updated`, + deleted: `${this.concreteEntityModel.singular}Deleted`, + relationship_deleted: `${this.concreteEntityModel.singular}RelationshipDeleted`, + relationship_created: `${this.concreteEntityModel.singular}RelationshipCreated`, + }, + }; + } + + public get fulltextTypeNames(): FulltextTypeNames { + return { + result: `${this.pascalCaseSingular}FulltextResult`, + where: `${this.pascalCaseSingular}FulltextWhere`, + sort: `${this.pascalCaseSingular}FulltextSort`, + }; + } + + public get aggregateTypeNames(): AggregateTypeNames { + return { + selection: `${this.concreteEntityModel.name}AggregateSelection`, + input: `${this.concreteEntityModel.name}AggregateSelectionInput`, + }; + } + + public get mutationResponseTypeNames(): MutationResponseTypeNames { + return { + create: `Create${this.pascalCasePlural}MutationResponse`, + update: `Update${this.pascalCasePlural}MutationResponse`, + }; + } + + public get subscriptionEventTypeNames(): SubscriptionEvents { + return { + create: `${this.pascalCaseSingular}CreatedEvent`, + update: `${this.pascalCaseSingular}UpdatedEvent`, + delete: `${this.pascalCaseSingular}DeletedEvent`, + create_relationship: `${this.pascalCaseSingular}RelationshipCreatedEvent`, + delete_relationship: `${this.pascalCaseSingular}RelationshipDeletedEvent`, + }; + } + + public get subscriptionEventPayloadFieldNames(): SubscriptionEvents { + return { + create: `created${this.pascalCaseSingular}`, + update: `updated${this.pascalCaseSingular}`, + delete: `deleted${this.pascalCaseSingular}`, + create_relationship: `${this.concreteEntityModel.singular}`, + delete_relationship: `${this.concreteEntityModel.singular}`, + }; + } + +} \ No newline at end of file diff --git a/packages/graphql/src/schema-model/generate-model.test.ts b/packages/graphql/src/schema-model/generate-model.test.ts index e9baba00ec..499aa5cf9c 100644 --- a/packages/graphql/src/schema-model/generate-model.test.ts +++ b/packages/graphql/src/schema-model/generate-model.test.ts @@ -365,7 +365,7 @@ describe("GraphQL models", () => { // entities userEntity = schemaModel.getConcreteEntityModel("User") as ConcreteEntityModel; userAccounts = userEntity.relationships.get("accounts") as RelationshipModel; - accountEntity = userAccounts.target as ConcreteEntityModel; + accountEntity = userAccounts.target as ConcreteEntityModel; // it's possible to obtain accountEntity using schemaModel.getConcreteEntityModel("Account") as well // user attributes id = userEntity?.attributes.get("id") as AttributeModel; @@ -386,7 +386,7 @@ describe("GraphQL models", () => { status = accountEntity?.attributes.get("status") as AttributeModel; aOrB = accountEntity?.attributes.get("aOrB") as AttributeModel; }); - + describe("attribute types", () => { test("ID", () => { expect(id.isID()).toBe(true); diff --git a/packages/graphql/src/schema-model/generate-model.ts b/packages/graphql/src/schema-model/generate-model.ts index a66629e2ac..020e613ffa 100644 --- a/packages/graphql/src/schema-model/generate-model.ts +++ b/packages/graphql/src/schema-model/generate-model.ts @@ -53,7 +53,6 @@ export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { generateConcreteEntity(node, definitionCollection) ); - // TODO: this error could happen directly in getDefinitionCollection instead of here, as because we moved to Map structure it will never be the case that we have duplicate nodes. const concreteEntitiesMap = concreteEntities.reduce((acc, entity) => { if (acc.has(entity.name)) { throw new Neo4jGraphQLSchemaValidationError(`Duplicate node ${entity.name}`); @@ -122,11 +121,14 @@ function generateCompositeEntity( } return concreteEntity; }); - if (!compositeFields.length) { + /* + // This is commented out because is currently possible to have leaf interfaces as demonstrated in the test + // packages/graphql/tests/integration/aggregations/where/node/string.int.test.ts + if (!compositeFields.length) { throw new Neo4jGraphQLSchemaValidationError( `Composite entity ${entityDefinitionName} has no concrete entities` ); - } + } */ // TODO: add annotations return new CompositeEntity({ name: entityDefinitionName, @@ -174,10 +176,11 @@ function generateRelationshipField( let attributes: Attribute[] = []; if (properties && typeof properties === "string") { const propertyInterface = definitionCollection.relationshipProperties.get(properties); - if (!propertyInterface) + if (!propertyInterface) { throw new Error( - `There is no matching interface defined with @relationshipProperties for properties "${properties}"` + `The \`@relationshipProperties\` directive could not be found on the \`${properties}\` interface` ); + } const fields = (propertyInterface.fields || []).map((field) => parseAttribute(field, definitionCollection)); diff --git a/packages/graphql/src/schema-model/utils/string-manipulation.ts b/packages/graphql/src/schema-model/utils/string-manipulation.ts index 387e97ec6c..b46a47a807 100644 --- a/packages/graphql/src/schema-model/utils/string-manipulation.ts +++ b/packages/graphql/src/schema-model/utils/string-manipulation.ts @@ -25,7 +25,7 @@ export function singular(name: string): string { return `${leadingUnderscores(name)}${singular}`; } -// TODO this has to be tested as is different from Node.generatePlural + export function plural(name: string): string { const plural = pluralize(camelcase(name)); return `${leadingUnderscores(name)}${plural}`; From cca74808bc8dc5f8f53ff32de7b5ee283bdced7a Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Thu, 20 Jul 2023 16:22:04 +0100 Subject: [PATCH 10/22] Apply suggestions from code review Co-authored-by: angrykoala --- packages/graphql/src/classes/Node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphql/src/classes/Node.ts b/packages/graphql/src/classes/Node.ts index 4e8ca1f7d7..ad11938cd5 100644 --- a/packages/graphql/src/classes/Node.ts +++ b/packages/graphql/src/classes/Node.ts @@ -170,7 +170,7 @@ class Node extends GraphElement { ...this.enumFields, ...this.objectFields, ...this.scalarFields, // this are just custom scalars - ...this.primitiveFields, // this are instead built-in scalars, confirmed By Alexandra + ...this.primitiveFields, // this are instead built-in scalars ...this.interfaceFields, ...this.objectFields, ...this.unionFields, From b622a673b3e14105eaadab8a7477271f531182f7 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Thu, 20 Jul 2023 16:26:42 +0100 Subject: [PATCH 11/22] lint fixes --- packages/graphql/src/classes/Node.ts | 2 +- .../schema-model/entity/graphql-models/CompositeEntityModel.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/graphql/src/classes/Node.ts b/packages/graphql/src/classes/Node.ts index ad11938cd5..2191323dda 100644 --- a/packages/graphql/src/classes/Node.ts +++ b/packages/graphql/src/classes/Node.ts @@ -329,7 +329,7 @@ class Node extends GraphElement { return `${this.leadingUnderscores(name)}${plural}`; } - + private leadingUnderscores(name: string): string { const re = /^(_+).+/; const match = re.exec(name); diff --git a/packages/graphql/src/schema-model/entity/graphql-models/CompositeEntityModel.ts b/packages/graphql/src/schema-model/entity/graphql-models/CompositeEntityModel.ts index fcf251bf6a..0ab3bab99b 100644 --- a/packages/graphql/src/schema-model/entity/graphql-models/CompositeEntityModel.ts +++ b/packages/graphql/src/schema-model/entity/graphql-models/CompositeEntityModel.ts @@ -30,4 +30,4 @@ export class CompositeEntityModel { this.name = name; this.concreteEntities = concreteEntities; } -} \ No newline at end of file +} From 4ed9e46b339483b34d271f1b0d3d45714334bb70 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Fri, 21 Jul 2023 11:08:08 +0100 Subject: [PATCH 12/22] rename graphql-models to model-adapters --- packages/graphql/src/classes/Node.ts | 6 +- .../schema-model/Neo4jGraphQLSchemaModel.ts | 14 +-- .../AggregationAdapter.ts} | 36 ++++---- .../AttributeAdapter.test.ts} | 88 +++++++++---------- .../AttributeAdapter.ts} | 37 ++++---- .../ListAdapter.ts} | 21 +++-- .../MathAdapter.ts} | 29 +++--- .../CompositeEntityAdapter.ts} | 8 +- .../ConcreteEntityAdapter.test.ts} | 34 +++---- .../ConcreteEntityAdapter.ts} | 29 +++--- .../ConcreteEntityOperations.ts | 47 +++++----- .../src/schema-model/generate-model.test.ts | 62 ++++++------- .../schema-model/relationship/Relationship.ts | 4 +- .../RelationshipAdapter.ts} | 18 ++-- 14 files changed, 220 insertions(+), 213 deletions(-) rename packages/graphql/src/schema-model/attribute/{graphql-models/AggregationModel.ts => model-adapters/AggregationAdapter.ts} (63%) rename packages/graphql/src/schema-model/attribute/{graphql-models/AttributeModel.test.ts => model-adapters/AttributeAdapter.test.ts} (89%) rename packages/graphql/src/schema-model/attribute/{graphql-models/AttributeModel.ts => model-adapters/AttributeAdapter.ts} (88%) rename packages/graphql/src/schema-model/attribute/{graphql-models/ListModel.ts => model-adapters/ListAdapter.ts} (63%) rename packages/graphql/src/schema-model/attribute/{graphql-models/MathModel.ts => model-adapters/MathAdapter.ts} (56%) rename packages/graphql/src/schema-model/entity/{graphql-models/CompositeEntityModel.ts => model-adapters/CompositeEntityAdapter.ts} (83%) rename packages/graphql/src/schema-model/entity/{graphql-models/ConcreteEntityModel.test.ts => model-adapters/ConcreteEntityAdapter.test.ts} (82%) rename packages/graphql/src/schema-model/entity/{graphql-models/ConcreteEntityModel.ts => model-adapters/ConcreteEntityAdapter.ts} (82%) rename packages/graphql/src/schema-model/entity/{graphql-models => model-adapters}/ConcreteEntityOperations.ts (63%) rename packages/graphql/src/schema-model/relationship/{graphql-model/RelationshipModel.ts => model-adapters/RelationshipAdapter.ts} (82%) diff --git a/packages/graphql/src/classes/Node.ts b/packages/graphql/src/classes/Node.ts index 2191323dda..d8233ff1da 100644 --- a/packages/graphql/src/classes/Node.ts +++ b/packages/graphql/src/classes/Node.ts @@ -169,8 +169,8 @@ class Node extends GraphElement { ...this.temporalFields, ...this.enumFields, ...this.objectFields, - ...this.scalarFields, // this are just custom scalars - ...this.primitiveFields, // this are instead built-in scalars + ...this.scalarFields, // these are just custom scalars + ...this.primitiveFields, // these are instead built-in scalars ...this.interfaceFields, ...this.objectFields, ...this.unionFields, @@ -329,7 +329,7 @@ class Node extends GraphElement { return `${this.leadingUnderscores(name)}${plural}`; } - + private leadingUnderscores(name: string): string { const re = /^(_+).+/; const match = re.exec(name); diff --git a/packages/graphql/src/schema-model/Neo4jGraphQLSchemaModel.ts b/packages/graphql/src/schema-model/Neo4jGraphQLSchemaModel.ts index 2943ec82f8..0ead6e0e93 100644 --- a/packages/graphql/src/schema-model/Neo4jGraphQLSchemaModel.ts +++ b/packages/graphql/src/schema-model/Neo4jGraphQLSchemaModel.ts @@ -22,9 +22,10 @@ import type { Operation } from "./Operation"; import type { Annotations, Annotation } from "./annotation/Annotation"; import { annotationToKey } from "./annotation/Annotation"; import { CompositeEntity } from "./entity/CompositeEntity"; -import { ConcreteEntity } from "./entity/ConcreteEntity"; +import { ConcreteEntity } from "./entity/ConcreteEntity"; import type { Entity } from "./entity/Entity"; -import { ConcreteEntityModel } from "./entity/graphql-models/ConcreteEntityModel"; +import { ConcreteEntityAdapter } from "./entity/model-adapters/ConcreteEntityAdapter"; + export type Operations = { Query?: Operation; Mutation?: Operation; @@ -53,7 +54,6 @@ export class Neo4jGraphQLSchemaModel { acc.set(entity.name, entity); return acc; }, new Map()); - this.concreteEntities = concreteEntities; this.compositeEntities = compositeEntities; @@ -63,14 +63,14 @@ export class Neo4jGraphQLSchemaModel { this.addAnnotation(annotation); } } - + public getEntity(name: string): Entity | undefined { return this.entities.get(name); } - public getConcreteEntityModel(name: string): ConcreteEntityModel | undefined { - const concreteEntityModel = this.concreteEntities.find((entity) => entity.name === name); - return concreteEntityModel ? new ConcreteEntityModel(concreteEntityModel) : undefined; + public getConcreteEntityAdapter(name: string): ConcreteEntityAdapter | undefined { + const concreteEntity = this.concreteEntities.find((entity) => entity.name === name); + return concreteEntity ? new ConcreteEntityAdapter(concreteEntity) : undefined; } public getEntitiesByLabels(labels: string[]): ConcreteEntity[] { diff --git a/packages/graphql/src/schema-model/attribute/graphql-models/AggregationModel.ts b/packages/graphql/src/schema-model/attribute/model-adapters/AggregationAdapter.ts similarity index 63% rename from packages/graphql/src/schema-model/attribute/graphql-models/AggregationModel.ts rename to packages/graphql/src/schema-model/attribute/model-adapters/AggregationAdapter.ts index 13593fd86b..bde5aa4fc9 100644 --- a/packages/graphql/src/schema-model/attribute/graphql-models/AggregationModel.ts +++ b/packages/graphql/src/schema-model/attribute/model-adapters/AggregationAdapter.ts @@ -17,19 +17,19 @@ * limitations under the License. */ -import type { AttributeModel } from "./AttributeModel"; +import type { AttributeAdapter } from "./AttributeAdapter"; import { AGGREGATION_COMPARISON_OPERATORS } from "../../../constants"; type ComparisonOperator = (typeof AGGREGATION_COMPARISON_OPERATORS)[number]; -export class AggregationModel { - readonly attributeModel: AttributeModel; - constructor(attributeModel: AttributeModel) { - if (!attributeModel.isScalar()) { +export class AggregationAdapter { + readonly AttributeAdapter: AttributeAdapter; + constructor(AttributeAdapter: AttributeAdapter) { + if (!AttributeAdapter.isScalar()) { throw new Error("Aggregation model available only for scalar attributes"); } - this.attributeModel = attributeModel; + this.AttributeAdapter = AttributeAdapter; } getAggregationComparators(): string[] { @@ -38,7 +38,7 @@ export class AggregationModel { aggregationList.push(this.getAverageComparator(comparator)); aggregationList.push(this.getMinComparator(comparator)); aggregationList.push(this.getMaxComparator(comparator)); - if (this.attributeModel.isNumeric()) { + if (this.AttributeAdapter.isNumeric()) { aggregationList.push(this.getSumComparator(comparator)); } return aggregationList; @@ -46,27 +46,27 @@ export class AggregationModel { } getAverageComparator(comparator: ComparisonOperator): string { - return this.attributeModel.isString() - ? `${this.attributeModel.name}_AVERAGE_LENGTH_${comparator}` - : `${this.attributeModel.name}_AVERAGE_${comparator}`; + return this.AttributeAdapter.isString() + ? `${this.AttributeAdapter.name}_AVERAGE_LENGTH_${comparator}` + : `${this.AttributeAdapter.name}_AVERAGE_${comparator}`; } getMinComparator(comparator: ComparisonOperator): string { - return this.attributeModel.isString() - ? `${this.attributeModel.name}_SHORTEST_LENGTH_${comparator}` - : `${this.attributeModel.name}_MIN_${comparator}`; + return this.AttributeAdapter.isString() + ? `${this.AttributeAdapter.name}_SHORTEST_LENGTH_${comparator}` + : `${this.AttributeAdapter.name}_MIN_${comparator}`; } getMaxComparator(comparator: ComparisonOperator): string { - return this.attributeModel.isString() - ? `${this.attributeModel.name}_LONGEST_LENGTH_${comparator}` - : `${this.attributeModel.name}_MAX_${comparator}`; + return this.AttributeAdapter.isString() + ? `${this.AttributeAdapter.name}_LONGEST_LENGTH_${comparator}` + : `${this.AttributeAdapter.name}_MAX_${comparator}`; } getSumComparator(comparator: ComparisonOperator): string { - if (!this.attributeModel.isNumeric()) { + if (!this.AttributeAdapter.isNumeric()) { throw new Error("Sum aggregation is available only for numeric attributes"); } - return `${this.attributeModel.name}_SUM_${comparator}`; + return `${this.AttributeAdapter.name}_SUM_${comparator}`; } } diff --git a/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.test.ts b/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.test.ts similarity index 89% rename from packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.test.ts rename to packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.test.ts index 180e6afa98..5be19eed4d 100644 --- a/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.test.ts +++ b/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.test.ts @@ -31,14 +31,14 @@ import { UserScalarType, } from "../AttributeType"; import { Attribute } from "../Attribute"; -import { AttributeModel } from "./AttributeModel"; +import { AttributeAdapter } from "./AttributeAdapter"; import { UniqueAnnotation } from "../../annotation/UniqueAnnotation"; import { CypherAnnotation } from "../../annotation/CypherAnnotation"; describe("Attribute", () => { describe("type assertions", () => { test("isID", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -50,7 +50,7 @@ describe("Attribute", () => { }); test("isBoolean", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -62,7 +62,7 @@ describe("Attribute", () => { }); test("isInt", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -74,7 +74,7 @@ describe("Attribute", () => { }); test("isFloat", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -85,7 +85,7 @@ describe("Attribute", () => { }); test("isString", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -96,7 +96,7 @@ describe("Attribute", () => { }); test("isCartesianPoint", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -108,7 +108,7 @@ describe("Attribute", () => { }); test("isPoint", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -120,7 +120,7 @@ describe("Attribute", () => { }); test("isBigInt", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -132,7 +132,7 @@ describe("Attribute", () => { }); test("isDate", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -144,7 +144,7 @@ describe("Attribute", () => { }); test("isDateTime", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -156,7 +156,7 @@ describe("Attribute", () => { }); test("isLocalDateTime", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -168,7 +168,7 @@ describe("Attribute", () => { }); test("isTime", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -180,7 +180,7 @@ describe("Attribute", () => { }); test("isLocalTime", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -192,7 +192,7 @@ describe("Attribute", () => { }); test("isDuration", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -204,7 +204,7 @@ describe("Attribute", () => { }); test("isObject", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -216,7 +216,7 @@ describe("Attribute", () => { }); test("isEnum", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -228,7 +228,7 @@ describe("Attribute", () => { }); test("isUserScalar", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -240,7 +240,7 @@ describe("Attribute", () => { }); test("isInterface", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -251,7 +251,7 @@ describe("Attribute", () => { }); test("isUnion", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -265,7 +265,7 @@ describe("Attribute", () => { test("isList", () => { const stringType = new ScalarType(GraphQLBuiltInScalarType.String, true); - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -279,7 +279,7 @@ describe("Attribute", () => { test("isListOf, should return false if attribute it's not a list", () => { const stringType = new ScalarType(GraphQLBuiltInScalarType.String, true); - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -293,7 +293,7 @@ describe("Attribute", () => { test("isListOf(Attribute), should return false if it's a list of a different type", () => { const stringType = new ScalarType(GraphQLBuiltInScalarType.String, true); - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -307,7 +307,7 @@ describe("Attribute", () => { test("isListOf(Attribute), should return true if it's a list of a the same type.", () => { const stringType = new ScalarType(GraphQLBuiltInScalarType.String, true); - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -321,7 +321,7 @@ describe("Attribute", () => { test("isListOf(string), should return false if it's a list of a different type", () => { const stringType = new ScalarType(GraphQLBuiltInScalarType.String, true); - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -334,7 +334,7 @@ describe("Attribute", () => { test("isListOf(string), should return true if it's a list of a the same type.", () => { const stringType = new ScalarType(GraphQLBuiltInScalarType.String, true); - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -348,7 +348,7 @@ describe("Attribute", () => { describe("category assertions", () => { test("isGraphQLBuiltInScalar", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -360,7 +360,7 @@ describe("Attribute", () => { }); test("isSpatial", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -372,7 +372,7 @@ describe("Attribute", () => { }); test("isTemporal", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -384,7 +384,7 @@ describe("Attribute", () => { }); test("isAbstract", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -397,7 +397,7 @@ describe("Attribute", () => { }); test("isRequired", () => { - const attributeRequired = new AttributeModel( + const attributeRequired = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -405,7 +405,7 @@ describe("Attribute", () => { }) ); - const attributeNotRequired = new AttributeModel( + const attributeNotRequired = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -418,7 +418,7 @@ describe("Attribute", () => { }); test("isRequired - List", () => { - const attributeRequired = new AttributeModel( + const attributeRequired = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -426,7 +426,7 @@ describe("Attribute", () => { }) ); - const attributeNotRequired = new AttributeModel( + const attributeNotRequired = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -439,7 +439,7 @@ describe("Attribute", () => { }); test("isListElementRequired", () => { - const listElementRequired = new AttributeModel( + const listElementRequired = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -447,7 +447,7 @@ describe("Attribute", () => { }) ); - const listElementNotRequired = new AttributeModel( + const listElementNotRequired = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -461,7 +461,7 @@ describe("Attribute", () => { describe("annotation assertions", () => { test("isUnique", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [new UniqueAnnotation({ constraintName: "test" })], @@ -472,7 +472,7 @@ describe("Attribute", () => { }); test("isCypher", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [ @@ -490,7 +490,7 @@ describe("Attribute", () => { describe("specialized models", () => { test("List Model", () => { - const listElementAttribute = new AttributeModel( + const listElementAttribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -498,7 +498,7 @@ describe("Attribute", () => { }) ); - expect(listElementAttribute).toBeInstanceOf(AttributeModel); + expect(listElementAttribute).toBeInstanceOf(AttributeAdapter); expect(listElementAttribute.listModel).toBeDefined(); expect(listElementAttribute.listModel.getIncludes()).toMatchInlineSnapshot(`"test_INCLUDES"`); expect(listElementAttribute.listModel.getNotIncludes()).toMatchInlineSnapshot(`"test_NOT_INCLUDES"`); @@ -507,7 +507,7 @@ describe("Attribute", () => { }); test("Aggregation Model", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -516,7 +516,7 @@ describe("Attribute", () => { ); // TODO: test it with String as well. - expect(attribute).toBeInstanceOf(AttributeModel); + expect(attribute).toBeInstanceOf(AttributeAdapter); expect(attribute.aggregationModel).toBeDefined(); expect(attribute.aggregationModel.getAggregationComparators()).toEqual( expect.arrayContaining([ @@ -571,7 +571,7 @@ describe("Attribute", () => { }); test("Math Model", () => { - const attribute = new AttributeModel( + const attribute = new AttributeAdapter( new Attribute({ name: "test", annotations: [], @@ -579,7 +579,7 @@ describe("Attribute", () => { }) ); // TODO: test it with float as well. - expect(attribute).toBeInstanceOf(AttributeModel); + expect(attribute).toBeInstanceOf(AttributeAdapter); expect(attribute.mathModel).toBeDefined(); expect(attribute.mathModel.getMathOperations()).toEqual( expect.arrayContaining(["test_INCREMENT", "test_DECREMENT", "test_MULTIPLY", "test_DIVIDE"]) diff --git a/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.ts b/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.ts similarity index 88% rename from packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.ts rename to packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.ts index ee00cc6e06..3876855ec3 100644 --- a/packages/graphql/src/schema-model/attribute/graphql-models/AttributeModel.ts +++ b/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.ts @@ -17,9 +17,9 @@ * limitations under the License. */ -import { MathModel } from "./MathModel"; -import { AggregationModel } from "./AggregationModel"; -import { ListModel } from "./ListModel"; +import { MathAdapter } from "./MathAdapter"; +import { AggregationAdapter } from "./AggregationAdapter"; +import { ListAdapter } from "./ListAdapter"; import type { Attribute } from "../Attribute"; import type { Annotations } from "../../annotation/Annotation"; import { @@ -36,12 +36,12 @@ import { UnionType, UserScalarType, } from "../AttributeType"; -import type { Neo4jGraphQLScalarType, AttributeType } from "../AttributeType"; +import type { AttributeType, Neo4jGraphQLScalarType } from "../AttributeType"; -export class AttributeModel { - private _listModel: ListModel | undefined; - private _mathModel: MathModel | undefined; - private _aggregationModel: AggregationModel | undefined; +export class AttributeAdapter { + private _listModel: ListAdapter | undefined; + private _mathModel: MathAdapter | undefined; + private _aggregationModel: AggregationAdapter | undefined; public name: string; public annotations: Partial; public type: AttributeType; @@ -99,9 +99,9 @@ export class AttributeModel { /** * @throws {Error} if the attribute is not a list */ - get listModel(): ListModel { + get listModel(): ListAdapter { if (!this._listModel) { - this._listModel = new ListModel(this); + this._listModel = new ListAdapter(this); } return this._listModel; } @@ -109,16 +109,16 @@ export class AttributeModel { /** * @throws {Error} if the attribute is not a scalar */ - get mathModel(): MathModel { + get mathModel(): MathAdapter { if (!this._mathModel) { - this._mathModel = new MathModel(this); + this._mathModel = new MathAdapter(this); } return this._mathModel; } - get aggregationModel(): AggregationModel { + get aggregationModel(): AggregationAdapter { if (!this._aggregationModel) { - this._aggregationModel = new AggregationModel(this); + this._aggregationModel = new AggregationAdapter(this); } return this._aggregationModel; } @@ -247,7 +247,13 @@ export class AttributeModel { } isScalar(): boolean { - return this.isGraphQLBuiltInScalar() || this.isUserScalar() || this.isSpatial() || this.isTemporal() || this.isBigInt(); + return ( + this.isGraphQLBuiltInScalar() || + this.isUserScalar() || + this.isSpatial() || + this.isTemporal() || + this.isBigInt() + ); } isNumeric(): boolean { @@ -261,5 +267,4 @@ export class AttributeModel { isCypher(): boolean { return this.annotations.cypher ? true : false; } - } diff --git a/packages/graphql/src/schema-model/attribute/graphql-models/ListModel.ts b/packages/graphql/src/schema-model/attribute/model-adapters/ListAdapter.ts similarity index 63% rename from packages/graphql/src/schema-model/attribute/graphql-models/ListModel.ts rename to packages/graphql/src/schema-model/attribute/model-adapters/ListAdapter.ts index fb2836b6c1..795077b5ff 100644 --- a/packages/graphql/src/schema-model/attribute/graphql-models/ListModel.ts +++ b/packages/graphql/src/schema-model/attribute/model-adapters/ListAdapter.ts @@ -17,32 +17,31 @@ * limitations under the License. */ -import type { AttributeModel } from "./AttributeModel"; +import type { AttributeAdapter } from "./AttributeAdapter"; -export class ListModel { - readonly attributeModel: AttributeModel; +export class ListAdapter { + readonly AttributeAdapter: AttributeAdapter; - constructor(attributeModel: AttributeModel) { - if (!attributeModel.isList()) { + constructor(AttributeAdapter: AttributeAdapter) { + if (!AttributeAdapter.isList()) { throw new Error("Attribute is not a list"); } - this.attributeModel = attributeModel; + this.AttributeAdapter = AttributeAdapter; } getPush(): string { - return `${this.attributeModel.name}_PUSH`; + return `${this.AttributeAdapter.name}_PUSH`; } getPop(): string { - return `${this.attributeModel.name}_POP`; + return `${this.AttributeAdapter.name}_POP`; } getIncludes(): string { - return `${this.attributeModel.name}_INCLUDES`; + return `${this.AttributeAdapter.name}_INCLUDES`; } getNotIncludes(): string { - return `${this.attributeModel.name}_NOT_INCLUDES`; + return `${this.AttributeAdapter.name}_NOT_INCLUDES`; } - } diff --git a/packages/graphql/src/schema-model/attribute/graphql-models/MathModel.ts b/packages/graphql/src/schema-model/attribute/model-adapters/MathAdapter.ts similarity index 56% rename from packages/graphql/src/schema-model/attribute/graphql-models/MathModel.ts rename to packages/graphql/src/schema-model/attribute/model-adapters/MathAdapter.ts index 0e2c0433b7..e40009e8d7 100644 --- a/packages/graphql/src/schema-model/attribute/graphql-models/MathModel.ts +++ b/packages/graphql/src/schema-model/attribute/model-adapters/MathAdapter.ts @@ -1,4 +1,3 @@ - /* * Copyright (c) "Neo4j" * Neo4j Sweden AB [http://neo4j.com] @@ -18,16 +17,16 @@ * limitations under the License. */ -import type { AttributeModel } from "./AttributeModel"; +import type { AttributeAdapter } from "./AttributeAdapter"; -export class MathModel { - readonly attributeModel: AttributeModel; +export class MathAdapter { + readonly AttributeAdapter: AttributeAdapter; - constructor(attributeModel: AttributeModel) { - if (!attributeModel.isNumeric()) { + constructor(AttributeAdapter: AttributeAdapter) { + if (!AttributeAdapter.isNumeric()) { throw new Error("Math model available only for numeric attributes"); } - this.attributeModel = attributeModel; + this.AttributeAdapter = AttributeAdapter; } getMathOperations(): string[] { @@ -35,22 +34,22 @@ export class MathModel { } getAdd(): string { - return this.attributeModel.isInt() || this.attributeModel.isBigInt() - ? `${this.attributeModel.name}_INCREMENT` - : `${this.attributeModel.name}_ADD`; + return this.AttributeAdapter.isInt() || this.AttributeAdapter.isBigInt() + ? `${this.AttributeAdapter.name}_INCREMENT` + : `${this.AttributeAdapter.name}_ADD`; } getSubtract(): string { - return this.attributeModel.isInt() || this.attributeModel.isBigInt() ? - `${this.attributeModel.name}_DECREMENT` :`${this.attributeModel.name}_SUBTRACT`; + return this.AttributeAdapter.isInt() || this.AttributeAdapter.isBigInt() + ? `${this.AttributeAdapter.name}_DECREMENT` + : `${this.AttributeAdapter.name}_SUBTRACT`; } getMultiply(): string { - return `${this.attributeModel.name}_MULTIPLY`; + return `${this.AttributeAdapter.name}_MULTIPLY`; } getDivide(): string { - return `${this.attributeModel.name}_DIVIDE`; + return `${this.AttributeAdapter.name}_DIVIDE`; } - } diff --git a/packages/graphql/src/schema-model/entity/graphql-models/CompositeEntityModel.ts b/packages/graphql/src/schema-model/entity/model-adapters/CompositeEntityAdapter.ts similarity index 83% rename from packages/graphql/src/schema-model/entity/graphql-models/CompositeEntityModel.ts rename to packages/graphql/src/schema-model/entity/model-adapters/CompositeEntityAdapter.ts index 0ab3bab99b..a61d88b0bc 100644 --- a/packages/graphql/src/schema-model/entity/graphql-models/CompositeEntityModel.ts +++ b/packages/graphql/src/schema-model/entity/model-adapters/CompositeEntityAdapter.ts @@ -17,16 +17,16 @@ * limitations under the License. */ -import type { ConcreteEntityModel } from "./ConcreteEntityModel"; +import type { ConcreteEntityAdapter } from "./ConcreteEntityAdapter"; // As the composite entity is not yet implemented, this is a placeholder -export class CompositeEntityModel { +export class CompositeEntityAdapter { public readonly name: string; - public concreteEntities: ConcreteEntityModel[]; + public concreteEntities: ConcreteEntityAdapter[]; // TODO: add type interface or union, and for interface add fields // TODO: add annotations - constructor({ name, concreteEntities }: { name: string; concreteEntities: ConcreteEntityModel[] }) { + constructor({ name, concreteEntities }: { name: string; concreteEntities: ConcreteEntityAdapter[] }) { this.name = name; this.concreteEntities = concreteEntities; } diff --git a/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.test.ts b/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.test.ts similarity index 82% rename from packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.test.ts rename to packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.test.ts index fce08c1574..f6d8bb2fd5 100644 --- a/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.test.ts +++ b/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.test.ts @@ -20,21 +20,21 @@ import { ConcreteEntity } from "../ConcreteEntity"; import { Attribute } from "../../attribute/Attribute"; import { GraphQLBuiltInScalarType, ScalarType } from "../../attribute/AttributeType"; -import { ConcreteEntityModel } from "./ConcreteEntityModel"; -import { AttributeModel } from "../../attribute/graphql-models/AttributeModel"; +import { ConcreteEntityAdapter } from "./ConcreteEntityAdapter"; +import { AttributeAdapter } from "../../attribute/model-adapters/AttributeAdapter"; import { CypherAnnotation } from "../../annotation/CypherAnnotation"; import { UniqueAnnotation } from "../../annotation/UniqueAnnotation"; -describe("ConcreteEntityModel", () => { - let userModel: ConcreteEntityModel; - let userId: AttributeModel; - let userName: AttributeModel; - let closestUser: AttributeModel; +describe("ConcreteEntityAdapter", () => { + let userModel: ConcreteEntityAdapter; + let userId: AttributeAdapter; + let userName: AttributeAdapter; + let closestUser: AttributeAdapter; beforeAll(() => { const idAttribute = new Attribute({ name: "id", - annotations: [new UniqueAnnotation( { constraintName: "User_id_unique" })], + annotations: [new UniqueAnnotation({ constraintName: "User_id_unique" })], type: new ScalarType(GraphQLBuiltInScalarType.ID, true), }); @@ -61,25 +61,25 @@ describe("ConcreteEntityModel", () => { attributes: [idAttribute, nameAttribute, closestUserAttribute], }); - userModel = new ConcreteEntityModel(userEntity); - userId = userModel.attributes.get("id") as AttributeModel; - userName = userModel.attributes.get("name") as AttributeModel; - closestUser = userModel.attributes.get("closestUser") as AttributeModel; + userModel = new ConcreteEntityAdapter(userEntity); + userId = userModel.attributes.get("id") as AttributeAdapter; + userName = userModel.attributes.get("name") as AttributeAdapter; + closestUser = userModel.attributes.get("closestUser") as AttributeAdapter; }); - test("should generate a valid ConcreteEntityModel model", () => { + test("should generate a valid ConcreteEntityAdapter model", () => { expect(userModel).toBeDefined(); - expect(userModel).toBeInstanceOf(ConcreteEntityModel); + expect(userModel).toBeInstanceOf(ConcreteEntityAdapter); expect(userModel.name).toBe("User"); expect(userModel.labels).toEqual(new Set(["User"])); expect(userModel.attributes.size).toBe(3); expect(userModel.relationships.size).toBe(0); expect(userId).toBeDefined(); - expect(userId).toBeInstanceOf(AttributeModel); + expect(userId).toBeInstanceOf(AttributeAdapter); expect(userName).toBeDefined(); - expect(userName).toBeInstanceOf(AttributeModel); + expect(userName).toBeInstanceOf(AttributeAdapter); expect(closestUser).toBeDefined(); - expect(closestUser).toBeInstanceOf(AttributeModel); + expect(closestUser).toBeInstanceOf(AttributeAdapter); }); test("should return the correct mutable fields, (Cypher fields are removed)", () => { diff --git a/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.ts b/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.ts similarity index 82% rename from packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.ts rename to packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.ts index 137485ab69..2d1dedcd4c 100644 --- a/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityModel.ts +++ b/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.ts @@ -17,22 +17,22 @@ * limitations under the License. */ -import { AttributeModel } from "../../attribute/graphql-models/AttributeModel"; +import { AttributeAdapter } from "../../attribute/model-adapters/AttributeAdapter"; import type { Relationship } from "../../relationship/Relationship"; import { getFromMap } from "../../utils/get-from-map"; import type { Entity } from "../Entity"; import { singular, plural } from "../../utils/string-manipulation"; import type { ConcreteEntity } from "../ConcreteEntity"; import type { Attribute } from "../../attribute/Attribute"; -import { RelationshipModel } from "../../relationship/graphql-model/RelationshipModel"; +import { RelationshipAdapter } from "../../relationship/model-adapters/RelationshipAdapter"; import type { Annotations } from "../../annotation/Annotation"; import { ConcreteEntityOperations } from "./ConcreteEntityOperations"; -export class ConcreteEntityModel { +export class ConcreteEntityAdapter { public readonly name: string; public readonly labels: Set; - public readonly attributes: Map = new Map(); - public readonly relationships: Map = new Map(); + public readonly attributes: Map = new Map(); + public readonly relationships: Map = new Map(); public readonly annotations: Partial; // These keys allow to store the keys of the map in memory and avoid keep iterating over the map. @@ -58,15 +58,15 @@ export class ConcreteEntityModel { private initAttributes(attributes: Map) { for (const [attributeName, attribute] of attributes.entries()) { - const attributeModel = new AttributeModel(attribute); - this.attributes.set(attributeName, attributeModel); - if (attributeModel.isMutable()) { + const attributeAdapter = new AttributeAdapter(attribute); + this.attributes.set(attributeName, attributeAdapter); + if (attributeAdapter.isMutable()) { this.mutableFieldsKeys.push(attribute.name); } - if (attributeModel.isConstrainable()) { + if (attributeAdapter.isConstrainable()) { this.constrainableFieldsKeys.push(attribute.name); - if (attributeModel.isUnique()) { + if (attributeAdapter.isUnique()) { this.uniqueFieldsKeys.push(attribute.name); } } @@ -78,20 +78,20 @@ export class ConcreteEntityModel { const { name, type, direction, target, attributes } = relationship; this.relationships.set( relationshipName, - new RelationshipModel({ name, type, direction, source: this, target, attributes }) + new RelationshipAdapter({ name, type, direction, source: this, target, attributes }) ); } } - public get mutableFields(): AttributeModel[] { + public get mutableFields(): AttributeAdapter[] { return this.mutableFieldsKeys.map((key) => getFromMap(this.attributes, key)); } - public get uniqueFields(): AttributeModel[] { + public get uniqueFields(): AttributeAdapter[] { return this.uniqueFieldsKeys.map((key) => getFromMap(this.attributes, key)); } - public get constrainableFields(): AttributeModel[] { + public get constrainableFields(): AttributeAdapter[] { return this.constrainableFieldsKeys.map((key) => getFromMap(this.attributes, key)); } @@ -114,7 +114,6 @@ export class ConcreteEntityModel { return this.getAllLabels()[0] as string; // getAllLabels always returns at least one label } - public get singular(): string { if (!this._singular) { this._singular = singular(this.name); diff --git a/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityOperations.ts b/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityOperations.ts similarity index 63% rename from packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityOperations.ts rename to packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityOperations.ts index 64e8a54aee..5589625f52 100644 --- a/packages/graphql/src/schema-model/entity/graphql-models/ConcreteEntityOperations.ts +++ b/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityOperations.ts @@ -18,33 +18,39 @@ */ import { upperFirst } from "../../../utils/upper-first"; -import type{ ConcreteEntityModel } from "./ConcreteEntityModel"; -import type { AggregateTypeNames, FulltextTypeNames, MutationResponseTypeNames, RootTypeFieldNames, SubscriptionEvents } from "../../../classes/Node"; +import type { ConcreteEntityAdapter } from "./ConcreteEntityAdapter"; +import type { + AggregateTypeNames, + FulltextTypeNames, + MutationResponseTypeNames, + RootTypeFieldNames, + SubscriptionEvents, +} from "../../../classes/Node"; export class ConcreteEntityOperations { - private readonly concreteEntityModel: ConcreteEntityModel; + private readonly ConcreteEntityAdapter: ConcreteEntityAdapter; private readonly pascalCasePlural: string; private readonly pascalCaseSingular: string; - constructor(concreteEntityModel: ConcreteEntityModel) { - this.concreteEntityModel = concreteEntityModel; - this.pascalCasePlural = upperFirst(this.concreteEntityModel.plural); - this.pascalCaseSingular = upperFirst(this.concreteEntityModel.singular); + constructor(ConcreteEntityAdapter: ConcreteEntityAdapter) { + this.ConcreteEntityAdapter = ConcreteEntityAdapter; + this.pascalCasePlural = upperFirst(this.ConcreteEntityAdapter.plural); + this.pascalCaseSingular = upperFirst(this.ConcreteEntityAdapter.singular); } public get rootTypeFieldNames(): RootTypeFieldNames { return { create: `create${this.pascalCasePlural}`, - read: this.concreteEntityModel.plural, + read: this.ConcreteEntityAdapter.plural, update: `update${this.pascalCasePlural}`, delete: `delete${this.pascalCasePlural}`, - aggregate: `${this.concreteEntityModel.plural}Aggregate`, + aggregate: `${this.ConcreteEntityAdapter.plural}Aggregate`, subscribe: { - created: `${this.concreteEntityModel.singular}Created`, - updated: `${this.concreteEntityModel.singular}Updated`, - deleted: `${this.concreteEntityModel.singular}Deleted`, - relationship_deleted: `${this.concreteEntityModel.singular}RelationshipDeleted`, - relationship_created: `${this.concreteEntityModel.singular}RelationshipCreated`, + created: `${this.ConcreteEntityAdapter.singular}Created`, + updated: `${this.ConcreteEntityAdapter.singular}Updated`, + deleted: `${this.ConcreteEntityAdapter.singular}Deleted`, + relationship_deleted: `${this.ConcreteEntityAdapter.singular}RelationshipDeleted`, + relationship_created: `${this.ConcreteEntityAdapter.singular}RelationshipCreated`, }, }; } @@ -59,8 +65,8 @@ export class ConcreteEntityOperations { public get aggregateTypeNames(): AggregateTypeNames { return { - selection: `${this.concreteEntityModel.name}AggregateSelection`, - input: `${this.concreteEntityModel.name}AggregateSelectionInput`, + selection: `${this.ConcreteEntityAdapter.name}AggregateSelection`, + input: `${this.ConcreteEntityAdapter.name}AggregateSelectionInput`, }; } @@ -82,13 +88,12 @@ export class ConcreteEntityOperations { } public get subscriptionEventPayloadFieldNames(): SubscriptionEvents { - return { + return { create: `created${this.pascalCaseSingular}`, update: `updated${this.pascalCaseSingular}`, delete: `deleted${this.pascalCaseSingular}`, - create_relationship: `${this.concreteEntityModel.singular}`, - delete_relationship: `${this.concreteEntityModel.singular}`, + create_relationship: `${this.ConcreteEntityAdapter.singular}`, + delete_relationship: `${this.ConcreteEntityAdapter.singular}`, }; } - -} \ No newline at end of file +} diff --git a/packages/graphql/src/schema-model/generate-model.test.ts b/packages/graphql/src/schema-model/generate-model.test.ts index 499aa5cf9c..6f8756fa8e 100644 --- a/packages/graphql/src/schema-model/generate-model.test.ts +++ b/packages/graphql/src/schema-model/generate-model.test.ts @@ -28,9 +28,9 @@ import { generateModel } from "./generate-model"; import type { Neo4jGraphQLSchemaModel } from "./Neo4jGraphQLSchemaModel"; import { SubscriptionsAuthorizationFilterEventRule } from "./annotation/SubscriptionsAuthorizationAnnotation"; import { AuthenticationAnnotation } from "./annotation/AuthenticationAnnotation"; -import type { AttributeModel } from "./attribute/graphql-models/AttributeModel"; -import type { ConcreteEntityModel } from "./entity/graphql-models/ConcreteEntityModel"; -import type { RelationshipModel } from "./relationship/graphql-model/RelationshipModel"; +import type { AttributeAdapter } from "./attribute/model-adapters/AttributeAdapter"; +import type { ConcreteEntityAdapter } from "./entity/model-adapters/ConcreteEntityAdapter"; +import type { RelationshipAdapter } from "./relationship/model-adapters/RelationshipAdapter"; describe("Schema model generation", () => { test("parses @authentication directive with no arguments", () => { @@ -294,28 +294,28 @@ describe("ComposeEntity generation", () => { describe("GraphQL models", () => { let schemaModel: Neo4jGraphQLSchemaModel; // entities - let userEntity: ConcreteEntityModel; - let accountEntity: ConcreteEntityModel; + let userEntity: ConcreteEntityAdapter; + let accountEntity: ConcreteEntityAdapter; // relationships - let userAccounts: RelationshipModel; + let userAccounts: RelationshipAdapter; // user attributes - let id: AttributeModel; - let name: AttributeModel; - let createdAt: AttributeModel; - let releaseDate: AttributeModel; - let runningTime: AttributeModel; - let accountSize: AttributeModel; - let favoriteColors: AttributeModel; - let password: AttributeModel; + let id: AttributeAdapter; + let name: AttributeAdapter; + let createdAt: AttributeAdapter; + let releaseDate: AttributeAdapter; + let runningTime: AttributeAdapter; + let accountSize: AttributeAdapter; + let favoriteColors: AttributeAdapter; + let password: AttributeAdapter; // hasAccount relationship attributes - let creationTime: AttributeModel; + let creationTime: AttributeAdapter; // account attributes - let status: AttributeModel; - let aOrB: AttributeModel; + let status: AttributeAdapter; + let aOrB: AttributeAdapter; beforeAll(() => { const typeDefs = gql` @@ -363,28 +363,28 @@ describe("GraphQL models", () => { schemaModel = generateModel(document); // entities - userEntity = schemaModel.getConcreteEntityModel("User") as ConcreteEntityModel; - userAccounts = userEntity.relationships.get("accounts") as RelationshipModel; - accountEntity = userAccounts.target as ConcreteEntityModel; // it's possible to obtain accountEntity using schemaModel.getConcreteEntityModel("Account") as well + userEntity = schemaModel.getConcreteEntityAdapter("User") as ConcreteEntityAdapter; + userAccounts = userEntity.relationships.get("accounts") as RelationshipAdapter; + accountEntity = userAccounts.target as ConcreteEntityAdapter; // it's possible to obtain accountEntity using schemaModel.getConcreteEntityAdapter("Account") as well // user attributes - id = userEntity?.attributes.get("id") as AttributeModel; - name = userEntity?.attributes.get("name") as AttributeModel; - createdAt = userEntity?.attributes.get("createdAt") as AttributeModel; - releaseDate = userEntity?.attributes.get("releaseDate") as AttributeModel; - runningTime = userEntity?.attributes.get("runningTime") as AttributeModel; - accountSize = userEntity?.attributes.get("accountSize") as AttributeModel; - favoriteColors = userEntity?.attributes.get("favoriteColors") as AttributeModel; + id = userEntity?.attributes.get("id") as AttributeAdapter; + name = userEntity?.attributes.get("name") as AttributeAdapter; + createdAt = userEntity?.attributes.get("createdAt") as AttributeAdapter; + releaseDate = userEntity?.attributes.get("releaseDate") as AttributeAdapter; + runningTime = userEntity?.attributes.get("runningTime") as AttributeAdapter; + accountSize = userEntity?.attributes.get("accountSize") as AttributeAdapter; + favoriteColors = userEntity?.attributes.get("favoriteColors") as AttributeAdapter; // extended attributes - password = userEntity?.attributes.get("password") as AttributeModel; + password = userEntity?.attributes.get("password") as AttributeAdapter; // hasAccount relationship attributes - creationTime = userAccounts?.attributes.get("creationTime") as AttributeModel; + creationTime = userAccounts?.attributes.get("creationTime") as AttributeAdapter; // account attributes - status = accountEntity?.attributes.get("status") as AttributeModel; - aOrB = accountEntity?.attributes.get("aOrB") as AttributeModel; + status = accountEntity?.attributes.get("status") as AttributeAdapter; + aOrB = accountEntity?.attributes.get("aOrB") as AttributeAdapter; }); describe("attribute types", () => { diff --git a/packages/graphql/src/schema-model/relationship/Relationship.ts b/packages/graphql/src/schema-model/relationship/Relationship.ts index 09b5fb8911..6ffaaea675 100644 --- a/packages/graphql/src/schema-model/relationship/Relationship.ts +++ b/packages/graphql/src/schema-model/relationship/Relationship.ts @@ -32,8 +32,8 @@ export class Relationship { public readonly source: ConcreteEntity; public readonly target: Entity; public readonly direction: RelationshipDirection; - - // TODO: Delegate to the RelationshipModel the following properties + + // TODO: Delegate to the RelationshipAdapter the following properties /**Note: Required for now to infer the types without ResolveTree */ public get connectionFieldTypename(): string { return `${this.source.name}${upperFirst(this.name)}Connection`; diff --git a/packages/graphql/src/schema-model/relationship/graphql-model/RelationshipModel.ts b/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts similarity index 82% rename from packages/graphql/src/schema-model/relationship/graphql-model/RelationshipModel.ts rename to packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts index 037d0f322f..fb4b80f567 100644 --- a/packages/graphql/src/schema-model/relationship/graphql-model/RelationshipModel.ts +++ b/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts @@ -19,18 +19,18 @@ import { upperFirst } from "graphql-compose"; import type { Entity } from "../../entity/Entity"; -import { ConcreteEntityModel } from "../../entity/graphql-models/ConcreteEntityModel"; +import { ConcreteEntityAdapter } from "../../entity/model-adapters/ConcreteEntityAdapter"; import type { RelationshipDirection } from "../Relationship"; -import { AttributeModel } from "../../attribute/graphql-models/AttributeModel"; +import { AttributeAdapter } from "../../attribute/model-adapters/AttributeAdapter"; import type { Attribute } from "../../attribute/Attribute"; import { ConcreteEntity } from "../../entity/ConcreteEntity"; import { CompositeEntity } from "../../entity/CompositeEntity"; -export class RelationshipModel { +export class RelationshipAdapter { public readonly name: string; public readonly type: string; - public readonly attributes: Map = new Map(); - public readonly source: ConcreteEntityModel; + public readonly attributes: Map = new Map(); + public readonly source: ConcreteEntityAdapter; private rawEntity: Entity; private _target: Entity | undefined; public readonly direction: RelationshipDirection; @@ -56,7 +56,7 @@ export class RelationshipModel { name: string; type: string; attributes?: Map; - source: ConcreteEntityModel; + source: ConcreteEntityAdapter; target: Entity; direction: RelationshipDirection; }) { @@ -70,8 +70,8 @@ export class RelationshipModel { private initAttributes(attributes: Map) { for (const [attributeName, attribute] of attributes.entries()) { - const attributeModel = new AttributeModel(attribute); - this.attributes.set(attributeName, attributeModel); + const attributeAdapter = new AttributeAdapter(attribute); + this.attributes.set(attributeName, attributeAdapter); } } @@ -79,7 +79,7 @@ export class RelationshipModel { get target(): Entity { if (!this._target) { if (this.rawEntity instanceof ConcreteEntity) { - this._target = new ConcreteEntityModel(this.rawEntity); + this._target = new ConcreteEntityAdapter(this.rawEntity); } else if (this.rawEntity instanceof CompositeEntity) { this._target = new CompositeEntity(this.rawEntity); } else { From 692cb2e892a503613a07261b182b395013199996 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Fri, 21 Jul 2023 11:54:18 +0100 Subject: [PATCH 13/22] remove @relationshipProperties annotation as it is implicit within the Relationship class --- .../src/schema-model/annotation/Annotation.ts | 5 ---- .../RelationshipPropertiesAnnotation.ts | 20 -------------- .../relationship-properties-annotation.ts | 26 ------------------- 3 files changed, 51 deletions(-) delete mode 100644 packages/graphql/src/schema-model/annotation/RelationshipPropertiesAnnotation.ts delete mode 100644 packages/graphql/src/schema-model/parser/annotations-parser/relationship-properties-annotation.ts diff --git a/packages/graphql/src/schema-model/annotation/Annotation.ts b/packages/graphql/src/schema-model/annotation/Annotation.ts index 27ce2192f2..2973044af1 100644 --- a/packages/graphql/src/schema-model/annotation/Annotation.ts +++ b/packages/graphql/src/schema-model/annotation/Annotation.ts @@ -37,7 +37,6 @@ import { PopulatedByAnnotation } from "./PopulatedByAnnotation"; import { PrivateAnnotation } from "./PrivateAnnotation"; import { QueryAnnotation } from "./QueryAnnotation"; import { QueryOptionsAnnotation } from "./QueryOptionsAnnotation"; -import { RelationshipPropertiesAnnotation } from "./RelationshipPropertiesAnnotation"; import { SelectableAnnotation } from "./SelectableAnnotation"; import { SettableAnnotation } from "./SettableAnnotation"; import { SubscriptionAnnotation } from "./SubscriptionAnnotation"; @@ -65,7 +64,6 @@ export type Annotation = | PopulatedByAnnotation | QueryAnnotation | PrivateAnnotation - | RelationshipPropertiesAnnotation | SelectableAnnotation | SettableAnnotation | TimestampAnnotation @@ -94,7 +92,6 @@ export enum AnnotationsKey { populatedBy = "populatedBy", query = "query", private = "private", - relationshipProperties = "relationshipProperties", selectable = "selectable", settable = "settable", timestamp = "timestamp", @@ -124,7 +121,6 @@ export type Annotations = { [AnnotationsKey.populatedBy]: PopulatedByAnnotation; [AnnotationsKey.query]: QueryAnnotation; [AnnotationsKey.private]: PrivateAnnotation; - [AnnotationsKey.relationshipProperties]: RelationshipPropertiesAnnotation; [AnnotationsKey.selectable]: SelectableAnnotation; [AnnotationsKey.settable]: SettableAnnotation; [AnnotationsKey.timestamp]: TimestampAnnotation; @@ -154,7 +150,6 @@ export function annotationToKey(ann: Annotation): keyof Annotations { if (ann instanceof PopulatedByAnnotation) return AnnotationsKey.populatedBy; if (ann instanceof QueryAnnotation) return AnnotationsKey.query; if (ann instanceof PrivateAnnotation) return AnnotationsKey.private; - if (ann instanceof RelationshipPropertiesAnnotation) return AnnotationsKey.relationshipProperties; if (ann instanceof SelectableAnnotation) return AnnotationsKey.selectable; if (ann instanceof SettableAnnotation) return AnnotationsKey.settable; if (ann instanceof TimestampAnnotation) return AnnotationsKey.timestamp; diff --git a/packages/graphql/src/schema-model/annotation/RelationshipPropertiesAnnotation.ts b/packages/graphql/src/schema-model/annotation/RelationshipPropertiesAnnotation.ts deleted file mode 100644 index 79bacffef2..0000000000 --- a/packages/graphql/src/schema-model/annotation/RelationshipPropertiesAnnotation.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export class RelationshipPropertiesAnnotation {} diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/relationship-properties-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/relationship-properties-annotation.ts deleted file mode 100644 index 8b07fc78a1..0000000000 --- a/packages/graphql/src/schema-model/parser/annotations-parser/relationship-properties-annotation.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { DirectiveNode } from "graphql"; -import { RelationshipPropertiesAnnotation } from "../../annotation/RelationshipPropertiesAnnotation"; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export function parseRelationshipPropertiesAnnotation(_directive: DirectiveNode): RelationshipPropertiesAnnotation { - return new RelationshipPropertiesAnnotation(); -} From bef4a2ff0e1a072b9e3fd8df9d5175054e724d1f Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Fri, 21 Jul 2023 13:37:44 +0100 Subject: [PATCH 14/22] remove code duplicity around leadingUnderscores utility and test it --- packages/graphql/src/classes/Node.ts | 11 ++---- .../schema-model/utils/string-manipulation.ts | 7 +--- .../src/utils/leading-underscore.test.ts | 34 +++++++++++++++++++ .../graphql/src/utils/leading-underscore.ts | 25 ++++++++++++++ packages/graphql/tests/utils/graphql-types.ts | 8 ++--- 5 files changed, 65 insertions(+), 20 deletions(-) create mode 100644 packages/graphql/src/utils/leading-underscore.test.ts create mode 100644 packages/graphql/src/utils/leading-underscore.ts diff --git a/packages/graphql/src/classes/Node.ts b/packages/graphql/src/classes/Node.ts index d8233ff1da..df84918c4f 100644 --- a/packages/graphql/src/classes/Node.ts +++ b/packages/graphql/src/classes/Node.ts @@ -44,6 +44,7 @@ import { GraphElement } from "./GraphElement"; import type { NodeDirective } from "./NodeDirective"; import type { QueryOptionsDirective } from "./QueryOptionsDirective"; import type { SchemaConfiguration } from "../schema/schema-configuration"; +import { leadingUnderscores } from "../utils/leading-underscore"; export interface NodeConstructor extends GraphElementConstructor { name: string; @@ -320,20 +321,14 @@ class Node extends GraphElement { private generateSingular(): string { const singular = camelcase(this.name); - return `${this.leadingUnderscores(this.name)}${singular}`; + return `${leadingUnderscores(this.name)}${singular}`; } private generatePlural(inputPlural: string | undefined): string { const name = inputPlural || this.plural || this.name; const plural = inputPlural || this.plural ? camelcase(name) : pluralize(camelcase(name)); - return `${this.leadingUnderscores(name)}${plural}`; - } - - private leadingUnderscores(name: string): string { - const re = /^(_+).+/; - const match = re.exec(name); - return match?.[1] || ""; + return `${leadingUnderscores(name)}${plural}`; } } diff --git a/packages/graphql/src/schema-model/utils/string-manipulation.ts b/packages/graphql/src/schema-model/utils/string-manipulation.ts index b46a47a807..557855db2f 100644 --- a/packages/graphql/src/schema-model/utils/string-manipulation.ts +++ b/packages/graphql/src/schema-model/utils/string-manipulation.ts @@ -19,6 +19,7 @@ import camelcase from "camelcase"; import pluralize from "pluralize"; +import { leadingUnderscores } from "../../utils/leading-underscore"; export function singular(name: string): string { const singular = camelcase(name); @@ -30,9 +31,3 @@ export function plural(name: string): string { const plural = pluralize(camelcase(name)); return `${leadingUnderscores(name)}${plural}`; } - -export function leadingUnderscores(name: string): string { - const re = /^(_+).+/; - const match = re.exec(name); - return match?.[1] || ""; -} diff --git a/packages/graphql/src/utils/leading-underscore.test.ts b/packages/graphql/src/utils/leading-underscore.test.ts new file mode 100644 index 0000000000..be30a04f59 --- /dev/null +++ b/packages/graphql/src/utils/leading-underscore.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { leadingUnderscores } from "./leading-underscore"; + +describe("leadingUnderscores", () => { + test("should return empty string if no leading underscores", () => { + expect(leadingUnderscores("test")).toBe(""); + }); + + test("should return single underscore if single leading underscore", () => { + expect(leadingUnderscores("_test")).toBe("_"); + }); + + test("should return multiple underscores if multiple leading underscores", () => { + expect(leadingUnderscores("___test")).toBe("___"); + }); +}); diff --git a/packages/graphql/src/utils/leading-underscore.ts b/packages/graphql/src/utils/leading-underscore.ts new file mode 100644 index 0000000000..413e7f19a6 --- /dev/null +++ b/packages/graphql/src/utils/leading-underscore.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// leadingUnderscores function returns the leading underscores from the beginning of a given string name. If there are no leading underscores, it returns an empty string. +export function leadingUnderscores(name: string): string { + const re = /^(_+).+/; + const match = re.exec(name); + return match?.[1] || ""; +} diff --git a/packages/graphql/tests/utils/graphql-types.ts b/packages/graphql/tests/utils/graphql-types.ts index c3c48ce211..c45a4cedda 100644 --- a/packages/graphql/tests/utils/graphql-types.ts +++ b/packages/graphql/tests/utils/graphql-types.ts @@ -21,6 +21,7 @@ import { generate } from "randomstring"; import pluralize from "pluralize"; import camelcase from "camelcase"; import { upperFirst } from "../../src/utils/upper-first"; +import { leadingUnderscores } from "../../src/utils/leading-underscore"; type UniqueTypeOperations = { create: string; @@ -62,7 +63,7 @@ export class UniqueType { public get singular(): string { const singular = camelcase(this.name); - return `${this.leadingUnderscores(this.name)}${singular}`; + return `${leadingUnderscores(this.name)}${singular}`; } public get operations(): UniqueTypeOperations { @@ -97,11 +98,6 @@ export class UniqueType { return this.name; } - private leadingUnderscores(name: string): string { - const re = /^(_+).+/; - const match = re.exec(name); - return match?.[1] || ""; - } } /** Generates unique type From 37da8bf9b3a23ee1be894dd36bd2060e7946e4cc Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Fri, 28 Jul 2023 23:40:30 +0100 Subject: [PATCH 15/22] parseArguments of directive using default values, add Schema Model tests for annotations and relationships --- .../arguments/enums/PopulatedByOperation.ts | 4 +- .../src/schema-model/annotation/Annotation.ts | 29 ++-- .../annotation/CustomResolverAnnotation.ts | 4 +- .../annotation/FilterableAnnotation.ts | 6 +- .../annotation/FullTextAnnotation.ts | 8 +- .../schema-model/annotation/NodeAnnotation.ts | 4 +- .../ConcreteEntityAdapter.test.ts | 42 +++--- .../model-adapters/ConcreteEntityAdapter.ts | 3 +- .../src/schema-model/generate-model.test.ts | 138 +++++++++++++++++- .../src/schema-model/generate-model.ts | 76 ++++------ .../annotations-parser/alias-annotation.ts | 5 +- .../authentication-annotation.ts | 5 +- .../authorization-annotation.ts | 4 +- .../coalesce-annotation.test.ts | 18 ++- .../annotations-parser/coalesce-annotation.ts | 27 +--- .../custom-resolver-annotation.test.ts | 5 +- .../custom-resolver-annotation.ts | 10 +- .../annotations-parser/cypher-annotation.ts | 5 +- .../default-annotation.test.ts | 17 ++- .../annotations-parser/default-annotation.ts | 27 +--- .../filterable-annotation.test.ts | 13 +- .../filterable-annotation.ts | 10 +- .../full-text-annotation.test.ts | 26 ++-- .../full-text-annotation.ts | 9 +- .../annotations-parser/id-annotation.test.ts | 8 +- .../annotations-parser/id-annotation.ts | 5 +- .../jwt-claim-annotation.test.ts | 3 +- .../jwt-claim-annotation.ts | 5 +- ...noatation.ts => jwt-payload-annotation.ts} | 0 .../annotations-parser/key-annotation.ts | 4 +- .../mutation-annotation.test.ts | 12 +- .../annotations-parser/mutation-annotation.ts | 5 +- .../node-annotation.test.ts | 5 +- .../annotations-parser/node-annotation.ts | 6 +- .../annotations-parser/parse-directives.ts | 109 ++++++++++++++ .../plural-annotation.test.ts | 3 +- .../annotations-parser/plural-annotation.ts | 5 +- .../populated-by-annotation.test.ts | 7 +- .../populated-by-annotation.ts | 8 +- .../query-annotation.test.ts | 5 +- .../annotations-parser/query-annotation.ts | 5 +- .../query-options-annotation.test.ts | 7 +- .../query-options-annotation.ts | 21 +-- .../selectable-annotation.test.ts | 5 +- .../selectable-annotation.ts | 8 +- .../settable-annotation.test.ts | 5 +- .../annotations-parser/settable-annotation.ts | 8 +- .../subscription-annotation.test.ts | 7 +- .../subscription-annotation.ts | 5 +- .../subscriptions-authorization-annotation.ts | 5 +- .../timestamp-annotation.test.ts | 7 +- .../timestamp-annotation.ts | 5 +- .../unique-annotation.test.ts | 3 +- .../annotations-parser/unique-annotation.ts | 5 +- .../parser/parse-arguments.test.ts | 138 ++++++++++++++++++ .../parser/parse-arguments.ts} | 17 ++- .../graphql/src/schema-model/parser/utils.ts | 10 +- .../schema-model/relationship/Relationship.ts | 19 ++- .../RelationshipAdapter.test.ts | 110 ++++++++++++++ .../model-adapters/RelationshipAdapter.ts | 51 ++++++- .../graphql/src/schema/get-obj-field-meta.ts | 8 +- .../src/schema/get-relationship-meta.ts | 5 +- .../src/schema/parse-mutation-directive.ts | 7 +- .../src/schema/parse-query-directive.ts | 4 +- .../schema/parse-subscription-directive.ts | 4 +- 65 files changed, 847 insertions(+), 307 deletions(-) rename packages/graphql/src/schema-model/parser/annotations-parser/{jwt-payload-annoatation.ts => jwt-payload-annotation.ts} (100%) create mode 100644 packages/graphql/src/schema-model/parser/annotations-parser/parse-directives.ts create mode 100644 packages/graphql/src/schema-model/parser/parse-arguments.test.ts rename packages/graphql/src/{utils/get-argument-values.ts => schema-model/parser/parse-arguments.ts} (88%) create mode 100644 packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.test.ts diff --git a/packages/graphql/src/graphql/directives/arguments/enums/PopulatedByOperation.ts b/packages/graphql/src/graphql/directives/arguments/enums/PopulatedByOperation.ts index 354426b554..d7f85dd8d2 100644 --- a/packages/graphql/src/graphql/directives/arguments/enums/PopulatedByOperation.ts +++ b/packages/graphql/src/graphql/directives/arguments/enums/PopulatedByOperation.ts @@ -23,7 +23,7 @@ export const PopulatedByOperationEnum = new GraphQLEnumType({ name: "PopulatedByOperation", description: "*For use in the @populatedBy directive only*", values: { - CREATE: {}, - UPDATE: {}, + CREATE: { value: "CREATE" }, + UPDATE: { value: "UPDATE"}, }, }); diff --git a/packages/graphql/src/schema-model/annotation/Annotation.ts b/packages/graphql/src/schema-model/annotation/Annotation.ts index 2973044af1..5cf422abe3 100644 --- a/packages/graphql/src/schema-model/annotation/Annotation.ts +++ b/packages/graphql/src/schema-model/annotation/Annotation.ts @@ -72,33 +72,34 @@ export type Annotation = | JWTClaimAnnotation | JWTPayloadAnnotation; + export enum AnnotationsKey { - cypher = "cypher", - authorization = "authorization", - authentication = "authentication", - key = "key", - subscriptionsAuthorization = "subscriptionsAuthorization", alias = "alias", - queryOptions = "queryOptions", - default = "default", + authentication = "authentication", + authorization = "authorization", coalesce = "coalesce", customResolver = "customResolver", - id = "id", - mutation = "mutation", - plural = "plural", + cypher = "cypher", + default = "default", filterable = "filterable", fulltext = "fulltext", + id = "id", + jwtClaim = "jwtClaim", + jwtPayload = "jwtPayload", + key = "key", + mutation = "mutation", node = "node", + plural = "plural", populatedBy = "populatedBy", - query = "query", private = "private", + query = "query", + queryOptions = "queryOptions", selectable = "selectable", settable = "settable", + subscription = "subscription", + subscriptionsAuthorization = "subscriptionsAuthorization", timestamp = "timestamp", unique = "unique", - subscription = "subscription", - jwtClaim = "jwtClaim", - jwtPayload = "jwtPayload", } export type Annotations = { diff --git a/packages/graphql/src/schema-model/annotation/CustomResolverAnnotation.ts b/packages/graphql/src/schema-model/annotation/CustomResolverAnnotation.ts index 4f5a71b2cf..2b530469a5 100644 --- a/packages/graphql/src/schema-model/annotation/CustomResolverAnnotation.ts +++ b/packages/graphql/src/schema-model/annotation/CustomResolverAnnotation.ts @@ -18,9 +18,9 @@ */ export class CustomResolverAnnotation { - public readonly requires: string[]; + public readonly requires: string; - constructor({ requires }: { requires: string[] }) { + constructor({ requires }: { requires: string }) { this.requires = requires; } } diff --git a/packages/graphql/src/schema-model/annotation/FilterableAnnotation.ts b/packages/graphql/src/schema-model/annotation/FilterableAnnotation.ts index a5617b0148..42e211dc50 100644 --- a/packages/graphql/src/schema-model/annotation/FilterableAnnotation.ts +++ b/packages/graphql/src/schema-model/annotation/FilterableAnnotation.ts @@ -19,10 +19,10 @@ export class FilterableAnnotation { public readonly byValue: boolean; - public readonly byAnnotation: boolean; + public readonly byAggregate: boolean; - constructor({ byValue, byAnnotation }: { byValue: boolean; byAnnotation: boolean }) { + constructor({ byValue, byAggregate }: { byValue: boolean; byAggregate: boolean }) { this.byValue = byValue; - this.byAnnotation = byAnnotation; + this.byAggregate = byAggregate; } } diff --git a/packages/graphql/src/schema-model/annotation/FullTextAnnotation.ts b/packages/graphql/src/schema-model/annotation/FullTextAnnotation.ts index 02e92b7ea6..3ce9f8b8c4 100644 --- a/packages/graphql/src/schema-model/annotation/FullTextAnnotation.ts +++ b/packages/graphql/src/schema-model/annotation/FullTextAnnotation.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -export type FullTextFields = { +export type FullTextField = { name: string; fields: string[]; queryName: string; @@ -25,9 +25,9 @@ export type FullTextFields = { }; export class FullTextAnnotation { - public readonly fields: FullTextFields; + public readonly indexes: FullTextField[]; - constructor({ fields }: { fields: FullTextFields }) { - this.fields = fields; + constructor({ indexes }: { indexes: FullTextField[] }) { + this.indexes = indexes; } } diff --git a/packages/graphql/src/schema-model/annotation/NodeAnnotation.ts b/packages/graphql/src/schema-model/annotation/NodeAnnotation.ts index 37e9888a53..f4268bfbfa 100644 --- a/packages/graphql/src/schema-model/annotation/NodeAnnotation.ts +++ b/packages/graphql/src/schema-model/annotation/NodeAnnotation.ts @@ -19,10 +19,8 @@ export class NodeAnnotation { public readonly labels: string[]; - public readonly label: string; - constructor({ labels, label }: { labels: string[]; label: string }) { + constructor({ labels }: { labels: string[] }) { this.labels = labels; - this.label = label; } } diff --git a/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.test.ts b/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.test.ts index f6d8bb2fd5..e585e4a9fc 100644 --- a/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.test.ts +++ b/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.test.ts @@ -26,7 +26,7 @@ import { CypherAnnotation } from "../../annotation/CypherAnnotation"; import { UniqueAnnotation } from "../../annotation/UniqueAnnotation"; describe("ConcreteEntityAdapter", () => { - let userModel: ConcreteEntityAdapter; + let userAdapter: ConcreteEntityAdapter; let userId: AttributeAdapter; let userName: AttributeAdapter; let closestUser: AttributeAdapter; @@ -61,19 +61,19 @@ describe("ConcreteEntityAdapter", () => { attributes: [idAttribute, nameAttribute, closestUserAttribute], }); - userModel = new ConcreteEntityAdapter(userEntity); - userId = userModel.attributes.get("id") as AttributeAdapter; - userName = userModel.attributes.get("name") as AttributeAdapter; - closestUser = userModel.attributes.get("closestUser") as AttributeAdapter; + userAdapter = new ConcreteEntityAdapter(userEntity); + userId = userAdapter.attributes.get("id") as AttributeAdapter; + userName = userAdapter.attributes.get("name") as AttributeAdapter; + closestUser = userAdapter.attributes.get("closestUser") as AttributeAdapter; }); test("should generate a valid ConcreteEntityAdapter model", () => { - expect(userModel).toBeDefined(); - expect(userModel).toBeInstanceOf(ConcreteEntityAdapter); - expect(userModel.name).toBe("User"); - expect(userModel.labels).toEqual(new Set(["User"])); - expect(userModel.attributes.size).toBe(3); - expect(userModel.relationships.size).toBe(0); + expect(userAdapter).toBeDefined(); + expect(userAdapter).toBeInstanceOf(ConcreteEntityAdapter); + expect(userAdapter.name).toBe("User"); + expect(userAdapter.labels).toEqual(new Set(["User"])); + expect(userAdapter.attributes.size).toBe(3); + expect(userAdapter.relationships.size).toBe(0); expect(userId).toBeDefined(); expect(userId).toBeInstanceOf(AttributeAdapter); expect(userName).toBeDefined(); @@ -83,35 +83,35 @@ describe("ConcreteEntityAdapter", () => { }); test("should return the correct mutable fields, (Cypher fields are removed)", () => { - expect(userModel.mutableFields).toHaveLength(2); - expect(userModel.mutableFields).toEqual([userId, userName]); + expect(userAdapter.mutableFields).toHaveLength(2); + expect(userAdapter.mutableFields).toEqual([userId, userName]); }); test("should return the correct labels", () => { - expect(userModel.getAllLabels()).toStrictEqual(["User"]); - expect(userModel.getMainLabel()).toBe("User"); + expect(userAdapter.getAllLabels()).toStrictEqual(["User"]); + expect(userAdapter.getMainLabel()).toBe("User"); }); test("should return the correct unique fields", () => { - expect(userModel.uniqueFields).toHaveLength(1); - expect(userModel.uniqueFields).toStrictEqual([userId]); + expect(userAdapter.uniqueFields).toHaveLength(1); + expect(userAdapter.uniqueFields).toStrictEqual([userId]); }); test("should return the correct singular name", () => { - expect(userModel.singular).toBe("user"); + expect(userAdapter.singular).toBe("user"); }); test("should return the correct plural name", () => { - expect(userModel.plural).toBe("users"); + expect(userAdapter.plural).toBe("users"); }); describe("ConcreteEntityOperations", () => { test("should construct a valid ConcreteEntityOperations", () => { - expect(userModel.operations).toBeDefined(); + expect(userAdapter.operations).toBeDefined(); }); test("should return the correct rootTypeFieldNames", () => { - expect(userModel.operations.rootTypeFieldNames).toStrictEqual({ + expect(userAdapter.operations.rootTypeFieldNames).toStrictEqual({ aggregate: "usersAggregate", create: "createUsers", delete: "deleteUsers", diff --git a/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.ts b/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.ts index 2d1dedcd4c..4527f1ff83 100644 --- a/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.ts +++ b/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.ts @@ -75,10 +75,9 @@ export class ConcreteEntityAdapter { private initRelationships(relationships: Map) { for (const [relationshipName, relationship] of relationships.entries()) { - const { name, type, direction, target, attributes } = relationship; this.relationships.set( relationshipName, - new RelationshipAdapter({ name, type, direction, source: this, target, attributes }) + new RelationshipAdapter(relationship, this) ); } } diff --git a/packages/graphql/src/schema-model/generate-model.test.ts b/packages/graphql/src/schema-model/generate-model.test.ts index 6f8756fa8e..5f8bebb8ac 100644 --- a/packages/graphql/src/schema-model/generate-model.test.ts +++ b/packages/graphql/src/schema-model/generate-model.test.ts @@ -31,6 +31,8 @@ import { AuthenticationAnnotation } from "./annotation/AuthenticationAnnotation" import type { AttributeAdapter } from "./attribute/model-adapters/AttributeAdapter"; import type { ConcreteEntityAdapter } from "./entity/model-adapters/ConcreteEntityAdapter"; import type { RelationshipAdapter } from "./relationship/model-adapters/RelationshipAdapter"; +import type { ConcreteEntity } from "./entity/ConcreteEntity"; +import type { Relationship } from "./relationship/Relationship"; describe("Schema model generation", () => { test("parses @authentication directive with no arguments", () => { @@ -263,7 +265,7 @@ describe("ComposeEntity generation", () => { ) { id: ID! name: String! - preferiteTool: Tool + favoriteTool: Tool } extend type User { @@ -289,9 +291,141 @@ describe("ComposeEntity generation", () => { const humanEntities = schemaModel.compositeEntities.find((e) => e.name === "Human"); expect(humanEntities?.concreteEntities).toHaveLength(1); // User }); + + test("concrete entity has correct attributes", () => { + const userEntity = schemaModel.concreteEntities.find((e) => e.name === "User"); + expect(userEntity?.attributes.has("id")).toBeTrue(); + expect(userEntity?.attributes.has("name")).toBeTrue(); + expect(userEntity?.attributes.has("password")).toBeTrue(); + expect(userEntity?.attributes.has("favoriteTool")).toBeTrue(); + }); +}); + +describe("Relationship", () => { + let schemaModel: Neo4jGraphQLSchemaModel; + + beforeAll(() => { + const typeDefs = gql` + type User { + id: ID! + name: String! + accounts: [Account!]! @relationship(type: "HAS_ACCOUNT", properties: "hasAccount", direction: OUT) + favoriteShow: [Show!]! @relationship(type: "FAVORITE_SHOW", direction: OUT) + } + + interface hasAccount @relationshipProperties { + creationTime: DateTime! + } + + union Show = Movie | TvShow + + type Movie { + name: String! + } + + type TvShow { + name: String! + episodes: Int + } + + type Account { + id: ID! + username: String! + } + + extend type User { + password: String! @authorization(filter: [{ where: { node: { id: { equals: "$jwt.sub" } } } }]) + } + `; + + const document = mergeTypeDefs(typeDefs); + schemaModel = generateModel(document); + }); + + test("concrete entity has correct relationship", () => { + const userEntity = schemaModel.concreteEntities.find((e) => e.name === "User"); + const accounts = userEntity?.relationships.get("accounts"); + expect(accounts).toBeDefined(); + expect(accounts?.type).toBe("HAS_ACCOUNT"); + expect(accounts?.direction).toBe("OUT"); + expect(accounts?.queryDirection).toBe("DEFAULT_DIRECTED"); + expect(accounts?.nestedOperations).toEqual([ + "CREATE", + "UPDATE", + "DELETE", + "CONNECT", + "DISCONNECT", + "CONNECT_OR_CREATE", + ]); + expect(accounts?.target.name).toBe("Account"); + expect(accounts?.attributes.has("creationTime")).toBeTrue(); + }); +}); + +describe.skip("Annotations", () => { + let schemaModel: Neo4jGraphQLSchemaModel; + let userEntity: ConcreteEntity; + let accountEntity: ConcreteEntity; + + beforeAll(() => { + const typeDefs = gql` + type User @query @mutation @subscription { + id: ID! + name: String! @selectable(onAggregate: true) + accounts: [Account!]! @relationship(type: "HAS_ACCOUNT", direction: OUT) + } + + type Account @subscription(operations: [CREATE]){ + id: ID! + username: String! @settable(onCreate: false) + } + + extend type User { + password: String! @authorization(filter: [{ where: { node: { id: { equals: "$jwt.sub" } } } }]) + } + `; + + const document = mergeTypeDefs(typeDefs); + schemaModel = generateModel(document); + userEntity = schemaModel.concreteEntities.find((e) => e.name === "User") as ConcreteEntity; + accountEntity = schemaModel.concreteEntities.find((e) => e.name === "Account") as ConcreteEntity; + }); + + test("concrete entities should be generated with the correct annotations", () => { + const userQuery = userEntity?.annotations[AnnotationsKey.query]; + expect(userQuery).toBeDefined(); + expect(userQuery?.read).toBe(true); + expect(userQuery?.aggregate).toBe(false); + + const userMutation = userEntity?.annotations[AnnotationsKey.mutation]; + expect(userMutation).toBeDefined(); + expect(userMutation?.operations).toStrictEqual(["CREATE", "UPDATE", "DELETE"]); + + const userSubscription = userEntity?.annotations[AnnotationsKey.mutation]; + expect(userSubscription).toBeDefined(); + expect(userSubscription?.operations).toStrictEqual(["CREATE", "UPDATE", "DELETE", "CREATE_RELATIONSHIP", "DELETE_RELATIONSHIP"]); + + const accountSubscription = accountEntity?.annotations[AnnotationsKey.subscription]; + expect(accountSubscription).toBeDefined(); + expect(accountSubscription?.operations).toStrictEqual(["CREATE"]); + }); + + test("attributes should be generated with the correct annotations", () => { + const userName = userEntity?.attributes.get("name"); + expect(userName?.annotations[AnnotationsKey.selectable]).toBeDefined(); + expect(userName?.annotations[AnnotationsKey.selectable]?.onRead).toBe(true); + expect(userName?.annotations[AnnotationsKey.selectable]?.onAggregate).toBe(true); + + const creationTime = accountEntity?.attributes.get("creationTime"); + expect(creationTime?.annotations[AnnotationsKey.settable]).toBeDefined(); + expect(creationTime?.annotations[AnnotationsKey.settable]?.onCreate).toBe(false); + expect(creationTime?.annotations[AnnotationsKey.settable]?.onUpdate).toBe(true); + }); + + }); -describe("GraphQL models", () => { +describe("GraphQL adapters", () => { let schemaModel: Neo4jGraphQLSchemaModel; // entities let userEntity: ConcreteEntityAdapter; diff --git a/packages/graphql/src/schema-model/generate-model.ts b/packages/graphql/src/schema-model/generate-model.ts index 020e613ffa..849f22a59f 100644 --- a/packages/graphql/src/schema-model/generate-model.ts +++ b/packages/graphql/src/schema-model/generate-model.ts @@ -22,21 +22,22 @@ import getFieldTypeMeta from "../schema/get-field-type-meta"; import { filterTruthy } from "../utils/utils"; import { Neo4jGraphQLSchemaModel } from "./Neo4jGraphQLSchemaModel"; import type { Operations } from "./Neo4jGraphQLSchemaModel"; -import type { Annotation } from "./annotation/Annotation"; +import { annotationToKey, type Annotation, AnnotationsKey } from "./annotation/Annotation"; import type { Attribute } from "./attribute/Attribute"; import { CompositeEntity } from "./entity/CompositeEntity"; import { ConcreteEntity } from "./entity/ConcreteEntity"; -import { parseAuthorizationAnnotation } from "./parser/annotations-parser/authorization-annotation"; -import { parseKeyAnnotation } from "./parser/annotations-parser/key-annotation"; -import { parseArguments, findDirective } from "./parser/utils"; -import type { RelationshipDirection } from "./relationship/Relationship"; +import { findDirective } from "./parser/utils"; +import { parseArguments } from "./parser/parse-arguments"; +import type { NestedOperation, QueryDirection, RelationshipDirection } from "./relationship/Relationship"; import { Relationship } from "./relationship/Relationship"; import type { DefinitionCollection } from "./parser/definition-collection"; import { getDefinitionCollection } from "./parser/definition-collection"; -import { parseAuthenticationAnnotation } from "./parser/annotations-parser/authentication-annotation"; import { Operation } from "./Operation"; -import { parseSubscriptionsAuthorizationAnnotation } from "./parser/annotations-parser/subscriptions-authorization-annotation"; import { parseAttribute, parseField } from "./parser/parse-attribute"; +import { relationshipDirective } from "../graphql/directives"; +import { parseKeyAnnotation } from "./parser/annotations-parser/key-annotation"; +import { parseDirectives } from "./parser/annotations-parser/parse-directives"; +import type { NodeAnnotation } from "./annotation/NodeAnnotation"; export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { const definitionCollection: DefinitionCollection = getDefinitionCollection(document); @@ -163,15 +164,17 @@ function generateRelationshipField( definitionCollection: DefinitionCollection ): Relationship | undefined { const fieldTypeMeta = getFieldTypeMeta(field.type); - const relationshipDirective = findDirective(field.directives || [], "relationship"); - if (!relationshipDirective) return undefined; + const relationshipUsage = findDirective(field.directives || [], "relationship"); + if (!relationshipUsage) return undefined; const fieldName = field.name.value; const relatedEntityName = fieldTypeMeta.name; const relatedToEntity = schema.getEntity(relatedEntityName); if (!relatedToEntity) throw new Error(`Entity ${relatedEntityName} Not Found`); - - const { type, direction, properties } = parseArguments(relationshipDirective); + const { type, direction, properties, queryDirection, nestedOperations, aggregate } = parseArguments( + relationshipDirective, + relationshipUsage + ); let attributes: Attribute[] = []; if (properties && typeof properties === "string") { @@ -193,6 +196,9 @@ function generateRelationshipField( source, target: relatedToEntity, direction: direction as RelationshipDirection, + queryDirection: queryDirection as QueryDirection, + nestedOperations: nestedOperations as NestedOperation[], + aggregate: aggregate as boolean, }); } @@ -204,28 +210,21 @@ function generateConcreteEntity( parseAttribute(fieldDefinition, definitionCollection) ); - const directives = (definition.directives || []).reduce((acc, directive) => { - acc.set(directive.name.value, parseArguments(directive)); - return acc; - }, new Map>()); - const labels = getLabels(definition, directives.get("node") || {}); - // TODO: add annotations inherited from interface + const annotations = createEntityAnnotations(definition.directives || []); + const nodeAnnotation = annotations.find((annotation: Annotation) => { + AnnotationsKey.node === annotationToKey(annotation); + }); + const labels = nodeAnnotation ? (nodeAnnotation as NodeAnnotation).labels : [definition.name.value]; + // TODO: add annotations inherited from interface return new ConcreteEntity({ name: definition.name.value, labels, attributes: filterTruthy(fields) as Attribute[], - annotations: createEntityAnnotations(definition.directives || []), + annotations, }); } -function getLabels(definition: ObjectTypeDefinitionNode, nodeDirectiveArguments: Record): string[] { - if ((nodeDirectiveArguments.labels as string[] | undefined)?.length) { - return nodeDirectiveArguments.labels as string[]; - } - return [definition.name.value]; -} - function createEntityAnnotations(directives: readonly DirectiveNode[]): Annotation[] { const entityAnnotations: Annotation[] = []; @@ -234,21 +233,7 @@ function createEntityAnnotations(directives: readonly DirectiveNode[]): Annotati if (keyDirectives) { entityAnnotations.push(parseKeyAnnotation(keyDirectives)); } - - const annotations: Annotation[] = filterTruthy( - directives.map((directive) => { - switch (directive.name.value) { - case "authorization": - return parseAuthorizationAnnotation(directive); - case "authentication": - return parseAuthenticationAnnotation(directive); - case "subscriptionsAuthorization": - return parseSubscriptionsAuthorizationAnnotation(directive); - default: - return undefined; - } - }) - ); + const annotations = parseDirectives(directives); return entityAnnotations.concat(annotations); } @@ -256,16 +241,7 @@ function createEntityAnnotations(directives: readonly DirectiveNode[]): Annotati function createSchemaModelAnnotations(directives: readonly DirectiveNode[]): Annotation[] { const schemaModelAnnotations: Annotation[] = []; - const annotations: Annotation[] = filterTruthy( - directives.map((directive) => { - switch (directive.name.value) { - case "authentication": - return parseAuthenticationAnnotation(directive); - default: - return undefined; - } - }) - ); + const annotations = parseDirectives(directives); return schemaModelAnnotations.concat(annotations); } diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/alias-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/alias-annotation.ts index 698cdd2a0b..c4940cd279 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/alias-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/alias-annotation.ts @@ -19,10 +19,11 @@ import type { DirectiveNode } from "graphql"; import { Neo4jGraphQLSchemaValidationError } from "../../../classes"; import { AliasAnnotation } from "../../annotation/AliasAnnotation"; -import { parseArguments } from "../utils"; +import { aliasDirective } from "../../../graphql/directives"; +import { parseArguments } from "../parse-arguments"; export function parseAliasAnnotation(directive: DirectiveNode): AliasAnnotation { - const { property, ...unrecognizedArguments } = parseArguments(directive) as { + const { property, ...unrecognizedArguments } = parseArguments(aliasDirective, directive) as { property: string; }; diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/authentication-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/authentication-annotation.ts index 0b957c7b84..a2671358c2 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/authentication-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/authentication-annotation.ts @@ -20,8 +20,9 @@ import type { DirectiveNode } from "graphql"; import type { GraphQLWhereArg } from "../../../types"; import type { AuthenticationOperation } from "../../annotation/AuthenticationAnnotation"; import { AuthenticationAnnotation } from "../../annotation/AuthenticationAnnotation"; +import { parseArgumentsFromUnknownDirective } from "../parse-arguments"; + -import { parseArguments } from "../utils"; const authenticationDefaultOperations: AuthenticationOperation[] = [ "READ", "AGGREGATE", @@ -33,7 +34,7 @@ const authenticationDefaultOperations: AuthenticationOperation[] = [ "SUBSCRIBE", ]; export function parseAuthenticationAnnotation(directive: DirectiveNode): AuthenticationAnnotation { - const args = parseArguments(directive) as { + const args = parseArgumentsFromUnknownDirective(directive) as { operations?: AuthenticationOperation[]; jwt?: GraphQLWhereArg; }; diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/authorization-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/authorization-annotation.ts index 0044cbd13f..6d493fa1a6 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/authorization-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/authorization-annotation.ts @@ -28,10 +28,10 @@ import { AuthorizationFilterRule, AuthorizationValidateRule, } from "../../annotation/AuthorizationAnnotation"; -import { parseArguments } from "../utils"; +import { parseArgumentsFromUnknownDirective } from "../parse-arguments"; export function parseAuthorizationAnnotation(directive: DirectiveNode): AuthorizationAnnotation { - const { filter, validate, ...unrecognizedArguments } = parseArguments(directive) as { + const { filter, validate, ...unrecognizedArguments } = parseArgumentsFromUnknownDirective(directive) as { filter?: Record[]; validate?: Record[]; }; diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/coalesce-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/coalesce-annotation.test.ts index 0ca137158a..ea4ca379ba 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/coalesce-annotation.test.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/coalesce-annotation.test.ts @@ -21,25 +21,26 @@ import { makeDirectiveNode } from "@graphql-tools/utils"; import type { DirectiveNode } from "graphql"; import { Kind } from "graphql"; import { parseCoalesceAnnotation } from "./coalesce-annotation"; +import { coalesceDirective } from "../../../graphql/directives"; describe("parseCoalesceAnnotation", () => { it("should parse correctly with string coalesce value", () => { - const directive = makeDirectiveNode("coalesce", { value: "myCoalesceValue" }); + const directive = makeDirectiveNode("coalesce", { value: "myCoalesceValue" }, coalesceDirective); const coalesceAnnotation = parseCoalesceAnnotation(directive); expect(coalesceAnnotation.value).toBe("myCoalesceValue"); }); it("should parse correctly with int coalesce value", () => { - const directive = makeDirectiveNode("coalesce", { value: 25 }); + const directive = makeDirectiveNode("coalesce", { value: 25 }, coalesceDirective); const coalesceAnnotation = parseCoalesceAnnotation(directive); expect(coalesceAnnotation.value).toBe(25); }); it("should parse correctly with float coalesce value", () => { - const directive = makeDirectiveNode("coalesce", { value: 25.5 }); + const directive = makeDirectiveNode("coalesce", { value: 25.5 }, coalesceDirective); const coalesceAnnotation = parseCoalesceAnnotation(directive); expect(coalesceAnnotation.value).toBe(25.5); }); it("should parse correctly with boolean coalesce value", () => { - const directive = makeDirectiveNode("coalesce", { value: true }); + const directive = makeDirectiveNode("coalesce", { value: true }, coalesceDirective); const coalesceAnnotation = parseCoalesceAnnotation(directive); expect(coalesceAnnotation.value).toBe(true); }); @@ -58,15 +59,16 @@ describe("parseCoalesceAnnotation", () => { value: "value", }, value: { - kind: Kind.ENUM, - value: "myEnumValue", + kind: Kind.STRING, + value: "myStringValue", }, }, ], }; const coalesceAnnotation = parseCoalesceAnnotation(directive); - expect(coalesceAnnotation.value).toBe("myEnumValue"); + expect(coalesceAnnotation.value).toBe("myStringValue"); }); + it("should throw error if no value is provided", () => { const directive: DirectiveNode = { kind: Kind.DIRECTIVE, @@ -76,6 +78,6 @@ describe("parseCoalesceAnnotation", () => { }, arguments: [], }; - expect(() => parseCoalesceAnnotation(directive)).toThrow("@coalesce directive must have a value"); + expect(() => parseCoalesceAnnotation(directive)).toThrow("Argument \"value\" of required type \"ScalarOrEnum!\" was not provided."); }); }); diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/coalesce-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/coalesce-annotation.ts index 9ad4c2f19d..8c3891f806 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/coalesce-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/coalesce-annotation.ts @@ -17,33 +17,20 @@ * limitations under the License. */ -import { Kind, type DirectiveNode } from "graphql"; -import { Neo4jGraphQLSchemaValidationError } from "../../../classes"; +import type { DirectiveNode } from "graphql"; import type { CoalesceAnnotationValue } from "../../annotation/CoalesceAnnotation"; import { CoalesceAnnotation } from "../../annotation/CoalesceAnnotation"; -import { parseValueNode } from "../parse-value-node"; +import { parseArguments } from "../parse-arguments"; +import { coalesceDirective } from "../../../graphql/directives"; export function parseCoalesceAnnotation(directive: DirectiveNode): CoalesceAnnotation { - if (!directive.arguments || !directive.arguments[0] || !directive.arguments[0].value.kind) { - throw new Error("@coalesce directive must have a value"); - } + const args = parseArguments(coalesceDirective, directive) as Record; - let value: CoalesceAnnotationValue; - switch (directive.arguments[0].value.kind) { - case Kind.ENUM: - case Kind.STRING: - case Kind.BOOLEAN: - case Kind.INT: - case Kind.FLOAT: - value = parseValueNode(directive.arguments[0].value) as CoalesceAnnotationValue; - break; - default: - throw new Neo4jGraphQLSchemaValidationError( - "@coalesce directive can only be used on types: Int | Float | String | Boolean | ID | DateTime | Enum" - ); + if (!args || args.value === undefined) { + throw new Error("@coalesce directive must have a value"); } return new CoalesceAnnotation({ - value, + value: args.value, }); } diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/custom-resolver-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/custom-resolver-annotation.test.ts index d0ebb8220b..dcdaa551d2 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/custom-resolver-annotation.test.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/custom-resolver-annotation.test.ts @@ -20,11 +20,12 @@ import { makeDirectiveNode } from "@graphql-tools/utils"; import type { DirectiveNode } from "graphql"; import { parseCustomResolverAnnotation } from "./custom-resolver-annotation"; +import { customResolverDirective } from "../../../graphql/directives"; describe("parseCustomResolverAnnotation", () => { it("should parse correctly", () => { - const directive: DirectiveNode = makeDirectiveNode("customResolver", { requires: ["firstName", "lastName"] }); + const directive: DirectiveNode = makeDirectiveNode("customResolver", { requires: "firstName lastName" }, customResolverDirective); const customResolverAnnotation = parseCustomResolverAnnotation(directive); - expect(customResolverAnnotation.requires).toEqual(["firstName", "lastName"]); + expect(customResolverAnnotation.requires).toBe("firstName lastName"); }); }); diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/custom-resolver-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/custom-resolver-annotation.ts index 6b1125bf2c..90aa681dcc 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/custom-resolver-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/custom-resolver-annotation.ts @@ -17,15 +17,13 @@ * limitations under the License. */ import type { DirectiveNode } from "graphql"; -import { Neo4jGraphQLSchemaValidationError } from "../../../classes"; import { CustomResolverAnnotation } from "../../annotation/CustomResolverAnnotation"; -import { parseArguments } from "../utils"; +import { parseArguments } from "../parse-arguments"; +import { customResolverDirective } from "../../../graphql/directives"; export function parseCustomResolverAnnotation(directive: DirectiveNode): CustomResolverAnnotation { - const { requires } = parseArguments(directive); - if (!Array.isArray(requires)) { - throw new Neo4jGraphQLSchemaValidationError("@customResolver requires must be an array"); - } + const { requires } = parseArguments(customResolverDirective, directive) as { requires: string }; + return new CustomResolverAnnotation({ requires, }); diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/cypher-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/cypher-annotation.ts index fc86d9543a..d9bf153b56 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/cypher-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/cypher-annotation.ts @@ -19,10 +19,11 @@ import type { DirectiveNode } from "graphql"; import { Neo4jGraphQLSchemaValidationError } from "../../../classes"; import { CypherAnnotation } from "../../annotation/CypherAnnotation"; -import { parseArguments } from "../utils"; +import { parseArguments } from "../parse-arguments"; +import { cypherDirective } from "../../../graphql/directives"; export function parseCypherAnnotation(directive: DirectiveNode): CypherAnnotation { - const { statement, columnName } = parseArguments(directive); + const { statement, columnName } = parseArguments(cypherDirective, directive); if (!statement || typeof statement !== "string") { throw new Neo4jGraphQLSchemaValidationError("@cypher statement required"); } diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/default-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/default-annotation.test.ts index b3fd66774a..aa82aa4e0b 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/default-annotation.test.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/default-annotation.test.ts @@ -21,25 +21,26 @@ import { makeDirectiveNode } from "@graphql-tools/utils"; import type { DirectiveNode } from "graphql"; import { Kind } from "graphql"; import { parseDefaultAnnotation } from "./default-annotation"; +import { defaultDirective } from "../../../graphql/directives"; describe("parseDefaultAnnotation", () => { it("should parse correctly with string default value", () => { - const directive = makeDirectiveNode("default", { value: "myDefaultValue" }); + const directive = makeDirectiveNode("default", { value: "myDefaultValue" }, defaultDirective); const defaultAnnotation = parseDefaultAnnotation(directive); expect(defaultAnnotation.value).toBe("myDefaultValue"); }); it("should parse correctly with int default value", () => { - const directive = makeDirectiveNode("default", { value: 25 }); + const directive = makeDirectiveNode("default", { value: 25 }, defaultDirective); const defaultAnnotation = parseDefaultAnnotation(directive); expect(defaultAnnotation.value).toBe(25); }); it("should parse correctly with float default value", () => { - const directive = makeDirectiveNode("default", { value: 25.5 }); + const directive = makeDirectiveNode("default", { value: 25.5 }, defaultDirective); const defaultAnnotation = parseDefaultAnnotation(directive); expect(defaultAnnotation.value).toBe(25.5); }); it("should parse correctly with boolean default value", () => { - const directive = makeDirectiveNode("default", { value: true }); + const directive = makeDirectiveNode("default", { value: true }, defaultDirective); const defaultAnnotation = parseDefaultAnnotation(directive); expect(defaultAnnotation.value).toBe(true); }); @@ -58,14 +59,14 @@ describe("parseDefaultAnnotation", () => { value: "value", }, value: { - kind: Kind.ENUM, - value: "myEnumValue", + kind: Kind.STRING, + value: "myStringValue", }, }, ], }; const defaultAnnotation = parseDefaultAnnotation(directive); - expect(defaultAnnotation.value).toBe("myEnumValue"); + expect(defaultAnnotation.value).toBe("myStringValue"); }); it("should throw error if no value is provided", () => { const directive: DirectiveNode = { @@ -76,6 +77,6 @@ describe("parseDefaultAnnotation", () => { }, arguments: [], }; - expect(() => parseDefaultAnnotation(directive)).toThrow("@default directive must have a value"); + expect(() => parseDefaultAnnotation(directive)).toThrow("Argument \"value\" of required type \"ScalarOrEnum!\" was not provided."); }); }); diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/default-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/default-annotation.ts index 7c1a7543c2..16c1f632f5 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/default-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/default-annotation.ts @@ -17,33 +17,20 @@ * limitations under the License. */ -import { Kind, type DirectiveNode } from "graphql"; -import { Neo4jGraphQLSchemaValidationError } from "../../../classes"; +import type { DirectiveNode } from "graphql"; import type { DefaultAnnotationValue } from "../../annotation/DefaultAnnotation"; import { DefaultAnnotation } from "../../annotation/DefaultAnnotation"; -import { parseValueNode } from "../parse-value-node"; +import { parseArguments } from "../parse-arguments"; +import { defaultDirective } from "../../../graphql/directives"; export function parseDefaultAnnotation(directive: DirectiveNode): DefaultAnnotation { - if (!directive.arguments || !directive.arguments[0] || !directive.arguments[0].value.kind) { - throw new Error("@default directive must have a value"); - } + const args = parseArguments(defaultDirective, directive) as Record; - let value: DefaultAnnotationValue; - switch (directive.arguments[0].value.kind) { - case Kind.ENUM: - case Kind.STRING: - case Kind.BOOLEAN: - case Kind.INT: - case Kind.FLOAT: - value = parseValueNode(directive.arguments[0].value) as DefaultAnnotationValue; - break; - default: - throw new Neo4jGraphQLSchemaValidationError( - "@default directive can only be used on types: Int | Float | String | Boolean | ID | DateTime | Enum" - ); + if (!args || args.value === undefined) { + throw new Error("@default directive must have a value"); } return new DefaultAnnotation({ - value, + value: args.value, }); } diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/filterable-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/filterable-annotation.test.ts index c875180f9c..3aeebc3d10 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/filterable-annotation.test.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/filterable-annotation.test.ts @@ -20,18 +20,19 @@ import { makeDirectiveNode } from "@graphql-tools/utils"; import type { DirectiveNode } from "graphql"; import { parseFilterableAnnotation } from "./filterable-annotation"; +import { filterableDirective } from "../../../graphql/directives"; describe("parseFilterableAnnotation", () => { - it("should parse correctly when byValue is set to true and byAnnotation is set to false", () => { - const directive: DirectiveNode = makeDirectiveNode("filterable", { byValue: true, byAnnotation: false }); + it("should parse correctly when byValue is set to true and byAggregate is set to false", () => { + const directive: DirectiveNode = makeDirectiveNode("filterable", { byValue: true, byAggregate: false }, filterableDirective); const filterableAnnotation = parseFilterableAnnotation(directive); expect(filterableAnnotation.byValue).toBe(true); - expect(filterableAnnotation.byAnnotation).toBe(false); + expect(filterableAnnotation.byAggregate).toBe(false); }); - it("should parse correctly when byValue is set to false and byAnnotation is set to true", () => { - const directive: DirectiveNode = makeDirectiveNode("filterable", { byValue: false, byAnnotation: true }); + it("should parse correctly when byValue is set to false and byAggregate is set to true", () => { + const directive: DirectiveNode = makeDirectiveNode("filterable", { byValue: false, byAggregate: true }, filterableDirective); const filterableAnnotation = parseFilterableAnnotation(directive); expect(filterableAnnotation.byValue).toBe(false); - expect(filterableAnnotation.byAnnotation).toBe(true); + expect(filterableAnnotation.byAggregate).toBe(true); }); }); diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/filterable-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/filterable-annotation.ts index 8b6ab4700d..f726ba0e5c 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/filterable-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/filterable-annotation.ts @@ -18,13 +18,17 @@ */ import type { DirectiveNode } from "graphql"; import { FilterableAnnotation } from "../../annotation/FilterableAnnotation"; -import { parseArguments } from "../utils"; +import { parseArguments } from "../parse-arguments"; +import { filterableDirective } from "../../../graphql/directives"; export function parseFilterableAnnotation(directive: DirectiveNode): FilterableAnnotation { - const { byValue, byAnnotation } = parseArguments(directive) as { byValue: boolean; byAnnotation: boolean }; + const { byValue, byAggregate } = parseArguments(filterableDirective, directive) as { + byValue: boolean; + byAggregate: boolean; + }; return new FilterableAnnotation({ - byAnnotation, + byAggregate, byValue, }); } diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/full-text-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/full-text-annotation.test.ts index 283f2a53e4..5ea7d1aac0 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/full-text-annotation.test.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/full-text-annotation.test.ts @@ -20,23 +20,23 @@ import { makeDirectiveNode } from "@graphql-tools/utils"; import type { DirectiveNode } from "graphql"; import { parseFullTextAnnotation } from "./full-text-annotation"; +import { fulltextDirective } from "../../../graphql/directives"; describe("parseFullTextAnnotation", () => { it("should parse correctly", () => { - const directive: DirectiveNode = makeDirectiveNode("fullText", { - fields: { - name: "myName", - fields: ["firstField", "secondField"], - queryName: "myQueryName", - indexName: "myIndexName", - }, - }); + const directive: DirectiveNode = makeDirectiveNode( + "fullText", + { indexes: [{ indexName: "ProductName", fields: ["name"] }] }, + fulltextDirective + ); const fullTextAnnotation = parseFullTextAnnotation(directive); - expect(fullTextAnnotation.fields).toEqual({ - name: "myName", - fields: ["firstField", "secondField"], - queryName: "myQueryName", - indexName: "myIndexName", + expect(fullTextAnnotation).toEqual({ + indexes: [ + { + fields: ["name"], + indexName: "ProductName", + }, + ], }); }); }); diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/full-text-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/full-text-annotation.ts index cf4384088d..f10b6a6fa4 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/full-text-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/full-text-annotation.ts @@ -17,14 +17,15 @@ * limitations under the License. */ import type { DirectiveNode } from "graphql"; -import type { FullTextFields } from "../../annotation/FullTextAnnotation"; +import type { FullTextField } from "../../annotation/FullTextAnnotation"; import { FullTextAnnotation } from "../../annotation/FullTextAnnotation"; -import { parseArguments } from "../utils"; +import { parseArguments } from "../parse-arguments"; +import { fulltextDirective } from "../../../graphql/directives"; export function parseFullTextAnnotation(directive: DirectiveNode): FullTextAnnotation { - const { fields } = parseArguments(directive) as { fields: FullTextFields }; + const { indexes } = parseArguments(fulltextDirective, directive) as { indexes: FullTextField[] }; return new FullTextAnnotation({ - fields, + indexes, }); } diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/id-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/id-annotation.test.ts index 7a38a3da52..64c0e90a9a 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/id-annotation.test.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/id-annotation.test.ts @@ -19,11 +19,11 @@ import { makeDirectiveNode } from "@graphql-tools/utils"; import { parseIDAnnotation } from "./id-annotation"; - +import { idDirective } from "../../../graphql/directives"; const tests = [ { name: "should parse correctly with all properties set to true", - directive: makeDirectiveNode("id", { autogenerate: true, unique: true, global: true }), + directive: makeDirectiveNode("id", { autogenerate: true, unique: true, global: true }, idDirective), expected: { autogenerate: true, unique: true, @@ -32,7 +32,7 @@ const tests = [ }, { name: "should parse correctly with all properties set to false", - directive: makeDirectiveNode("id", { autogenerate: false, unique: false, global: false }), + directive: makeDirectiveNode("id", { autogenerate: false, unique: false, global: false }, idDirective), expected: { autogenerate: false, unique: false, @@ -41,7 +41,7 @@ const tests = [ }, { name: "should parse correctly with autogenerate set to false, unique and global set to true", - directive: makeDirectiveNode("id", { autogenerate: false, unique: true, global: true }), + directive: makeDirectiveNode("id", { autogenerate: false, unique: true, global: true }, idDirective), expected: { autogenerate: false, unique: true, diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/id-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/id-annotation.ts index 397ece6ea0..1819c4aea8 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/id-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/id-annotation.ts @@ -19,10 +19,11 @@ import { type DirectiveNode } from "graphql"; import { IDAnnotation } from "../../annotation/IDAnnotation"; -import { parseArguments } from "../utils"; +import { parseArguments } from "../parse-arguments"; +import { idDirective } from "../../../graphql/directives"; export function parseIDAnnotation(directive: DirectiveNode): IDAnnotation { - const { autogenerate, unique, global } = parseArguments(directive) as { + const { autogenerate, unique, global } = parseArguments(idDirective, directive) as { autogenerate: boolean; unique: boolean; global: boolean; diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/jwt-claim-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/jwt-claim-annotation.test.ts index 6282bd6a2e..9f620a745b 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/jwt-claim-annotation.test.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/jwt-claim-annotation.test.ts @@ -20,10 +20,11 @@ import { makeDirectiveNode } from "@graphql-tools/utils"; import type { DirectiveNode } from "graphql"; import { parseJWTClaimAnnotation } from "./jwt-claim-annotation"; +import { jwtClaim } from "../../../graphql/directives"; describe("parseJWTClaimAnnotation", () => { test("should correctly parse jwtClaim path", () => { - const directive: DirectiveNode = makeDirectiveNode("jwtClaim", { path: "jwtClaimPath" }); + const directive: DirectiveNode = makeDirectiveNode("jwtClaim", { path: "jwtClaimPath" }, jwtClaim); const jwtClaimAnnotation = parseJWTClaimAnnotation(directive); expect(jwtClaimAnnotation.path).toBe("jwtClaimPath"); diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/jwt-claim-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/jwt-claim-annotation.ts index 9903f14b78..3f2b877fb0 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/jwt-claim-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/jwt-claim-annotation.ts @@ -18,10 +18,11 @@ */ import type { DirectiveNode } from "graphql"; import { JWTClaimAnnotation } from "../../annotation/JWTClaimAnnotation"; -import { parseArguments } from "../utils"; +import { parseArguments } from "../parse-arguments"; +import { jwtClaim } from "../../../graphql/directives"; export function parseJWTClaimAnnotation(directive: DirectiveNode): JWTClaimAnnotation { - const { path } = parseArguments(directive) as { path: string }; + const { path } = parseArguments(jwtClaim, directive) as { path: string }; return new JWTClaimAnnotation({ path, diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/jwt-payload-annoatation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/jwt-payload-annotation.ts similarity index 100% rename from packages/graphql/src/schema-model/parser/annotations-parser/jwt-payload-annoatation.ts rename to packages/graphql/src/schema-model/parser/annotations-parser/jwt-payload-annotation.ts diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/key-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/key-annotation.ts index bb1a8275b5..f5f9d08542 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/key-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/key-annotation.ts @@ -19,14 +19,14 @@ import type { DirectiveNode } from "graphql"; import { Neo4jGraphQLSchemaValidationError } from "../../../classes"; import { KeyAnnotation } from "../../annotation/KeyAnnotation"; -import { parseArguments } from "../utils"; +import { parseArgumentsFromUnknownDirective } from "../parse-arguments"; export function parseKeyAnnotation(directives: readonly DirectiveNode[]): KeyAnnotation { let isResolvable = false; directives.forEach((directive) => { // fields is a recognized argument but we don't use it, hence we ignore the non-usage of the variable. - const { fields, resolvable, ...unrecognizedArguments } = parseArguments(directive) as { + const { fields, resolvable, ...unrecognizedArguments } = parseArgumentsFromUnknownDirective(directive) as { fields: string; resolvable: boolean; }; diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/mutation-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/mutation-annotation.test.ts index 127532e218..f9023ae73c 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/mutation-annotation.test.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/mutation-annotation.test.ts @@ -18,37 +18,37 @@ */ import { makeDirectiveNode } from "@graphql-tools/utils"; -import { MutationOperations } from "../../../graphql/directives/mutation"; +import { MutationOperations, mutationDirective } from "../../../graphql/directives/mutation"; import { parseMutationAnnotation } from "./mutation-annotation"; const tests = [ { name: "should parse correctly with a CREATE operation set", - directive: makeDirectiveNode("mutation", { operations: [MutationOperations.CREATE] }), + directive: makeDirectiveNode("mutation", { operations: [MutationOperations.CREATE] }, mutationDirective), expected: { operations: [MutationOperations.CREATE] }, }, { name: "should parse correctly with an UPDATE operation set", - directive: makeDirectiveNode("mutation", { operations: [MutationOperations.UPDATE] }), + directive: makeDirectiveNode("mutation", { operations: [MutationOperations.UPDATE] }, mutationDirective), expected: { operations: [MutationOperations.UPDATE] }, }, { name: "should parse correctly with a DELETE operation set", - directive: makeDirectiveNode("mutation", { operations: [MutationOperations.DELETE] }), + directive: makeDirectiveNode("mutation", { operations: [MutationOperations.DELETE] }, mutationDirective), expected: { operations: [MutationOperations.DELETE] }, }, { name: "should parse correctly with a CREATE and UPDATE operation set", directive: makeDirectiveNode("mutation", { operations: [MutationOperations.CREATE, MutationOperations.UPDATE], - }), + }, mutationDirective), expected: { operations: [MutationOperations.CREATE, MutationOperations.UPDATE] }, }, { name: "should parse correctly with a CREATE, UPDATE and DELETE operation set", directive: makeDirectiveNode("mutation", { operations: [MutationOperations.CREATE, MutationOperations.UPDATE, MutationOperations.DELETE], - }), + }, mutationDirective), expected: { operations: [MutationOperations.CREATE, MutationOperations.UPDATE, MutationOperations.DELETE], }, diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/mutation-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/mutation-annotation.ts index 961b3f118f..c14e3648c5 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/mutation-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/mutation-annotation.ts @@ -19,10 +19,11 @@ import type { DirectiveNode } from "graphql"; import { Neo4jGraphQLSchemaValidationError } from "../../../classes"; import { MutationAnnotation } from "../../annotation/MutationAnnotation"; -import { parseArguments } from "../utils"; +import { mutationDirective } from "../../../graphql/directives"; +import { parseArguments } from "../parse-arguments"; export function parseMutationAnnotation(directive: DirectiveNode): MutationAnnotation { - const { operations } = parseArguments(directive); + const { operations } = parseArguments(mutationDirective, directive); if (!Array.isArray(operations)) { throw new Neo4jGraphQLSchemaValidationError("@mutation operations must be an array"); } diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/node-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/node-annotation.test.ts index 8d570aec50..28164dc919 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/node-annotation.test.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/node-annotation.test.ts @@ -20,15 +20,14 @@ import { makeDirectiveNode } from "@graphql-tools/utils"; import type { DirectiveNode } from "graphql"; import { parseNodeAnnotation } from "./node-annotation"; +import { nodeDirective } from "../../../graphql/directives"; describe("parseNodeAnnotation", () => { it("should parse correctly", () => { const directive: DirectiveNode = makeDirectiveNode("node", { - label: "Movie", labels: ["Movie", "Person"], - }); + }, nodeDirective); const nodeAnnotation = parseNodeAnnotation(directive); - expect(nodeAnnotation.label).toBe("Movie"); expect(nodeAnnotation.labels).toEqual(["Movie", "Person"]); }); }); diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/node-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/node-annotation.ts index 126505040c..d2288fcf0d 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/node-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/node-annotation.ts @@ -18,13 +18,13 @@ */ import type { DirectiveNode } from "graphql"; import { NodeAnnotation } from "../../annotation/NodeAnnotation"; -import { parseArguments } from "../utils"; +import { parseArguments } from "../parse-arguments"; +import { nodeDirective } from "../../../graphql/directives"; export function parseNodeAnnotation(directive: DirectiveNode): NodeAnnotation { - const { label, labels } = parseArguments(directive) as { label: string; labels: string[] }; + const { labels } = parseArguments(nodeDirective, directive) as { label: string; labels: string[] }; return new NodeAnnotation({ - label, labels, }); } diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/parse-directives.ts b/packages/graphql/src/schema-model/parser/annotations-parser/parse-directives.ts new file mode 100644 index 0000000000..a1bebef9ee --- /dev/null +++ b/packages/graphql/src/schema-model/parser/annotations-parser/parse-directives.ts @@ -0,0 +1,109 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { DirectiveNode } from "graphql"; +import { parseAliasAnnotation } from "./alias-annotation"; +import { parseCoalesceAnnotation } from "./coalesce-annotation"; +import { parseCypherAnnotation } from "./cypher-annotation"; +import { parseCustomResolverAnnotation } from "./custom-resolver-annotation"; +import { parseDefaultAnnotation } from "./default-annotation"; +import { parseIDAnnotation } from "./id-annotation"; +import { parseFilterableAnnotation } from "./filterable-annotation"; +import { parseMutationAnnotation } from "./mutation-annotation"; +import { parseNodeAnnotation } from "./node-annotation"; +import { parsePluralAnnotation } from "./plural-annotation"; +import { parsePopulatedByAnnotation } from "./populated-by-annotation"; +import { parsePrivateAnnotation } from "./private-annotation"; +import { parseQueryAnnotation } from "./query-annotation"; +import { parseQueryOptionsAnnotation } from "./query-options-annotation"; +import { parseSelectableAnnotation } from "./selectable-annotation"; +import { parseSettableAnnotation } from "./settable-annotation"; +import { parseSubscriptionAnnotation } from "./subscription-annotation"; +import { parseTimestampAnnotation } from "./timestamp-annotation"; +import { parseUniqueAnnotation } from "./unique-annotation"; +import { parseFullTextAnnotation } from "./full-text-annotation"; +import { parseJWTClaimAnnotation } from "./jwt-claim-annotation"; +import { parseJWTPayloadAnnotation } from "./jwt-payload-annotation"; +import { parseAuthorizationAnnotation } from "./authorization-annotation"; +import { parseAuthenticationAnnotation } from "./authentication-annotation"; +import { parseSubscriptionsAuthorizationAnnotation } from "./subscriptions-authorization-annotation"; +import { filterTruthy } from "../../../utils/utils"; +import type { Annotation } from "../../annotation/Annotation"; +import { AnnotationsKey } from "../../annotation/Annotation"; + +export function parseDirectives(directives: readonly DirectiveNode[]): Annotation[] { + return filterTruthy( + directives.map((directive) => { + switch (directive.name.value) { + case AnnotationsKey.alias: + return parseAliasAnnotation(directive); + case AnnotationsKey.authentication: + return parseAuthenticationAnnotation(directive); + case AnnotationsKey.authorization: + return parseAuthorizationAnnotation(directive); + case AnnotationsKey.coalesce: + return parseCoalesceAnnotation(directive); + case AnnotationsKey.customResolver: + return parseCustomResolverAnnotation(directive); + case AnnotationsKey.cypher: + return parseCypherAnnotation(directive); + case AnnotationsKey.default: + return parseDefaultAnnotation(directive); + case AnnotationsKey.filterable: + return parseFilterableAnnotation(directive); + case AnnotationsKey.fulltext: + return parseFullTextAnnotation(directive); + case AnnotationsKey.id: + return parseIDAnnotation(directive); + case AnnotationsKey.jwtClaim: + return parseJWTClaimAnnotation(directive); + case AnnotationsKey.jwtPayload: + return parseJWTPayloadAnnotation(directive); + case AnnotationsKey.mutation: + return parseMutationAnnotation(directive); + case AnnotationsKey.node: + return parseNodeAnnotation(directive); + case AnnotationsKey.plural: + return parsePluralAnnotation(directive); + case AnnotationsKey.populatedBy: + return parsePopulatedByAnnotation(directive); + case AnnotationsKey.private: + return parsePrivateAnnotation(directive); + case AnnotationsKey.query: + return parseQueryAnnotation(directive); + case AnnotationsKey.queryOptions: + return parseQueryOptionsAnnotation(directive); + case AnnotationsKey.selectable: + return parseSelectableAnnotation(directive); + case AnnotationsKey.settable: + return parseSettableAnnotation(directive); + case AnnotationsKey.subscription: + return parseSubscriptionAnnotation(directive); + case AnnotationsKey.subscriptionsAuthorization: + return parseSubscriptionsAuthorizationAnnotation(directive); + case AnnotationsKey.timestamp: + return parseTimestampAnnotation(directive); + case AnnotationsKey.unique: + return parseUniqueAnnotation(directive); + default: + return undefined; + } + }) + ); +} diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/plural-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/plural-annotation.test.ts index 0d87346646..0a5b7f81f7 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/plural-annotation.test.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/plural-annotation.test.ts @@ -20,10 +20,11 @@ import { makeDirectiveNode } from "@graphql-tools/utils"; import type { DirectiveNode } from "graphql"; import { parsePluralAnnotation } from "./plural-annotation"; +import { pluralDirective } from "../../../graphql/directives"; describe("parsePluralAnnotation", () => { it("should parse correctly", () => { - const directive: DirectiveNode = makeDirectiveNode("Plural", { value: "myPluralString" }); + const directive: DirectiveNode = makeDirectiveNode("Plural", { value: "myPluralString" }, pluralDirective); const pluralAnnotation = parsePluralAnnotation(directive); expect(pluralAnnotation.value).toBe("myPluralString"); }); diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/plural-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/plural-annotation.ts index a4c39af0bf..b33ace44f1 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/plural-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/plural-annotation.ts @@ -18,10 +18,11 @@ */ import type { DirectiveNode } from "graphql"; import { PluralAnnotation } from "../../annotation/PluralAnnotation"; -import { parseArguments } from "../utils"; +import { parseArguments } from "../parse-arguments"; +import { pluralDirective } from "../../../graphql/directives"; export function parsePluralAnnotation(directive: DirectiveNode): PluralAnnotation { - const { value } = parseArguments(directive) as { value: string }; + const { value } = parseArguments(pluralDirective, directive) as { value: string }; return new PluralAnnotation({ value, }); diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/populated-by-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/populated-by-annotation.test.ts index ec6cef02bd..00ce0ec7ec 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/populated-by-annotation.test.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/populated-by-annotation.test.ts @@ -20,15 +20,16 @@ import { makeDirectiveNode } from "@graphql-tools/utils"; import type { DirectiveNode } from "graphql"; import { parsePopulatedByAnnotation } from "./populated-by-annotation"; +import { populatedByDirective } from "../../../graphql/directives"; describe("parsePopulatedByAnnotation", () => { it("should parse correctly", () => { const directive: DirectiveNode = makeDirectiveNode("populatedBy", { callback: "callback", - operations: ["create", "update"], - }); + operations: ["CREATE", "UPDATE"], + }, populatedByDirective); const populatedByAnnotation = parsePopulatedByAnnotation(directive); expect(populatedByAnnotation.callback).toBe("callback"); - expect(populatedByAnnotation.operations).toEqual(["create", "update"]); + expect(populatedByAnnotation.operations).toEqual(["CREATE", "UPDATE"]); }); }); diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/populated-by-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/populated-by-annotation.ts index 58f11722c8..e92793d593 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/populated-by-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/populated-by-annotation.ts @@ -18,10 +18,14 @@ */ import type { DirectiveNode } from "graphql"; import { PopulatedByAnnotation } from "../../annotation/PopulatedByAnnotation"; -import { parseArguments } from "../utils"; +import { parseArguments } from "../parse-arguments"; +import { populatedByDirective } from "../../../graphql/directives"; export function parsePopulatedByAnnotation(directive: DirectiveNode): PopulatedByAnnotation { - const { callback, operations } = parseArguments(directive) as { callback: string; operations: string[] }; + const { callback, operations } = parseArguments(populatedByDirective, directive) as { + callback: string; + operations: string[]; + }; return new PopulatedByAnnotation({ callback, diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/query-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/query-annotation.test.ts index 4730b075b2..c78e8c09e3 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/query-annotation.test.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/query-annotation.test.ts @@ -19,6 +19,7 @@ import { makeDirectiveNode } from "@graphql-tools/utils"; import { parseQueryAnnotation } from "./query-annotation"; +import { queryDirective } from "../../../graphql/directives"; const tests = [ { @@ -26,7 +27,7 @@ const tests = [ directive: makeDirectiveNode("query", { read: true, aggregate: false, - }), + }, queryDirective), expected: { read: true, aggregate: false, @@ -37,7 +38,7 @@ const tests = [ directive: makeDirectiveNode("query", { read: false, aggregate: true, - }), + }, queryDirective), expected: { read: false, aggregate: true, diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/query-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/query-annotation.ts index 0412d1d085..334c31eeec 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/query-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/query-annotation.ts @@ -19,10 +19,11 @@ import type { DirectiveNode } from "graphql"; import { QueryAnnotation } from "../../annotation/QueryAnnotation"; -import { parseArguments } from "../utils"; +import { parseArguments } from "../parse-arguments"; +import { queryDirective } from "../../../graphql/directives"; export function parseQueryAnnotation(directive: DirectiveNode): QueryAnnotation { - const { read, aggregate } = parseArguments(directive) as { read: boolean; aggregate: boolean }; + const { read, aggregate } = parseArguments(queryDirective, directive) as { read: boolean; aggregate: boolean }; return new QueryAnnotation({ read, diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/query-options-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/query-options-annotation.test.ts index 9619f6cb83..ee73023677 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/query-options-annotation.test.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/query-options-annotation.test.ts @@ -19,6 +19,7 @@ import { makeDirectiveNode } from "@graphql-tools/utils"; import { parseQueryOptionsAnnotation } from "./query-options-annotation"; +import { queryOptionsDirective } from "../../../graphql/directives"; const tests = [ { @@ -28,7 +29,7 @@ const tests = [ default: 25, max: 100, }, - }), + }, queryOptionsDirective), expected: { limit: { default: 25, @@ -42,7 +43,7 @@ const tests = [ limit: { default: 25, }, - }), + }, queryOptionsDirective), expected: { limit: { default: 25, @@ -56,7 +57,7 @@ const tests = [ limit: { max: 100, }, - }), + }, queryOptionsDirective), expected: { limit: { default: undefined, diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/query-options-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/query-options-annotation.ts index 9e2f7ce0ec..353eb56987 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/query-options-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/query-options-annotation.ts @@ -20,31 +20,26 @@ import type { DirectiveNode } from "graphql"; import { Neo4jGraphQLSchemaValidationError } from "../../../classes"; import { QueryOptionsAnnotation } from "../../annotation/QueryOptionsAnnotation"; -import { parseArguments } from "../utils"; +import { parseArguments } from "../parse-arguments"; +import { queryOptionsDirective } from "../../../graphql/directives"; export function parseQueryOptionsAnnotation(directive: DirectiveNode): QueryOptionsAnnotation { - const { limit } = parseArguments(directive) as { + const { limit } = parseArguments(queryOptionsDirective, directive) as { limit: { - default?: string; - max?: string; + default?: number; + max?: number; }; resolvable: boolean; }; - - const parsedLimit = { - default: limit.default ? parseInt(limit.default) : undefined, - max: limit.max ? parseInt(limit.max) : undefined, - }; - - if (parsedLimit.default && typeof parsedLimit.default !== "number") { + if (limit.default && typeof limit.default !== "number") { throw new Neo4jGraphQLSchemaValidationError(`@queryOptions limit.default must be a number`); } - if (parsedLimit.max && typeof parsedLimit.max !== "number") { + if (limit.max && typeof limit.max !== "number") { throw new Neo4jGraphQLSchemaValidationError(`@queryOptions limit.max must be a number`); } return new QueryOptionsAnnotation({ - limit: parsedLimit, + limit, }); } diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/selectable-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/selectable-annotation.test.ts index a8ea8550ef..6496ffbcb2 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/selectable-annotation.test.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/selectable-annotation.test.ts @@ -19,6 +19,7 @@ import { makeDirectiveNode } from "@graphql-tools/utils"; import { parseSelectableAnnotation } from "./selectable-annotation"; +import { selectableDirective } from "../../../graphql/directives"; const tests = [ { @@ -26,7 +27,7 @@ const tests = [ directive: makeDirectiveNode("selectable", { onRead: true, onAggregate: true, - }), + }, selectableDirective), expected: { onRead: true, onAggregate: true, @@ -37,7 +38,7 @@ const tests = [ directive: makeDirectiveNode("selectable", { onRead: true, onAggregate: false, - }), + }, selectableDirective), expected: { onRead: true, onAggregate: false, diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/selectable-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/selectable-annotation.ts index c6c8c8efdb..e3ee20812c 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/selectable-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/selectable-annotation.ts @@ -18,10 +18,14 @@ */ import type { DirectiveNode } from "graphql"; import { SelectableAnnotation } from "../../annotation/SelectableAnnotation"; -import { parseArguments } from "../utils"; +import { parseArguments } from "../parse-arguments"; +import { selectableDirective } from "../../../graphql/directives"; export function parseSelectableAnnotation(directive: DirectiveNode): SelectableAnnotation { - const { onRead, onAggregate } = parseArguments(directive) as { onRead: boolean; onAggregate: boolean }; + const { onRead, onAggregate } = parseArguments(selectableDirective, directive) as { + onRead: boolean; + onAggregate: boolean; + }; return new SelectableAnnotation({ onRead, diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/settable-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/settable-annotation.test.ts index cd145f2dfe..6a41a61573 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/settable-annotation.test.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/settable-annotation.test.ts @@ -19,6 +19,7 @@ import { makeDirectiveNode } from "@graphql-tools/utils"; import { parseSettableAnnotation } from "./settable-annotation"; +import { settableDirective } from "../../../graphql/directives"; const tests = [ { @@ -26,7 +27,7 @@ const tests = [ directive: makeDirectiveNode("settable", { onCreate: true, onUpdate: true, - }), + }, settableDirective), expected: { onCreate: true, onUpdate: true, @@ -37,7 +38,7 @@ const tests = [ directive: makeDirectiveNode("settable", { onCreate: true, onUpdate: false, - }), + }, settableDirective), expected: { onCreate: true, onUpdate: false, diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/settable-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/settable-annotation.ts index 06c1b6eada..e82acf46c2 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/settable-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/settable-annotation.ts @@ -18,10 +18,14 @@ */ import type { DirectiveNode } from "graphql"; import { SettableAnnotation } from "../../annotation/SettableAnnotation"; -import { parseArguments } from "../utils"; +import { parseArguments } from "../parse-arguments"; +import { settableDirective } from "../../../graphql/directives"; export function parseSettableAnnotation(directive: DirectiveNode): SettableAnnotation { - const { onCreate, onUpdate } = parseArguments(directive) as { onCreate: boolean; onUpdate: boolean }; + const { onCreate, onUpdate } = parseArguments(settableDirective, directive) as { + onCreate: boolean; + onUpdate: boolean; + }; return new SettableAnnotation({ onCreate, diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/subscription-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/subscription-annotation.test.ts index e4fd5d1db0..048b5c4b94 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/subscription-annotation.test.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/subscription-annotation.test.ts @@ -19,23 +19,24 @@ import { makeDirectiveNode } from "@graphql-tools/utils"; import { parseSubscriptionAnnotation } from "./subscription-annotation"; +import { subscriptionDirective } from "../../../graphql/directives"; const tests = [ { name: "should parse correctly when CREATE operation is passed", - directive: makeDirectiveNode("subscription", { operations: ["CREATE"] }), + directive: makeDirectiveNode("subscription", { operations: ["CREATE"] }, subscriptionDirective), operations: ["CREATE"], expected: { operations: ["CREATE"] }, }, { name: "should parse correctly when UPDATE operation is passed", - directive: makeDirectiveNode("subscription", { operations: ["UPDATE"] }), + directive: makeDirectiveNode("subscription", { operations: ["UPDATE"] }, subscriptionDirective), operations: ["UPDATE"], expected: { operations: ["UPDATE"] }, }, { name: "should parse correctly when CREATE and UPDATE operations are passed", - directive: makeDirectiveNode("subscription", { operations: ["CREATE", "UPDATE"] }), + directive: makeDirectiveNode("subscription", { operations: ["CREATE", "UPDATE"] }, subscriptionDirective), operations: ["CREATE", "UPDATE"], expected: { operations: ["CREATE", "UPDATE"] }, }, diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/subscription-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/subscription-annotation.ts index 9d8e654b01..44f8b7f485 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/subscription-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/subscription-annotation.ts @@ -18,10 +18,11 @@ */ import type { DirectiveNode } from "graphql"; import { SubscriptionAnnotation } from "../../annotation/SubscriptionAnnotation"; -import { parseArguments } from "../utils"; +import { parseArguments } from "../parse-arguments"; +import { subscriptionDirective } from "../../../graphql/directives"; export function parseSubscriptionAnnotation(directive: DirectiveNode): SubscriptionAnnotation { - const { operations } = parseArguments(directive) as { operations: string[] }; + const { operations } = parseArguments(subscriptionDirective, directive) as { operations: string[] }; return new SubscriptionAnnotation({ operations, diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/subscriptions-authorization-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/subscriptions-authorization-annotation.ts index 8ab0e5df55..cf0da6e6fc 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/subscriptions-authorization-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/subscriptions-authorization-annotation.ts @@ -18,7 +18,8 @@ */ import type { DirectiveNode } from "graphql"; import { Neo4jGraphQLSchemaValidationError } from "../../../classes"; -import { parseArguments } from "../utils"; +import { parseArgumentsFromUnknownDirective } from "../parse-arguments"; + import type { SubscriptionsAuthorizationFilterRuleConstructor } from "../../annotation/SubscriptionsAuthorizationAnnotation"; import { SubscriptionsAuthorizationAnnotation, @@ -29,7 +30,7 @@ import { export function parseSubscriptionsAuthorizationAnnotation( directive: DirectiveNode ): SubscriptionsAuthorizationAnnotation { - const { filter, ...unrecognizedArguments } = parseArguments(directive) as { + const { filter, ...unrecognizedArguments } = parseArgumentsFromUnknownDirective(directive) as { filter?: Record[]; }; if (!filter) { diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/timestamp-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/timestamp-annotation.test.ts index 689110e89b..becc917c0e 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/timestamp-annotation.test.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/timestamp-annotation.test.ts @@ -19,23 +19,24 @@ import { makeDirectiveNode } from "@graphql-tools/utils"; import { parseTimestampAnnotation } from "./timestamp-annotation"; +import { timestampDirective } from "../../../graphql/directives"; const tests = [ { name: "should parse correctly when CREATE operation is passed", - directive: makeDirectiveNode("timestamp", { operations: ["CREATE"] }), + directive: makeDirectiveNode("timestamp", { operations: ["CREATE"] }, timestampDirective), operations: ["CREATE"], expected: { operations: ["CREATE"] }, }, { name: "should parse correctly when UPDATE operation is passed", - directive: makeDirectiveNode("timestamp", { operations: ["UPDATE"] }), + directive: makeDirectiveNode("timestamp", { operations: ["UPDATE"] }, timestampDirective), operations: ["UPDATE"], expected: { operations: ["UPDATE"] }, }, { name: "should parse correctly when CREATE and UPDATE operations are passed", - directive: makeDirectiveNode("timestamp", { operations: ["CREATE", "UPDATE"] }), + directive: makeDirectiveNode("timestamp", { operations: ["CREATE", "UPDATE"] }, timestampDirective), operations: ["CREATE", "UPDATE"], expected: { operations: ["CREATE", "UPDATE"] }, }, diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/timestamp-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/timestamp-annotation.ts index 86569c468f..c8ec8fdff2 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/timestamp-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/timestamp-annotation.ts @@ -18,10 +18,11 @@ */ import type { DirectiveNode } from "graphql"; import { TimestampAnnotation } from "../../annotation/TimestampAnnotation"; -import { parseArguments } from "../utils"; +import { parseArguments } from "../parse-arguments"; +import { timestampDirective } from "../../../graphql/directives"; export function parseTimestampAnnotation(directive: DirectiveNode): TimestampAnnotation { - const { operations } = parseArguments(directive) as { operations: string[] }; + const { operations } = parseArguments(timestampDirective, directive) as { operations: string[] }; return new TimestampAnnotation({ operations, diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/unique-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/unique-annotation.test.ts index 81d85c3426..f744105963 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/unique-annotation.test.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/unique-annotation.test.ts @@ -20,10 +20,11 @@ import { makeDirectiveNode } from "@graphql-tools/utils"; import type { DirectiveNode } from "graphql"; import { parseUniqueAnnotation } from "./unique-annotation"; +import { uniqueDirective } from "../../../graphql/directives"; describe("parseUniqueAnnotation", () => { test("should correctly parse unique constraint name", () => { - const directive: DirectiveNode = makeDirectiveNode("unique", { constraintName: "uniqueConstraintName" }); + const directive: DirectiveNode = makeDirectiveNode("unique", { constraintName: "uniqueConstraintName" }, uniqueDirective); const uniqueAnnotation = parseUniqueAnnotation(directive); expect(uniqueAnnotation.constraintName).toBe("uniqueConstraintName"); diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/unique-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/unique-annotation.ts index 87b9b74a63..31215fd676 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/unique-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/unique-annotation.ts @@ -18,10 +18,11 @@ */ import type { DirectiveNode } from "graphql"; import { UniqueAnnotation } from "../../annotation/UniqueAnnotation"; -import { parseArguments } from "../utils"; +import { parseArguments } from "../parse-arguments"; +import { uniqueDirective } from "../../../graphql/directives"; export function parseUniqueAnnotation(directive: DirectiveNode): UniqueAnnotation { - const { constraintName } = parseArguments(directive) as { constraintName: string }; + const { constraintName } = parseArguments(uniqueDirective, directive) as { constraintName: string }; return new UniqueAnnotation({ constraintName, diff --git a/packages/graphql/src/schema-model/parser/parse-arguments.test.ts b/packages/graphql/src/schema-model/parser/parse-arguments.test.ts new file mode 100644 index 0000000000..c9f9ac9b58 --- /dev/null +++ b/packages/graphql/src/schema-model/parser/parse-arguments.test.ts @@ -0,0 +1,138 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { DirectiveNode, ObjectTypeDefinitionNode } from "graphql"; +import { + DirectiveLocation, + GraphQLBoolean, + GraphQLDirective, + GraphQLFloat, + GraphQLInt, + GraphQLNonNull, + Kind, + parse, +} from "graphql"; +import { parseArguments, parseArgumentsFromUnknownDirective } from "./parse-arguments"; +import { ScalarOrEnumType } from "../../graphql/directives/arguments/scalars/ScalarOrEnum"; + +describe("parseArguments", () => { + let testDirectiveDefinition: GraphQLDirective; + + beforeAll(() => { + testDirectiveDefinition = new GraphQLDirective({ + name: "testDirective", + locations: [DirectiveLocation.FIELD_DEFINITION], + args: { + booleanArgument: { + type: new GraphQLNonNull(GraphQLBoolean), + defaultValue: true, + }, + intArgument: { + type: new GraphQLNonNull(GraphQLInt), + defaultValue: 1, + }, + floatArgument: { + type: GraphQLFloat, + defaultValue: 3.0, + }, + customScalar: { + type: ScalarOrEnumType, + defaultValue: "test", + }, + }, + }); + }); + + test("should parse arguments", () => { + const typeDefs = ` + + type User { + name: String @testDirective(booleanArgument: false, intArgument: 2, floatArgument: 4.0, customScalar: "123") + } + `; + + const definitions = parse(typeDefs).definitions; + const user = definitions.find( + (def) => def.kind === Kind.OBJECT_TYPE_DEFINITION && def.name.value === "User" + ) as ObjectTypeDefinitionNode; + + const userName = user.fields?.find((field) => field.name.value === "name"); + expect(userName).toBeDefined(); + + const nameCoalesceUsage = userName?.directives?.find( + (dir) => dir.name.value === "testDirective" + ) as DirectiveNode; + expect(nameCoalesceUsage).toBeDefined(); + + const args = parseArguments(testDirectiveDefinition, nameCoalesceUsage); + + expect(args).toEqual({ booleanArgument: false, intArgument: 2, floatArgument: 4.0, customScalar: "123" }); + }); + + test("should use default values", () => { + const typeDefs = ` + type User { + name: String @testDirective(booleanArgument: false, intArgument: 2,) + } + `; + + const definitions = parse(typeDefs).definitions; + const user = definitions.find( + (def) => def.kind === Kind.OBJECT_TYPE_DEFINITION && def.name.value === "User" + ) as ObjectTypeDefinitionNode; + + const userName = user.fields?.find((field) => field.name.value === "name"); + expect(userName).toBeDefined(); + + const nameCoalesceUsage = userName?.directives?.find( + (dir) => dir.name.value === "testDirective" + ) as DirectiveNode; + expect(nameCoalesceUsage).toBeDefined(); + + const args = parseArguments(testDirectiveDefinition, nameCoalesceUsage); + + expect(args).toEqual({ booleanArgument: false, intArgument: 2, floatArgument: 3.0, customScalar: "test" }); + }); + + test("parseArgumentsFromUnknownDirective", () => { + const typeDefs = ` + type User { + name: String @testDirective(booleanArgument: false, intArgument: 2) + } + `; + + const definitions = parse(typeDefs).definitions; + const user = definitions.find( + (def) => def.kind === Kind.OBJECT_TYPE_DEFINITION && def.name.value === "User" + ) as ObjectTypeDefinitionNode; + + const userName = user.fields?.find((field) => field.name.value === "name"); + expect(userName).toBeDefined(); + + const nameCoalesceUsage = userName?.directives?.find( + (dir) => dir.name.value === "testDirective" + ) as DirectiveNode; + expect(nameCoalesceUsage).toBeDefined(); + + const args = parseArgumentsFromUnknownDirective(nameCoalesceUsage); + + expect(args).toEqual({ booleanArgument: false, intArgument: 2}); + }); + +}); diff --git a/packages/graphql/src/utils/get-argument-values.ts b/packages/graphql/src/schema-model/parser/parse-arguments.ts similarity index 88% rename from packages/graphql/src/utils/get-argument-values.ts rename to packages/graphql/src/schema-model/parser/parse-arguments.ts index 22c7ca17a3..4a54f5fc09 100644 --- a/packages/graphql/src/utils/get-argument-values.ts +++ b/packages/graphql/src/schema-model/parser/parse-arguments.ts @@ -1,3 +1,4 @@ + /* * Copyright (c) "Neo4j" * Neo4j Sweden AB [http://neo4j.com] @@ -19,12 +20,20 @@ import { inspect } from "@graphql-tools/utils"; import type { Maybe } from "@graphql-tools/utils/typings/types"; -import type { DirectiveNode, FieldNode, GraphQLDirective, GraphQLField } from "graphql"; import { isNonNullType, Kind, valueFromAST, print } from "graphql"; import type { ObjMap } from "graphql/jsutils/ObjMap"; +import { parseValueNode } from "./parse-value-node"; +import type { DirectiveNode, FieldNode, GraphQLDirective, GraphQLField } from "graphql"; + +export function parseArgumentsFromUnknownDirective(directive: DirectiveNode): Record { + return (directive.arguments || [])?.reduce((acc, argument) => { + acc[argument.name.value] = parseValueNode(argument.value); + return acc; + }, {}); +} /** - * Polyfill of GraphQL-JS getArgumentValues, remove it after dropping the support of GraphQL-JS 15.0 + * Polyfill of GraphQL-JS parseArguments, remove it after dropping the support of GraphQL-JS 15.0 * * Prepares an object map of argument values given a list of argument * definitions and list of argument AST nodes. @@ -33,7 +42,7 @@ import type { ObjMap } from "graphql/jsutils/ObjMap"; * exposed to user code. Care should be taken to not pull values from the * Object prototype. */ -export function getArgumentValues( +export function parseArguments( def: GraphQLField | GraphQLDirective, node: FieldNode | DirectiveNode, variableValues?: Maybe> @@ -86,4 +95,4 @@ export function getArgumentValues( coercedValues[name] = coercedValue; } return coercedValues; -} +} \ No newline at end of file diff --git a/packages/graphql/src/schema-model/parser/utils.ts b/packages/graphql/src/schema-model/parser/utils.ts index de13c998f5..ee6a56e0aa 100644 --- a/packages/graphql/src/schema-model/parser/utils.ts +++ b/packages/graphql/src/schema-model/parser/utils.ts @@ -16,15 +16,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type { DirectiveNode } from "graphql"; -import { parseValueNode } from "./parse-value-node"; -export function parseArguments(directive: DirectiveNode): Record { - return (directive.arguments || [])?.reduce((acc, argument) => { - acc[argument.name.value] = parseValueNode(argument.value); - return acc; - }, {}); -} + +import type { DirectiveNode } from "graphql"; export function findDirective(directives: readonly DirectiveNode[], name: string): DirectiveNode | undefined { return directives.find((d) => { diff --git a/packages/graphql/src/schema-model/relationship/Relationship.ts b/packages/graphql/src/schema-model/relationship/Relationship.ts index 6ffaaea675..c923928061 100644 --- a/packages/graphql/src/schema-model/relationship/Relationship.ts +++ b/packages/graphql/src/schema-model/relationship/Relationship.ts @@ -24,6 +24,8 @@ import type { ConcreteEntity } from "../entity/ConcreteEntity"; import type { Entity } from "../entity/Entity"; export type RelationshipDirection = "IN" | "OUT"; +export type QueryDirection = "DEFAULT_DIRECTED" | "DEFAULT_UNDIRECTED" | "DIRECTED_ONLY" | "UNDIRECTED_ONLY"; +export type NestedOperation = "CREATE" | "UPDATE" | "DELETE" | "CONNECT" | "DISCONNECT" | "CONNECT_OR_CREATE"; export class Relationship { public readonly name: string; // name of the relationship field, e.g. friends @@ -32,8 +34,11 @@ export class Relationship { public readonly source: ConcreteEntity; public readonly target: Entity; public readonly direction: RelationshipDirection; + public readonly queryDirection: QueryDirection; + public readonly nestedOperations: NestedOperation[]; + public readonly aggregate: boolean; - // TODO: Delegate to the RelationshipAdapter the following properties + // TODO: Remove connectionFieldTypename and relationshipFieldTypename and delegate to the adapter /**Note: Required for now to infer the types without ResolveTree */ public get connectionFieldTypename(): string { return `${this.source.name}${upperFirst(this.name)}Connection`; @@ -51,6 +56,9 @@ export class Relationship { source, target, direction, + queryDirection, + nestedOperations, + aggregate, }: { name: string; type: string; @@ -58,12 +66,18 @@ export class Relationship { source: ConcreteEntity; target: Entity; direction: RelationshipDirection; + queryDirection: QueryDirection; + nestedOperations: NestedOperation[]; + aggregate: boolean; }) { this.type = type; this.source = source; this.target = target; this.name = name; this.direction = direction; + this.queryDirection = queryDirection; + this.nestedOperations = nestedOperations; + this.aggregate = aggregate; for (const attribute of attributes) { this.addAttribute(attribute); @@ -78,6 +92,9 @@ export class Relationship { source: this.source, target: this.target, direction: this.direction, + queryDirection: this.queryDirection, + nestedOperations: this.nestedOperations, + aggregate: this.aggregate, }); } diff --git a/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.test.ts b/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.test.ts new file mode 100644 index 0000000000..e10adf191f --- /dev/null +++ b/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.test.ts @@ -0,0 +1,110 @@ +import { Attribute } from "../../attribute/Attribute"; +import { GraphQLBuiltInScalarType, ScalarType } from "../../attribute/AttributeType"; +import { UniqueAnnotation } from "../../annotation/UniqueAnnotation"; +import { ConcreteEntity } from "../../entity/ConcreteEntity"; +import { ConcreteEntityAdapter } from "../../entity/model-adapters/ConcreteEntityAdapter"; +import { Relationship } from "../Relationship"; +import { RelationshipAdapter } from "./RelationshipAdapter"; + +describe("RelationshipAdapter", () => { + let userAdapter: ConcreteEntityAdapter; + let relationship: Relationship; + + beforeAll(() => { + const userId = new Attribute({ + name: "id", + annotations: [new UniqueAnnotation({ constraintName: "User_id_unique" })], + type: new ScalarType(GraphQLBuiltInScalarType.ID, true), + }); + + const userName = new Attribute({ + name: "name", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.String, true), + }); + + const accountId = new Attribute({ + name: "id", + annotations: [new UniqueAnnotation({ constraintName: "User_id_unique" })], + type: new ScalarType(GraphQLBuiltInScalarType.ID, true), + }); + + const accountUsername = new Attribute({ + name: "username", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.String, true), + }); + + const userEntity = new ConcreteEntity({ + name: "User", + labels: ["User"], + attributes: [userId, userName], + }); + + const accountEntity = new ConcreteEntity({ + name: "Account", + labels: ["Account"], + attributes: [accountId, accountUsername], + }); + + const accountAlias = new Attribute({ + name: "accountAlias", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.String, true), + }); + + relationship = new Relationship({ + name: "accounts", + type: "HAS_ACCOUNT", + source: userEntity, + target: accountEntity, + direction: "OUT", + attributes: [accountAlias], + queryDirection: "DEFAULT_DIRECTED", + nestedOperations: ["CREATE", "UPDATE", "DELETE", "CONNECT", "DISCONNECT", "CONNECT_OR_CREATE"], + aggregate: false, + }); + userEntity.addRelationship(relationship); + + userAdapter = new ConcreteEntityAdapter(userEntity); + }); + + test("should be possible to get a correct instance of a RelationshipAdapter from a ConcreteEntity", () => { + expect(userAdapter).toBeDefined(); + expect(userAdapter).toBeInstanceOf(ConcreteEntityAdapter); + expect(userAdapter.relationships.size).toBe(1); + const relationshipAdapter = userAdapter.relationships.get("accounts"); + expect(relationshipAdapter).toBeDefined(); + expect(relationshipAdapter).toBeInstanceOf(RelationshipAdapter); + expect(relationshipAdapter?.attributes.size).toBe(1); + expect(relationshipAdapter?.type).toBe("HAS_ACCOUNT"); + expect(relationshipAdapter?.direction).toBe("OUT"); + expect(relationshipAdapter?.source).toBeInstanceOf(ConcreteEntityAdapter); + expect(relationshipAdapter?.source.name).toBe("User"); + expect(relationshipAdapter?.target).toBeInstanceOf(ConcreteEntityAdapter); + expect(relationshipAdapter?.target.name).toBe("Account"); + }); + + test("should be possible to get a correct instance of a RelationshipAdapter starting from a Relationship", () => { + const relationshipAdapter = new RelationshipAdapter(relationship); + expect(relationshipAdapter).toBeDefined(); + expect(relationshipAdapter).toBeInstanceOf(RelationshipAdapter); + expect(relationshipAdapter?.attributes.size).toBe(1); + expect(relationshipAdapter?.type).toBe("HAS_ACCOUNT"); + expect(relationshipAdapter?.direction).toBe("OUT"); + expect(relationshipAdapter?.source).toBeInstanceOf(ConcreteEntityAdapter); + expect(relationshipAdapter?.source.name).toBe("User"); + expect(relationshipAdapter?.target).toBeInstanceOf(ConcreteEntityAdapter); + expect(relationshipAdapter?.target.name).toBe("Account"); + }); + + test("should generate a valid connectionFieldTypename", () => { + const relationshipAdapter = userAdapter.relationships.get("accounts"); + expect(relationshipAdapter?.connectionFieldTypename).toBe("UserAccountsConnection"); + }); + + test("should generate a valid relationshipFieldTypename", () => { + const relationshipAdapter = userAdapter.relationships.get("accounts"); + expect(relationshipAdapter?.relationshipFieldTypename).toBe("UserAccountsRelationship"); + }); +}); diff --git a/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts b/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts index fb4b80f567..13190a40b5 100644 --- a/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts +++ b/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts @@ -20,7 +20,7 @@ import { upperFirst } from "graphql-compose"; import type { Entity } from "../../entity/Entity"; import { ConcreteEntityAdapter } from "../../entity/model-adapters/ConcreteEntityAdapter"; -import type { RelationshipDirection } from "../Relationship"; +import type { NestedOperation, QueryDirection, Relationship, RelationshipDirection } from "../Relationship"; import { AttributeAdapter } from "../../attribute/model-adapters/AttributeAdapter"; import type { Attribute } from "../../attribute/Attribute"; import { ConcreteEntity } from "../../entity/ConcreteEntity"; @@ -34,6 +34,9 @@ export class RelationshipAdapter { private rawEntity: Entity; private _target: Entity | undefined; public readonly direction: RelationshipDirection; + public readonly queryDirection: QueryDirection; + public readonly nestedOperations: NestedOperation[]; + public readonly aggregate: boolean; /**Note: Required for now to infer the types without ResolveTree */ public get connectionFieldTypename(): string { @@ -45,25 +48,65 @@ export class RelationshipAdapter { return `${this.source.name}${upperFirst(this.name)}Relationship`; } - constructor({ + /* constructor({ name, type, attributes = new Map(), source, target, direction, + queryDirection, + nestedOperations, + aggregate, }: { name: string; type: string; attributes?: Map; - source: ConcreteEntityAdapter; + source: ConcreteEntity | ConcreteEntityAdapter; target: Entity; direction: RelationshipDirection; + queryDirection: QueryDirection; + nestedOperations: NestedOperation[]; + aggregate: boolean; }) { this.name = name; this.type = type; - this.source = source; + if (source instanceof ConcreteEntity) { + this.source = new ConcreteEntityAdapter(source); + } else { + this.source = source; + } + this.direction = direction; + this.queryDirection = queryDirection; + this.nestedOperations = nestedOperations; + this.aggregate = aggregate; + this.rawEntity = target; + this.initAttributes(attributes); + } */ + + constructor(relationship: Relationship, sourceAdapter?: ConcreteEntityAdapter) { + const { + name, + type, + attributes = new Map(), + source, + target, + direction, + queryDirection, + nestedOperations, + aggregate, + } = relationship; + this.name = name; + this.type = type; + if (sourceAdapter) { + this.source = sourceAdapter; + } else { + this.source = new ConcreteEntityAdapter(source); + } this.direction = direction; + this.queryDirection = queryDirection; + this.nestedOperations = nestedOperations; + this.aggregate = aggregate; this.rawEntity = target; this.initAttributes(attributes); } diff --git a/packages/graphql/src/schema/get-obj-field-meta.ts b/packages/graphql/src/schema/get-obj-field-meta.ts index c4cdc5ed11..dc10bf53d5 100644 --- a/packages/graphql/src/schema/get-obj-field-meta.ts +++ b/packages/graphql/src/schema/get-obj-field-meta.ts @@ -64,7 +64,7 @@ import { parseValueNode } from "../schema-model/parser/parse-value-node"; import checkDirectiveCombinations from "./check-directive-combinations"; import { upperFirst } from "../utils/upper-first"; import { getPopulatedByMeta } from "./get-populated-by-meta"; -import { parseArguments } from "../schema-model/parser/utils"; +import { parseArgumentsFromUnknownDirective } from "../schema-model/parser/parse-arguments"; export interface ObjectFields { relationFields: RelationField[]; @@ -657,7 +657,7 @@ function parseSelectableDirective(directive: DirectiveNode | undefined): Selecta onAggregate: true, }; - const args: Partial = directive ? parseArguments(directive) : {}; + const args: Partial = directive ? parseArgumentsFromUnknownDirective(directive) : {}; return { onRead: args.onRead ?? defaultArguments.onRead, @@ -671,7 +671,7 @@ function parseSettableDirective(directive: DirectiveNode | undefined): SettableO onUpdate: true, }; - const args: Partial = directive ? parseArguments(directive) : {}; + const args: Partial = directive ? parseArgumentsFromUnknownDirective(directive) : {}; return { onCreate: args.onCreate ?? defaultArguments.onCreate, @@ -685,7 +685,7 @@ function parseFilterableDirective(directive: DirectiveNode | undefined): Filtera byAggregate: directive === undefined ? true : false, }; - const args: Partial = directive ? parseArguments(directive) : {}; + const args: Partial = directive ? parseArgumentsFromUnknownDirective(directive) : {}; return { byValue: args.byValue ?? defaultArguments.byValue, diff --git a/packages/graphql/src/schema/get-relationship-meta.ts b/packages/graphql/src/schema/get-relationship-meta.ts index 54b270030d..3ed126add8 100644 --- a/packages/graphql/src/schema/get-relationship-meta.ts +++ b/packages/graphql/src/schema/get-relationship-meta.ts @@ -20,7 +20,8 @@ import type { DirectiveNode, FieldDefinitionNode } from "graphql"; import type { RelationshipNestedOperationsOption, RelationshipQueryDirectionOption } from "../constants"; import { relationshipDirective } from "../graphql/directives/relationship"; -import { getArgumentValues } from "../utils/get-argument-values"; +import { parseArguments } from "../schema-model/parser/parse-arguments"; + import Cypher from "@neo4j/cypher-builder"; type RelationshipDirection = "IN" | "OUT"; @@ -58,7 +59,7 @@ function getRelationshipMeta( } function getRelationshipDirectiveArguments(directiveNode: DirectiveNode) { - return getArgumentValues(relationshipDirective, directiveNode); + return parseArguments(relationshipDirective, directiveNode); } export default getRelationshipMeta; diff --git a/packages/graphql/src/schema/parse-mutation-directive.ts b/packages/graphql/src/schema/parse-mutation-directive.ts index c7207f101b..8bd2641c95 100644 --- a/packages/graphql/src/schema/parse-mutation-directive.ts +++ b/packages/graphql/src/schema/parse-mutation-directive.ts @@ -19,15 +19,16 @@ import type { DirectiveNode } from "graphql"; import { MutationDirective } from "../classes/MutationDirective"; +import type { MutationOperations} from "../graphql/directives/mutation"; import { mutationDirective as mutationDirectiveDefinition } from "../graphql/directives/mutation"; -import { getArgumentValues } from "../utils/get-argument-values"; +import { parseArguments } from "../schema-model/parser/parse-arguments"; function parseMutationDirective(directiveNode: DirectiveNode | undefined) { if (!directiveNode || directiveNode.name.value !== mutationDirectiveDefinition.name) { throw new Error("Undefined or incorrect directive passed into parseMutationDirective function"); } - const arg = getArgumentValues(mutationDirectiveDefinition, directiveNode) as { - operations: ConstructorParameters[0]; + const arg = parseArguments(mutationDirectiveDefinition, directiveNode) as { + operations: MutationOperations[]; }; return new MutationDirective(arg.operations); diff --git a/packages/graphql/src/schema/parse-query-directive.ts b/packages/graphql/src/schema/parse-query-directive.ts index eb99109c06..37e9d98f8c 100644 --- a/packages/graphql/src/schema/parse-query-directive.ts +++ b/packages/graphql/src/schema/parse-query-directive.ts @@ -18,7 +18,7 @@ */ import type { DirectiveNode } from "graphql"; -import { getArgumentValues } from "../utils/get-argument-values"; +import { parseArguments } from "../schema-model/parser/parse-arguments"; import { QueryDirective } from "../classes/QueryDirective"; import { queryDirective as queryDirectiveDefinition } from "../graphql/directives/query"; @@ -26,7 +26,7 @@ function parseQueryDirective(directiveNode: DirectiveNode | undefined) { if (!directiveNode || directiveNode.name.value !== queryDirectiveDefinition.name) { throw new Error("Undefined or incorrect directive passed into parseQueryDirective function"); } - const arg = getArgumentValues(queryDirectiveDefinition, directiveNode) as ConstructorParameters< + const arg = parseArguments(queryDirectiveDefinition, directiveNode) as ConstructorParameters< typeof QueryDirective >[0]; diff --git a/packages/graphql/src/schema/parse-subscription-directive.ts b/packages/graphql/src/schema/parse-subscription-directive.ts index c6e32ae255..f19f334dd0 100644 --- a/packages/graphql/src/schema/parse-subscription-directive.ts +++ b/packages/graphql/src/schema/parse-subscription-directive.ts @@ -18,7 +18,7 @@ */ import type { DirectiveNode } from "graphql"; -import { getArgumentValues } from "../utils/get-argument-values"; +import { parseArguments } from "../schema-model/parser/parse-arguments"; import { SubscriptionDirective } from "../classes/SubscriptionDirective"; import { subscriptionDirective as subscriptionDirectiveDefinition } from "../graphql/directives/subscription"; @@ -26,7 +26,7 @@ function parseSubscriptionDirective(directiveNode: DirectiveNode | undefined) { if (!directiveNode || directiveNode.name.value !== subscriptionDirectiveDefinition.name) { throw new Error("Undefined or incorrect directive passed into parseSubscriptionDirective function"); } - const arg = getArgumentValues(subscriptionDirectiveDefinition, directiveNode) as { + const arg = parseArguments(subscriptionDirectiveDefinition, directiveNode) as { operations: ConstructorParameters[0]; }; From 5828de29bcd490a748b013cbee54d0225aaab93f Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Sat, 29 Jul 2023 00:23:55 +0100 Subject: [PATCH 16/22] remove not necessary node directive --- .../src/schema-model/annotation/Annotation.ts | 5 --- .../schema-model/annotation/NodeAnnotation.ts | 26 --------------- .../ConcreteEntityAdapter.test.ts | 2 +- .../model-adapters/ConcreteEntityAdapter.ts | 9 ++--- .../src/schema-model/generate-model.test.ts | 2 +- .../src/schema-model/generate-model.ts | 22 ++++++++----- .../node-annotation.test.ts | 33 ------------------- .../annotations-parser/node-annotation.ts | 30 ----------------- .../annotations-parser/parse-directives.ts | 3 -- 9 files changed, 19 insertions(+), 113 deletions(-) delete mode 100644 packages/graphql/src/schema-model/annotation/NodeAnnotation.ts delete mode 100644 packages/graphql/src/schema-model/parser/annotations-parser/node-annotation.test.ts delete mode 100644 packages/graphql/src/schema-model/parser/annotations-parser/node-annotation.ts diff --git a/packages/graphql/src/schema-model/annotation/Annotation.ts b/packages/graphql/src/schema-model/annotation/Annotation.ts index 5cf422abe3..bceaf859ad 100644 --- a/packages/graphql/src/schema-model/annotation/Annotation.ts +++ b/packages/graphql/src/schema-model/annotation/Annotation.ts @@ -31,7 +31,6 @@ import { JWTClaimAnnotation } from "./JWTClaimAnnotation"; import { JWTPayloadAnnotation } from "./JWTPayloadAnnotation"; import { KeyAnnotation } from "./KeyAnnotation"; import { MutationAnnotation } from "./MutationAnnotation"; -import { NodeAnnotation } from "./NodeAnnotation"; import { PluralAnnotation } from "./PluralAnnotation"; import { PopulatedByAnnotation } from "./PopulatedByAnnotation"; import { PrivateAnnotation } from "./PrivateAnnotation"; @@ -60,7 +59,6 @@ export type Annotation = | PluralAnnotation | FilterableAnnotation | FullTextAnnotation - | NodeAnnotation | PopulatedByAnnotation | QueryAnnotation | PrivateAnnotation @@ -88,7 +86,6 @@ export enum AnnotationsKey { jwtPayload = "jwtPayload", key = "key", mutation = "mutation", - node = "node", plural = "plural", populatedBy = "populatedBy", private = "private", @@ -118,7 +115,6 @@ export type Annotations = { [AnnotationsKey.plural]: PluralAnnotation; [AnnotationsKey.filterable]: FilterableAnnotation; [AnnotationsKey.fulltext]: FullTextAnnotation; - [AnnotationsKey.node]: NodeAnnotation; [AnnotationsKey.populatedBy]: PopulatedByAnnotation; [AnnotationsKey.query]: QueryAnnotation; [AnnotationsKey.private]: PrivateAnnotation; @@ -147,7 +143,6 @@ export function annotationToKey(ann: Annotation): keyof Annotations { if (ann instanceof PluralAnnotation) return AnnotationsKey.plural; if (ann instanceof FilterableAnnotation) return AnnotationsKey.filterable; if (ann instanceof FullTextAnnotation) return AnnotationsKey.fulltext; - if (ann instanceof NodeAnnotation) return AnnotationsKey.node; if (ann instanceof PopulatedByAnnotation) return AnnotationsKey.populatedBy; if (ann instanceof QueryAnnotation) return AnnotationsKey.query; if (ann instanceof PrivateAnnotation) return AnnotationsKey.private; diff --git a/packages/graphql/src/schema-model/annotation/NodeAnnotation.ts b/packages/graphql/src/schema-model/annotation/NodeAnnotation.ts deleted file mode 100644 index f4268bfbfa..0000000000 --- a/packages/graphql/src/schema-model/annotation/NodeAnnotation.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export class NodeAnnotation { - public readonly labels: string[]; - - constructor({ labels }: { labels: string[] }) { - this.labels = labels; - } -} diff --git a/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.test.ts b/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.test.ts index e585e4a9fc..3e8148259b 100644 --- a/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.test.ts +++ b/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.test.ts @@ -88,7 +88,7 @@ describe("ConcreteEntityAdapter", () => { }); test("should return the correct labels", () => { - expect(userAdapter.getAllLabels()).toStrictEqual(["User"]); + expect(userAdapter.getLabels()).toStrictEqual(["User"]); expect(userAdapter.getMainLabel()).toBe("User"); }); diff --git a/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.ts b/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.ts index 4527f1ff83..def5dbcdde 100644 --- a/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.ts +++ b/packages/graphql/src/schema-model/entity/model-adapters/ConcreteEntityAdapter.ts @@ -102,15 +102,12 @@ export class ConcreteEntityAdapter { } // TODO: identify usage of old Node.[getLabels | getLabelsString] and migrate them if needed - public getAllLabels(): string[] { - if (this.annotations.node?.labels) { - return this.annotations.node.labels; - } - return [this.name]; + public getLabels(): string[] { + return Array.from(this.labels); } public getMainLabel(): string { - return this.getAllLabels()[0] as string; // getAllLabels always returns at least one label + return this.getLabels()[0] as string; } public get singular(): string { diff --git a/packages/graphql/src/schema-model/generate-model.test.ts b/packages/graphql/src/schema-model/generate-model.test.ts index 5f8bebb8ac..462de7cb29 100644 --- a/packages/graphql/src/schema-model/generate-model.test.ts +++ b/packages/graphql/src/schema-model/generate-model.test.ts @@ -362,7 +362,7 @@ describe("Relationship", () => { }); }); -describe.skip("Annotations", () => { +describe("Annotations", () => { let schemaModel: Neo4jGraphQLSchemaModel; let userEntity: ConcreteEntity; let accountEntity: ConcreteEntity; diff --git a/packages/graphql/src/schema-model/generate-model.ts b/packages/graphql/src/schema-model/generate-model.ts index 849f22a59f..cf26afa0c6 100644 --- a/packages/graphql/src/schema-model/generate-model.ts +++ b/packages/graphql/src/schema-model/generate-model.ts @@ -22,7 +22,7 @@ import getFieldTypeMeta from "../schema/get-field-type-meta"; import { filterTruthy } from "../utils/utils"; import { Neo4jGraphQLSchemaModel } from "./Neo4jGraphQLSchemaModel"; import type { Operations } from "./Neo4jGraphQLSchemaModel"; -import { annotationToKey, type Annotation, AnnotationsKey } from "./annotation/Annotation"; +import type { Annotation } from "./annotation/Annotation"; import type { Attribute } from "./attribute/Attribute"; import { CompositeEntity } from "./entity/CompositeEntity"; import { ConcreteEntity } from "./entity/ConcreteEntity"; @@ -34,10 +34,9 @@ import type { DefinitionCollection } from "./parser/definition-collection"; import { getDefinitionCollection } from "./parser/definition-collection"; import { Operation } from "./Operation"; import { parseAttribute, parseField } from "./parser/parse-attribute"; -import { relationshipDirective } from "../graphql/directives"; +import { nodeDirective, relationshipDirective } from "../graphql/directives"; import { parseKeyAnnotation } from "./parser/annotations-parser/key-annotation"; import { parseDirectives } from "./parser/annotations-parser/parse-directives"; -import type { NodeAnnotation } from "./annotation/NodeAnnotation"; export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { const definitionCollection: DefinitionCollection = getDefinitionCollection(document); @@ -211,20 +210,27 @@ function generateConcreteEntity( ); const annotations = createEntityAnnotations(definition.directives || []); - const nodeAnnotation = annotations.find((annotation: Annotation) => { - AnnotationsKey.node === annotationToKey(annotation); - }); - const labels = nodeAnnotation ? (nodeAnnotation as NodeAnnotation).labels : [definition.name.value]; // TODO: add annotations inherited from interface return new ConcreteEntity({ name: definition.name.value, - labels, + labels: getLabels(definition), attributes: filterTruthy(fields) as Attribute[], annotations, }); } +function getLabels(entityDefinition: ObjectTypeDefinitionNode): string[] { + const nodeDirectiveUsage = findDirective(entityDefinition.directives || [], "node"); + if (nodeDirectiveUsage) { + const nodeArguments = parseArguments(nodeDirective, nodeDirectiveUsage) as { labels?: string[] }; + if (nodeArguments.labels?.length) { + return nodeArguments.labels; + } + } + return [entityDefinition.name.value]; +} + function createEntityAnnotations(directives: readonly DirectiveNode[]): Annotation[] { const entityAnnotations: Annotation[] = []; diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/node-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/node-annotation.test.ts deleted file mode 100644 index 28164dc919..0000000000 --- a/packages/graphql/src/schema-model/parser/annotations-parser/node-annotation.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { makeDirectiveNode } from "@graphql-tools/utils"; -import type { DirectiveNode } from "graphql"; -import { parseNodeAnnotation } from "./node-annotation"; -import { nodeDirective } from "../../../graphql/directives"; - -describe("parseNodeAnnotation", () => { - it("should parse correctly", () => { - const directive: DirectiveNode = makeDirectiveNode("node", { - labels: ["Movie", "Person"], - }, nodeDirective); - const nodeAnnotation = parseNodeAnnotation(directive); - expect(nodeAnnotation.labels).toEqual(["Movie", "Person"]); - }); -}); diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/node-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/node-annotation.ts deleted file mode 100644 index d2288fcf0d..0000000000 --- a/packages/graphql/src/schema-model/parser/annotations-parser/node-annotation.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import type { DirectiveNode } from "graphql"; -import { NodeAnnotation } from "../../annotation/NodeAnnotation"; -import { parseArguments } from "../parse-arguments"; -import { nodeDirective } from "../../../graphql/directives"; - -export function parseNodeAnnotation(directive: DirectiveNode): NodeAnnotation { - const { labels } = parseArguments(nodeDirective, directive) as { label: string; labels: string[] }; - - return new NodeAnnotation({ - labels, - }); -} diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/parse-directives.ts b/packages/graphql/src/schema-model/parser/annotations-parser/parse-directives.ts index a1bebef9ee..5b188b5671 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/parse-directives.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/parse-directives.ts @@ -26,7 +26,6 @@ import { parseDefaultAnnotation } from "./default-annotation"; import { parseIDAnnotation } from "./id-annotation"; import { parseFilterableAnnotation } from "./filterable-annotation"; import { parseMutationAnnotation } from "./mutation-annotation"; -import { parseNodeAnnotation } from "./node-annotation"; import { parsePluralAnnotation } from "./plural-annotation"; import { parsePopulatedByAnnotation } from "./populated-by-annotation"; import { parsePrivateAnnotation } from "./private-annotation"; @@ -77,8 +76,6 @@ export function parseDirectives(directives: readonly DirectiveNode[]): Annotatio return parseJWTPayloadAnnotation(directive); case AnnotationsKey.mutation: return parseMutationAnnotation(directive); - case AnnotationsKey.node: - return parseNodeAnnotation(directive); case AnnotationsKey.plural: return parsePluralAnnotation(directive); case AnnotationsKey.populatedBy: From 1ae82b0e4b1db64ddce4797ab9f4a228b53243f1 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Sat, 29 Jul 2023 01:18:29 +0100 Subject: [PATCH 17/22] polishing --- .../src/schema-model/generate-model.test.ts | 27 +++++---- .../src/schema-model/generate-model.ts | 6 +- .../coalesce-annotation.test.ts | 2 +- .../annotations-parser/coalesce-annotation.ts | 5 +- .../default-annotation.test.ts | 2 +- .../annotations-parser/default-annotation.ts | 5 +- ...arse-directives.ts => parse-annotation.ts} | 56 +++++++++---------- .../parser/parse-arguments.test.ts | 13 +++-- .../schema-model/parser/parse-attribute.ts | 34 ++--------- 9 files changed, 66 insertions(+), 84 deletions(-) rename packages/graphql/src/schema-model/parser/{annotations-parser/parse-directives.ts => parse-annotation.ts} (60%) diff --git a/packages/graphql/src/schema-model/generate-model.test.ts b/packages/graphql/src/schema-model/generate-model.test.ts index 462de7cb29..c1f1ba23e5 100644 --- a/packages/graphql/src/schema-model/generate-model.test.ts +++ b/packages/graphql/src/schema-model/generate-model.test.ts @@ -32,7 +32,6 @@ import type { AttributeAdapter } from "./attribute/model-adapters/AttributeAdapt import type { ConcreteEntityAdapter } from "./entity/model-adapters/ConcreteEntityAdapter"; import type { RelationshipAdapter } from "./relationship/model-adapters/RelationshipAdapter"; import type { ConcreteEntity } from "./entity/ConcreteEntity"; -import type { Relationship } from "./relationship/Relationship"; describe("Schema model generation", () => { test("parses @authentication directive with no arguments", () => { @@ -375,9 +374,9 @@ describe("Annotations", () => { accounts: [Account!]! @relationship(type: "HAS_ACCOUNT", direction: OUT) } - type Account @subscription(operations: [CREATE]){ + type Account @subscription(operations: [CREATE]) { id: ID! - username: String! @settable(onCreate: false) + accountName: String! @settable(onCreate: false) } extend type User { @@ -396,14 +395,20 @@ describe("Annotations", () => { expect(userQuery).toBeDefined(); expect(userQuery?.read).toBe(true); expect(userQuery?.aggregate).toBe(false); - + const userMutation = userEntity?.annotations[AnnotationsKey.mutation]; expect(userMutation).toBeDefined(); expect(userMutation?.operations).toStrictEqual(["CREATE", "UPDATE", "DELETE"]); - const userSubscription = userEntity?.annotations[AnnotationsKey.mutation]; + const userSubscription = userEntity?.annotations[AnnotationsKey.subscription]; expect(userSubscription).toBeDefined(); - expect(userSubscription?.operations).toStrictEqual(["CREATE", "UPDATE", "DELETE", "CREATE_RELATIONSHIP", "DELETE_RELATIONSHIP"]); + expect(userSubscription?.operations).toStrictEqual([ + "CREATE", + "UPDATE", + "DELETE", + "CREATE_RELATIONSHIP", + "DELETE_RELATIONSHIP", + ]); const accountSubscription = accountEntity?.annotations[AnnotationsKey.subscription]; expect(accountSubscription).toBeDefined(); @@ -416,13 +421,11 @@ describe("Annotations", () => { expect(userName?.annotations[AnnotationsKey.selectable]?.onRead).toBe(true); expect(userName?.annotations[AnnotationsKey.selectable]?.onAggregate).toBe(true); - const creationTime = accountEntity?.attributes.get("creationTime"); - expect(creationTime?.annotations[AnnotationsKey.settable]).toBeDefined(); - expect(creationTime?.annotations[AnnotationsKey.settable]?.onCreate).toBe(false); - expect(creationTime?.annotations[AnnotationsKey.settable]?.onUpdate).toBe(true); + const accountName = accountEntity?.attributes.get("accountName"); + expect(accountName?.annotations[AnnotationsKey.settable]).toBeDefined(); + expect(accountName?.annotations[AnnotationsKey.settable]?.onCreate).toBe(false); + expect(accountName?.annotations[AnnotationsKey.settable]?.onUpdate).toBe(true); }); - - }); describe("GraphQL adapters", () => { diff --git a/packages/graphql/src/schema-model/generate-model.ts b/packages/graphql/src/schema-model/generate-model.ts index cf26afa0c6..f1591b2254 100644 --- a/packages/graphql/src/schema-model/generate-model.ts +++ b/packages/graphql/src/schema-model/generate-model.ts @@ -36,7 +36,7 @@ import { Operation } from "./Operation"; import { parseAttribute, parseField } from "./parser/parse-attribute"; import { nodeDirective, relationshipDirective } from "../graphql/directives"; import { parseKeyAnnotation } from "./parser/annotations-parser/key-annotation"; -import { parseDirectives } from "./parser/annotations-parser/parse-directives"; +import { parseAnnotations } from "./parser/parse-annotation"; export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { const definitionCollection: DefinitionCollection = getDefinitionCollection(document); @@ -239,7 +239,7 @@ function createEntityAnnotations(directives: readonly DirectiveNode[]): Annotati if (keyDirectives) { entityAnnotations.push(parseKeyAnnotation(keyDirectives)); } - const annotations = parseDirectives(directives); + const annotations = parseAnnotations(directives); return entityAnnotations.concat(annotations); } @@ -247,7 +247,7 @@ function createEntityAnnotations(directives: readonly DirectiveNode[]): Annotati function createSchemaModelAnnotations(directives: readonly DirectiveNode[]): Annotation[] { const schemaModelAnnotations: Annotation[] = []; - const annotations = parseDirectives(directives); + const annotations = parseAnnotations(directives); return schemaModelAnnotations.concat(annotations); } diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/coalesce-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/coalesce-annotation.test.ts index ea4ca379ba..0ffcde13b9 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/coalesce-annotation.test.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/coalesce-annotation.test.ts @@ -78,6 +78,6 @@ describe("parseCoalesceAnnotation", () => { }, arguments: [], }; - expect(() => parseCoalesceAnnotation(directive)).toThrow("Argument \"value\" of required type \"ScalarOrEnum!\" was not provided."); + expect(() => parseCoalesceAnnotation(directive)).toThrow("@coalesce directive must have a value"); }); }); diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/coalesce-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/coalesce-annotation.ts index 8c3891f806..417748e488 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/coalesce-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/coalesce-annotation.ts @@ -20,11 +20,10 @@ import type { DirectiveNode } from "graphql"; import type { CoalesceAnnotationValue } from "../../annotation/CoalesceAnnotation"; import { CoalesceAnnotation } from "../../annotation/CoalesceAnnotation"; -import { parseArguments } from "../parse-arguments"; -import { coalesceDirective } from "../../../graphql/directives"; +import { parseArgumentsFromUnknownDirective } from "../parse-arguments"; export function parseCoalesceAnnotation(directive: DirectiveNode): CoalesceAnnotation { - const args = parseArguments(coalesceDirective, directive) as Record; + const args = parseArgumentsFromUnknownDirective(directive) as Record; if (!args || args.value === undefined) { throw new Error("@coalesce directive must have a value"); diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/default-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/default-annotation.test.ts index aa82aa4e0b..6ac2ad92fb 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/default-annotation.test.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/default-annotation.test.ts @@ -77,6 +77,6 @@ describe("parseDefaultAnnotation", () => { }, arguments: [], }; - expect(() => parseDefaultAnnotation(directive)).toThrow("Argument \"value\" of required type \"ScalarOrEnum!\" was not provided."); + expect(() => parseDefaultAnnotation(directive)).toThrow("@default directive must have a value"); }); }); diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/default-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/default-annotation.ts index 16c1f632f5..44a7c2126b 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/default-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/default-annotation.ts @@ -20,11 +20,10 @@ import type { DirectiveNode } from "graphql"; import type { DefaultAnnotationValue } from "../../annotation/DefaultAnnotation"; import { DefaultAnnotation } from "../../annotation/DefaultAnnotation"; -import { parseArguments } from "../parse-arguments"; -import { defaultDirective } from "../../../graphql/directives"; +import { parseArgumentsFromUnknownDirective } from "../parse-arguments"; export function parseDefaultAnnotation(directive: DirectiveNode): DefaultAnnotation { - const args = parseArguments(defaultDirective, directive) as Record; + const args = parseArgumentsFromUnknownDirective(directive) as Record; if (!args || args.value === undefined) { throw new Error("@default directive must have a value"); diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/parse-directives.ts b/packages/graphql/src/schema-model/parser/parse-annotation.ts similarity index 60% rename from packages/graphql/src/schema-model/parser/annotations-parser/parse-directives.ts rename to packages/graphql/src/schema-model/parser/parse-annotation.ts index 5b188b5671..319937aeaa 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/parse-directives.ts +++ b/packages/graphql/src/schema-model/parser/parse-annotation.ts @@ -18,35 +18,35 @@ */ import type { DirectiveNode } from "graphql"; -import { parseAliasAnnotation } from "./alias-annotation"; -import { parseCoalesceAnnotation } from "./coalesce-annotation"; -import { parseCypherAnnotation } from "./cypher-annotation"; -import { parseCustomResolverAnnotation } from "./custom-resolver-annotation"; -import { parseDefaultAnnotation } from "./default-annotation"; -import { parseIDAnnotation } from "./id-annotation"; -import { parseFilterableAnnotation } from "./filterable-annotation"; -import { parseMutationAnnotation } from "./mutation-annotation"; -import { parsePluralAnnotation } from "./plural-annotation"; -import { parsePopulatedByAnnotation } from "./populated-by-annotation"; -import { parsePrivateAnnotation } from "./private-annotation"; -import { parseQueryAnnotation } from "./query-annotation"; -import { parseQueryOptionsAnnotation } from "./query-options-annotation"; -import { parseSelectableAnnotation } from "./selectable-annotation"; -import { parseSettableAnnotation } from "./settable-annotation"; -import { parseSubscriptionAnnotation } from "./subscription-annotation"; -import { parseTimestampAnnotation } from "./timestamp-annotation"; -import { parseUniqueAnnotation } from "./unique-annotation"; -import { parseFullTextAnnotation } from "./full-text-annotation"; -import { parseJWTClaimAnnotation } from "./jwt-claim-annotation"; -import { parseJWTPayloadAnnotation } from "./jwt-payload-annotation"; -import { parseAuthorizationAnnotation } from "./authorization-annotation"; -import { parseAuthenticationAnnotation } from "./authentication-annotation"; -import { parseSubscriptionsAuthorizationAnnotation } from "./subscriptions-authorization-annotation"; -import { filterTruthy } from "../../../utils/utils"; -import type { Annotation } from "../../annotation/Annotation"; -import { AnnotationsKey } from "../../annotation/Annotation"; +import { parseAliasAnnotation } from "./annotations-parser/alias-annotation"; +import { parseCoalesceAnnotation } from "./annotations-parser/coalesce-annotation"; +import { parseCypherAnnotation } from "./annotations-parser/cypher-annotation"; +import { parseCustomResolverAnnotation } from "./annotations-parser/custom-resolver-annotation"; +import { parseDefaultAnnotation } from "./annotations-parser/default-annotation"; +import { parseIDAnnotation } from "./annotations-parser/id-annotation"; +import { parseFilterableAnnotation } from "./annotations-parser/filterable-annotation"; +import { parseMutationAnnotation } from "./annotations-parser/mutation-annotation"; +import { parsePluralAnnotation } from "./annotations-parser/plural-annotation"; +import { parsePopulatedByAnnotation } from "./annotations-parser/populated-by-annotation"; +import { parsePrivateAnnotation } from "./annotations-parser/private-annotation"; +import { parseQueryAnnotation } from "./annotations-parser/query-annotation"; +import { parseQueryOptionsAnnotation } from "./annotations-parser/query-options-annotation"; +import { parseSelectableAnnotation } from "./annotations-parser/selectable-annotation"; +import { parseSettableAnnotation } from "./annotations-parser/settable-annotation"; +import { parseSubscriptionAnnotation } from "./annotations-parser/subscription-annotation"; +import { parseTimestampAnnotation } from "./annotations-parser/timestamp-annotation"; +import { parseUniqueAnnotation } from "./annotations-parser/unique-annotation"; +import { parseFullTextAnnotation } from "./annotations-parser/full-text-annotation"; +import { parseJWTClaimAnnotation } from "./annotations-parser/jwt-claim-annotation"; +import { parseJWTPayloadAnnotation } from "./annotations-parser/jwt-payload-annotation"; +import { parseAuthorizationAnnotation } from "./annotations-parser/authorization-annotation"; +import { parseAuthenticationAnnotation } from "./annotations-parser/authentication-annotation"; +import { parseSubscriptionsAuthorizationAnnotation } from "./annotations-parser/subscriptions-authorization-annotation"; +import { filterTruthy } from "../../utils/utils"; +import type { Annotation } from "../annotation/Annotation"; +import { AnnotationsKey } from "../annotation/Annotation"; -export function parseDirectives(directives: readonly DirectiveNode[]): Annotation[] { +export function parseAnnotations(directives: readonly DirectiveNode[]): Annotation[] { return filterTruthy( directives.map((directive) => { switch (directive.name.value) { diff --git a/packages/graphql/src/schema-model/parser/parse-arguments.test.ts b/packages/graphql/src/schema-model/parser/parse-arguments.test.ts index c9f9ac9b58..bb070dc95c 100644 --- a/packages/graphql/src/schema-model/parser/parse-arguments.test.ts +++ b/packages/graphql/src/schema-model/parser/parse-arguments.test.ts @@ -24,6 +24,7 @@ import { GraphQLDirective, GraphQLFloat, GraphQLInt, + GraphQLList, GraphQLNonNull, Kind, parse, @@ -55,6 +56,10 @@ describe("parseArguments", () => { type: ScalarOrEnumType, defaultValue: "test", }, + customListScalar: { + type: new GraphQLList(ScalarOrEnumType), + defaultValue: ["test"], + }, }, }); }); @@ -63,7 +68,7 @@ describe("parseArguments", () => { const typeDefs = ` type User { - name: String @testDirective(booleanArgument: false, intArgument: 2, floatArgument: 4.0, customScalar: "123") + name: String @testDirective(booleanArgument: false, intArgument: 2, floatArgument: 4.0, customScalar: "123", customListScalar: ["123"]) } `; @@ -82,13 +87,13 @@ describe("parseArguments", () => { const args = parseArguments(testDirectiveDefinition, nameCoalesceUsage); - expect(args).toEqual({ booleanArgument: false, intArgument: 2, floatArgument: 4.0, customScalar: "123" }); + expect(args).toEqual({ booleanArgument: false, intArgument: 2, floatArgument: 4.0, customScalar: "123", customListScalar: ["123"] }); }); test("should use default values", () => { const typeDefs = ` type User { - name: String @testDirective(booleanArgument: false, intArgument: 2,) + name: String @testDirective(booleanArgument: false, intArgument: 2) } `; @@ -107,7 +112,7 @@ describe("parseArguments", () => { const args = parseArguments(testDirectiveDefinition, nameCoalesceUsage); - expect(args).toEqual({ booleanArgument: false, intArgument: 2, floatArgument: 3.0, customScalar: "test" }); + expect(args).toEqual({ booleanArgument: false, intArgument: 2, floatArgument: 3.0, customScalar: "test", customListScalar: ["test"] }); }); test("parseArgumentsFromUnknownDirective", () => { diff --git a/packages/graphql/src/schema-model/parser/parse-attribute.ts b/packages/graphql/src/schema-model/parser/parse-attribute.ts index 999be2985b..8533d95370 100644 --- a/packages/graphql/src/schema-model/parser/parse-attribute.ts +++ b/packages/graphql/src/schema-model/parser/parse-attribute.ts @@ -17,10 +17,8 @@ * limitations under the License. */ -import type { FieldDefinitionNode, TypeNode, DirectiveNode } from "graphql"; +import type { FieldDefinitionNode, TypeNode } from "graphql"; import { Kind } from "graphql"; -import { filterTruthy } from "../../utils/utils"; -import type { Annotation } from "../annotation/Annotation"; import type { AttributeType, Neo4jGraphQLScalarType } from "../attribute/AttributeType"; import { ScalarType, @@ -37,20 +35,16 @@ import { } from "../attribute/AttributeType"; import { Attribute } from "../attribute/Attribute"; import { Field } from "../attribute/Field"; -import { parseAuthenticationAnnotation } from "./annotations-parser/authentication-annotation"; -import { parseAuthorizationAnnotation } from "./annotations-parser/authorization-annotation"; -import { parseCypherAnnotation } from "./annotations-parser/cypher-annotation"; import type { DefinitionCollection } from "./definition-collection"; -import { parseSubscriptionsAuthorizationAnnotation } from "./annotations-parser/subscriptions-authorization-annotation"; +import { parseAnnotations } from "./parse-annotation"; -// TODO: figure out difference between field and attribute export function parseAttribute( field: FieldDefinitionNode, definitionCollection: DefinitionCollection ): Attribute | Field { const name = field.name.value; const type = parseTypeNode(definitionCollection, field.type); - const annotations = createFieldAnnotations(field.directives || []); + const annotations = parseAnnotations(field.directives || []); return new Attribute({ name, @@ -59,9 +53,10 @@ export function parseAttribute( }); } +// we may want to remove Fields from the schema model export function parseField(field: FieldDefinitionNode): Field { const name = field.name.value; - const annotations = createFieldAnnotations(field.directives || []); + const annotations = parseAnnotations(field.directives || []); return new Field({ name, annotations, @@ -145,22 +140,3 @@ function isNeo4jGraphQLNumberType(value: string): value is Neo4jGraphQLNumberTyp function isNeo4jGraphQLTemporalType(value: string): value is Neo4jGraphQLTemporalType { return Object.values(Neo4jGraphQLTemporalType).includes(value); } - -function createFieldAnnotations(directives: readonly DirectiveNode[]): Annotation[] { - return filterTruthy( - directives.map((directive) => { - switch (directive.name.value) { - case "cypher": - return parseCypherAnnotation(directive); - case "authorization": - return parseAuthorizationAnnotation(directive); - case "authentication": - return parseAuthenticationAnnotation(directive); - case "subscriptionsAuthorization": - return parseSubscriptionsAuthorizationAnnotation(directive); - default: - return undefined; - } - }) - ); -} From 4ed44b7c62d747b748a836e450bfadf1b77aba0b Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Sat, 29 Jul 2023 01:22:11 +0100 Subject: [PATCH 18/22] add license header --- .../RelationshipAdapter.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.test.ts b/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.test.ts index e10adf191f..38ef7b3f34 100644 --- a/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.test.ts +++ b/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.test.ts @@ -1,3 +1,22 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { Attribute } from "../../attribute/Attribute"; import { GraphQLBuiltInScalarType, ScalarType } from "../../attribute/AttributeType"; import { UniqueAnnotation } from "../../annotation/UniqueAnnotation"; From 68a189fb10100e8394b511433def824b4bdc0af2 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Mon, 31 Jul 2023 09:25:53 +0100 Subject: [PATCH 19/22] Update packages/graphql/src/schema-model/parser/parse-arguments.ts --- packages/graphql/src/schema-model/parser/parse-arguments.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphql/src/schema-model/parser/parse-arguments.ts b/packages/graphql/src/schema-model/parser/parse-arguments.ts index 4a54f5fc09..1da4690099 100644 --- a/packages/graphql/src/schema-model/parser/parse-arguments.ts +++ b/packages/graphql/src/schema-model/parser/parse-arguments.ts @@ -95,4 +95,4 @@ export function parseArguments( coercedValues[name] = coercedValue; } return coercedValues; -} \ No newline at end of file +} From dac22c2d1a8f1d098edd3bd8bc2f7d7848ceeb53 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Thu, 3 Aug 2023 18:04:57 +0100 Subject: [PATCH 20/22] add graphqlDefaultValue to Attribute --- .../annotation/AliasAnnotation.test.ts | 29 ----- .../annotation/AliasAnnotation.ts | 26 ----- .../src/schema-model/annotation/Annotation.ts | 5 - .../src/schema-model/attribute/Attribute.ts | 46 +++++++- .../model-adapters/AttributeAdapter.test.ts | 106 ++++++++++++++++++ .../model-adapters/AttributeAdapter.ts | 27 ++++- .../src/schema-model/generate-model.test.ts | 25 ++++- .../src/schema-model/generate-model.ts | 4 +- .../alias-annotation.test.ts | 29 ----- .../annotations-parser/alias-annotation.ts | 39 ------- .../parser/definition-collection.ts | 3 +- .../schema-model/parser/parse-annotation.ts | 3 - .../schema-model/parser/parse-attribute.ts | 34 +++++- .../graphql/src/schema-model/parser/utils.ts | 2 +- .../model-adapters/RelationshipAdapter.ts | 36 ------ 15 files changed, 235 insertions(+), 179 deletions(-) delete mode 100644 packages/graphql/src/schema-model/annotation/AliasAnnotation.test.ts delete mode 100644 packages/graphql/src/schema-model/annotation/AliasAnnotation.ts delete mode 100644 packages/graphql/src/schema-model/parser/annotations-parser/alias-annotation.test.ts delete mode 100644 packages/graphql/src/schema-model/parser/annotations-parser/alias-annotation.ts diff --git a/packages/graphql/src/schema-model/annotation/AliasAnnotation.test.ts b/packages/graphql/src/schema-model/annotation/AliasAnnotation.test.ts deleted file mode 100644 index 0d2796a434..0000000000 --- a/packages/graphql/src/schema-model/annotation/AliasAnnotation.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { AliasAnnotation } from "./AliasAnnotation"; - -describe("AliasAnnotation", () => { - it("initialize class correctly when property param is set", () => { - const aliasAnnotation = new AliasAnnotation({ - property: "test", - }); - expect(aliasAnnotation.property).toBe("test"); - }); -}); diff --git a/packages/graphql/src/schema-model/annotation/AliasAnnotation.ts b/packages/graphql/src/schema-model/annotation/AliasAnnotation.ts deleted file mode 100644 index ed65256827..0000000000 --- a/packages/graphql/src/schema-model/annotation/AliasAnnotation.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export class AliasAnnotation { - public readonly property: string; - - constructor({ property }: { property: string }) { - this.property = property; - } -} diff --git a/packages/graphql/src/schema-model/annotation/Annotation.ts b/packages/graphql/src/schema-model/annotation/Annotation.ts index bceaf859ad..99084de0d6 100644 --- a/packages/graphql/src/schema-model/annotation/Annotation.ts +++ b/packages/graphql/src/schema-model/annotation/Annotation.ts @@ -17,7 +17,6 @@ * limitations under the License. */ -import { AliasAnnotation } from "./AliasAnnotation"; import { AuthenticationAnnotation } from "./AuthenticationAnnotation"; import { AuthorizationAnnotation } from "./AuthorizationAnnotation"; import { CoalesceAnnotation } from "./CoalesceAnnotation"; @@ -49,7 +48,6 @@ export type Annotation = | AuthenticationAnnotation | KeyAnnotation | SubscriptionsAuthorizationAnnotation - | AliasAnnotation | QueryOptionsAnnotation | DefaultAnnotation | CoalesceAnnotation @@ -72,7 +70,6 @@ export type Annotation = export enum AnnotationsKey { - alias = "alias", authentication = "authentication", authorization = "authorization", coalesce = "coalesce", @@ -105,7 +102,6 @@ export type Annotations = { [AnnotationsKey.authentication]: AuthenticationAnnotation; [AnnotationsKey.key]: KeyAnnotation; [AnnotationsKey.subscriptionsAuthorization]: SubscriptionsAuthorizationAnnotation; - [AnnotationsKey.alias]: AliasAnnotation; [AnnotationsKey.queryOptions]: QueryOptionsAnnotation; [AnnotationsKey.default]: DefaultAnnotation; [AnnotationsKey.coalesce]: CoalesceAnnotation; @@ -133,7 +129,6 @@ export function annotationToKey(ann: Annotation): keyof Annotations { if (ann instanceof AuthenticationAnnotation) return AnnotationsKey.authentication; if (ann instanceof KeyAnnotation) return AnnotationsKey.key; if (ann instanceof SubscriptionsAuthorizationAnnotation) return AnnotationsKey.subscriptionsAuthorization; - if (ann instanceof AliasAnnotation) return AnnotationsKey.alias; if (ann instanceof QueryOptionsAnnotation) return AnnotationsKey.queryOptions; if (ann instanceof DefaultAnnotation) return AnnotationsKey.default; if (ann instanceof CoalesceAnnotation) return AnnotationsKey.coalesce; diff --git a/packages/graphql/src/schema-model/attribute/Attribute.ts b/packages/graphql/src/schema-model/attribute/Attribute.ts index d04e602bc4..f70607fc30 100644 --- a/packages/graphql/src/schema-model/attribute/Attribute.ts +++ b/packages/graphql/src/schema-model/attribute/Attribute.ts @@ -17,28 +17,68 @@ * limitations under the License. */ +import type { DateTime, Duration, Integer, LocalDateTime, LocalTime, Time } from "neo4j-driver"; import { Neo4jGraphQLSchemaValidationError } from "../../classes/Error"; import { annotationToKey, type Annotation, type Annotations } from "../annotation/Annotation"; import type { AttributeType } from "./AttributeType"; +export type PopulatedBy = { + callback: string; + when: ("CREATE" | "UPDATE")[]; +}; + +export type GraphQLDefaultValueType = { + value?: + | string + | number + | boolean + | Time + | LocalTime + | LocalDateTime + | Duration + | DateTime + | Date + | Integer; + populatedBy?: PopulatedBy; +}; + export class Attribute { public readonly name: string; public readonly annotations: Partial = {}; public readonly type: AttributeType; - - constructor({ name, annotations = [], type }: { name: string; annotations: Annotation[]; type: AttributeType }) { + public readonly databaseName: string; + public readonly defaultValue?: GraphQLDefaultValueType; + + constructor({ + name, + annotations = [], + type, + databaseName, + defaultValue, + }: { + name: string; + annotations: Annotation[]; + type: AttributeType; + databaseName?: string; + defaultValue?: GraphQLDefaultValueType; + }) { this.name = name; this.type = type; + this.databaseName = databaseName ?? name; + this.defaultValue = defaultValue; + for (const annotation of annotations) { this.addAnnotation(annotation); } - } + } public clone(): Attribute { return new Attribute({ name: this.name, annotations: Object.values(this.annotations), type: this.type, + databaseName: this.databaseName, + defaultValue: this.defaultValue, }); } diff --git a/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.test.ts b/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.test.ts index 5be19eed4d..86754a1703 100644 --- a/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.test.ts +++ b/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.test.ts @@ -591,4 +591,110 @@ describe("Attribute", () => { expect(attribute.mathModel.getDivide()).toMatchInlineSnapshot(`"test_DIVIDE"`); }); }); + + describe("getGraphQLDefaultCallBack", () => { + test("getGraphQLDefaultCallBack should return default value wrapped as a callback", () => { + const attribute = new AttributeAdapter( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.Boolean, true), + defaultValue: { + value: false, + }, + }) + ); + + const cb = attribute.getGraphQLDefaultCallBack(); + expect(cb).toBeDefined(); + expect(cb).toBeInstanceOf(Function); + expect((cb as () => any)()).toBe(false); + }); + + test("getGraphQLDefaultCallBack should return undefined if no default value is set", () => { + const attribute = new AttributeAdapter( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.Boolean, true), + }) + ); + + const slugCB = () => { + return true; + }; + + expect(attribute.getGraphQLDefaultCallBack("CREATE", { slug: slugCB })).toBeUndefined(); + }); + + test("getGraphQLDefaultCallBack should return undefined if a default value is set but for a different event", () => { + const attribute = new AttributeAdapter( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.Boolean, true), + defaultValue: { + populatedBy: { + callback: "slug", + when: ["CREATE"], + }, + }, + }) + ); + + const slugCB = () => { + return true; + }; + + expect(attribute.getGraphQLDefaultCallBack("UPDATE", { slug: slugCB })).toBeUndefined(); + }); + + test("getGraphQLDefaultCallBack should return a callback if a default value is set for the correct event", () => { + const attribute = new AttributeAdapter( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.Boolean, true), + defaultValue: { + populatedBy: { + callback: "slug", + when: ["CREATE"], + }, + }, + }) + ); + + const slugCB = () => { + return true; + }; + const cb = attribute.getGraphQLDefaultCallBack("CREATE", { slug: slugCB }); + expect(cb).toBeDefined(); + expect(cb).toBeInstanceOf(Function); + expect((cb as () => any)()).toBe(true); + }); + + test("getGraphQLDefaultCallBack should return undefined if the user callback is not defined", () => { + const attribute = new AttributeAdapter( + new Attribute({ + name: "test", + annotations: [], + type: new ScalarType(GraphQLBuiltInScalarType.Boolean, true), + defaultValue: { + populatedBy: { + callback: "slug", + when: ["CREATE"], + }, + }, + }) + ); + + const slugCB = () => { + return true; + }; + + const cb = attribute.getGraphQLDefaultCallBack("CREATE", { notTheRightSlug: slugCB }); + expect(cb).toBeUndefined(); + }); + + }); }); diff --git a/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.ts b/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.ts index 3876855ec3..92ad5cb428 100644 --- a/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.ts +++ b/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.ts @@ -20,7 +20,7 @@ import { MathAdapter } from "./MathAdapter"; import { AggregationAdapter } from "./AggregationAdapter"; import { ListAdapter } from "./ListAdapter"; -import type { Attribute } from "../Attribute"; +import type { Attribute, GraphQLDefaultValueType } from "../Attribute"; import type { Annotations } from "../../annotation/Annotation"; import { EnumType, @@ -37,6 +37,7 @@ import { UserScalarType, } from "../AttributeType"; import type { AttributeType, Neo4jGraphQLScalarType } from "../AttributeType"; +import type { Neo4jGraphQLCallback, Neo4jGraphQLCallbacks } from "../../../types"; export class AttributeAdapter { private _listModel: ListAdapter | undefined; @@ -45,11 +46,15 @@ export class AttributeAdapter { public name: string; public annotations: Partial; public type: AttributeType; + public databaseName: string; + private defaultValue?: GraphQLDefaultValueType; constructor(attribute: Attribute) { this.name = attribute.name; this.type = attribute.type; this.annotations = attribute.annotations; + this.databaseName = attribute.databaseName; + this.defaultValue = attribute.defaultValue; } /** @@ -267,4 +272,24 @@ export class AttributeAdapter { isCypher(): boolean { return this.annotations.cypher ? true : false; } + /** + * Returns a callback function that returns the default value for the attribute if the user has provided one by using the populatedBy or default directives, + * if the user has not provided a default value or the trigger event does not match then returns undefined. + */ + getGraphQLDefaultCallBack( + when?: "CREATE" | "UPDATE", + callbacks?: Neo4jGraphQLCallbacks + ): Neo4jGraphQLCallback | (() => GraphQLDefaultValueType["value"]) | undefined { + if (this.defaultValue?.value !== undefined) { + return () => this.defaultValue?.value; + } + if ( + when && + callbacks && + this.defaultValue?.populatedBy && + this.defaultValue?.populatedBy.when.some((w) => w === when) + ) { + return callbacks[this.defaultValue.populatedBy.callback]; + } + } } diff --git a/packages/graphql/src/schema-model/generate-model.test.ts b/packages/graphql/src/schema-model/generate-model.test.ts index c1f1ba23e5..8016ccd95c 100644 --- a/packages/graphql/src/schema-model/generate-model.test.ts +++ b/packages/graphql/src/schema-model/generate-model.test.ts @@ -361,7 +361,7 @@ describe("Relationship", () => { }); }); -describe("Annotations", () => { +describe("Annotations & Attributes", () => { let schemaModel: Neo4jGraphQLSchemaModel; let userEntity: ConcreteEntity; let accountEntity: ConcreteEntity; @@ -370,7 +370,9 @@ describe("Annotations", () => { const typeDefs = gql` type User @query @mutation @subscription { id: ID! - name: String! @selectable(onAggregate: true) + name: String! @selectable(onAggregate: true) @alias(property: "dbName") + defaultName: String! @default(value: "John") + age: Int! @populatedBy(callback: "thisCallback" operations: [CREATE]) accounts: [Account!]! @relationship(type: "HAS_ACCOUNT", direction: OUT) } @@ -421,10 +423,29 @@ describe("Annotations", () => { expect(userName?.annotations[AnnotationsKey.selectable]?.onRead).toBe(true); expect(userName?.annotations[AnnotationsKey.selectable]?.onAggregate).toBe(true); + expect(userName?.databaseName).toBeDefined(); + expect(userName?.databaseName).toBe("dbName"); + + const defaultName = userEntity?.attributes.get("defaultName"); + expect(defaultName).toBeDefined(); + expect(defaultName?.defaultValue).toBeDefined(); + expect(defaultName?.defaultValue?.value).toBe("John"); + + const age = userEntity?.attributes.get("age"); + expect(age).toBeDefined(); + expect(age?.defaultValue).toBeDefined(); + expect(age?.defaultValue?.populatedBy).toBeDefined(); + expect(age?.defaultValue?.populatedBy?.callback).toBe("thisCallback"); + expect(age?.defaultValue?.populatedBy?.when).toStrictEqual(["CREATE"]); + const accountName = accountEntity?.attributes.get("accountName"); expect(accountName?.annotations[AnnotationsKey.settable]).toBeDefined(); expect(accountName?.annotations[AnnotationsKey.settable]?.onCreate).toBe(false); expect(accountName?.annotations[AnnotationsKey.settable]?.onUpdate).toBe(true); + + expect(accountName?.databaseName).toBeDefined(); + expect(accountName?.databaseName).toBe("accountName"); + }); }); diff --git a/packages/graphql/src/schema-model/generate-model.ts b/packages/graphql/src/schema-model/generate-model.ts index f1591b2254..0f72261bec 100644 --- a/packages/graphql/src/schema-model/generate-model.ts +++ b/packages/graphql/src/schema-model/generate-model.ts @@ -163,7 +163,7 @@ function generateRelationshipField( definitionCollection: DefinitionCollection ): Relationship | undefined { const fieldTypeMeta = getFieldTypeMeta(field.type); - const relationshipUsage = findDirective(field.directives || [], "relationship"); + const relationshipUsage = findDirective(field.directives, "relationship"); if (!relationshipUsage) return undefined; const fieldName = field.name.value; @@ -221,7 +221,7 @@ function generateConcreteEntity( } function getLabels(entityDefinition: ObjectTypeDefinitionNode): string[] { - const nodeDirectiveUsage = findDirective(entityDefinition.directives || [], "node"); + const nodeDirectiveUsage = findDirective(entityDefinition.directives, nodeDirective.name); if (nodeDirectiveUsage) { const nodeArguments = parseArguments(nodeDirective, nodeDirectiveUsage) as { labels?: string[] }; if (nodeArguments.labels?.length) { diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/alias-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/alias-annotation.test.ts deleted file mode 100644 index ecc499420e..0000000000 --- a/packages/graphql/src/schema-model/parser/annotations-parser/alias-annotation.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { makeDirectiveNode } from "@graphql-tools/utils"; -import { parseAliasAnnotation } from "./alias-annotation"; - -describe("parseAliasAnnotation", () => { - it("should parse correctly", () => { - const directive = makeDirectiveNode("alias", { property: "dbId" }); - const aliasAnnotation = parseAliasAnnotation(directive); - expect(aliasAnnotation.property).toBe("dbId"); - }); -}); diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/alias-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/alias-annotation.ts deleted file mode 100644 index c4940cd279..0000000000 --- a/packages/graphql/src/schema-model/parser/annotations-parser/alias-annotation.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import type { DirectiveNode } from "graphql"; -import { Neo4jGraphQLSchemaValidationError } from "../../../classes"; -import { AliasAnnotation } from "../../annotation/AliasAnnotation"; -import { aliasDirective } from "../../../graphql/directives"; -import { parseArguments } from "../parse-arguments"; - -export function parseAliasAnnotation(directive: DirectiveNode): AliasAnnotation { - const { property, ...unrecognizedArguments } = parseArguments(aliasDirective, directive) as { - property: string; - }; - - if (Object.keys(unrecognizedArguments).length) { - throw new Neo4jGraphQLSchemaValidationError( - `@alias unrecognized arguments: ${Object.keys(unrecognizedArguments).join(", ")}` - ); - } - - return new AliasAnnotation({ - property, - }); -} diff --git a/packages/graphql/src/schema-model/parser/definition-collection.ts b/packages/graphql/src/schema-model/parser/definition-collection.ts index fc05b68106..be1dc5ec3d 100644 --- a/packages/graphql/src/schema-model/parser/definition-collection.ts +++ b/packages/graphql/src/schema-model/parser/definition-collection.ts @@ -56,7 +56,7 @@ export function getDefinitionCollection(document: DocumentNode): DefinitionColle definitionCollection.scalarTypes.set(definition.name.value, definition); break; case Kind.OBJECT_TYPE_DEFINITION: - if (definition.directives && findDirective(definition.directives, jwt.name)) { + if (findDirective(definition.directives, jwt.name)) { definitionCollection.jwtPayload = definition; } else if (!isRootType(definition)) { definitionCollection.nodes.set(definition.name.value, definition); @@ -69,7 +69,6 @@ export function getDefinitionCollection(document: DocumentNode): DefinitionColle break; case Kind.INTERFACE_TYPE_DEFINITION: if ( - definition.directives && findDirective(definition.directives, relationshipPropertiesDirective.name) ) { definitionCollection.relationshipProperties.set(definition.name.value, definition); diff --git a/packages/graphql/src/schema-model/parser/parse-annotation.ts b/packages/graphql/src/schema-model/parser/parse-annotation.ts index 319937aeaa..dbdcff7951 100644 --- a/packages/graphql/src/schema-model/parser/parse-annotation.ts +++ b/packages/graphql/src/schema-model/parser/parse-annotation.ts @@ -18,7 +18,6 @@ */ import type { DirectiveNode } from "graphql"; -import { parseAliasAnnotation } from "./annotations-parser/alias-annotation"; import { parseCoalesceAnnotation } from "./annotations-parser/coalesce-annotation"; import { parseCypherAnnotation } from "./annotations-parser/cypher-annotation"; import { parseCustomResolverAnnotation } from "./annotations-parser/custom-resolver-annotation"; @@ -50,8 +49,6 @@ export function parseAnnotations(directives: readonly DirectiveNode[]): Annotati return filterTruthy( directives.map((directive) => { switch (directive.name.value) { - case AnnotationsKey.alias: - return parseAliasAnnotation(directive); case AnnotationsKey.authentication: return parseAuthenticationAnnotation(directive); case AnnotationsKey.authorization: diff --git a/packages/graphql/src/schema-model/parser/parse-attribute.ts b/packages/graphql/src/schema-model/parser/parse-attribute.ts index 8533d95370..c01a829b95 100644 --- a/packages/graphql/src/schema-model/parser/parse-attribute.ts +++ b/packages/graphql/src/schema-model/parser/parse-attribute.ts @@ -33,10 +33,14 @@ import { Neo4jGraphQLNumberType, Neo4jGraphQLTemporalType, } from "../attribute/AttributeType"; +import type { GraphQLDefaultValueType } from "../attribute/Attribute"; import { Attribute } from "../attribute/Attribute"; import { Field } from "../attribute/Field"; import type { DefinitionCollection } from "./definition-collection"; import { parseAnnotations } from "./parse-annotation"; +import { aliasDirective, defaultDirective, populatedByDirective } from "../../graphql/directives"; +import { parseArguments } from "./parse-arguments"; +import { findDirective } from "./utils"; export function parseAttribute( field: FieldDefinitionNode, @@ -45,14 +49,42 @@ export function parseAttribute( const name = field.name.value; const type = parseTypeNode(definitionCollection, field.type); const annotations = parseAnnotations(field.directives || []); - + const databaseName = getDatabaseName(field); + const defaultValue = getDefaultValue(field); return new Attribute({ name, annotations, type, + databaseName, + defaultValue, }); } +function getDefaultValue(fieldDefinitionNode: FieldDefinitionNode): GraphQLDefaultValueType | undefined { + const defaultUsage = findDirective(fieldDefinitionNode.directives, defaultDirective.name); + + if (defaultUsage) { + const { value } = parseArguments(defaultDirective, defaultUsage) as { value: string }; + return { value }; + } + const populatedByUsage = findDirective(fieldDefinitionNode.directives, populatedByDirective.name); + if (populatedByUsage) { + const { callback, operations: when } = parseArguments(populatedByDirective, populatedByUsage) as { + callback: string; + operations: ("CREATE" | "UPDATE")[]; + }; + return { populatedBy: { callback, when } }; + } +} + +function getDatabaseName(fieldDefinitionNode: FieldDefinitionNode): string | undefined { + const aliasUsage = findDirective(fieldDefinitionNode.directives, aliasDirective.name); + if (aliasUsage) { + const { property } = parseArguments(aliasDirective, aliasUsage) as { property: string }; + return property; + } +} + // we may want to remove Fields from the schema model export function parseField(field: FieldDefinitionNode): Field { const name = field.name.value; diff --git a/packages/graphql/src/schema-model/parser/utils.ts b/packages/graphql/src/schema-model/parser/utils.ts index ee6a56e0aa..e3bb622a0d 100644 --- a/packages/graphql/src/schema-model/parser/utils.ts +++ b/packages/graphql/src/schema-model/parser/utils.ts @@ -20,7 +20,7 @@ import type { DirectiveNode } from "graphql"; -export function findDirective(directives: readonly DirectiveNode[], name: string): DirectiveNode | undefined { +export function findDirective(directives: readonly DirectiveNode[] = [], name: string): DirectiveNode | undefined { return directives.find((d) => { return d.name.value === name; }); diff --git a/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts b/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts index 13190a40b5..777ebd6d31 100644 --- a/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts +++ b/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts @@ -48,42 +48,6 @@ export class RelationshipAdapter { return `${this.source.name}${upperFirst(this.name)}Relationship`; } - /* constructor({ - name, - type, - attributes = new Map(), - source, - target, - direction, - queryDirection, - nestedOperations, - aggregate, - }: { - name: string; - type: string; - attributes?: Map; - source: ConcreteEntity | ConcreteEntityAdapter; - target: Entity; - direction: RelationshipDirection; - queryDirection: QueryDirection; - nestedOperations: NestedOperation[]; - aggregate: boolean; - }) { - this.name = name; - this.type = type; - if (source instanceof ConcreteEntity) { - this.source = new ConcreteEntityAdapter(source); - } else { - this.source = source; - } - this.direction = direction; - this.queryDirection = queryDirection; - this.nestedOperations = nestedOperations; - this.aggregate = aggregate; - this.rawEntity = target; - this.initAttributes(attributes); - } */ - constructor(relationship: Relationship, sourceAdapter?: ConcreteEntityAdapter) { const { name, From 73fbe01de49f007b55b95c9201c4c66cfd6bef95 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Fri, 4 Aug 2023 10:36:34 +0100 Subject: [PATCH 21/22] remove the graphqlDefault logic --- .../src/schema-model/attribute/Attribute.ts | 26 ----- .../model-adapters/AttributeAdapter.test.ts | 106 ------------------ .../model-adapters/AttributeAdapter.ts | 25 +---- .../src/schema-model/generate-model.test.ts | 11 +- .../schema-model/parser/parse-attribute.ts | 22 +--- 5 files changed, 7 insertions(+), 183 deletions(-) diff --git a/packages/graphql/src/schema-model/attribute/Attribute.ts b/packages/graphql/src/schema-model/attribute/Attribute.ts index f70607fc30..8bed16fc5c 100644 --- a/packages/graphql/src/schema-model/attribute/Attribute.ts +++ b/packages/graphql/src/schema-model/attribute/Attribute.ts @@ -17,55 +17,30 @@ * limitations under the License. */ -import type { DateTime, Duration, Integer, LocalDateTime, LocalTime, Time } from "neo4j-driver"; import { Neo4jGraphQLSchemaValidationError } from "../../classes/Error"; import { annotationToKey, type Annotation, type Annotations } from "../annotation/Annotation"; import type { AttributeType } from "./AttributeType"; -export type PopulatedBy = { - callback: string; - when: ("CREATE" | "UPDATE")[]; -}; - -export type GraphQLDefaultValueType = { - value?: - | string - | number - | boolean - | Time - | LocalTime - | LocalDateTime - | Duration - | DateTime - | Date - | Integer; - populatedBy?: PopulatedBy; -}; - export class Attribute { public readonly name: string; public readonly annotations: Partial = {}; public readonly type: AttributeType; public readonly databaseName: string; - public readonly defaultValue?: GraphQLDefaultValueType; constructor({ name, annotations = [], type, databaseName, - defaultValue, }: { name: string; annotations: Annotation[]; type: AttributeType; databaseName?: string; - defaultValue?: GraphQLDefaultValueType; }) { this.name = name; this.type = type; this.databaseName = databaseName ?? name; - this.defaultValue = defaultValue; for (const annotation of annotations) { this.addAnnotation(annotation); @@ -78,7 +53,6 @@ export class Attribute { annotations: Object.values(this.annotations), type: this.type, databaseName: this.databaseName, - defaultValue: this.defaultValue, }); } diff --git a/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.test.ts b/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.test.ts index 86754a1703..5be19eed4d 100644 --- a/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.test.ts +++ b/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.test.ts @@ -591,110 +591,4 @@ describe("Attribute", () => { expect(attribute.mathModel.getDivide()).toMatchInlineSnapshot(`"test_DIVIDE"`); }); }); - - describe("getGraphQLDefaultCallBack", () => { - test("getGraphQLDefaultCallBack should return default value wrapped as a callback", () => { - const attribute = new AttributeAdapter( - new Attribute({ - name: "test", - annotations: [], - type: new ScalarType(GraphQLBuiltInScalarType.Boolean, true), - defaultValue: { - value: false, - }, - }) - ); - - const cb = attribute.getGraphQLDefaultCallBack(); - expect(cb).toBeDefined(); - expect(cb).toBeInstanceOf(Function); - expect((cb as () => any)()).toBe(false); - }); - - test("getGraphQLDefaultCallBack should return undefined if no default value is set", () => { - const attribute = new AttributeAdapter( - new Attribute({ - name: "test", - annotations: [], - type: new ScalarType(GraphQLBuiltInScalarType.Boolean, true), - }) - ); - - const slugCB = () => { - return true; - }; - - expect(attribute.getGraphQLDefaultCallBack("CREATE", { slug: slugCB })).toBeUndefined(); - }); - - test("getGraphQLDefaultCallBack should return undefined if a default value is set but for a different event", () => { - const attribute = new AttributeAdapter( - new Attribute({ - name: "test", - annotations: [], - type: new ScalarType(GraphQLBuiltInScalarType.Boolean, true), - defaultValue: { - populatedBy: { - callback: "slug", - when: ["CREATE"], - }, - }, - }) - ); - - const slugCB = () => { - return true; - }; - - expect(attribute.getGraphQLDefaultCallBack("UPDATE", { slug: slugCB })).toBeUndefined(); - }); - - test("getGraphQLDefaultCallBack should return a callback if a default value is set for the correct event", () => { - const attribute = new AttributeAdapter( - new Attribute({ - name: "test", - annotations: [], - type: new ScalarType(GraphQLBuiltInScalarType.Boolean, true), - defaultValue: { - populatedBy: { - callback: "slug", - when: ["CREATE"], - }, - }, - }) - ); - - const slugCB = () => { - return true; - }; - const cb = attribute.getGraphQLDefaultCallBack("CREATE", { slug: slugCB }); - expect(cb).toBeDefined(); - expect(cb).toBeInstanceOf(Function); - expect((cb as () => any)()).toBe(true); - }); - - test("getGraphQLDefaultCallBack should return undefined if the user callback is not defined", () => { - const attribute = new AttributeAdapter( - new Attribute({ - name: "test", - annotations: [], - type: new ScalarType(GraphQLBuiltInScalarType.Boolean, true), - defaultValue: { - populatedBy: { - callback: "slug", - when: ["CREATE"], - }, - }, - }) - ); - - const slugCB = () => { - return true; - }; - - const cb = attribute.getGraphQLDefaultCallBack("CREATE", { notTheRightSlug: slugCB }); - expect(cb).toBeUndefined(); - }); - - }); }); diff --git a/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.ts b/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.ts index 92ad5cb428..12fff4c7b7 100644 --- a/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.ts +++ b/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.ts @@ -20,7 +20,7 @@ import { MathAdapter } from "./MathAdapter"; import { AggregationAdapter } from "./AggregationAdapter"; import { ListAdapter } from "./ListAdapter"; -import type { Attribute, GraphQLDefaultValueType } from "../Attribute"; +import type { Attribute } from "../Attribute"; import type { Annotations } from "../../annotation/Annotation"; import { EnumType, @@ -37,7 +37,6 @@ import { UserScalarType, } from "../AttributeType"; import type { AttributeType, Neo4jGraphQLScalarType } from "../AttributeType"; -import type { Neo4jGraphQLCallback, Neo4jGraphQLCallbacks } from "../../../types"; export class AttributeAdapter { private _listModel: ListAdapter | undefined; @@ -47,14 +46,12 @@ export class AttributeAdapter { public annotations: Partial; public type: AttributeType; public databaseName: string; - private defaultValue?: GraphQLDefaultValueType; constructor(attribute: Attribute) { this.name = attribute.name; this.type = attribute.type; this.annotations = attribute.annotations; this.databaseName = attribute.databaseName; - this.defaultValue = attribute.defaultValue; } /** @@ -272,24 +269,4 @@ export class AttributeAdapter { isCypher(): boolean { return this.annotations.cypher ? true : false; } - /** - * Returns a callback function that returns the default value for the attribute if the user has provided one by using the populatedBy or default directives, - * if the user has not provided a default value or the trigger event does not match then returns undefined. - */ - getGraphQLDefaultCallBack( - when?: "CREATE" | "UPDATE", - callbacks?: Neo4jGraphQLCallbacks - ): Neo4jGraphQLCallback | (() => GraphQLDefaultValueType["value"]) | undefined { - if (this.defaultValue?.value !== undefined) { - return () => this.defaultValue?.value; - } - if ( - when && - callbacks && - this.defaultValue?.populatedBy && - this.defaultValue?.populatedBy.when.some((w) => w === when) - ) { - return callbacks[this.defaultValue.populatedBy.callback]; - } - } } diff --git a/packages/graphql/src/schema-model/generate-model.test.ts b/packages/graphql/src/schema-model/generate-model.test.ts index 8016ccd95c..4ed390c7c8 100644 --- a/packages/graphql/src/schema-model/generate-model.test.ts +++ b/packages/graphql/src/schema-model/generate-model.test.ts @@ -428,15 +428,14 @@ describe("Annotations & Attributes", () => { const defaultName = userEntity?.attributes.get("defaultName"); expect(defaultName).toBeDefined(); - expect(defaultName?.defaultValue).toBeDefined(); - expect(defaultName?.defaultValue?.value).toBe("John"); + expect(defaultName?.annotations[AnnotationsKey.default]).toBeDefined(); + expect(defaultName?.annotations[AnnotationsKey.default]?.value).toBe("John"); const age = userEntity?.attributes.get("age"); expect(age).toBeDefined(); - expect(age?.defaultValue).toBeDefined(); - expect(age?.defaultValue?.populatedBy).toBeDefined(); - expect(age?.defaultValue?.populatedBy?.callback).toBe("thisCallback"); - expect(age?.defaultValue?.populatedBy?.when).toStrictEqual(["CREATE"]); + expect(age?.annotations[AnnotationsKey.populatedBy]).toBeDefined(); + expect(age?.annotations[AnnotationsKey.populatedBy]?.callback).toBe("thisCallback"); + expect(age?.annotations[AnnotationsKey.populatedBy]?.operations).toStrictEqual(["CREATE"]); const accountName = accountEntity?.attributes.get("accountName"); expect(accountName?.annotations[AnnotationsKey.settable]).toBeDefined(); diff --git a/packages/graphql/src/schema-model/parser/parse-attribute.ts b/packages/graphql/src/schema-model/parser/parse-attribute.ts index c01a829b95..374bde6c3a 100644 --- a/packages/graphql/src/schema-model/parser/parse-attribute.ts +++ b/packages/graphql/src/schema-model/parser/parse-attribute.ts @@ -33,12 +33,11 @@ import { Neo4jGraphQLNumberType, Neo4jGraphQLTemporalType, } from "../attribute/AttributeType"; -import type { GraphQLDefaultValueType } from "../attribute/Attribute"; import { Attribute } from "../attribute/Attribute"; import { Field } from "../attribute/Field"; import type { DefinitionCollection } from "./definition-collection"; import { parseAnnotations } from "./parse-annotation"; -import { aliasDirective, defaultDirective, populatedByDirective } from "../../graphql/directives"; +import { aliasDirective } from "../../graphql/directives"; import { parseArguments } from "./parse-arguments"; import { findDirective } from "./utils"; @@ -50,33 +49,14 @@ export function parseAttribute( const type = parseTypeNode(definitionCollection, field.type); const annotations = parseAnnotations(field.directives || []); const databaseName = getDatabaseName(field); - const defaultValue = getDefaultValue(field); return new Attribute({ name, annotations, type, databaseName, - defaultValue, }); } -function getDefaultValue(fieldDefinitionNode: FieldDefinitionNode): GraphQLDefaultValueType | undefined { - const defaultUsage = findDirective(fieldDefinitionNode.directives, defaultDirective.name); - - if (defaultUsage) { - const { value } = parseArguments(defaultDirective, defaultUsage) as { value: string }; - return { value }; - } - const populatedByUsage = findDirective(fieldDefinitionNode.directives, populatedByDirective.name); - if (populatedByUsage) { - const { callback, operations: when } = parseArguments(populatedByDirective, populatedByUsage) as { - callback: string; - operations: ("CREATE" | "UPDATE")[]; - }; - return { populatedBy: { callback, when } }; - } -} - function getDatabaseName(fieldDefinitionNode: FieldDefinitionNode): string | undefined { const aliasUsage = findDirective(fieldDefinitionNode.directives, aliasDirective.name); if (aliasUsage) { From 8409271b8355c3f9868c94b3bc2b8890e11878f1 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Fri, 4 Aug 2023 10:37:56 +0100 Subject: [PATCH 22/22] add comment on the PluralAnnotation --- packages/graphql/src/schema-model/annotation/PluralAnnotation.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/graphql/src/schema-model/annotation/PluralAnnotation.ts b/packages/graphql/src/schema-model/annotation/PluralAnnotation.ts index 92101c33af..dbd62f712d 100644 --- a/packages/graphql/src/schema-model/annotation/PluralAnnotation.ts +++ b/packages/graphql/src/schema-model/annotation/PluralAnnotation.ts @@ -17,6 +17,7 @@ * limitations under the License. */ +// TODO: maybe this can be a field on the concrete entity export class PluralAnnotation { public readonly value: string;