From 36a4d3e18414ec41d2714b294af2b2325417a944 Mon Sep 17 00:00:00 2001 From: William Allen <16820599+williamjallen@users.noreply.github.com> Date: Tue, 16 Dec 2025 10:19:38 -0500 Subject: [PATCH] Add GraphQL Url scalar --- app/GraphQL/Scalars/Url.php | 72 +++++++++++++++++++++++ graphql/schema.graphql | 14 ++++- phpstan-baseline.neon | 18 ++++++ tests/Feature/GraphQL/ProjectTypeTest.php | 14 ++--- 4 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 app/GraphQL/Scalars/Url.php diff --git a/app/GraphQL/Scalars/Url.php b/app/GraphQL/Scalars/Url.php new file mode 100644 index 0000000000..67e3712427 --- /dev/null +++ b/app/GraphQL/Scalars/Url.php @@ -0,0 +1,72 @@ + $value, + ], [ + 'value' => 'url', + ])->passes(); + } + + /** + * Serializes an internal value to include in a response. + * + * @throws InvariantViolation + */ + public function serialize(mixed $value): string + { + if (!$this->validate($value)) { + throw new InvariantViolation("Could not serialize {$value} as URL."); + } + + return $this->parseValue($value); + } + + /** + * Parses an externally provided value (query variable) to use as an input. + * + * @throws InvariantViolation + */ + public function parseValue(mixed $value): string + { + if (!$this->validate($value)) { + throw new InvariantViolation("Could not parse {$value} as URL."); + } + + return (string) $value; + } + + /** + * Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input. + * + * Should throw an exception with a client friendly message on invalid value nodes. + * + * @param ValueNode&Node $valueNode + * @param array|null $variables + * + * @throws Error + */ + public function parseLiteral(Node $valueNode, ?array $variables = null): string + { + if (!($valueNode instanceof StringValueNode)) { + throw new Error("Query error: Can only parse Strings, got {$valueNode->kind}.", $valueNode); + } + + return (string) $valueNode->value; + } +} diff --git a/graphql/schema.graphql b/graphql/schema.graphql index 5db70c0b4f..536caca038 100644 --- a/graphql/schema.graphql +++ b/graphql/schema.graphql @@ -11,6 +11,8 @@ scalar NonNegativeSeconds @scalar(class: "App\\GraphQL\\Scalars\\NonNegativeSeco "A non-negative integer number of milliseconds." scalar NonNegativeIntegerMilliseconds @scalar(class: "App\\GraphQL\\Scalars\\NonNegativeIntegerMilliseconds") +scalar Url @scalar(class: "App\\GraphQL\\Scalars\\Url") + "Indicates what fields are available at the top level of a query operation." type Query { @@ -144,7 +146,10 @@ type Project { description: String! "Homepage for this project." - homeurl: String! + homeurl: Url @deprecated(reason: "Use 'homeUrl' instead.") + + "Homepage for this project." + homeUrl: Url @rename(attribute: "homeurl") "Visibility." visibility: ProjectVisibility! @rename(attribute: "public") @filterable @@ -201,13 +206,16 @@ enum ProjectVisibility { input CreateProjectInput { "Unique name." - name: String! + name: String! @rules(apply: ["App\\Rules\\ProjectVisibilityRule"]) "Description." description: String! "Project homepage" - homeurl: String! + homeurl: Url @deprecated(reason: "Use 'homeUrl' instead.") @rules(apply: ["prohibits:homeUrl"]) + + "Project homepage" + homeUrl: Url @rename(attribute: "homeurl") @rules(apply: ["prohibits:homeurl"]) "Visibility." visibility: ProjectVisibility! @rename(attribute: "public") @rules(attribute: "public", apply: ["App\\Rules\\ProjectVisibilityRule"]) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 573895b0f3..9f38521142 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -342,6 +342,24 @@ parameters: count: 2 path: app/GraphQL/Scalars/NonNegativeSeconds.php + - + rawMessage: Cannot cast mixed to string. + identifier: cast.string + count: 1 + path: app/GraphQL/Scalars/Url.php + + - + rawMessage: Casting to string something that's already string. + identifier: cast.useless + count: 1 + path: app/GraphQL/Scalars/Url.php + + - + rawMessage: 'Part $value (mixed) of encapsed string cannot be cast to string.' + identifier: encapsedStringPart.nonString + count: 2 + path: app/GraphQL/Scalars/Url.php + - rawMessage: Class App\Http\Controllers\AbstractBuildController has an uninitialized property $build. Give it default value or assign it in the constructor. identifier: property.uninitialized diff --git a/tests/Feature/GraphQL/ProjectTypeTest.php b/tests/Feature/GraphQL/ProjectTypeTest.php index f6e4629942..ad7635aedd 100644 --- a/tests/Feature/GraphQL/ProjectTypeTest.php +++ b/tests/Feature/GraphQL/ProjectTypeTest.php @@ -351,7 +351,7 @@ public function testCreateProjectNoUser(): void 'input' => [ 'name' => $name, 'description' => 'test', - 'homeurl' => 'https://cdash.org', + 'homeUrl' => 'https://cdash.org', 'visibility' => 'PUBLIC', 'authenticateSubmissions' => false, ], @@ -377,7 +377,7 @@ public function testCreateProjectUnauthorizedUser(): void 'input' => [ 'name' => $name, 'description' => 'test', - 'homeurl' => 'https://cdash.org', + 'homeUrl' => 'https://cdash.org', 'visibility' => 'PUBLIC', 'authenticateSubmissions' => false, ], @@ -405,7 +405,7 @@ public function testCreateProjectUserCreateProjectNoUser(): void 'input' => [ 'name' => $name, 'description' => 'test', - 'homeurl' => 'https://cdash.org', + 'homeUrl' => 'https://cdash.org', 'visibility' => 'PUBLIC', 'authenticateSubmissions' => false, ], @@ -433,7 +433,7 @@ public function testCreateProjectUserCreateProject(): void 'input' => [ 'name' => $name, 'description' => 'test', - 'homeurl' => 'https://cdash.org', + 'homeUrl' => 'https://cdash.org', 'visibility' => 'PUBLIC', 'authenticateSubmissions' => false, ], @@ -467,7 +467,7 @@ public function testCreateProjectAdmin(): void 'input' => [ 'name' => $name, 'description' => 'test', - 'homeurl' => 'https://cdash.org', + 'homeUrl' => 'https://cdash.org', 'visibility' => 'PUBLIC', 'authenticateSubmissions' => false, ], @@ -952,7 +952,7 @@ public function testCreateProjectMaxVisibility(string $user, string $visibility, 'input' => [ 'name' => $name, 'description' => 'test', - 'homeurl' => 'https://cdash.org', + 'homeUrl' => 'https://cdash.org', 'visibility' => $visibility, 'authenticateSubmissions' => false, ], @@ -1023,7 +1023,7 @@ public function testRequireAuthenticatedSubmissions( 'input' => [ 'name' => $name, 'description' => 'test', - 'homeurl' => 'https://cdash.org', + 'homeUrl' => 'https://cdash.org', 'visibility' => 'PUBLIC', 'authenticateSubmissions' => $use_authenticated_submits, ],