Skip to content

Commit

Permalink
Allow update and create on timestamp fields (#4449)
Browse files Browse the repository at this point in the history
* fix: add specific checks for timestamp annotation operations

* test: update schema test snapshots

* fix: update tck so DateTime usage is nullable
  • Loading branch information
mjfwebb authored Dec 22, 2023
1 parent 3e2e5f2 commit 2bb8f8b
Show file tree
Hide file tree
Showing 16 changed files with 123 additions and 37 deletions.
7 changes: 7 additions & 0 deletions .changeset/early-foxes-tickle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@neo4j/graphql": patch
---

Fix: allow non-generated mutations on timestamp fields

Before this patch, it wasn't possible to update a field with a timestamp directive even when the directive specified that only the UPDATE or CREATE operation field should be generated by the database.
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,23 @@ export class AttributeAdapter {
}

isOnCreateField(): boolean {
if (!this.isNonGeneratedField()) {
return false;
}

if (this.annotations.settable?.onCreate === false) {
return false;
}

if (this.timestampCreateIsGenerated()) {
return false;
}

return (
this.isNonGeneratedField() &&
(this.typeHelper.isScalar() ||
this.typeHelper.isSpatial() ||
this.typeHelper.isEnum() ||
this.typeHelper.isAbstract())
this.typeHelper.isScalar() ||
this.typeHelper.isSpatial() ||
this.typeHelper.isEnum() ||
this.typeHelper.isAbstract()
);
}

Expand Down Expand Up @@ -192,22 +203,65 @@ export class AttributeAdapter {
}

isCreateInputField(): boolean {
return this.isNonGeneratedField() && this.annotations.settable?.onCreate !== false;
return (
this.isNonGeneratedField() &&
this.annotations.settable?.onCreate !== false &&
!this.timestampCreateIsGenerated()
);
}

isNonGeneratedField(): boolean {
isUpdateInputField(): boolean {
return (
this.isCypher() === false &&
this.isCustomResolvable() === false &&
(this.typeHelper.isEnum() || this.typeHelper.isScalar() || this.typeHelper.isSpatial()) &&
!this.annotations.id &&
!this.annotations.populatedBy &&
!this.annotations.timestamp
this.isNonGeneratedField() &&
this.annotations.settable?.onUpdate !== false &&
!this.timestampUpdateIsGenerated()
);
}

isUpdateInputField(): boolean {
return this.isNonGeneratedField() && this.annotations.settable?.onUpdate !== false;
timestampCreateIsGenerated(): boolean {
if (!this.annotations.timestamp) {
// The timestamp directive is not set on the field
return false;
}

if (this.annotations.timestamp.operations.includes("CREATE")) {
// The timestamp directive is set to generate on create
return true;
}

// The timestamp directive is not set to generate on create
return false;
}

isNonGeneratedField(): boolean {
if (this.isCypher() || this.isCustomResolvable()) {
return false;
}

if (this.annotations.id || this.annotations.populatedBy) {
return false;
}

if (this.typeHelper.isEnum() || this.typeHelper.isScalar() || this.typeHelper.isSpatial()) {
return true;
}

return false;
}

timestampUpdateIsGenerated(): boolean {
if (!this.annotations.timestamp) {
// The timestamp directive is not set on the field
return false;
}

if (this.annotations.timestamp.operations.includes("UPDATE")) {
// The timestamp directive is set to generate on update
return true;
}

// The timestamp directive is not set to generate on update
return false;
}

isArrayMethodField(): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,29 @@
*/

import { makeDirectiveNode } from "@graphql-tools/utils";
import { parseTimestampAnnotation } from "./timestamp-annotation";
import { timestampDirective } from "../../../graphql/directives";
import { parseTimestampAnnotation } from "./timestamp-annotation";

const tests = [
{
name: "should parse correctly when CREATE operation is passed",
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"] }, timestampDirective),
operations: ["UPDATE"],
expected: { operations: ["UPDATE"] },
},
{
name: "should parse correctly when CREATE and UPDATE operations are passed",
directive: makeDirectiveNode("timestamp", { operations: ["CREATE", "UPDATE"] }, timestampDirective),
operations: ["CREATE", "UPDATE"],
expected: { operations: ["CREATE", "UPDATE"] },
},
{
name: "should parse correctly when CREATE and UPDATE operations are passed",
directive: makeDirectiveNode("timestamp", { operations: [] }, timestampDirective),
operations: ["CREATE", "UPDATE"],
},
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ import { parseArguments } from "../parse-arguments";
export function parseTimestampAnnotation(directive: DirectiveNode): TimestampAnnotation {
const { operations } = parseArguments<{ operations: string[] }>(timestampDirective, directive);

if (operations.length === 0) {
operations.push("CREATE", "UPDATE");
}

return new TimestampAnnotation({
operations,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,13 +258,9 @@ export class RelationshipAdapter {
return this.nestedOperations.size > 0 && !onlyConnectOrCreateAndNoUniqueFields;
}

public get nonGeneratedProperties(): AttributeAdapter[] {
return Array.from(this.attributes.values()).filter((attribute) => attribute.isNonGeneratedField());
public get hasNonNullCreateInputFields(): boolean {
return this.createInputFields.some((property) => property.typeHelper.isRequired());
}
public get hasNonNullNonGeneratedProperties(): boolean {
return this.nonGeneratedProperties.some((property) => property.typeHelper.isRequired());
}

/**
* Categories
* = a grouping of attributes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ export class RelationshipOperations {

public get edgeCreateInputTypeName(): string {
return `${this.relationship.propertiesTypeName}CreateInput${
this.relationship.hasNonNullNonGeneratedProperties ? `!` : ""
this.relationship.hasNonNullCreateInputFields ? `!` : ""
}`;
}
public get createInputTypeName(): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ export function createOnCreateITC({
const onCreateFields: InputTypeComposerFieldConfigMapDefinition = {
node: onCreateInput.NonNull,
};
if (relationshipAdapter.nonGeneratedProperties.length > 0) {
if (relationshipAdapter.createInputFields.length > 0) {
const edgeFieldType = withCreateInputType({
entityAdapter: relationshipAdapter,
userDefinedFieldDirectives,
composer: schemaComposer,
});
onCreateFields["edge"] = relationshipAdapter.hasNonNullNonGeneratedProperties
onCreateFields["edge"] = relationshipAdapter.hasNonNullCreateInputFields
? edgeFieldType.NonNull
: edgeFieldType;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/graphql/src/schema/generation/connect-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ function makeConnectFieldInputTypeFields({
ifUnionMemberEntity?: ConcreteEntityAdapter;
}): InputTypeComposerFieldConfigMapDefinition {
const fields = {};
const hasNonGeneratedProperties = relationshipAdapter.nonGeneratedProperties.length > 0;
const hasNonGeneratedProperties = relationshipAdapter.createInputFields.length > 0;
if (hasNonGeneratedProperties) {
fields["edge"] = relationshipAdapter.operations.edgeCreateInputTypeName;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/graphql/src/schema/generation/relation-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ function makeCreateFieldInputTypeFields({
userDefinedFieldDirectives: Map<string, DirectiveNode[]>;
}): InputTypeComposerFieldConfigMapDefinition {
const fields = {};
const hasNonGeneratedProperties = relationshipAdapter.nonGeneratedProperties.length > 0;
const hasNonGeneratedProperties = relationshipAdapter.createInputFields.length > 0;
if (hasNonGeneratedProperties) {
fields["edge"] = relationshipAdapter.operations.edgeCreateInputTypeName;
}
Expand Down
8 changes: 4 additions & 4 deletions packages/graphql/src/schema/generation/update-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ import { UnionEntityAdapter } from "../../schema-model/entity/model-adapters/Uni
import { RelationshipAdapter } from "../../schema-model/relationship/model-adapters/RelationshipAdapter";
import { concreteEntityToUpdateInputFields, withArrayOperators, withMathOperators } from "../to-compose";
import { withConnectFieldInputType } from "./connect-input";
import { withDisconnectFieldInputType } from "./disconnect-input";
import { withConnectOrCreateFieldInputType } from "./connect-or-create-input";
import { withDeleteFieldInputType } from "./delete-input";
import { withDisconnectFieldInputType } from "./disconnect-input";
import { makeImplementationsUpdateInput } from "./implementation-inputs";
import { makeConnectionWhereInputType } from "./where-input";
import { withConnectOrCreateFieldInputType } from "./connect-or-create-input";
import { withCreateFieldInputType } from "./relation-input";
import { makeConnectionWhereInputType } from "./where-input";

export function withUpdateInputType({
entityAdapter,
Expand Down Expand Up @@ -428,7 +428,7 @@ function makeUpdateConnectionFieldInputTypeFields({
// fields["node"] = updateInputType;
fields["node"] = relationshipAdapter.target.operations.updateInputTypeName;
}
const hasNonGeneratedProperties = relationshipAdapter.nonGeneratedProperties.length > 0;
const hasNonGeneratedProperties = relationshipAdapter.updateInputFields.length > 0;
if (hasNonGeneratedProperties) {
fields["edge"] = relationshipAdapter.operations.edgeUpdateInputTypeName;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/graphql/tests/schema/directives/timestamps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ describe("Timestamps", () => {
input MovieCreateInput {
id: ID
updatedAt: DateTime!
}
type MovieEdge {
Expand All @@ -117,6 +118,7 @@ describe("Timestamps", () => {
}
input MovieUpdateInput {
createdAt: DateTime
id: ID
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@ describe("https://github.com/neo4j/graphql/issues/2377", () => {
properties: [Property!]
tags: [Tag!]
type: ResourceType!
updatedAt: DateTime!
}
input ResourceDeleteInput {
Expand Down Expand Up @@ -456,6 +457,7 @@ describe("https://github.com/neo4j/graphql/issues/2377", () => {
properties: [Property!]
tags: [Tag!]
type: ResourceType!
updatedAt: DateTime!
}
input ResourceOptions {
Expand Down Expand Up @@ -506,6 +508,7 @@ describe("https://github.com/neo4j/graphql/issues/2377", () => {
input ResourceUpdateInput {
containedBy: [ResourceContainedByUpdateFieldInput!]
createdAt: DateTime
externalIds: [ID!]
externalIds_POP: Int
externalIds_PUSH: [ID!]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ describe("https://github.com/neo4j/graphql/issues/2993", () => {
since: SortDirection
}
input FOLLOWSUpdateInput {
since: DateTime
}
input FOLLOWSWhere {
AND: [FOLLOWSWhere!]
NOT: FOLLOWSWhere
Expand Down Expand Up @@ -348,6 +352,7 @@ describe("https://github.com/neo4j/graphql/issues/2993", () => {
}
input UserFollowingUpdateConnectionInput {
edge: FOLLOWSUpdateInput
node: ProfileUpdateInput
}
Expand Down Expand Up @@ -518,6 +523,10 @@ describe("https://github.com/neo4j/graphql/issues/2993", () => {
since: SortDirection
}
input FOLLOWSUpdateInput {
since: DateTime
}
input FOLLOWSWhere {
AND: [FOLLOWSWhere!]
NOT: FOLLOWSWhere
Expand Down Expand Up @@ -776,6 +785,7 @@ describe("https://github.com/neo4j/graphql/issues/2993", () => {
}
input UserFollowingUpdateConnectionInput {
edge: FOLLOWSUpdateInput
node: ProfileUpdateInput
}
Expand Down
3 changes: 3 additions & 0 deletions packages/graphql/tests/schema/issues/2377.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ describe("https://github.com/neo4j/graphql/issues/2377", () => {
properties: [Property!]
tags: [Tag!]
type: ResourceType!
updatedAt: DateTime!
}
input ResourceDeleteInput {
Expand Down Expand Up @@ -387,6 +388,7 @@ describe("https://github.com/neo4j/graphql/issues/2377", () => {
properties: [Property!]
tags: [Tag!]
type: ResourceType!
updatedAt: DateTime!
}
input ResourceOptions {
Expand Down Expand Up @@ -437,6 +439,7 @@ describe("https://github.com/neo4j/graphql/issues/2377", () => {
input ResourceUpdateInput {
containedBy: [ResourceContainedByUpdateFieldInput!]
createdAt: DateTime
externalIds: [ID!]
externalIds_POP: Int
externalIds_PUSH: [ID!]
Expand Down
5 changes: 5 additions & 0 deletions packages/graphql/tests/schema/issues/2993.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ describe("https://github.com/neo4j/graphql/issues/2993", () => {
since: SortDirection
}
input FOLLOWSUpdateInput {
since: DateTime
}
input FOLLOWSWhere {
AND: [FOLLOWSWhere!]
NOT: FOLLOWSWhere
Expand Down Expand Up @@ -330,6 +334,7 @@ describe("https://github.com/neo4j/graphql/issues/2993", () => {
}
input UserFollowingUpdateConnectionInput {
edge: FOLLOWSUpdateInput
node: ProfileUpdateInput
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
* limitations under the License.
*/

import { gql } from "graphql-tag";
import type { DocumentNode } from "graphql";
import gql from "graphql-tag";
import { Neo4jGraphQL } from "../../../../src";
import { formatCypher, translateQuery, formatParams } from "../../utils/tck-test-utils";
import { formatCypher, formatParams, translateQuery } from "../../utils/tck-test-utils";

describe("Create or Connect", () => {
describe("Simple", () => {
Expand Down Expand Up @@ -464,7 +464,7 @@ describe("Create or Connect", () => {
interface ActedIn @relationshipProperties {
id: ID! @id
createdAt: DateTime! @timestamp(operations: [CREATE])
updatedAt: DateTime! @timestamp(operations: [UPDATE])
updatedAt: DateTime @timestamp(operations: [UPDATE])
screentime: Int!
}
`;
Expand Down

0 comments on commit 2bb8f8b

Please sign in to comment.