feat: optimized saved view previews & thumbnails (#5563)
* init new API routes * WIP output & migration * WIP endpoint * endpoint works * frontend adjusted fully * aiven extras fixx + migration * simpler migration * add deprecation notice * test fixes * gqlgen * testss fix
This commit is contained in:
committed by
GitHub
parent
fc118bc82c
commit
43803b9517
@@ -2,9 +2,7 @@ services:
|
||||
# Actual Speckle Server dependencies
|
||||
|
||||
postgres:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: utils/postgres/Dockerfile
|
||||
image: 'postgres:16.4-alpine3.20@sha256:d898b0b78a2627cb4ee63464a14efc9d296884f1b28c841b0ab7d7c42f1fffdf'
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_DB: speckle
|
||||
@@ -19,9 +17,7 @@ services:
|
||||
command: postgres -c max_prepared_transactions=150
|
||||
|
||||
postgres-region1:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: utils/postgres/Dockerfile
|
||||
image: 'postgres:16.4-alpine3.20@sha256:d898b0b78a2627cb4ee63464a14efc9d296884f1b28c841b0ab7d7c42f1fffdf'
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_DB: speckle
|
||||
@@ -36,9 +32,7 @@ services:
|
||||
command: postgres -c max_prepared_transactions=150
|
||||
|
||||
postgres-region2:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: utils/postgres/Dockerfile
|
||||
image: 'postgres:16.4-alpine3.20@sha256:d898b0b78a2627cb4ee63464a14efc9d296884f1b28c841b0ab7d7c42f1fffdf'
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_DB: speckle
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<form @submit="onSubmit">
|
||||
<div class="flex flex-col gap-2">
|
||||
<img
|
||||
:src="slide?.screenshot"
|
||||
:src="slide?.thumbnailUrl"
|
||||
:alt="slide?.name"
|
||||
class="w-full object-cover rounded-lg border border-outline-3 h-64"
|
||||
/>
|
||||
@@ -41,7 +41,7 @@ graphql(`
|
||||
projectId
|
||||
name
|
||||
description
|
||||
screenshot
|
||||
thumbnailUrl
|
||||
}
|
||||
`)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@click="onSelectSlide"
|
||||
>
|
||||
<img
|
||||
:src="slide.screenshot"
|
||||
:src="slide.thumbnailUrl"
|
||||
:alt="slide.name"
|
||||
class="w-full aspect-[3/2] md:aspect-video object-cover"
|
||||
/>
|
||||
@@ -28,7 +28,7 @@ graphql(`
|
||||
fragment PresentationSlideListSlide_SavedView on SavedView {
|
||||
id
|
||||
name
|
||||
screenshot
|
||||
thumbnailUrl
|
||||
}
|
||||
`)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="flex items-center shrink-0">
|
||||
<div class="relative">
|
||||
<img
|
||||
:src="view.screenshot"
|
||||
:src="view.thumbnailUrl"
|
||||
alt="View screenshot"
|
||||
class="w-20 h-[60px] object-cover rounded border border-outline-3 bg-foundation-page cursor-pointer"
|
||||
/>
|
||||
@@ -149,7 +149,7 @@ graphql(`
|
||||
id
|
||||
name
|
||||
description
|
||||
screenshot
|
||||
thumbnailUrl
|
||||
visibility
|
||||
isHomeView
|
||||
resourceIds
|
||||
|
||||
@@ -77,8 +77,8 @@ type Documents = {
|
||||
"\n mutation PresentationShareToken($input: SavedViewGroupShareInput!) {\n projectMutations {\n savedViewMutations {\n share(input: $input) {\n id\n revoked\n content\n }\n }\n }\n }\n": typeof types.PresentationShareTokenDocument,
|
||||
"\n mutation PresentationShareEnableToken($input: SavedViewGroupShareUpdateInput!) {\n projectMutations {\n savedViewMutations {\n enableShare(input: $input) {\n id\n revoked\n content\n }\n }\n }\n }\n": typeof types.PresentationShareEnableTokenDocument,
|
||||
"\n mutation PresentationShareDisableToken($input: SavedViewGroupShareUpdateInput!) {\n projectMutations {\n savedViewMutations {\n disableShare(input: $input) {\n id\n revoked\n content\n }\n }\n }\n }\n": typeof types.PresentationShareDisableTokenDocument,
|
||||
"\n fragment PresentationSlideEditDialog_SavedView on SavedView {\n id\n projectId\n name\n description\n screenshot\n }\n": typeof types.PresentationSlideEditDialog_SavedViewFragmentDoc,
|
||||
"\n fragment PresentationSlideListSlide_SavedView on SavedView {\n id\n name\n screenshot\n }\n": typeof types.PresentationSlideListSlide_SavedViewFragmentDoc,
|
||||
"\n fragment PresentationSlideEditDialog_SavedView on SavedView {\n id\n projectId\n name\n description\n thumbnailUrl\n }\n": typeof types.PresentationSlideEditDialog_SavedViewFragmentDoc,
|
||||
"\n fragment PresentationSlideListSlide_SavedView on SavedView {\n id\n name\n thumbnailUrl\n }\n": typeof types.PresentationSlideListSlide_SavedViewFragmentDoc,
|
||||
"\n fragment PresentationSlideList_SavedViewGroup on SavedViewGroup {\n id\n views(input: $input) {\n items {\n id\n ...PresentationSlideListSlide_SavedView\n }\n }\n }\n": typeof types.PresentationSlideList_SavedViewGroupFragmentDoc,
|
||||
"\n fragment PresentationViewerPageWrapper_SavedViewGroup on SavedViewGroup {\n id\n views(input: $input) {\n totalCount\n items {\n id\n resourceIdString\n }\n }\n }\n": typeof types.PresentationViewerPageWrapper_SavedViewGroupFragmentDoc,
|
||||
"\n fragment ProjectCardImportFileArea_Project on Project {\n id\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n ...UseFileImport_Project\n }\n": typeof types.ProjectCardImportFileArea_ProjectFragmentDoc,
|
||||
@@ -190,7 +190,7 @@ type Documents = {
|
||||
"\n fragment ViewerSavedViewsPanel_Project on Project {\n id\n permissions {\n canCreateSavedView {\n ...FullPermissionCheckResult\n }\n }\n workspace {\n id\n seatType\n planSupportsSavedViews: hasAccessToFeature(featureName: savedViews)\n }\n }\n": typeof types.ViewerSavedViewsPanel_ProjectFragmentDoc,
|
||||
"\n fragment ViewerSavedViewsPanelGroups_Project on Project {\n id\n savedViewGroups(input: $savedViewGroupsInput) {\n totalCount\n cursor\n items {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n ...ViewerSavedViewsPanelViewsGroup_Project\n }\n": typeof types.ViewerSavedViewsPanelGroups_ProjectFragmentDoc,
|
||||
"\n query ViewerSavedViewsPanelGroups_SavedViewGroups(\n $projectId: String!\n $savedViewGroupsInput: SavedViewGroupsInput!\n ) {\n project(id: $projectId) {\n id\n ...ViewerSavedViewsPanelGroups_Project\n }\n }\n": typeof types.ViewerSavedViewsPanelGroups_SavedViewGroupsDocument,
|
||||
"\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n visibility\n isHomeView\n resourceIds\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n ...UseDraggableView_SavedView\n }\n": typeof types.ViewerSavedViewsPanelView_SavedViewFragmentDoc,
|
||||
"\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n thumbnailUrl\n visibility\n isHomeView\n resourceIds\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n ...UseDraggableView_SavedView\n }\n": typeof types.ViewerSavedViewsPanelView_SavedViewFragmentDoc,
|
||||
"\n fragment ViewerSavedViewsPanelViewDeleteDialog_SavedView on SavedView {\n id\n name\n ...UseDeleteSavedView_SavedView\n }\n": typeof types.ViewerSavedViewsPanelViewDeleteDialog_SavedViewFragmentDoc,
|
||||
"\n fragment ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {\n id\n name\n description\n visibility\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n }\n": typeof types.ViewerSavedViewsPanelViewEditDialog_SavedViewFragmentDoc,
|
||||
"\n fragment ViewerSavedViewsPanelViewMoveDialog_SavedView on SavedView {\n id\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n }\n": typeof types.ViewerSavedViewsPanelViewMoveDialog_SavedViewFragmentDoc,
|
||||
@@ -302,7 +302,7 @@ type Documents = {
|
||||
"\n query NavigationWorkspaceInvites {\n activeUser {\n id\n workspaceInvites {\n ...HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaborator\n }\n }\n }\n": typeof types.NavigationWorkspaceInvitesDocument,
|
||||
"\n mutation UpdatePresentationSlide($input: UpdateSavedViewInput!) {\n projectMutations {\n savedViewMutations {\n updateView(input: $input) {\n id\n name\n description\n }\n }\n }\n }\n": typeof types.UpdatePresentationSlideDocument,
|
||||
"\n query PresentationAccessCheck($savedViewGroupId: ID!, $projectId: String!) {\n project(id: $projectId) {\n id\n savedViewGroup(id: $savedViewGroupId) {\n id\n }\n }\n }\n": typeof types.PresentationAccessCheckDocument,
|
||||
"\n query ProjectPresentationPage(\n $input: SavedViewGroupViewsInput!\n $savedViewGroupId: ID!\n $projectId: String!\n ) {\n project(id: $projectId) {\n id\n limitedWorkspace {\n id\n ...PresentationLeftSidebar_LimitedWorkspace\n ...PresentationLoading_LimitedWorkspace\n }\n savedViewGroup(id: $savedViewGroupId) {\n id\n title\n ...PresentationViewerPageWrapper_SavedViewGroup\n ...PresentationHeader_SavedViewGroup\n ...PresentationSlideList_SavedViewGroup\n ...PresentationPageWrapper_SavedViewGroup\n ...PresentationLoading_SavedViewGroup\n views(input: $input) {\n totalCount\n items {\n id\n name\n description\n screenshot\n projectId\n visibility\n ...PresentationInfoSidebar_SavedView\n group {\n id\n }\n }\n }\n }\n }\n }\n": typeof types.ProjectPresentationPageDocument,
|
||||
"\n query ProjectPresentationPage(\n $input: SavedViewGroupViewsInput!\n $savedViewGroupId: ID!\n $projectId: String!\n ) {\n project(id: $projectId) {\n id\n limitedWorkspace {\n id\n ...PresentationLeftSidebar_LimitedWorkspace\n ...PresentationLoading_LimitedWorkspace\n }\n savedViewGroup(id: $savedViewGroupId) {\n id\n title\n ...PresentationViewerPageWrapper_SavedViewGroup\n ...PresentationHeader_SavedViewGroup\n ...PresentationSlideList_SavedViewGroup\n ...PresentationPageWrapper_SavedViewGroup\n ...PresentationLoading_SavedViewGroup\n views(input: $input) {\n totalCount\n items {\n id\n name\n description\n thumbnailUrl\n projectId\n visibility\n ...PresentationInfoSidebar_SavedView\n group {\n id\n }\n }\n }\n }\n }\n }\n": typeof types.ProjectPresentationPageDocument,
|
||||
"\n fragment UseCopyModelLink_Model on Model {\n id\n projectId\n ...GetModelItemRoute_Model\n }\n": typeof types.UseCopyModelLink_ModelFragmentDoc,
|
||||
"\n fragment UseCanCreatePersonalProject_User on User {\n permissions {\n canCreatePersonalProject {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.UseCanCreatePersonalProject_UserFragmentDoc,
|
||||
"\n fragment UseCanCreateWorkspace_User on User {\n permissions {\n canCreateWorkspace {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.UseCanCreateWorkspace_UserFragmentDoc,
|
||||
@@ -618,8 +618,8 @@ const documents: Documents = {
|
||||
"\n mutation PresentationShareToken($input: SavedViewGroupShareInput!) {\n projectMutations {\n savedViewMutations {\n share(input: $input) {\n id\n revoked\n content\n }\n }\n }\n }\n": types.PresentationShareTokenDocument,
|
||||
"\n mutation PresentationShareEnableToken($input: SavedViewGroupShareUpdateInput!) {\n projectMutations {\n savedViewMutations {\n enableShare(input: $input) {\n id\n revoked\n content\n }\n }\n }\n }\n": types.PresentationShareEnableTokenDocument,
|
||||
"\n mutation PresentationShareDisableToken($input: SavedViewGroupShareUpdateInput!) {\n projectMutations {\n savedViewMutations {\n disableShare(input: $input) {\n id\n revoked\n content\n }\n }\n }\n }\n": types.PresentationShareDisableTokenDocument,
|
||||
"\n fragment PresentationSlideEditDialog_SavedView on SavedView {\n id\n projectId\n name\n description\n screenshot\n }\n": types.PresentationSlideEditDialog_SavedViewFragmentDoc,
|
||||
"\n fragment PresentationSlideListSlide_SavedView on SavedView {\n id\n name\n screenshot\n }\n": types.PresentationSlideListSlide_SavedViewFragmentDoc,
|
||||
"\n fragment PresentationSlideEditDialog_SavedView on SavedView {\n id\n projectId\n name\n description\n thumbnailUrl\n }\n": types.PresentationSlideEditDialog_SavedViewFragmentDoc,
|
||||
"\n fragment PresentationSlideListSlide_SavedView on SavedView {\n id\n name\n thumbnailUrl\n }\n": types.PresentationSlideListSlide_SavedViewFragmentDoc,
|
||||
"\n fragment PresentationSlideList_SavedViewGroup on SavedViewGroup {\n id\n views(input: $input) {\n items {\n id\n ...PresentationSlideListSlide_SavedView\n }\n }\n }\n": types.PresentationSlideList_SavedViewGroupFragmentDoc,
|
||||
"\n fragment PresentationViewerPageWrapper_SavedViewGroup on SavedViewGroup {\n id\n views(input: $input) {\n totalCount\n items {\n id\n resourceIdString\n }\n }\n }\n": types.PresentationViewerPageWrapper_SavedViewGroupFragmentDoc,
|
||||
"\n fragment ProjectCardImportFileArea_Project on Project {\n id\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n ...UseFileImport_Project\n }\n": types.ProjectCardImportFileArea_ProjectFragmentDoc,
|
||||
@@ -731,7 +731,7 @@ const documents: Documents = {
|
||||
"\n fragment ViewerSavedViewsPanel_Project on Project {\n id\n permissions {\n canCreateSavedView {\n ...FullPermissionCheckResult\n }\n }\n workspace {\n id\n seatType\n planSupportsSavedViews: hasAccessToFeature(featureName: savedViews)\n }\n }\n": types.ViewerSavedViewsPanel_ProjectFragmentDoc,
|
||||
"\n fragment ViewerSavedViewsPanelGroups_Project on Project {\n id\n savedViewGroups(input: $savedViewGroupsInput) {\n totalCount\n cursor\n items {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n ...ViewerSavedViewsPanelViewsGroup_Project\n }\n": types.ViewerSavedViewsPanelGroups_ProjectFragmentDoc,
|
||||
"\n query ViewerSavedViewsPanelGroups_SavedViewGroups(\n $projectId: String!\n $savedViewGroupsInput: SavedViewGroupsInput!\n ) {\n project(id: $projectId) {\n id\n ...ViewerSavedViewsPanelGroups_Project\n }\n }\n": types.ViewerSavedViewsPanelGroups_SavedViewGroupsDocument,
|
||||
"\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n visibility\n isHomeView\n resourceIds\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n ...UseDraggableView_SavedView\n }\n": types.ViewerSavedViewsPanelView_SavedViewFragmentDoc,
|
||||
"\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n thumbnailUrl\n visibility\n isHomeView\n resourceIds\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n ...UseDraggableView_SavedView\n }\n": types.ViewerSavedViewsPanelView_SavedViewFragmentDoc,
|
||||
"\n fragment ViewerSavedViewsPanelViewDeleteDialog_SavedView on SavedView {\n id\n name\n ...UseDeleteSavedView_SavedView\n }\n": types.ViewerSavedViewsPanelViewDeleteDialog_SavedViewFragmentDoc,
|
||||
"\n fragment ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {\n id\n name\n description\n visibility\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n }\n": types.ViewerSavedViewsPanelViewEditDialog_SavedViewFragmentDoc,
|
||||
"\n fragment ViewerSavedViewsPanelViewMoveDialog_SavedView on SavedView {\n id\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n }\n": types.ViewerSavedViewsPanelViewMoveDialog_SavedViewFragmentDoc,
|
||||
@@ -843,7 +843,7 @@ const documents: Documents = {
|
||||
"\n query NavigationWorkspaceInvites {\n activeUser {\n id\n workspaceInvites {\n ...HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaborator\n }\n }\n }\n": types.NavigationWorkspaceInvitesDocument,
|
||||
"\n mutation UpdatePresentationSlide($input: UpdateSavedViewInput!) {\n projectMutations {\n savedViewMutations {\n updateView(input: $input) {\n id\n name\n description\n }\n }\n }\n }\n": types.UpdatePresentationSlideDocument,
|
||||
"\n query PresentationAccessCheck($savedViewGroupId: ID!, $projectId: String!) {\n project(id: $projectId) {\n id\n savedViewGroup(id: $savedViewGroupId) {\n id\n }\n }\n }\n": types.PresentationAccessCheckDocument,
|
||||
"\n query ProjectPresentationPage(\n $input: SavedViewGroupViewsInput!\n $savedViewGroupId: ID!\n $projectId: String!\n ) {\n project(id: $projectId) {\n id\n limitedWorkspace {\n id\n ...PresentationLeftSidebar_LimitedWorkspace\n ...PresentationLoading_LimitedWorkspace\n }\n savedViewGroup(id: $savedViewGroupId) {\n id\n title\n ...PresentationViewerPageWrapper_SavedViewGroup\n ...PresentationHeader_SavedViewGroup\n ...PresentationSlideList_SavedViewGroup\n ...PresentationPageWrapper_SavedViewGroup\n ...PresentationLoading_SavedViewGroup\n views(input: $input) {\n totalCount\n items {\n id\n name\n description\n screenshot\n projectId\n visibility\n ...PresentationInfoSidebar_SavedView\n group {\n id\n }\n }\n }\n }\n }\n }\n": types.ProjectPresentationPageDocument,
|
||||
"\n query ProjectPresentationPage(\n $input: SavedViewGroupViewsInput!\n $savedViewGroupId: ID!\n $projectId: String!\n ) {\n project(id: $projectId) {\n id\n limitedWorkspace {\n id\n ...PresentationLeftSidebar_LimitedWorkspace\n ...PresentationLoading_LimitedWorkspace\n }\n savedViewGroup(id: $savedViewGroupId) {\n id\n title\n ...PresentationViewerPageWrapper_SavedViewGroup\n ...PresentationHeader_SavedViewGroup\n ...PresentationSlideList_SavedViewGroup\n ...PresentationPageWrapper_SavedViewGroup\n ...PresentationLoading_SavedViewGroup\n views(input: $input) {\n totalCount\n items {\n id\n name\n description\n thumbnailUrl\n projectId\n visibility\n ...PresentationInfoSidebar_SavedView\n group {\n id\n }\n }\n }\n }\n }\n }\n": types.ProjectPresentationPageDocument,
|
||||
"\n fragment UseCopyModelLink_Model on Model {\n id\n projectId\n ...GetModelItemRoute_Model\n }\n": types.UseCopyModelLink_ModelFragmentDoc,
|
||||
"\n fragment UseCanCreatePersonalProject_User on User {\n permissions {\n canCreatePersonalProject {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.UseCanCreatePersonalProject_UserFragmentDoc,
|
||||
"\n fragment UseCanCreateWorkspace_User on User {\n permissions {\n canCreateWorkspace {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.UseCanCreateWorkspace_UserFragmentDoc,
|
||||
@@ -1365,11 +1365,11 @@ export function graphql(source: "\n mutation PresentationShareDisableToken($inp
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment PresentationSlideEditDialog_SavedView on SavedView {\n id\n projectId\n name\n description\n screenshot\n }\n"): (typeof documents)["\n fragment PresentationSlideEditDialog_SavedView on SavedView {\n id\n projectId\n name\n description\n screenshot\n }\n"];
|
||||
export function graphql(source: "\n fragment PresentationSlideEditDialog_SavedView on SavedView {\n id\n projectId\n name\n description\n thumbnailUrl\n }\n"): (typeof documents)["\n fragment PresentationSlideEditDialog_SavedView on SavedView {\n id\n projectId\n name\n description\n thumbnailUrl\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment PresentationSlideListSlide_SavedView on SavedView {\n id\n name\n screenshot\n }\n"): (typeof documents)["\n fragment PresentationSlideListSlide_SavedView on SavedView {\n id\n name\n screenshot\n }\n"];
|
||||
export function graphql(source: "\n fragment PresentationSlideListSlide_SavedView on SavedView {\n id\n name\n thumbnailUrl\n }\n"): (typeof documents)["\n fragment PresentationSlideListSlide_SavedView on SavedView {\n id\n name\n thumbnailUrl\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -1817,7 +1817,7 @@ export function graphql(source: "\n query ViewerSavedViewsPanelGroups_SavedView
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n visibility\n isHomeView\n resourceIds\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n ...UseDraggableView_SavedView\n }\n"): (typeof documents)["\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n visibility\n isHomeView\n resourceIds\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n ...UseDraggableView_SavedView\n }\n"];
|
||||
export function graphql(source: "\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n thumbnailUrl\n visibility\n isHomeView\n resourceIds\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n ...UseDraggableView_SavedView\n }\n"): (typeof documents)["\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n thumbnailUrl\n visibility\n isHomeView\n resourceIds\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n ...UseDraggableView_SavedView\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -2265,7 +2265,7 @@ export function graphql(source: "\n query PresentationAccessCheck($savedViewGro
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query ProjectPresentationPage(\n $input: SavedViewGroupViewsInput!\n $savedViewGroupId: ID!\n $projectId: String!\n ) {\n project(id: $projectId) {\n id\n limitedWorkspace {\n id\n ...PresentationLeftSidebar_LimitedWorkspace\n ...PresentationLoading_LimitedWorkspace\n }\n savedViewGroup(id: $savedViewGroupId) {\n id\n title\n ...PresentationViewerPageWrapper_SavedViewGroup\n ...PresentationHeader_SavedViewGroup\n ...PresentationSlideList_SavedViewGroup\n ...PresentationPageWrapper_SavedViewGroup\n ...PresentationLoading_SavedViewGroup\n views(input: $input) {\n totalCount\n items {\n id\n name\n description\n screenshot\n projectId\n visibility\n ...PresentationInfoSidebar_SavedView\n group {\n id\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query ProjectPresentationPage(\n $input: SavedViewGroupViewsInput!\n $savedViewGroupId: ID!\n $projectId: String!\n ) {\n project(id: $projectId) {\n id\n limitedWorkspace {\n id\n ...PresentationLeftSidebar_LimitedWorkspace\n ...PresentationLoading_LimitedWorkspace\n }\n savedViewGroup(id: $savedViewGroupId) {\n id\n title\n ...PresentationViewerPageWrapper_SavedViewGroup\n ...PresentationHeader_SavedViewGroup\n ...PresentationSlideList_SavedViewGroup\n ...PresentationPageWrapper_SavedViewGroup\n ...PresentationLoading_SavedViewGroup\n views(input: $input) {\n totalCount\n items {\n id\n name\n description\n screenshot\n projectId\n visibility\n ...PresentationInfoSidebar_SavedView\n group {\n id\n }\n }\n }\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n query ProjectPresentationPage(\n $input: SavedViewGroupViewsInput!\n $savedViewGroupId: ID!\n $projectId: String!\n ) {\n project(id: $projectId) {\n id\n limitedWorkspace {\n id\n ...PresentationLeftSidebar_LimitedWorkspace\n ...PresentationLoading_LimitedWorkspace\n }\n savedViewGroup(id: $savedViewGroupId) {\n id\n title\n ...PresentationViewerPageWrapper_SavedViewGroup\n ...PresentationHeader_SavedViewGroup\n ...PresentationSlideList_SavedViewGroup\n ...PresentationPageWrapper_SavedViewGroup\n ...PresentationLoading_SavedViewGroup\n views(input: $input) {\n totalCount\n items {\n id\n name\n description\n thumbnailUrl\n projectId\n visibility\n ...PresentationInfoSidebar_SavedView\n group {\n id\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query ProjectPresentationPage(\n $input: SavedViewGroupViewsInput!\n $savedViewGroupId: ID!\n $projectId: String!\n ) {\n project(id: $projectId) {\n id\n limitedWorkspace {\n id\n ...PresentationLeftSidebar_LimitedWorkspace\n ...PresentationLoading_LimitedWorkspace\n }\n savedViewGroup(id: $savedViewGroupId) {\n id\n title\n ...PresentationViewerPageWrapper_SavedViewGroup\n ...PresentationHeader_SavedViewGroup\n ...PresentationSlideList_SavedViewGroup\n ...PresentationPageWrapper_SavedViewGroup\n ...PresentationLoading_SavedViewGroup\n views(input: $input) {\n totalCount\n items {\n id\n name\n description\n thumbnailUrl\n projectId\n visibility\n ...PresentationInfoSidebar_SavedView\n group {\n id\n }\n }\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -38,7 +38,7 @@ export const projectPresentationPageQuery = graphql(`
|
||||
id
|
||||
name
|
||||
description
|
||||
screenshot
|
||||
thumbnailUrl
|
||||
projectId
|
||||
visibility
|
||||
...PresentationInfoSidebar_SavedView
|
||||
|
||||
@@ -39,9 +39,12 @@ type SavedView {
|
||||
"""
|
||||
viewerState: JSONObject!
|
||||
"""
|
||||
Encoded screenshot of the view
|
||||
Encoded screenshot of the view. Can be a very large value, its preferred you
|
||||
use the thumbnailUrl or previewUrl fields to load the image from a separate endpoint
|
||||
"""
|
||||
screenshot: String!
|
||||
screenshot: String! @deprecated(reason: "Use thumbnailUrl or previewUrl instead")
|
||||
thumbnailUrl: String!
|
||||
previewUrl: String!
|
||||
"""
|
||||
For figuring out position in the group
|
||||
"""
|
||||
|
||||
@@ -3691,13 +3691,19 @@ export type SavedView = {
|
||||
permissions: SavedViewPermissionChecks;
|
||||
/** For figuring out position in the group */
|
||||
position: Scalars['Float']['output'];
|
||||
previewUrl: Scalars['String']['output'];
|
||||
projectId: Scalars['ID']['output'];
|
||||
/** Original resource ID string that this view is associated with. */
|
||||
resourceIdString: Scalars['String']['output'];
|
||||
/** Same as resourceIdString, but split into an array of resource IDs. */
|
||||
resourceIds: Array<Scalars['String']['output']>;
|
||||
/** Encoded screenshot of the view */
|
||||
/**
|
||||
* Encoded screenshot of the view. Can be a very large value, its preferred you
|
||||
* use the thumbnailUrl or previewUrl fields to load the image from a separate endpoint
|
||||
* @deprecated Use thumbnailUrl or previewUrl instead
|
||||
*/
|
||||
screenshot: Scalars['String']['output'];
|
||||
thumbnailUrl: Scalars['String']['output'];
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
/** Viewer state, the actual view configuration */
|
||||
viewerState: Scalars['JSONObject']['output'];
|
||||
@@ -8274,10 +8280,12 @@ export type SavedViewResolvers<ContextType = GraphQLContext, ParentType extends
|
||||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
permissions?: Resolver<ResolversTypes['SavedViewPermissionChecks'], ParentType, ContextType>;
|
||||
position?: Resolver<ResolversTypes['Float'], ParentType, ContextType>;
|
||||
previewUrl?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
projectId?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
resourceIdString?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
resourceIds?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
screenshot?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
thumbnailUrl?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
updatedAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
|
||||
viewerState?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
|
||||
visibility?: Resolver<ResolversTypes['SavedViewVisibility'], ParentType, ContextType>;
|
||||
|
||||
@@ -51,6 +51,7 @@ import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper'
|
||||
import { throwIfResourceAccessNotAllowed } from '@/modules/core/helpers/token'
|
||||
import { TokenResourceIdentifierType } from '@/modules/core/domain/tokens/types'
|
||||
import { withOperationLogging } from '@/observability/domain/businessLogging'
|
||||
import { getThumbnailUrl } from '@/modules/viewer/helpers/savedViews'
|
||||
|
||||
export default {
|
||||
User: {
|
||||
@@ -199,7 +200,10 @@ export default {
|
||||
})
|
||||
|
||||
if (homeView) {
|
||||
return homeView.screenshot
|
||||
return getThumbnailUrl({
|
||||
projectId: parent.streamId,
|
||||
viewId: homeView.id
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import { moduleLogger } from '@/observability/logging'
|
||||
import { addMocksToSchema } from '@graphql-tools/mock'
|
||||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
import type { Optional } from '@speckle/shared'
|
||||
import { isNonNullable, TIME_MS } from '@speckle/shared'
|
||||
import { Authz, isNonNullable, TIME_MS } from '@speckle/shared'
|
||||
import type { SpeckleModule } from '@/modules/shared/helpers/typeHelper'
|
||||
import type { Express } from 'express'
|
||||
import type { RequestDataLoadersBuilder } from '@/modules/shared/helpers/graphqlHelper'
|
||||
@@ -51,6 +51,7 @@ import type {
|
||||
AuthCheckContextLoaders
|
||||
} from '@speckle/shared/authz'
|
||||
import { AuthCheckContextLoaderKeys } from '@speckle/shared/authz'
|
||||
import type { AuthContext } from '@/modules/shared/authz'
|
||||
|
||||
/**
|
||||
* Cached speckle module requires
|
||||
@@ -462,3 +463,17 @@ export const moduleAuthLoaders = async (params: {
|
||||
internalCache: cache
|
||||
}
|
||||
}
|
||||
|
||||
export const buildAuthPolicies = async (params: {
|
||||
/**
|
||||
* Undefined means - treat it as an anonymous req
|
||||
*/
|
||||
authContext?: AuthContext
|
||||
}) => {
|
||||
const dataLoaders: RequestDataLoaders | undefined = params.authContext
|
||||
? await buildRequestLoaders(params.authContext)
|
||||
: undefined
|
||||
|
||||
const authLoaders = await moduleAuthLoaders({ dataLoaders })
|
||||
return Authz.authPoliciesFactory(authLoaders.loaders)
|
||||
}
|
||||
|
||||
@@ -276,11 +276,11 @@ const dropReplicationIfExists = async ({
|
||||
}
|
||||
|
||||
try {
|
||||
const { rows: aivenExists } = await to.public.raw(
|
||||
const { rows: aivenExists } = (await to.public.raw(
|
||||
"SELECT * FROM pg_extension WHERE extname = 'aiven_extras';"
|
||||
)
|
||||
)) as { rows: Array<unknown> }
|
||||
|
||||
if (!aivenExists) return
|
||||
if (!aivenExists.length) return
|
||||
|
||||
const {
|
||||
rows: [sub]
|
||||
|
||||
@@ -161,7 +161,7 @@ export const validateResourceAccess: AuthPipelineFunction = async ({
|
||||
if (authHasFailed(authResult)) return { context, authResult }
|
||||
if (!resourceAccessRules?.length) return authSuccess(context)
|
||||
|
||||
const streamId = context.stream?.id || params?.streamId
|
||||
const streamId = context.stream?.id || params?.streamId || params?.projectId
|
||||
if (!streamId) {
|
||||
return authSuccess(context)
|
||||
}
|
||||
@@ -225,8 +225,9 @@ export const validateRequiredStreamFactory =
|
||||
// IoC baby...
|
||||
async ({ context, authResult, params }) => {
|
||||
const { getStream } = deps
|
||||
const streamId = params?.streamId || params?.projectId
|
||||
|
||||
if (!params?.streamId)
|
||||
if (!streamId)
|
||||
return authFailed(
|
||||
context,
|
||||
new ContextError("The context doesn't have a streamId")
|
||||
@@ -240,7 +241,7 @@ export const validateRequiredStreamFactory =
|
||||
// keep the pipeline rolling
|
||||
try {
|
||||
const stream = await getStream({
|
||||
streamId: params.streamId,
|
||||
streamId,
|
||||
userId: context?.userId
|
||||
})
|
||||
|
||||
@@ -250,12 +251,13 @@ export const validateRequiredStreamFactory =
|
||||
new NotFoundError(
|
||||
'Project ID is malformed and cannot be found, or the project does not exist',
|
||||
{
|
||||
info: { projectId: params.streamId }
|
||||
info: { projectId: streamId }
|
||||
}
|
||||
),
|
||||
true
|
||||
)
|
||||
context.stream = stream
|
||||
context.project = stream
|
||||
return { context, authResult }
|
||||
} catch (err) {
|
||||
// this prob needs some more detailing to not leak internal errors
|
||||
@@ -328,8 +330,9 @@ const validateStreamPolicyAccessFactory =
|
||||
const { context, params, authResult } = authData
|
||||
|
||||
if (authHasFailed(authResult)) return { context, authResult }
|
||||
const streamId = params?.streamId || params?.projectId
|
||||
|
||||
if (!params?.streamId)
|
||||
if (!streamId)
|
||||
return authFailed(
|
||||
context,
|
||||
new ContextError("The context doesn't have a streamId")
|
||||
@@ -348,7 +351,7 @@ const validateStreamPolicyAccessFactory =
|
||||
new NotFoundError(
|
||||
'Project ID is malformed and cannot be found, or the project does not exist',
|
||||
{
|
||||
info: { projectId: params.streamId }
|
||||
info: { projectId: streamId }
|
||||
}
|
||||
),
|
||||
true
|
||||
@@ -369,7 +372,7 @@ export const streamWritePermissionsPipelineFactory = (deps: {
|
||||
policyInvoker: async ({ authData, policies }) =>
|
||||
policies.project.version.canCreate({
|
||||
userId: authData.context.userId,
|
||||
projectId: authData.params!.streamId!
|
||||
projectId: authData.params!.streamId! || authData.params!.projectId!
|
||||
})
|
||||
})
|
||||
]
|
||||
@@ -385,7 +388,7 @@ export const streamCommentsWritePermissionsPipelineFactory = (deps: {
|
||||
policyInvoker: async ({ authData, policies }) =>
|
||||
policies.project.comment.canCreate({
|
||||
userId: authData.context.userId,
|
||||
projectId: authData.params!.streamId!
|
||||
projectId: authData.params!.streamId! || authData.params!.projectId!
|
||||
})
|
||||
})
|
||||
]
|
||||
@@ -401,7 +404,7 @@ export const streamReadPermissionsPipelineFactory = (deps: {
|
||||
policyInvoker: async ({ authData, policies }) =>
|
||||
policies.project.canRead({
|
||||
userId: authData.context.userId,
|
||||
projectId: authData.params!.streamId!
|
||||
projectId: authData.params!.streamId! || authData.params!.projectId!
|
||||
})
|
||||
})
|
||||
]
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface AuthContext {
|
||||
token?: string
|
||||
scopes?: string[]
|
||||
stream?: StreamWithOptionalRole
|
||||
project?: StreamWithOptionalRole
|
||||
err?: Error | BaseError
|
||||
/**
|
||||
* Set if authenticated with an app token
|
||||
@@ -28,6 +29,8 @@ export interface AuthResult {
|
||||
|
||||
export interface AuthParams {
|
||||
streamId?: string
|
||||
projectId?: string
|
||||
viewId?: string
|
||||
}
|
||||
|
||||
export interface AuthData {
|
||||
|
||||
@@ -4,8 +4,14 @@ import type {
|
||||
SavedViewGroup,
|
||||
SavedViewVisibility
|
||||
} from '@/modules/viewer/domain/types/savedViews'
|
||||
import type { MaybeNullOrUndefined, NullableKeysToOptional } from '@speckle/shared'
|
||||
import type { StringEnumValues } from '@speckle/shared'
|
||||
import {
|
||||
StringEnum,
|
||||
type MaybeNullOrUndefined,
|
||||
type NullableKeysToOptional
|
||||
} from '@speckle/shared'
|
||||
import type { SerializedViewerState } from '@speckle/shared/viewer/state'
|
||||
import type { Response } from 'express'
|
||||
import type { Exact, SetOptional } from 'type-fest'
|
||||
|
||||
/////////////////////
|
||||
@@ -307,3 +313,17 @@ export type UpdateSavedViewGroup = (params: {
|
||||
}
|
||||
userId: string
|
||||
}) => Promise<SavedViewGroup>
|
||||
|
||||
export const SavedViewPreviewType = StringEnum(['preview', 'thumbnail'])
|
||||
export type SavedViewPreviewType = StringEnumValues<typeof SavedViewPreviewType>
|
||||
|
||||
export type OutputSavedViewPreview = (params: {
|
||||
res: Response
|
||||
projectId: string
|
||||
viewId: string
|
||||
type: SavedViewPreviewType
|
||||
}) => Promise<void>
|
||||
|
||||
export type DownscaleScreenshotForThumbnail = (params: {
|
||||
screenshot: string
|
||||
}) => Promise<string>
|
||||
|
||||
@@ -29,6 +29,7 @@ export type SavedView = {
|
||||
visibility: SavedViewVisibility
|
||||
viewerState: VersionedSerializedViewerState
|
||||
screenshot: string
|
||||
thumbnail: string
|
||||
position: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
|
||||
@@ -47,3 +47,15 @@ export class SavedViewPositionUpdateError extends BaseError {
|
||||
static defaultMessage = 'Failed to update saved view position'
|
||||
static statusCode = 400
|
||||
}
|
||||
|
||||
export class SavedViewPreviewRetrievalError extends BaseError {
|
||||
static code = 'SAVED_VIEW_PREVIEW_RETRIEVAL_ERROR'
|
||||
static defaultMessage = 'Could not retrieve saved view preview'
|
||||
static statusCode = 400
|
||||
}
|
||||
|
||||
export class SavedViewScreenshotError extends BaseError {
|
||||
static code = 'SAVED_VIEW_SCREENSHOT_ERROR'
|
||||
static defaultMessage = 'Could not process saved view screenshot'
|
||||
static statusCode = 400
|
||||
}
|
||||
|
||||
@@ -15,7 +15,11 @@ import { getStreamObjectsFactory } from '@/modules/core/repositories/objects'
|
||||
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
||||
import { LogicError, NotFoundError, NotImplementedError } from '@/modules/shared/errors'
|
||||
import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper'
|
||||
import { buildDefaultGroupId } from '@/modules/viewer/helpers/savedViews'
|
||||
import {
|
||||
buildDefaultGroupId,
|
||||
getPreviewUrl,
|
||||
getThumbnailUrl
|
||||
} from '@/modules/viewer/helpers/savedViews'
|
||||
import {
|
||||
deleteSavedViewGroupRecordFactory,
|
||||
deleteSavedViewRecordFactory,
|
||||
@@ -59,6 +63,7 @@ import {
|
||||
} from '@/modules/viewer/repositories/dataLoaders/savedViews'
|
||||
import type { RequestDataLoaders } from '@/modules/core/loaders'
|
||||
import { omit } from 'lodash-es'
|
||||
import { downscaleScreenshotForThumbnailFactory } from '@/modules/viewer/services/savedViewPreviews'
|
||||
|
||||
const buildGetViewerResourceGroups = (params: {
|
||||
projectDb: Knex
|
||||
@@ -238,6 +243,18 @@ const resolvers: Resolvers = {
|
||||
}
|
||||
|
||||
return group
|
||||
},
|
||||
previewUrl(parent) {
|
||||
return getPreviewUrl({
|
||||
projectId: parent.projectId,
|
||||
viewId: parent.id
|
||||
})
|
||||
},
|
||||
thumbnailUrl(parent) {
|
||||
return getThumbnailUrl({
|
||||
projectId: parent.projectId,
|
||||
viewId: parent.id
|
||||
})
|
||||
}
|
||||
},
|
||||
SavedViewGroup: {
|
||||
@@ -316,7 +333,8 @@ const resolvers: Resolvers = {
|
||||
getNewViewSpecificPosition: getNewViewSpecificPositionFactory({
|
||||
db: projectDb
|
||||
}),
|
||||
rebalanceViewPositions: rebalancingViewPositionsFactory({ db: projectDb })
|
||||
rebalanceViewPositions: rebalancingViewPositionsFactory({ db: projectDb }),
|
||||
downscaleScreenshotForThumbnail: downscaleScreenshotForThumbnailFactory()
|
||||
})
|
||||
return await createSavedView({ input: args.input, authorId: ctx.userId! })
|
||||
},
|
||||
@@ -410,7 +428,8 @@ const resolvers: Resolvers = {
|
||||
rebalanceViewPositions: rebalancingViewPositionsFactory({ db: projectDb }),
|
||||
getNewViewSpecificPosition: getNewViewSpecificPositionFactory({
|
||||
db: projectDb
|
||||
})
|
||||
}),
|
||||
downscaleScreenshotForThumbnail: downscaleScreenshotForThumbnailFactory()
|
||||
})
|
||||
|
||||
const updatedView = await updateSavedView({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getServerOrigin } from '@/modules/shared/helpers/envHelper'
|
||||
import {
|
||||
formatResourceIdsForGroup,
|
||||
buildDefaultGroupId,
|
||||
@@ -5,5 +6,24 @@ import {
|
||||
type DefaultGroupMetadata
|
||||
} from '@speckle/shared/saved-views'
|
||||
|
||||
export const thumbnailRoute =
|
||||
'/api/v1/projects/:projectId/saved-views/:viewId/thumbnail'
|
||||
export const fullPreviewRoute =
|
||||
'/api/v1/projects/:projectId/saved-views/:viewId/preview'
|
||||
|
||||
export const getThumbnailUrl = (params: { projectId: string; viewId: string }) => {
|
||||
const route = thumbnailRoute
|
||||
.replace(':projectId', params.projectId)
|
||||
.replace(':viewId', params.viewId)
|
||||
return new URL(route, getServerOrigin()).toString()
|
||||
}
|
||||
|
||||
export const getPreviewUrl = (params: { projectId: string; viewId: string }) => {
|
||||
const route = fullPreviewRoute
|
||||
.replace(':projectId', params.projectId)
|
||||
.replace(':viewId', params.viewId)
|
||||
return new URL(route, getServerOrigin()).toString()
|
||||
}
|
||||
|
||||
export { formatResourceIdsForGroup, buildDefaultGroupId, decodeDefaultGroupId }
|
||||
export type { DefaultGroupMetadata }
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
import type { SpeckleModule } from '@/modules/shared/helpers/typeHelper'
|
||||
import { getSavedViewsRouter } from '@/modules/viewer/rest/savedViews'
|
||||
import { viewerLogger } from '@/observability/logging'
|
||||
|
||||
const viewerModule: SpeckleModule = {
|
||||
init: async () => {
|
||||
init: async ({ app }) => {
|
||||
if (!getFeatureFlags().FF_SAVED_VIEWS_ENABLED) return
|
||||
|
||||
viewerLogger.info('🤩 Initializing viewer module...')
|
||||
app.use(getSavedViewsRouter())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { Knex } from 'knex'
|
||||
|
||||
const tableName = 'saved_views'
|
||||
const col = 'thumbnail'
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(tableName, (table) => {
|
||||
table.text(col).defaultTo('')
|
||||
})
|
||||
|
||||
// Update all existing rows to copy screenshot -> thumbnail
|
||||
await knex.raw(`UPDATE ${tableName} SET ${col} = screenshot WHERE ${col} = ''`)
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(tableName, (table) => {
|
||||
table.dropColumn(col)
|
||||
})
|
||||
}
|
||||
@@ -69,6 +69,7 @@ export const SavedViews = buildTableHelper('saved_views', [
|
||||
'visibility',
|
||||
'viewerState',
|
||||
'screenshot',
|
||||
'thumbnail',
|
||||
'position',
|
||||
'createdAt',
|
||||
'updatedAt'
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import type { ErrorRequestHandler, Request, Response } from 'express'
|
||||
import { Router } from 'express'
|
||||
import cors from 'cors'
|
||||
import { allowCrossOriginResourceAccessMiddelware } from '@/modules/shared/middleware/security'
|
||||
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
||||
import { outputSavedViewPreviewFactory } from '@/modules/viewer/services/savedViewPreviews'
|
||||
import { getSavedViewFactory } from '@/modules/viewer/repositories/savedViews'
|
||||
import { SavedViewPreviewType } from '@/modules/viewer/domain/operations/savedViews'
|
||||
import { ensureError } from '@speckle/shared'
|
||||
import { resolveStatusCode } from '@/modules/core/rest/defaultErrorHandler'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { buildAuthPolicies } from '@/modules'
|
||||
import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper'
|
||||
import { StreamNotFoundError } from '@/modules/core/errors/stream'
|
||||
import { NotFoundError } from '@/modules/shared/errors'
|
||||
import { fullPreviewRoute, thumbnailRoute } from '@/modules/viewer/helpers/savedViews'
|
||||
|
||||
const previewErrorPath = () =>
|
||||
fileURLToPath(import.meta.resolve('#/assets/previews/images/preview_error.png'))
|
||||
const preview404Path = () =>
|
||||
fileURLToPath(import.meta.resolve('#/assets/previews/images/preview_404.png'))
|
||||
const preview401Path = () =>
|
||||
fileURLToPath(import.meta.resolve('#/assets/previews/images/preview_401.png'))
|
||||
|
||||
const previewErrHandler: ErrorRequestHandler = (err, req, res, next) => {
|
||||
if (!err) return next()
|
||||
|
||||
// Return failure image, instead of throwing
|
||||
const error = ensureError(err)
|
||||
const status = resolveStatusCode(error)
|
||||
res.header('X-Error-Message', error.message)
|
||||
res.header('Cache-Control', 'no-cache, no-store')
|
||||
res.status(status)
|
||||
|
||||
if (error instanceof StreamNotFoundError || error instanceof NotFoundError) {
|
||||
return res.sendFile(preview404Path())
|
||||
} else if (status === 401) {
|
||||
return res.sendFile(preview401Path())
|
||||
} else {
|
||||
return res.sendFile(previewErrorPath())
|
||||
}
|
||||
}
|
||||
|
||||
const buildPreviewRoute = (
|
||||
router: Router,
|
||||
type: SavedViewPreviewType,
|
||||
route: string
|
||||
) => {
|
||||
router.options(route, cors(), allowCrossOriginResourceAccessMiddelware())
|
||||
router.get(
|
||||
route,
|
||||
cors(),
|
||||
allowCrossOriginResourceAccessMiddelware(),
|
||||
async (req: Request, res: Response) => {
|
||||
const projectId = req.params.projectId
|
||||
const viewId = req.params.viewId
|
||||
|
||||
// Access check
|
||||
const authz = await buildAuthPolicies({
|
||||
authContext: req.context
|
||||
})
|
||||
const authResults = await Promise.all([
|
||||
authz.project.canRead({
|
||||
userId: req.context.userId,
|
||||
projectId
|
||||
}),
|
||||
authz.project.savedViews.canRead({
|
||||
userId: req.context.userId,
|
||||
projectId,
|
||||
savedViewId: viewId,
|
||||
allowNonExistent: true // we check inside the service layer anyway
|
||||
})
|
||||
])
|
||||
authResults.forEach(throwIfAuthNotOk)
|
||||
|
||||
// Access is fine - look for the view
|
||||
const projectDb = await getProjectDbClient({ projectId })
|
||||
const outputSavedViewPreview = outputSavedViewPreviewFactory({
|
||||
getSavedView: getSavedViewFactory({ db: projectDb })
|
||||
})
|
||||
|
||||
await outputSavedViewPreview({ res, projectId, viewId, type })
|
||||
},
|
||||
previewErrHandler
|
||||
)
|
||||
}
|
||||
|
||||
export const getSavedViewsRouter = (): Router => {
|
||||
const router = Router()
|
||||
|
||||
buildPreviewRoute(router, SavedViewPreviewType.thumbnail, thumbnailRoute)
|
||||
buildPreviewRoute(router, SavedViewPreviewType.preview, fullPreviewRoute)
|
||||
|
||||
return router
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
SavedViewPreviewType,
|
||||
type DownscaleScreenshotForThumbnail,
|
||||
type GetSavedView,
|
||||
type OutputSavedViewPreview
|
||||
} from '@/modules/viewer/domain/operations/savedViews'
|
||||
import { SavedViewPreviewRetrievalError } from '@/modules/viewer/errors/savedViews'
|
||||
import sharp from 'sharp'
|
||||
|
||||
const THUMBNAIL_WIDTH = 420
|
||||
const THUMBNAIL_HEIGHT = 240
|
||||
|
||||
const screenshotToBuffer = (screenshot: string) => {
|
||||
// no `data:image/png;base64,` prefix
|
||||
const preview = screenshot.replace(/^data:image\/png;base64,/, '')
|
||||
return Buffer.from(preview, 'base64')
|
||||
}
|
||||
|
||||
export const outputSavedViewPreviewFactory =
|
||||
(deps: { getSavedView: GetSavedView }): OutputSavedViewPreview =>
|
||||
async (params) => {
|
||||
const { res, projectId, viewId, type } = params
|
||||
const view = await deps.getSavedView({ projectId, id: viewId })
|
||||
if (!view) {
|
||||
throw new SavedViewPreviewRetrievalError('Could not find view', {
|
||||
info: { projectId, viewId }
|
||||
})
|
||||
}
|
||||
|
||||
// both should be set, but early on in development we only had the one
|
||||
const image =
|
||||
(type === SavedViewPreviewType.preview ? view.screenshot : view.thumbnail) ||
|
||||
view.screenshot
|
||||
const imgBuffer = screenshotToBuffer(image)
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'image/png',
|
||||
'Content-Length': imgBuffer.length,
|
||||
'Cache-Control': 'no-cache, no-store'
|
||||
})
|
||||
|
||||
res.end(imgBuffer)
|
||||
}
|
||||
|
||||
export const downscaleScreenshotForThumbnailFactory =
|
||||
(): DownscaleScreenshotForThumbnail => async (params: { screenshot: string }) => {
|
||||
const { screenshot } = params
|
||||
const imgBuffer = screenshotToBuffer(screenshot)
|
||||
|
||||
// Use sharp to get metadata
|
||||
const image = sharp(imgBuffer)
|
||||
const meta = await image.metadata()
|
||||
const { width: srcW, height: srcH } = meta
|
||||
|
||||
// If source is already smaller or equal in both dimensions, do nothing
|
||||
if (srcW <= THUMBNAIL_WIDTH && srcH <= THUMBNAIL_HEIGHT) {
|
||||
return screenshot
|
||||
}
|
||||
|
||||
// Otherwise, resize (downscale). Use withoutEnlargement to guard.
|
||||
const outBuf = await image
|
||||
.resize(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, {
|
||||
fit: 'inside', // ensures we maintain aspect ratio and fit *within* box
|
||||
withoutEnlargement: true
|
||||
})
|
||||
// Optionally, set output format / quality depending on mimeType
|
||||
.toFormat(meta.format || 'png', { quality: 100 })
|
||||
.toBuffer()
|
||||
|
||||
// Convert back to base64 with prefix
|
||||
const outB64 = outBuf.toString('base64')
|
||||
const prefix = `data:image/png;base64,`
|
||||
return `${prefix}${outB64}`
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
DeleteSavedViewGroup,
|
||||
DeleteSavedViewGroupRecord,
|
||||
DeleteSavedViewRecord,
|
||||
DownscaleScreenshotForThumbnail,
|
||||
GetGroupSavedViews,
|
||||
GetGroupSavedViewsPageItems,
|
||||
GetGroupSavedViewsTotalCount,
|
||||
@@ -34,6 +35,7 @@ import {
|
||||
SavedViewGroupUpdateValidationError,
|
||||
SavedViewInvalidHomeViewSettingsError,
|
||||
SavedViewInvalidResourceTargetError,
|
||||
SavedViewScreenshotError,
|
||||
SavedViewUpdateValidationError
|
||||
} from '@/modules/viewer/errors/savedViews'
|
||||
import type {
|
||||
@@ -53,6 +55,25 @@ import { removeNullOrUndefinedKeys, firstDefinedValue } from '@speckle/shared'
|
||||
import { isUngroupedGroup } from '@speckle/shared/saved-views'
|
||||
import { NotFoundError } from '@/modules/shared/errors'
|
||||
|
||||
const formatIncomingScreenshotFactory =
|
||||
(deps: { downscaleScreenshotForThumbnail: DownscaleScreenshotForThumbnail }) =>
|
||||
async (params: { screenshot: string; errorMetadata: Record<string, unknown> }) => {
|
||||
const screenshot = params.screenshot.trim()
|
||||
if (!isValidBase64Image(screenshot)) {
|
||||
throw new SavedViewScreenshotError(
|
||||
'Invalid screenshot provided. Must be a valid base64 encoded image.',
|
||||
{
|
||||
info: params.errorMetadata
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
screenshot,
|
||||
thumbnail: await deps.downscaleScreenshotForThumbnail({ screenshot })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an incoming resourceIdString against the resources in the project and returns the validated list (as a builder)
|
||||
*/
|
||||
@@ -231,6 +252,7 @@ export const createSavedViewFactory =
|
||||
setNewHomeView: SetNewHomeView
|
||||
getNewViewSpecificPosition: GetNewViewSpecificPosition
|
||||
rebalanceViewPositions: RebalanceViewPositions
|
||||
downscaleScreenshotForThumbnail: DownscaleScreenshotForThumbnail
|
||||
}): CreateSavedView =>
|
||||
async ({ input, authorId }) => {
|
||||
const { resourceIdString, projectId, position: positionInput } = input
|
||||
@@ -249,18 +271,10 @@ export const createSavedViewFactory =
|
||||
}
|
||||
})
|
||||
|
||||
const screenshot = input.screenshot.trim()
|
||||
if (!isValidBase64Image(screenshot)) {
|
||||
throw new SavedViewCreationValidationError(
|
||||
'Invalid screenshot provided. Must be a valid base64 encoded image.',
|
||||
{
|
||||
info: {
|
||||
input,
|
||||
authorId
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
const { screenshot, thumbnail } = await formatIncomingScreenshotFactory(deps)({
|
||||
screenshot: input.screenshot,
|
||||
errorMetadata: { input, authorId }
|
||||
})
|
||||
|
||||
// Validate state
|
||||
const state = validateViewerStateFactory()({
|
||||
@@ -333,6 +347,7 @@ export const createSavedViewFactory =
|
||||
description,
|
||||
viewerState: state,
|
||||
screenshot,
|
||||
thumbnail,
|
||||
visibility,
|
||||
position,
|
||||
authorId,
|
||||
@@ -494,6 +509,7 @@ export const updateSavedViewFactory =
|
||||
setNewHomeView: SetNewHomeView
|
||||
getNewViewSpecificPosition: GetNewViewSpecificPosition
|
||||
rebalanceViewPositions: RebalanceViewPositions
|
||||
downscaleScreenshotForThumbnail: DownscaleScreenshotForThumbnail
|
||||
} & DependenciesOf<typeof validateProjectResourceIdStringFactory>
|
||||
): UpdateSavedView =>
|
||||
async (params) => {
|
||||
@@ -521,7 +537,7 @@ export const updateSavedViewFactory =
|
||||
const hasResourceIdString = 'resourceIdString' in input && input.resourceIdString
|
||||
const hasViewerState = 'viewerState' in input && input.viewerState
|
||||
const hasScreenshot = 'screenshot' in input && input.screenshot
|
||||
if (hasResourceIdString || hasViewerState) {
|
||||
if (hasResourceIdString || hasViewerState || hasScreenshot) {
|
||||
if (!hasResourceIdString || !hasViewerState || !hasScreenshot) {
|
||||
throw new SavedViewUpdateValidationError(
|
||||
'If the resourceIdString or viewerState are being updated, resourceIdString, viewerState and screenshot must all be submitted.',
|
||||
@@ -586,17 +602,16 @@ export const updateSavedViewFactory =
|
||||
delete changes.groupId // the key shouldnt even be there
|
||||
}
|
||||
|
||||
// Validate screenshot
|
||||
if (changes.screenshot && !isValidBase64Image(changes.screenshot)) {
|
||||
throw new SavedViewUpdateValidationError(
|
||||
'Invalid screenshot provided. Must be a valid base64 encoded image.',
|
||||
{
|
||||
info: {
|
||||
input,
|
||||
userId
|
||||
}
|
||||
}
|
||||
)
|
||||
// Format screenshot
|
||||
let newScreenshot: { screenshot: string; thumbnail: string } | undefined = undefined
|
||||
if (changes.screenshot) {
|
||||
const { screenshot, thumbnail } = await formatIncomingScreenshotFactory(deps)({
|
||||
screenshot: changes.screenshot,
|
||||
errorMetadata: { input, userId }
|
||||
})
|
||||
|
||||
newScreenshot = { screenshot, thumbnail }
|
||||
delete changes['screenshot']
|
||||
}
|
||||
|
||||
// Validate name
|
||||
@@ -659,7 +674,8 @@ export const updateSavedViewFactory =
|
||||
viewerState
|
||||
}
|
||||
: {}),
|
||||
...(!isUndefined(position) ? { position } : {})
|
||||
...(!isUndefined(position) ? { position } : {}),
|
||||
...(newScreenshot ? newScreenshot : {})
|
||||
}
|
||||
|
||||
// Check if there's any actual changes
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
setNewHomeViewFactory,
|
||||
storeSavedViewFactory
|
||||
} from '@/modules/viewer/repositories/savedViews'
|
||||
import { downscaleScreenshotForThumbnailFactory } from '@/modules/viewer/services/savedViewPreviews'
|
||||
import { createSavedViewFactory } from '@/modules/viewer/services/savedViewsManagement'
|
||||
import { getViewerResourceGroupsFactory } from '@/modules/viewer/services/viewerResources'
|
||||
import type { BasicTestUser } from '@/test/authHelper'
|
||||
@@ -40,7 +41,7 @@ export const fakeScreenshot =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PiQ2YQAAAABJRU5ErkJggg=='
|
||||
|
||||
export const fakeScreenshot2 =
|
||||
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAICAgICAgICAgICAgICAwUDAwMDAwYEBAMFBQYGBQYGBwcICQoJCQkJCQoMCgsMDAwMDAwP/2wBDAwMDAwQDBAgEBAgQEBAgMCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgP/wAARCAABAAEDAREAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIQAxAAAAHEAP/EABQQAQAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAQUCf//EABQRAQAAAAAAAAAAAAAAAAAAAD/2gAIAQMBAT8BP//EABQRAQAAAAAAAAAAAAAAAAAAAD/2gAIAQIBAT8BP//Z'
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAIAAAAmkwkpAAAAQ0lEQVR4nGLW+WrL4H6p2dAx/J4S05cr7eGufmJpqo8vsDGlf7q0bGK4Nu88wc+rGb79lZDwi7y6X3x15VpAAAAA//85FRbiEsMfqwAAAABJRU5ErkJggg=='
|
||||
|
||||
export const buildFakeSerializedViewerState = (
|
||||
overrides?: PartialDeep<ViewerState.SerializedViewerState>
|
||||
@@ -93,6 +94,7 @@ export const buildTestSavedView = (overrides?: Partial<SavedView>): SavedView =>
|
||||
})
|
||||
),
|
||||
screenshot: fakeScreenshot,
|
||||
thumbnail: fakeScreenshot,
|
||||
position: 0,
|
||||
createdAt: new Date(Date.now() - 10000),
|
||||
updatedAt: new Date(Date.now() - 10000)
|
||||
@@ -139,7 +141,8 @@ export const createTestSavedView = async (params?: {
|
||||
getNewViewSpecificPosition: getNewViewSpecificPositionFactory({
|
||||
db
|
||||
}),
|
||||
rebalanceViewPositions: rebalancingViewPositionsFactory({ db })
|
||||
rebalanceViewPositions: rebalancingViewPositionsFactory({ db }),
|
||||
downscaleScreenshotForThumbnail: downscaleScreenshotForThumbnailFactory()
|
||||
})
|
||||
|
||||
const createdView = await createSavedView({
|
||||
|
||||
@@ -45,12 +45,12 @@ import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
import type { FactoryResultOf } from '@/modules/shared/helpers/factory'
|
||||
import { SavedViewVisibility } from '@/modules/viewer/domain/types/savedViews'
|
||||
import {
|
||||
SavedViewCreationValidationError,
|
||||
SavedViewGroupCreationValidationError,
|
||||
SavedViewGroupNotFoundError,
|
||||
SavedViewGroupUpdateValidationError,
|
||||
SavedViewInvalidHomeViewSettingsError,
|
||||
SavedViewInvalidResourceTargetError,
|
||||
SavedViewScreenshotError,
|
||||
SavedViewUpdateValidationError
|
||||
} from '@/modules/viewer/errors/savedViews'
|
||||
import {
|
||||
@@ -748,7 +748,7 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
|
||||
)
|
||||
|
||||
expect(res).to.haveGraphQLErrors({
|
||||
code: SavedViewCreationValidationError.code
|
||||
code: SavedViewScreenshotError.code
|
||||
})
|
||||
expect(res.data?.projectMutations.savedViewMutations.createView).to.not.be.ok
|
||||
})
|
||||
@@ -1896,7 +1896,7 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
|
||||
input: {
|
||||
id: testView.id,
|
||||
projectId: updatablesProject.id,
|
||||
screenshot: 'invalid'
|
||||
screenshot: fakeScreenshot2
|
||||
}
|
||||
})
|
||||
expect(res).to.haveGraphQLErrors({ code: SavedViewUpdateValidationError.code })
|
||||
@@ -1922,10 +1922,19 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
|
||||
id: testView.id,
|
||||
projectId: updatablesProject.id,
|
||||
screenshot: 'not-base64',
|
||||
name: 'x'
|
||||
name: 'x',
|
||||
resourceIdString: models[0].id,
|
||||
viewerState: fakeViewerState({
|
||||
projectId: updatablesProject.id,
|
||||
resources: {
|
||||
request: {
|
||||
resourceIdString: models[0].id
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
expect(res).to.haveGraphQLErrors({ code: SavedViewUpdateValidationError.code })
|
||||
expect(res).to.haveGraphQLErrors({ code: SavedViewScreenshotError.code })
|
||||
expect(res.data?.projectMutations.savedViewMutations.updateView).to.not.be.ok
|
||||
})
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
"rate-limiter-flexible": "^2.4.1",
|
||||
"response-time": "^2.3.2",
|
||||
"sanitize-html": "^2.7.1",
|
||||
"sharp": "^0.34.3",
|
||||
"sharp": "^0.34.4",
|
||||
"string-pixel-width": "^1.10.0",
|
||||
"stripe": "^17.1.0",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
|
||||
@@ -4288,7 +4288,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@emnapi/runtime@npm:^1.4.4, @emnapi/runtime@npm:^1.4.5":
|
||||
"@emnapi/runtime@npm:^1.4.5":
|
||||
version: 1.4.5
|
||||
resolution: "@emnapi/runtime@npm:1.4.5"
|
||||
dependencies:
|
||||
@@ -4297,6 +4297,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@emnapi/runtime@npm:^1.5.0":
|
||||
version: 1.5.0
|
||||
resolution: "@emnapi/runtime@npm:1.5.0"
|
||||
dependencies:
|
||||
tslib: "npm:^2.4.0"
|
||||
checksum: 10/5311ce854306babc77f4bd94c2f973722714a0fab93c126239104ad52dea16a147bfed4c4cff3ca1eb32709607221c25d2f747ae8524cbeb9088058f02ff962b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@emnapi/wasi-threads@npm:1.0.4":
|
||||
version: 1.0.4
|
||||
resolution: "@emnapi/wasi-threads@npm:1.0.4"
|
||||
@@ -6722,11 +6731,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-darwin-arm64@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-darwin-arm64@npm:0.34.3"
|
||||
"@img/colour@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@img/colour@npm:1.0.0"
|
||||
checksum: 10/bd248d7c4b8ba99a72b22a005a63f1d3309ee8343a74b6d0d1314bae300a3096919991a09e9a9243cf6ca50e393b4c5a7e065488ed616c3b58d052473240b812
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-darwin-arm64@npm:0.34.4":
|
||||
version: 0.34.4
|
||||
resolution: "@img/sharp-darwin-arm64@npm:0.34.4"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-darwin-arm64": "npm:1.2.0"
|
||||
"@img/sharp-libvips-darwin-arm64": "npm:1.2.3"
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-darwin-arm64":
|
||||
optional: true
|
||||
@@ -6734,11 +6750,11 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-darwin-x64@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-darwin-x64@npm:0.34.3"
|
||||
"@img/sharp-darwin-x64@npm:0.34.4":
|
||||
version: 0.34.4
|
||||
resolution: "@img/sharp-darwin-x64@npm:0.34.4"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-darwin-x64": "npm:1.2.0"
|
||||
"@img/sharp-libvips-darwin-x64": "npm:1.2.3"
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-darwin-x64":
|
||||
optional: true
|
||||
@@ -6746,74 +6762,74 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64@npm:1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "@img/sharp-libvips-darwin-arm64@npm:1.2.0"
|
||||
"@img/sharp-libvips-darwin-arm64@npm:1.2.3":
|
||||
version: 1.2.3
|
||||
resolution: "@img/sharp-libvips-darwin-arm64@npm:1.2.3"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-darwin-x64@npm:1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "@img/sharp-libvips-darwin-x64@npm:1.2.0"
|
||||
"@img/sharp-libvips-darwin-x64@npm:1.2.3":
|
||||
version: 1.2.3
|
||||
resolution: "@img/sharp-libvips-darwin-x64@npm:1.2.3"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-linux-arm64@npm:1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "@img/sharp-libvips-linux-arm64@npm:1.2.0"
|
||||
"@img/sharp-libvips-linux-arm64@npm:1.2.3":
|
||||
version: 1.2.3
|
||||
resolution: "@img/sharp-libvips-linux-arm64@npm:1.2.3"
|
||||
conditions: os=linux & cpu=arm64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-linux-arm@npm:1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "@img/sharp-libvips-linux-arm@npm:1.2.0"
|
||||
"@img/sharp-libvips-linux-arm@npm:1.2.3":
|
||||
version: 1.2.3
|
||||
resolution: "@img/sharp-libvips-linux-arm@npm:1.2.3"
|
||||
conditions: os=linux & cpu=arm & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-linux-ppc64@npm:1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "@img/sharp-libvips-linux-ppc64@npm:1.2.0"
|
||||
"@img/sharp-libvips-linux-ppc64@npm:1.2.3":
|
||||
version: 1.2.3
|
||||
resolution: "@img/sharp-libvips-linux-ppc64@npm:1.2.3"
|
||||
conditions: os=linux & cpu=ppc64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-linux-s390x@npm:1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "@img/sharp-libvips-linux-s390x@npm:1.2.0"
|
||||
"@img/sharp-libvips-linux-s390x@npm:1.2.3":
|
||||
version: 1.2.3
|
||||
resolution: "@img/sharp-libvips-linux-s390x@npm:1.2.3"
|
||||
conditions: os=linux & cpu=s390x & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-linux-x64@npm:1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "@img/sharp-libvips-linux-x64@npm:1.2.0"
|
||||
"@img/sharp-libvips-linux-x64@npm:1.2.3":
|
||||
version: 1.2.3
|
||||
resolution: "@img/sharp-libvips-linux-x64@npm:1.2.3"
|
||||
conditions: os=linux & cpu=x64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-arm64@npm:1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "@img/sharp-libvips-linuxmusl-arm64@npm:1.2.0"
|
||||
"@img/sharp-libvips-linuxmusl-arm64@npm:1.2.3":
|
||||
version: 1.2.3
|
||||
resolution: "@img/sharp-libvips-linuxmusl-arm64@npm:1.2.3"
|
||||
conditions: os=linux & cpu=arm64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64@npm:1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "@img/sharp-libvips-linuxmusl-x64@npm:1.2.0"
|
||||
"@img/sharp-libvips-linuxmusl-x64@npm:1.2.3":
|
||||
version: 1.2.3
|
||||
resolution: "@img/sharp-libvips-linuxmusl-x64@npm:1.2.3"
|
||||
conditions: os=linux & cpu=x64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-linux-arm64@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-linux-arm64@npm:0.34.3"
|
||||
"@img/sharp-linux-arm64@npm:0.34.4":
|
||||
version: 0.34.4
|
||||
resolution: "@img/sharp-linux-arm64@npm:0.34.4"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-linux-arm64": "npm:1.2.0"
|
||||
"@img/sharp-libvips-linux-arm64": "npm:1.2.3"
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-linux-arm64":
|
||||
optional: true
|
||||
@@ -6821,11 +6837,11 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-linux-arm@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-linux-arm@npm:0.34.3"
|
||||
"@img/sharp-linux-arm@npm:0.34.4":
|
||||
version: 0.34.4
|
||||
resolution: "@img/sharp-linux-arm@npm:0.34.4"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-linux-arm": "npm:1.2.0"
|
||||
"@img/sharp-libvips-linux-arm": "npm:1.2.3"
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-linux-arm":
|
||||
optional: true
|
||||
@@ -6833,11 +6849,11 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-linux-ppc64@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-linux-ppc64@npm:0.34.3"
|
||||
"@img/sharp-linux-ppc64@npm:0.34.4":
|
||||
version: 0.34.4
|
||||
resolution: "@img/sharp-linux-ppc64@npm:0.34.4"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-linux-ppc64": "npm:1.2.0"
|
||||
"@img/sharp-libvips-linux-ppc64": "npm:1.2.3"
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-linux-ppc64":
|
||||
optional: true
|
||||
@@ -6845,11 +6861,11 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-linux-s390x@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-linux-s390x@npm:0.34.3"
|
||||
"@img/sharp-linux-s390x@npm:0.34.4":
|
||||
version: 0.34.4
|
||||
resolution: "@img/sharp-linux-s390x@npm:0.34.4"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-linux-s390x": "npm:1.2.0"
|
||||
"@img/sharp-libvips-linux-s390x": "npm:1.2.3"
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-linux-s390x":
|
||||
optional: true
|
||||
@@ -6857,11 +6873,11 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-linux-x64@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-linux-x64@npm:0.34.3"
|
||||
"@img/sharp-linux-x64@npm:0.34.4":
|
||||
version: 0.34.4
|
||||
resolution: "@img/sharp-linux-x64@npm:0.34.4"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-linux-x64": "npm:1.2.0"
|
||||
"@img/sharp-libvips-linux-x64": "npm:1.2.3"
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-linux-x64":
|
||||
optional: true
|
||||
@@ -6869,11 +6885,11 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-linuxmusl-arm64@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-linuxmusl-arm64@npm:0.34.3"
|
||||
"@img/sharp-linuxmusl-arm64@npm:0.34.4":
|
||||
version: 0.34.4
|
||||
resolution: "@img/sharp-linuxmusl-arm64@npm:0.34.4"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "npm:1.2.0"
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "npm:1.2.3"
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-linuxmusl-arm64":
|
||||
optional: true
|
||||
@@ -6881,11 +6897,11 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-linuxmusl-x64@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-linuxmusl-x64@npm:0.34.3"
|
||||
"@img/sharp-linuxmusl-x64@npm:0.34.4":
|
||||
version: 0.34.4
|
||||
resolution: "@img/sharp-linuxmusl-x64@npm:0.34.4"
|
||||
dependencies:
|
||||
"@img/sharp-libvips-linuxmusl-x64": "npm:1.2.0"
|
||||
"@img/sharp-libvips-linuxmusl-x64": "npm:1.2.3"
|
||||
dependenciesMeta:
|
||||
"@img/sharp-libvips-linuxmusl-x64":
|
||||
optional: true
|
||||
@@ -6893,32 +6909,32 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-wasm32@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-wasm32@npm:0.34.3"
|
||||
"@img/sharp-wasm32@npm:0.34.4":
|
||||
version: 0.34.4
|
||||
resolution: "@img/sharp-wasm32@npm:0.34.4"
|
||||
dependencies:
|
||||
"@emnapi/runtime": "npm:^1.4.4"
|
||||
"@emnapi/runtime": "npm:^1.5.0"
|
||||
conditions: cpu=wasm32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-win32-arm64@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-win32-arm64@npm:0.34.3"
|
||||
"@img/sharp-win32-arm64@npm:0.34.4":
|
||||
version: 0.34.4
|
||||
resolution: "@img/sharp-win32-arm64@npm:0.34.4"
|
||||
conditions: os=win32 & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-win32-ia32@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-win32-ia32@npm:0.34.3"
|
||||
"@img/sharp-win32-ia32@npm:0.34.4":
|
||||
version: 0.34.4
|
||||
resolution: "@img/sharp-win32-ia32@npm:0.34.4"
|
||||
conditions: os=win32 & cpu=ia32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@img/sharp-win32-x64@npm:0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "@img/sharp-win32-x64@npm:0.34.3"
|
||||
"@img/sharp-win32-x64@npm:0.34.4":
|
||||
version: 0.34.4
|
||||
resolution: "@img/sharp-win32-x64@npm:0.34.4"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -11382,7 +11398,7 @@ __metadata:
|
||||
response-time: "npm:^2.3.2"
|
||||
rimraf: "npm:^5.0.7"
|
||||
sanitize-html: "npm:^2.7.1"
|
||||
sharp: "npm:^0.34.3"
|
||||
sharp: "npm:^0.34.4"
|
||||
string-pixel-width: "npm:^1.10.0"
|
||||
stripe: "npm:^17.1.0"
|
||||
subscriptions-transport-ws: "npm:^0.11.0"
|
||||
@@ -20098,13 +20114,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"detect-libc@npm:^2.0.0, detect-libc@npm:^2.0.2, detect-libc@npm:^2.0.4":
|
||||
"detect-libc@npm:^2.0.0, detect-libc@npm:^2.0.2":
|
||||
version: 2.0.4
|
||||
resolution: "detect-libc@npm:2.0.4"
|
||||
checksum: 10/136e995f8c5ffbc515955b0175d441b967defd3d5f2268e89fa695e9c7170d8bed17993e31a34b04f0fad33d844a3a598e0fd519a8e9be3cad5f67662d96fee0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"detect-libc@npm:^2.1.0":
|
||||
version: 2.1.1
|
||||
resolution: "detect-libc@npm:2.1.1"
|
||||
checksum: 10/23244632be44caa726f68f0b257f58d1fd86a60918674737bca9acf40d6509a919c60252998256c81e73d4a8350f0a53eef8a4eef538f80e3906986fb61a64eb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"detect-newline@npm:^3.0.0":
|
||||
version: 3.1.0
|
||||
resolution: "detect-newline@npm:3.1.0"
|
||||
@@ -35436,34 +35459,34 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"sharp@npm:^0.34.3":
|
||||
version: 0.34.3
|
||||
resolution: "sharp@npm:0.34.3"
|
||||
"sharp@npm:^0.34.4":
|
||||
version: 0.34.4
|
||||
resolution: "sharp@npm:0.34.4"
|
||||
dependencies:
|
||||
"@img/sharp-darwin-arm64": "npm:0.34.3"
|
||||
"@img/sharp-darwin-x64": "npm:0.34.3"
|
||||
"@img/sharp-libvips-darwin-arm64": "npm:1.2.0"
|
||||
"@img/sharp-libvips-darwin-x64": "npm:1.2.0"
|
||||
"@img/sharp-libvips-linux-arm": "npm:1.2.0"
|
||||
"@img/sharp-libvips-linux-arm64": "npm:1.2.0"
|
||||
"@img/sharp-libvips-linux-ppc64": "npm:1.2.0"
|
||||
"@img/sharp-libvips-linux-s390x": "npm:1.2.0"
|
||||
"@img/sharp-libvips-linux-x64": "npm:1.2.0"
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "npm:1.2.0"
|
||||
"@img/sharp-libvips-linuxmusl-x64": "npm:1.2.0"
|
||||
"@img/sharp-linux-arm": "npm:0.34.3"
|
||||
"@img/sharp-linux-arm64": "npm:0.34.3"
|
||||
"@img/sharp-linux-ppc64": "npm:0.34.3"
|
||||
"@img/sharp-linux-s390x": "npm:0.34.3"
|
||||
"@img/sharp-linux-x64": "npm:0.34.3"
|
||||
"@img/sharp-linuxmusl-arm64": "npm:0.34.3"
|
||||
"@img/sharp-linuxmusl-x64": "npm:0.34.3"
|
||||
"@img/sharp-wasm32": "npm:0.34.3"
|
||||
"@img/sharp-win32-arm64": "npm:0.34.3"
|
||||
"@img/sharp-win32-ia32": "npm:0.34.3"
|
||||
"@img/sharp-win32-x64": "npm:0.34.3"
|
||||
color: "npm:^4.2.3"
|
||||
detect-libc: "npm:^2.0.4"
|
||||
"@img/colour": "npm:^1.0.0"
|
||||
"@img/sharp-darwin-arm64": "npm:0.34.4"
|
||||
"@img/sharp-darwin-x64": "npm:0.34.4"
|
||||
"@img/sharp-libvips-darwin-arm64": "npm:1.2.3"
|
||||
"@img/sharp-libvips-darwin-x64": "npm:1.2.3"
|
||||
"@img/sharp-libvips-linux-arm": "npm:1.2.3"
|
||||
"@img/sharp-libvips-linux-arm64": "npm:1.2.3"
|
||||
"@img/sharp-libvips-linux-ppc64": "npm:1.2.3"
|
||||
"@img/sharp-libvips-linux-s390x": "npm:1.2.3"
|
||||
"@img/sharp-libvips-linux-x64": "npm:1.2.3"
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "npm:1.2.3"
|
||||
"@img/sharp-libvips-linuxmusl-x64": "npm:1.2.3"
|
||||
"@img/sharp-linux-arm": "npm:0.34.4"
|
||||
"@img/sharp-linux-arm64": "npm:0.34.4"
|
||||
"@img/sharp-linux-ppc64": "npm:0.34.4"
|
||||
"@img/sharp-linux-s390x": "npm:0.34.4"
|
||||
"@img/sharp-linux-x64": "npm:0.34.4"
|
||||
"@img/sharp-linuxmusl-arm64": "npm:0.34.4"
|
||||
"@img/sharp-linuxmusl-x64": "npm:0.34.4"
|
||||
"@img/sharp-wasm32": "npm:0.34.4"
|
||||
"@img/sharp-win32-arm64": "npm:0.34.4"
|
||||
"@img/sharp-win32-ia32": "npm:0.34.4"
|
||||
"@img/sharp-win32-x64": "npm:0.34.4"
|
||||
detect-libc: "npm:^2.1.0"
|
||||
semver: "npm:^7.7.2"
|
||||
dependenciesMeta:
|
||||
"@img/sharp-darwin-arm64":
|
||||
@@ -35510,7 +35533,7 @@ __metadata:
|
||||
optional: true
|
||||
"@img/sharp-win32-x64":
|
||||
optional: true
|
||||
checksum: 10/b8ca871c99b48601c47f5dfabf32e38e60071a93e359b3c765d398f708a7cf3735d1bd804b72a957246a3b215fd281a17f887d9c36ebfa690c90fa5fe142d2cd
|
||||
checksum: 10/8e6268e3b0fba7704291684e63c2829963a5ec311d8a8ebbcd32d750c4efb0b01594d925d289ccb5ac0ac373df40fedf5a05a8f331470db799b9c78c48923cba
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user