From cb7fb8f5e955733ede7a73fb3efd6e728bc43e8f Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Mon, 22 Jun 2026 11:24:43 +0200 Subject: [PATCH] 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 --- apps/trust/src/pages/DocumentPage.tsx | 10 +- apps/trust/src/pages/DocumentPageLoader.tsx | 2 +- pkg/server/api/trust/v1/base_resolvers.go | 139 +++++++++++++++-- pkg/server/api/trust/v1/graphql/base.graphql | 3 +- pkg/server/api/trust/v1/node_resolver.go | 148 ------------------- 5 files changed, 138 insertions(+), 164 deletions(-) delete mode 100644 pkg/server/api/trust/v1/node_resolver.go diff --git a/apps/trust/src/pages/DocumentPage.tsx b/apps/trust/src/pages/DocumentPage.tsx index 8d6898886..bc147ba11 100644 --- a/apps/trust/src/pages/DocumentPage.tsx +++ b/apps/trust/src/pages/DocumentPage.tsx @@ -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(documentPageQuery, queryRef); const trustCenter = data.currentTrustCenter; - const node = data.node; + const node = data.aliasedNode; if ( node.__typename !== "Document" diff --git a/apps/trust/src/pages/DocumentPageLoader.tsx b/apps/trust/src/pages/DocumentPageLoader.tsx index f69a559f4..760a3104c 100644 --- a/apps/trust/src/pages/DocumentPageLoader.tsx +++ b/apps/trust/src/pages/DocumentPageLoader.tsx @@ -27,7 +27,7 @@ function DocumentPageQueryLoader() { useEffect(() => { if (documentId) { - loadQuery({ id: documentId }); + loadQuery({ alias: documentId }); } }, [documentId, loadQuery]); diff --git a/pkg/server/api/trust/v1/base_resolvers.go b/pkg/server/api/trust/v1/base_resolvers.go index 6aba6f962..9cb508646 100644 --- a/pkg/server/api/trust/v1/base_resolvers.go +++ b/pkg/server/api/trust/v1/base_resolvers.go @@ -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 diff --git a/pkg/server/api/trust/v1/graphql/base.graphql b/pkg/server/api/trust/v1/graphql/base.graphql index c69e94524..d5f6d09ae 100644 --- a/pkg/server/api/trust/v1/graphql/base.graphql +++ b/pkg/server/api/trust/v1/graphql/base.graphql @@ -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) diff --git a/pkg/server/api/trust/v1/node_resolver.go b/pkg/server/api/trust/v1/node_resolver.go deleted file mode 100644 index 008b55964..000000000 --- a/pkg/server/api/trust/v1/node_resolver.go +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright (c) 2025-2026 Probo Inc . -// -// 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) - } -}