fix(server): viewerResources failing w/ nonexistent view (#5271)

This commit is contained in:
Kristaps Fabians Geikins
2025-08-20 13:06:17 +03:00
committed by GitHub
parent 3adf458e7a
commit cf298994a8
7 changed files with 56 additions and 25 deletions
@@ -40,7 +40,8 @@ const extendedViewerResourcesResolver = async (
const canRead = await ctx.authPolicies.project.savedViews.canRead({
userId: ctx.userId,
projectId,
savedViewId
savedViewId,
allowNonExistent: true // ignore missing view
})
throwIfAuthNotOk(canRead)
}
@@ -14,7 +14,6 @@ import type {
ViewerResourceItem
} from '@/modules/core/graph/generated/graphql'
import type { CommitRecord } from '@/modules/core/helpers/types'
import { NotFoundError } from '@/modules/shared/errors'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import type { DependenciesOf } from '@/modules/shared/helpers/factory'
import type {
@@ -370,9 +369,9 @@ const adjustResourceIdStringWithSpecificSavedViewSettingsFactory =
: params.savedViewId
if (!savedView) {
throw new NotFoundError(
`Saved view with ID ${params.savedViewId} not found in project ${projectId}`
)
// We don't want this to fail, cause this would break the app when nonexistant views are referenced
// Just ignore the view
return emptyReturn
}
const savedViewResources = resourceBuilder().addFromString(savedView.resourceIds)
@@ -10,7 +10,6 @@ import {
} from '@/modules/core/repositories/commits'
import { getStreamObjectsFactory } from '@/modules/core/repositories/objects'
import { buildBasicTestProject } from '@/modules/core/tests/helpers/creation'
import { NotFoundError } from '@/modules/shared/errors'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import type { ViewerResourceGroup } from '@/modules/viewer/domain/types/resources'
import type { SavedView } from '@/modules/viewer/domain/types/savedViews'
@@ -25,7 +24,7 @@ import {
viewerResourcesToString
} from '@/modules/viewer/services/viewerResources'
import { createTestSavedView } from '@/modules/viewer/tests/helpers/savedViews'
import { expectToThrow, itEach } from '@/test/assertionHelper'
import { itEach } from '@/test/assertionHelper'
import type { BasicTestUser } from '@/test/authHelper'
import { buildBasicTestUser, createTestUser } from '@/test/authHelper'
import {
@@ -323,21 +322,20 @@ describe('Viewer Resources Collection Service', () => {
secondModelBasicView = views[1]
})
it('throws if setting nonexistant view', async () => {
it('should not throw if setting nonexistant view', async () => {
const sut = buildSUT()
const err = await expectToThrow(
async () =>
await sut({
projectId: myProject.id,
resourceIdString: resourceBuilder()
.addResources(firstModel().id)
.toString(),
savedViewId: 'aaa'
})
)
expect(err instanceof NotFoundError).to.be.true
expect(err.message).to.include('Saved view')
// shouldnt throw so that deleting a view while its referred to in the URL doesn't
// cause the entire app to break - just ignore the value then
const res = await sut({
projectId: myProject.id,
resourceIdString: resourceBuilder()
.addResources(firstModel().id)
.toString(),
savedViewId: 'aaa'
})
expect(res).to.be.ok
})
itEach(
@@ -229,6 +229,24 @@ describe('ensureCanAccessSavedViewFragment', () => {
})
}
)
it.each(<const>['read', 'write'])(
`doesn't fail if view doesnt exist and allowNonExistent set (%s)`,
async (access) => {
const sut = buildWorkspaceSUT({
getSavedView: async () => null
})
const result = await sut({
userId,
projectId,
savedViewId,
access,
allowNonExistent: true
})
expect(result).toBeAuthOKResult()
}
)
})
})
@@ -52,6 +52,10 @@ export const ensureCanAccessSavedViewFragment: AuthPolicyEnsureFragment<
ProjectContext &
SavedViewContext & {
access: 'read' | 'write'
/**
* In some cases we want to just ignore a view being non-existant, instead of throwing
*/
allowNonExistent?: boolean
},
InstanceType<
| typeof SavedViewNotFoundError
@@ -71,7 +75,7 @@ export const ensureCanAccessSavedViewFragment: AuthPolicyEnsureFragment<
>
> =
(loaders) =>
async ({ userId, projectId, savedViewId, access }) => {
async ({ userId, projectId, savedViewId, access, allowNonExistent }) => {
const canUseSavedViews = await ensureCanUseProjectWorkspacePlanFeatureFragment(
loaders
)({
@@ -81,7 +85,10 @@ export const ensureCanAccessSavedViewFragment: AuthPolicyEnsureFragment<
if (canUseSavedViews.isErr) return err(canUseSavedViews.error)
const savedView = await loaders.getSavedView({ projectId, savedViewId })
if (!savedView) return err(new SavedViewNotFoundError())
if (!savedView) {
if (allowNonExistent) return ok()
return err(new SavedViewNotFoundError())
}
const isPublic = savedView.visibility === SavedViewVisibility.public
if (isPublic && access === 'read') {
@@ -34,7 +34,14 @@ export const canReadSavedViewPolicy: AuthPolicy<
| typeof Loaders.getWorkspaceSsoProvider
| typeof Loaders.getWorkspaceSsoSession
| typeof Loaders.getProjectRole,
MaybeUserContext & ProjectContext & SavedViewContext,
MaybeUserContext &
ProjectContext &
SavedViewContext & {
/**
* In some cases we want to just ignore a view being non-existant, instead of throwing
*/
allowNonExistent?: boolean
},
InstanceType<
| typeof SavedViewNotFoundError
| typeof SavedViewNoAccessError
@@ -53,11 +60,12 @@ export const canReadSavedViewPolicy: AuthPolicy<
>
> =
(loaders) =>
async ({ userId, projectId, savedViewId }) => {
async ({ userId, projectId, savedViewId, allowNonExistent }) => {
return await ensureCanAccessSavedViewFragment(loaders)({
userId,
projectId,
savedViewId,
access: 'read'
access: 'read',
allowNonExistent
})
}