Split trust node lookup into node and aliasedNode

The trust node query previously accepted a String and resolved both GIDs
and slugs through one field, which forced the frontend to lose the ID
type guarantee. Restore node(id: ID!) as a strict GID lookup and add a
dedicated aliasedNode(alias: String!) that parses a GID first and falls
back to slug resolution before delegating to Node.

Inline the former nodeByGID switch directly into Node and drop the helper
file. Point the trust DocumentPage query at aliasedNode so slug-or-ID URLs
keep working.

Signed-off-by: Bryan Frimin <bryan@probo.com>
This commit is contained in:
Bryan Frimin
2026-06-22 11:24:43 +02:00
parent bfd672e0fe
commit cb7fb8f5e9
5 changed files with 138 additions and 164 deletions

View File

@@ -45,7 +45,7 @@ import type { DocumentPageRequestReportAccessMutation } from "./__generated__/Do
import type { DocumentPageRequestTrustCenterFileAccessMutation } from "./__generated__/DocumentPageRequestTrustCenterFileAccessMutation.graphql";
export const documentPageQuery = graphql`
query DocumentPageQuery($id: String!) {
query DocumentPageQuery($alias: String!) {
currentTrustCenter {
logo {
downloadUrl
@@ -54,7 +54,7 @@ export const documentPageQuery = graphql`
downloadUrl
}
}
node(id: $id) @required(action: THROW) {
aliasedNode(alias: $alias) @required(action: THROW) {
__typename
... on Document {
id
@@ -183,7 +183,7 @@ function extractMimeType(dataUri: string): string {
return match?.[1] ?? "application/octet-stream";
}
function getNodeTitle(node: DocumentPageQueryType["response"]["node"]): string | undefined {
function getNodeTitle(node: DocumentPageQueryType["response"]["aliasedNode"]): string | undefined {
switch (node.__typename) {
case "Document":
return node.title;
@@ -196,7 +196,7 @@ function getNodeTitle(node: DocumentPageQueryType["response"]["node"]): string |
}
}
function getNodeId(node: DocumentPageQueryType["response"]["node"]): string | undefined {
function getNodeId(node: DocumentPageQueryType["response"]["aliasedNode"]): string | undefined {
switch (node.__typename) {
case "Document":
case "TrustCenterFile":
@@ -220,7 +220,7 @@ export function DocumentPage({ queryRef }: Props) {
const data = usePreloadedQuery<DocumentPageQueryType>(documentPageQuery, queryRef);
const trustCenter = data.currentTrustCenter;
const node = data.node;
const node = data.aliasedNode;
if (
node.__typename !== "Document"

View File

@@ -27,7 +27,7 @@ function DocumentPageQueryLoader() {
useEffect(() => {
if (documentId) {
loadQuery({ id: documentId });
loadQuery({ alias: documentId });
}
}, [documentId, loadQuery]);

View File

@@ -18,6 +18,7 @@ import (
"go.probo.inc/probo/pkg/server/api/trust/v1/schema"
"go.probo.inc/probo/pkg/server/api/trust/v1/types"
"go.probo.inc/probo/pkg/server/gqlutils"
"go.probo.inc/probo/pkg/trust"
)
// Viewer is the resolver for the viewer field.
@@ -39,8 +40,125 @@ func (r *queryResolver) Viewer(ctx context.Context) (*types.Identity, error) {
}
// Node is the resolver for the node field.
func (r *queryResolver) Node(ctx context.Context, id string) (types.Node, error) {
resourceID, err := gid.ParseGID(id)
func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error) {
scope := coredata.NewScopeFromObjectID(id)
trustService := r.trust
switch id.EntityType() {
case coredata.OrganizationEntityType:
organization, err := trustService.Organizations.Get(ctx, scope, id)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewOrganization(organization), nil
case coredata.DocumentEntityType:
trustCenter := compliancepage.CompliancePageFromContext(ctx)
document, err := trustService.Documents.Get(ctx, scope, trustCenter.OrganizationID, id)
if err != nil {
if errors.Is(err, trust.ErrDocumentNotFound) || errors.Is(err, trust.ErrDocumentNotVisible) || errors.Is(err, coredata.ErrResourceNotFound) {
return nil, gqlutils.NotFoundf(ctx, "node %q not found", id)
}
if _, ok := errors.AsType[*trust.ErrDocumentArchived](err); ok {
return nil, gqlutils.NotFoundf(ctx, "node %q not found", id)
}
r.logger.ErrorCtx(ctx, "cannot get document", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewDocument(document), nil
case coredata.FrameworkEntityType:
framework, err := trustService.Frameworks.Get(ctx, scope, id)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot get framework", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewFramework(framework), nil
case coredata.FileEntityType:
trustCenter := compliancepage.CompliancePageFromContext(ctx)
file, err := trustService.Reports.Get(ctx, scope, trustCenter.OrganizationID, id)
if err != nil {
if errors.Is(err, trust.ErrReportNotFound) || errors.Is(err, coredata.ErrResourceNotFound) {
return nil, gqlutils.NotFoundf(ctx, "node %q not found", id)
}
r.logger.ErrorCtx(ctx, "cannot get audit report file", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewAuditReport(file), nil
case coredata.AuditEntityType:
audit, err := trustService.Audits.Get(ctx, scope, id)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot get audit", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewAudit(audit), nil
case coredata.ThirdPartyEntityType:
thirdParty, err := trustService.ThirdParties.Get(ctx, scope, id)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot get thirdParty", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewSubprocessor(thirdParty), nil
case coredata.TrustCenterEntityType:
trustCenter, err := trustService.TrustCenters.Get(ctx, scope, id)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot get trust center", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewTrustCenter(trustCenter), nil
case coredata.TrustCenterReferenceEntityType:
reference, err := trustService.TrustCenterReferences.Get(ctx, scope, id)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot get trust center reference", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewTrustCenterReference(reference), nil
case coredata.TrustCenterFileEntityType:
trustCenter := compliancepage.CompliancePageFromContext(ctx)
trustCenterFile, err := trustService.TrustCenterFiles.Get(ctx, scope, trustCenter.OrganizationID, id)
if err != nil {
if errors.Is(err, trust.ErrTrustCenterFileNotFound) || errors.Is(err, trust.ErrTrustCenterFileNotVisible) {
return nil, gqlutils.NotFoundf(ctx, "node %q not found", id)
}
r.logger.ErrorCtx(ctx, "cannot get trust center file", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewTrustCenterFile(trustCenterFile), nil
default:
return nil, gqlutils.NotFoundf(ctx, "node %q not found", id)
}
}
// AliasedNode is the resolver for the aliasedNode field.
func (r *queryResolver) AliasedNode(ctx context.Context, alias string) (types.Node, error) {
resourceID, err := gid.ParseGID(alias)
if err != nil {
trustCenter := compliancepage.CompliancePageFromContext(ctx)
scope := coredata.NewScopeFromObjectID(trustCenter.ID)
@@ -48,11 +166,11 @@ func (r *queryResolver) Node(ctx context.Context, id string) (types.Node, error)
resourceID, err = r.resourceAlias.ResolveAlias(
ctx,
scope,
id,
alias,
)
if err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return nil, gqlutils.NotFoundf(ctx, "node %q not found", id)
return nil, gqlutils.NotFoundf(ctx, "node %q not found", alias)
}
r.logger.ErrorCtx(ctx, "cannot resolve resource alias", log.Error(err))
@@ -61,7 +179,7 @@ func (r *queryResolver) Node(ctx context.Context, id string) (types.Node, error)
}
}
return r.nodeByGID(ctx, resourceID, id)
return r.Node(ctx, resourceID)
}
// CurrentTrustCenter is the resolver for the currentTrustCenter field.
@@ -96,10 +214,13 @@ func (r *queryResolver) OidcProviders(ctx context.Context) ([]*types.OIDCProvide
for _, p := range providers {
name := strings.ToLower(p.String())
result = append(result, &types.OIDCProviderInfo{
Name: name,
LoginURL: r.baseURL.WithPath("/api/connect/v1/oidc/" + name + "/login").MustString(),
})
result = append(
result,
&types.OIDCProviderInfo{
Name: name,
LoginURL: r.baseURL.WithPath("/api/connect/v1/oidc/" + name + "/login").MustString(),
},
)
}
return result, nil

View File

@@ -24,7 +24,8 @@ interface Node {
type Query {
viewer: Identity
node(id: String!): Node
node(id: ID!): Node
aliasedNode(alias: String!): Node
currentTrustCenter: TrustCenter
oidcProviders: [OIDCProviderInfo!]!
@goField(forceResolver: true)

View File

@@ -1,148 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package trust_v1
import (
"context"
"errors"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/server/api/compliancepage"
"go.probo.inc/probo/pkg/server/api/trust/v1/types"
"go.probo.inc/probo/pkg/server/gqlutils"
"go.probo.inc/probo/pkg/trust"
)
func (r *queryResolver) nodeByGID(
ctx context.Context,
id gid.GID,
notFoundLabel string,
) (types.Node, error) {
scope := coredata.NewScopeFromObjectID(id)
trustService := r.trust
switch id.EntityType() {
case coredata.OrganizationEntityType:
organization, err := trustService.Organizations.Get(ctx, scope, id)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot get organization", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewOrganization(organization), nil
case coredata.DocumentEntityType:
trustCenter := compliancepage.CompliancePageFromContext(ctx)
document, err := trustService.Documents.Get(ctx, scope, trustCenter.OrganizationID, id)
if err != nil {
if errors.Is(err, trust.ErrDocumentNotFound) || errors.Is(err, trust.ErrDocumentNotVisible) || errors.Is(err, coredata.ErrResourceNotFound) {
return nil, gqlutils.NotFoundf(ctx, "node %q not found", notFoundLabel)
}
if _, ok := errors.AsType[*trust.ErrDocumentArchived](err); ok {
return nil, gqlutils.NotFoundf(ctx, "node %q not found", notFoundLabel)
}
r.logger.ErrorCtx(ctx, "cannot get document", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewDocument(document), nil
case coredata.FrameworkEntityType:
framework, err := trustService.Frameworks.Get(ctx, scope, id)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot get framework", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewFramework(framework), nil
case coredata.FileEntityType:
trustCenter := compliancepage.CompliancePageFromContext(ctx)
file, err := trustService.Reports.Get(ctx, scope, trustCenter.OrganizationID, id)
if err != nil {
if errors.Is(err, trust.ErrReportNotFound) || errors.Is(err, coredata.ErrResourceNotFound) {
return nil, gqlutils.NotFoundf(ctx, "node %q not found", notFoundLabel)
}
r.logger.ErrorCtx(ctx, "cannot get audit report file", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewAuditReport(file), nil
case coredata.AuditEntityType:
audit, err := trustService.Audits.Get(ctx, scope, id)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot get audit", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewAudit(audit), nil
case coredata.ThirdPartyEntityType:
thirdParty, err := trustService.ThirdParties.Get(ctx, scope, id)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot get thirdParty", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewSubprocessor(thirdParty), nil
case coredata.TrustCenterEntityType:
trustCenter, err := trustService.TrustCenters.Get(ctx, scope, id)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot get trust center", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewTrustCenter(trustCenter), nil
case coredata.TrustCenterReferenceEntityType:
reference, err := trustService.TrustCenterReferences.Get(ctx, scope, id)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot get trust center reference", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewTrustCenterReference(reference), nil
case coredata.TrustCenterFileEntityType:
trustCenter := compliancepage.CompliancePageFromContext(ctx)
trustCenterFile, err := trustService.TrustCenterFiles.Get(ctx, scope, trustCenter.OrganizationID, id)
if err != nil {
if errors.Is(err, trust.ErrTrustCenterFileNotFound) || errors.Is(err, trust.ErrTrustCenterFileNotVisible) {
return nil, gqlutils.NotFoundf(ctx, "node %q not found", notFoundLabel)
}
r.logger.ErrorCtx(ctx, "cannot get trust center file", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewTrustCenterFile(trustCenterFile), nil
default:
return nil, gqlutils.NotFoundf(ctx, "node %q not found", notFoundLabel)
}
}