Merge branch 'main' into andrew/fix-member-table-underline-reactivity
This commit is contained in:
@@ -84,9 +84,6 @@ jobs:
|
||||
run: YARN_ENABLE_HARDENED_MODE=0 PUPPETEER_SKIP_DOWNLOAD=true PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 yarn --immutable
|
||||
- name: Build public packages
|
||||
run: yarn build:public
|
||||
- name: Lint viewer-sandbox
|
||||
run: yarn lint:ci
|
||||
working-directory: 'packages/viewer-sandbox'
|
||||
- name: Build viewer-sandbox
|
||||
run: yarn build
|
||||
working-directory: 'packages/viewer-sandbox'
|
||||
|
||||
@@ -69,6 +69,7 @@ jobs:
|
||||
test-frontend-2:
|
||||
name: Frontend
|
||||
runs-on: blacksmith
|
||||
if: false # disabled as there is nothing to run
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- uses: useblacksmith/setup-node@v5
|
||||
@@ -79,9 +80,6 @@ jobs:
|
||||
run: YARN_ENABLE_HARDENED_MODE=0 PUPPETEER_SKIP_DOWNLOAD=true PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 yarn --immutable
|
||||
- name: Build public packages
|
||||
run: yarn build:public
|
||||
- name: Lint everything
|
||||
run: yarn lint:ci
|
||||
working-directory: 'packages/frontend-2'
|
||||
|
||||
test-viewer:
|
||||
name: Viewer
|
||||
@@ -96,15 +94,9 @@ jobs:
|
||||
run: YARN_ENABLE_HARDENED_MODE=0 PUPPETEER_SKIP_DOWNLOAD=true PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 yarn --immutable
|
||||
- name: Build public packages
|
||||
run: yarn build:public
|
||||
- name: Lint viewer
|
||||
run: yarn lint:ci
|
||||
working-directory: 'packages/viewer'
|
||||
- name: Run tests
|
||||
run: yarn test
|
||||
working-directory: 'packages/viewer'
|
||||
- name: Lint viewer-sandbox
|
||||
run: yarn lint:ci
|
||||
working-directory: 'packages/viewer-sandbox'
|
||||
- name: Build viewer-sandbox
|
||||
run: yarn build
|
||||
working-directory: 'packages/viewer-sandbox'
|
||||
@@ -141,9 +133,6 @@ jobs:
|
||||
cache: yarn
|
||||
- name: Install dependencies
|
||||
run: YARN_ENABLE_HARDENED_MODE=0 PUPPETEER_SKIP_DOWNLOAD=true PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 yarn --immutable
|
||||
- name: Lint
|
||||
run: yarn lint:ci
|
||||
working-directory: 'packages/shared'
|
||||
- name: Run tests (all FFs)
|
||||
run: ENABLE_ALL_FFS=1 yarn test:ci
|
||||
working-directory: 'packages/shared'
|
||||
@@ -179,7 +168,7 @@ jobs:
|
||||
run: YARN_ENABLE_HARDENED_MODE=0 PUPPETEER_SKIP_DOWNLOAD=true PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 yarn --immutable
|
||||
- name: Build public packages
|
||||
run: yarn build:public
|
||||
- name: Lint everything
|
||||
- name: Test object sender
|
||||
run: yarn test:ci
|
||||
working-directory: 'packages/objectsender'
|
||||
- uses: codecov/codecov-action@v5
|
||||
@@ -202,15 +191,6 @@ jobs:
|
||||
run: YARN_ENABLE_HARDENED_MODE=0 PUPPETEER_SKIP_DOWNLOAD=true yarn --immutable # we need PLAYWRIGHT
|
||||
- name: Build public packages
|
||||
run: yarn build:public
|
||||
- name: Lint tailwind theme
|
||||
run: yarn lint:ci
|
||||
working-directory: 'packages/tailwind-theme'
|
||||
- name: Lint ui components
|
||||
run: yarn lint:ci
|
||||
working-directory: 'packages/ui-components'
|
||||
- name: Lint component nuxt package
|
||||
run: yarn lint:ci
|
||||
working-directory: 'packages/ui-components-nuxt'
|
||||
- name: Test via Storybook
|
||||
run: yarn storybook:test:ci
|
||||
working-directory: 'packages/ui-components'
|
||||
@@ -218,6 +198,7 @@ jobs:
|
||||
test-preview-service:
|
||||
name: Preview service
|
||||
runs-on: blacksmith
|
||||
if: false # disabled as there is nothing to run
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- uses: useblacksmith/setup-node@v5
|
||||
@@ -228,9 +209,6 @@ jobs:
|
||||
run: YARN_ENABLE_HARDENED_MODE=0 PUPPETEER_SKIP_DOWNLOAD=true PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 yarn --immutable
|
||||
- name: Build public packages
|
||||
run: yarn build:public
|
||||
- name: Lint everything
|
||||
run: yarn lint:ci
|
||||
working-directory: 'packages/preview-service'
|
||||
|
||||
docker-build-postgres-container:
|
||||
runs-on: blacksmith
|
||||
@@ -319,9 +297,6 @@ jobs:
|
||||
run: yarn build:public
|
||||
- run: cp .env.test-example .env.test
|
||||
working-directory: 'packages/server'
|
||||
- name: 'Lint'
|
||||
run: yarn lint:ci
|
||||
working-directory: 'packages/server'
|
||||
- name: 'Run test'
|
||||
run: yarn test:report
|
||||
working-directory: 'packages/server'
|
||||
|
||||
@@ -32,6 +32,7 @@ services:
|
||||
NUXT_PUBLIC_BACKEND_API_ORIGIN: 'http://speckle-server:3000'
|
||||
NUXT_PUBLIC_LOG_LEVEL: 'warn'
|
||||
NUXT_REDIS_URL: 'redis://redis'
|
||||
NUXT_PUBLIC_FF_LARGE_FILE_IMPORTS_ENABLED: 'true'
|
||||
LOG_LEVEL: 'info'
|
||||
LOG_PRETTY: 'true'
|
||||
depends_on:
|
||||
@@ -79,9 +80,11 @@ services:
|
||||
REDIS_URL: 'redis://redis'
|
||||
PREVIEW_SERVICE_USE_PRIVATE_OBJECTS_SERVER_URL: 'true'
|
||||
PREVIEW_SERVICE_REDIS_URL: 'redis://redis'
|
||||
FILEIMPORT_SERVICE_USE_PRIVATE_OBJECTS_SERVER_URL: 'true'
|
||||
FILEIMPORT_SERVICE_REDIS_URL: 'redis://redis'
|
||||
|
||||
S3_ENDPOINT: 'http://minio:9000'
|
||||
S3_PUBLIC_ENDPOINT: 'http://127.0.0.1:9000'
|
||||
S3_ACCESS_KEY: 'minioadmin'
|
||||
S3_SECRET_KEY: 'minioadmin'
|
||||
S3_BUCKET: 'speckle-server'
|
||||
@@ -92,6 +95,8 @@ services:
|
||||
|
||||
FRONTEND_ORIGIN: 'http://127.0.0.1'
|
||||
ONBOARDING_STREAM_URL: 'https://latest.speckle.systems/projects/843d07eb10'
|
||||
|
||||
FF_LARGE_FILE_IMPORTS_ENABLED: 'true'
|
||||
depends_on:
|
||||
[]
|
||||
# - minio
|
||||
|
||||
@@ -4,7 +4,8 @@ POSTGRES_MAX_CONNECTIONS_FILE_IMPORT_SERVICE='1'
|
||||
POSTGRES_CONNECTION_ACQUIRE_TIMEOUT_MILLIS='16000'
|
||||
POSTGRES_CONNECTION_CREATE_TIMEOUT_MILLIS='5000'
|
||||
FF_WORKSPACES_MULTI_REGION_ENABLED=false
|
||||
USE_LEGACY_IFC_IMPORTER=true
|
||||
FF_LEGACY_IFC_IMPORTER_ENABLED=true
|
||||
FF_EXPERIMENTAL_IFC_IMPORTER_ENABLED=false
|
||||
# IFC_DOTNET_DLL_PATH='packages/fileimport-service/src/ifc-dotnet/ifc-converter.dll'
|
||||
|
||||
##########################################################
|
||||
|
||||
@@ -12,6 +12,7 @@ WORKDIR /speckle-server
|
||||
|
||||
# configure tini
|
||||
ARG TINI_VERSION=v0.19.0
|
||||
ARG SPECKLE_IFC_VERSION=0.2.0
|
||||
|
||||
# hadolint ignore=DL3008
|
||||
RUN apt-get update -y \
|
||||
@@ -20,6 +21,10 @@ RUN apt-get update -y \
|
||||
ca-certificates=20240203 \
|
||||
curl=8.5.0-2ubuntu10.6 \
|
||||
gosu=1.17-1ubuntu0.24.04.3 \
|
||||
&& curl -L -o speckleifc.tar.gz https://github.com/specklesystems/speckleifc/archive/refs/tags/v${SPECKLE_IFC_VERSION}.tar.gz \
|
||||
&& mkdir speckleifc \
|
||||
&& tar --strip-components=1 -C speckleifc -xzf speckleifc.tar.gz speckleifc-${SPECKLE_IFC_VERSION} \
|
||||
&& rm speckleifc.tar.gz \
|
||||
&& curl -fsSL https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini -o /usr/bin/tini \
|
||||
&& chmod +x /usr/bin/tini \
|
||||
&& curl -fsSL https://deb.nodesource.com/setup_22.x -o nodesource_setup.sh \
|
||||
|
||||
@@ -3,3 +3,4 @@ specklepy==3.0.1
|
||||
structlog==23.3.0
|
||||
numpy==1.26.4 # not directly required, pinned to avoid a vulnerability in <1.22.2
|
||||
python-util==1.2.1 # not directly required, peer dependency of numpy-stl
|
||||
ifcopenshell==0.8.2 # required for speckleifc
|
||||
|
||||
@@ -5,26 +5,24 @@ import {
|
||||
metricOperationErrors
|
||||
} from '@/controller/prometheusMetrics.js'
|
||||
import { DbClient, getDbClients } from '@/clients/knex.js'
|
||||
|
||||
import { downloadFile } from '@/controller/filesApi.js'
|
||||
import fs from 'fs'
|
||||
|
||||
import { ServerAPI } from '@/controller/api.js'
|
||||
import { downloadDependencies } from '@/controller/objDependencies.js'
|
||||
import { logger } from '@/observability/logging.js'
|
||||
import { Nullable, Scopes, wait, TIME_MS } from '@speckle/shared'
|
||||
import { Knex } from 'knex'
|
||||
import {
|
||||
getIfcDllPath,
|
||||
isProdEnv,
|
||||
useLegacyIfcImporter
|
||||
} from '@/controller/helpers/env.js'
|
||||
import { getIfcDllPath, isProdEnv } from '@/controller/helpers/env.js'
|
||||
import { isErrorOutput, isSuccessOutput } from '@/common/output.js'
|
||||
import { runProcessWithTimeout } from '@/common/processHandling.js'
|
||||
import {
|
||||
getConnectionSettings,
|
||||
obfuscateConnectionString
|
||||
} from '@speckle/shared/environment/db'
|
||||
import { getFeatureFlags } from '@speckle/shared/environment'
|
||||
|
||||
const { FF_LEGACY_IFC_IMPORTER_ENABLED, FF_EXPERIMENTAL_IFC_IMPORTER_ENABLED } =
|
||||
getFeatureFlags()
|
||||
|
||||
const HEALTHCHECK_FILE_PATH = '/tmp/last_successful_query'
|
||||
|
||||
@@ -183,7 +181,7 @@ async function doTask(
|
||||
if (info.fileType.toLowerCase() === 'ifc') {
|
||||
if (
|
||||
info.fileName.toLowerCase().endsWith('.legacyimporter.ifc') ||
|
||||
useLegacyIfcImporter()
|
||||
FF_LEGACY_IFC_IMPORTER_ENABLED
|
||||
) {
|
||||
await runProcessWithTimeout(
|
||||
taskLogger,
|
||||
@@ -208,7 +206,10 @@ async function doTask(
|
||||
TIME_LIMIT,
|
||||
TMP_RESULTS_PATH
|
||||
)
|
||||
} else {
|
||||
} else if (
|
||||
info.fileName.toLowerCase().endsWith('.dotnetimporter.ifc') ||
|
||||
!FF_EXPERIMENTAL_IFC_IMPORTER_ENABLED
|
||||
) {
|
||||
await runProcessWithTimeout(
|
||||
taskLogger,
|
||||
process.env['DOTNET_BINARY_PATH'] || 'dotnet',
|
||||
@@ -228,6 +229,27 @@ async function doTask(
|
||||
TIME_LIMIT,
|
||||
TMP_RESULTS_PATH
|
||||
)
|
||||
} else {
|
||||
await runProcessWithTimeout(
|
||||
taskLogger,
|
||||
process.env['PYTHON_BINARY_PATH'] || 'python3',
|
||||
[
|
||||
'-m',
|
||||
'speckleifc',
|
||||
TMP_FILE_PATH,
|
||||
TMP_RESULTS_PATH,
|
||||
info.streamId,
|
||||
`File upload: ${info.fileName}`,
|
||||
existingBranch?.id || ''
|
||||
],
|
||||
{
|
||||
USER_TOKEN: tempUserToken,
|
||||
//speckleifc is not installed to sys (e.g. via pip), so we need to point it to the directory explicitly
|
||||
PYTHONPATH: '/speckle-server/speckleifc/src/'
|
||||
},
|
||||
TIME_LIMIT,
|
||||
TMP_RESULTS_PATH
|
||||
)
|
||||
}
|
||||
} else if (info.fileType.toLowerCase() === 'stl') {
|
||||
await runProcessWithTimeout(
|
||||
|
||||
@@ -16,10 +16,6 @@ export function isProdEnv() {
|
||||
|
||||
export const isDevOrTestEnv = () => isDevEnv() || isTestEnv()
|
||||
|
||||
export const useLegacyIfcImporter = () => {
|
||||
return ['true', '1'].includes(process.env.USE_LEGACY_IFC_IMPORTER || 'false')
|
||||
}
|
||||
|
||||
export const getPackageRootDirPath = () => {
|
||||
const __filename = url.fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
@@ -11,6 +11,9 @@ import { DOTNET_BINARY_PATH, RHINO_IMPORTER_PATH } from './config.js'
|
||||
import { getIfcDllPath } from '@/controller/helpers/env.js'
|
||||
import { z } from 'zod'
|
||||
import { TIME_MS } from '@speckle/shared'
|
||||
import { getFeatureFlags } from '@speckle/shared/environment'
|
||||
|
||||
const { FF_EXPERIMENTAL_IFC_IMPORTER_ENABLED } = getFeatureFlags()
|
||||
|
||||
const jobSuccess = z.object({
|
||||
success: z.literal(true),
|
||||
@@ -120,26 +123,55 @@ export const jobProcessor = async ({
|
||||
switch (fileType) {
|
||||
case 'ifc':
|
||||
parserUsed = 'ifc'
|
||||
await runProcessWithTimeout(
|
||||
taskLogger,
|
||||
DOTNET_BINARY_PATH,
|
||||
[
|
||||
getIfcDllPath(),
|
||||
sourceFilePath,
|
||||
resultsPath,
|
||||
job.projectId,
|
||||
`File upload: ${job.fileName}`,
|
||||
job.modelId,
|
||||
'bogus',
|
||||
'regionName'
|
||||
],
|
||||
{
|
||||
SPECKLE_SERVER_URL: job.serverUrl,
|
||||
USER_TOKEN: job.token
|
||||
},
|
||||
Math.min(timeout, job.timeOutSeconds * TIME_MS.second),
|
||||
resultsPath
|
||||
)
|
||||
const useDotnetIfcImporter =
|
||||
job.fileName.toLowerCase().endsWith('.dotnetimporter.ifc') ||
|
||||
!FF_EXPERIMENTAL_IFC_IMPORTER_ENABLED
|
||||
|
||||
if (useDotnetIfcImporter) {
|
||||
await runProcessWithTimeout(
|
||||
taskLogger,
|
||||
DOTNET_BINARY_PATH,
|
||||
[
|
||||
getIfcDllPath(),
|
||||
sourceFilePath,
|
||||
resultsPath,
|
||||
job.projectId,
|
||||
`File upload: ${job.fileName}`,
|
||||
job.modelId,
|
||||
'bogus',
|
||||
'regionName'
|
||||
],
|
||||
{
|
||||
SPECKLE_SERVER_URL: job.serverUrl,
|
||||
USER_TOKEN: job.token
|
||||
},
|
||||
Math.min(timeout, job.timeOutSeconds * TIME_MS.second),
|
||||
resultsPath
|
||||
)
|
||||
} else {
|
||||
await runProcessWithTimeout(
|
||||
taskLogger,
|
||||
process.env['PYTHON_BINARY_PATH'] || 'python3',
|
||||
[
|
||||
'-m',
|
||||
'speckleifc',
|
||||
sourceFilePath,
|
||||
resultsPath,
|
||||
job.projectId,
|
||||
`File upload: ${job.fileName}`,
|
||||
job.modelId
|
||||
],
|
||||
{
|
||||
USER_TOKEN: job.token,
|
||||
SPECKLE_SERVER_URL: job.serverUrl,
|
||||
//speckleifc is not installed to sys (e.g. via pip), so we need to point it to the directory explicitly
|
||||
PYTHONPATH: '/speckle-server/speckleifc/src/'
|
||||
},
|
||||
Math.min(timeout, job.timeOutSeconds * TIME_MS.second),
|
||||
resultsPath
|
||||
)
|
||||
}
|
||||
|
||||
break
|
||||
case 'stl':
|
||||
case 'obj':
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<CommonCard
|
||||
class="flex flex-1 flex-col gap-1 !p-4 !pt-2 !pb-3 h-full hover:bg-foundation"
|
||||
>
|
||||
<CommonCard class="flex flex-1 flex-col gap-1 !p-4 !pt-2 !pb-3 h-full">
|
||||
<div class="flex gap-2 items-center">
|
||||
<div v-if="connector.images" class="relative flex items-start mr-2">
|
||||
<div
|
||||
@@ -29,20 +27,32 @@
|
||||
{{ connector.description }}
|
||||
</p>
|
||||
<div class="flex gap-1 mt-2">
|
||||
<FormButton
|
||||
color="outline"
|
||||
size="sm"
|
||||
:disabled="enableButton"
|
||||
external
|
||||
:to="latestAvailableVersion?.Url"
|
||||
@click="
|
||||
mixpanel.track('Connector Card Install Clicked', {
|
||||
connector: props.connector.slug
|
||||
})
|
||||
<div
|
||||
v-tippy="
|
||||
canDownload
|
||||
? undefined
|
||||
: {
|
||||
content: `Please <a href='${loginRoute}'>login</a> or <a href='${registerRoute}'>register</a> to download connectors`,
|
||||
allowHTML: true,
|
||||
interactive: true
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ connector.isComingSoon ? 'Coming soon' : 'Install for Windows' }}
|
||||
</FormButton>
|
||||
<FormButton
|
||||
color="outline"
|
||||
size="sm"
|
||||
:disabled="enableButton"
|
||||
external
|
||||
:to="canDownload ? latestAvailableVersion?.Url : undefined"
|
||||
@click="
|
||||
mixpanel.track('Connector Card Install Clicked', {
|
||||
connector: props.connector.slug
|
||||
})
|
||||
"
|
||||
>
|
||||
{{ connector.isComingSoon ? 'Coming soon' : 'Install for Windows' }}
|
||||
</FormButton>
|
||||
</div>
|
||||
<FormButton
|
||||
v-if="connector.url"
|
||||
color="subtle"
|
||||
@@ -66,20 +76,22 @@
|
||||
<script setup lang="ts">
|
||||
import type { ConnectorItem, Version, Versions } from '~~/lib/dashboard/helpers/types'
|
||||
import { useMixpanel } from '~/lib/core/composables/mp'
|
||||
import { loginRoute, registerRoute } from '~~/lib/common/helpers/route'
|
||||
|
||||
const props = defineProps<{
|
||||
connector: ConnectorItem
|
||||
canDownload: boolean
|
||||
}>()
|
||||
|
||||
const mixpanel = useMixpanel()
|
||||
const { data: versionData, status } = useFetch(
|
||||
`https://releases.speckle.dev/manager2/feeds/${props.connector.slug}-v3.json`,
|
||||
{
|
||||
immediate: !props.connector.isComingSoon
|
||||
immediate: !props.connector.isComingSoon && props.canDownload
|
||||
}
|
||||
)
|
||||
|
||||
const enableButton = computed(() => status.value !== 'success')
|
||||
const enableButton = computed(() => status.value !== 'success' || !props.canDownload)
|
||||
|
||||
const latestAvailableVersion = computed<Version | null>(() => {
|
||||
if (versionData.value) {
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
v-for="connector in filteredConnectors"
|
||||
:key="connector.title"
|
||||
:connector="connector"
|
||||
:can-download="isLoggedIn"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
@@ -96,6 +97,7 @@ const {
|
||||
|
||||
const labelId = useId()
|
||||
const buttonId = useId()
|
||||
const { isLoggedIn } = useActiveUser()
|
||||
|
||||
const selectedCategory = ref<CategoryFilter>()
|
||||
const connectors = shallowRef<ConnectorItem[]>(connectorItems)
|
||||
|
||||
@@ -1000,6 +1000,22 @@ export type EmailVerificationRequestInput = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type EmbedToken = {
|
||||
__typename?: 'EmbedToken';
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
id: Scalars['String']['output'];
|
||||
lastUsed: Scalars['DateTime']['output'];
|
||||
lifespan: Scalars['BigInt']['output'];
|
||||
modelIds: Scalars['String']['output'];
|
||||
name: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type EmbedTokenCreateInput = {
|
||||
lifespan?: InputMaybe<Scalars['BigInt']['input']>;
|
||||
modelIds: Scalars['String']['input'];
|
||||
projectId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type FileUpload = {
|
||||
__typename?: 'FileUpload';
|
||||
branchName: Scalars['String']['output'];
|
||||
@@ -2095,6 +2111,7 @@ export type Project = {
|
||||
description?: Maybe<Scalars['String']['output']>;
|
||||
/** Public project-level configuration for embedded viewer */
|
||||
embedOptions: ProjectEmbedOptions;
|
||||
embedTokens: Array<EmbedToken>;
|
||||
hasAccessToFeature: Scalars['Boolean']['output'];
|
||||
id: Scalars['ID']['output'];
|
||||
invitableCollaborators: WorkspaceCollaboratorCollection;
|
||||
@@ -2576,6 +2593,7 @@ export type ProjectMutations = {
|
||||
batchDelete: Scalars['Boolean']['output'];
|
||||
/** Create new project */
|
||||
create: Project;
|
||||
createEmbedToken: Scalars['String']['output'];
|
||||
/**
|
||||
* Create onboarding/tutorial project. If one is already created for the active user, that
|
||||
* one will be returned instead.
|
||||
@@ -2587,6 +2605,7 @@ export type ProjectMutations = {
|
||||
invites: ProjectInviteMutations;
|
||||
/** Leave a project. Only possible if you're not the last remaining owner. */
|
||||
leave: Scalars['Boolean']['output'];
|
||||
revokeEmbedToken: Scalars['Boolean']['output'];
|
||||
/** Updates an existing project */
|
||||
update: Project;
|
||||
/** Update role for a collaborator */
|
||||
@@ -2609,6 +2628,11 @@ export type ProjectMutationsCreateArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type ProjectMutationsCreateEmbedTokenArgs = {
|
||||
token: EmbedTokenCreateInput;
|
||||
};
|
||||
|
||||
|
||||
export type ProjectMutationsDeleteArgs = {
|
||||
id: Scalars['String']['input'];
|
||||
};
|
||||
@@ -2619,6 +2643,11 @@ export type ProjectMutationsLeaveArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type ProjectMutationsRevokeEmbedTokenArgs = {
|
||||
token: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type ProjectMutationsUpdateArgs = {
|
||||
update: ProjectUpdateInput;
|
||||
};
|
||||
@@ -7901,6 +7930,7 @@ export type AllObjectTypes = {
|
||||
CommitCollection: CommitCollection,
|
||||
CountOnlyCollection: CountOnlyCollection,
|
||||
CurrencyBasedPrices: CurrencyBasedPrices,
|
||||
EmbedToken: EmbedToken,
|
||||
FileUpload: FileUpload,
|
||||
FileUploadCollection: FileUploadCollection,
|
||||
FileUploadMutations: FileUploadMutations,
|
||||
@@ -8363,6 +8393,14 @@ export type CurrencyBasedPricesFieldArgs = {
|
||||
gbp: {},
|
||||
usd: {},
|
||||
}
|
||||
export type EmbedTokenFieldArgs = {
|
||||
createdAt: {},
|
||||
id: {},
|
||||
lastUsed: {},
|
||||
lifespan: {},
|
||||
modelIds: {},
|
||||
name: {},
|
||||
}
|
||||
export type FileUploadFieldArgs = {
|
||||
branchName: {},
|
||||
convertedCommitId: {},
|
||||
@@ -8656,6 +8694,7 @@ export type ProjectFieldArgs = {
|
||||
createdAt: {},
|
||||
description: {},
|
||||
embedOptions: {},
|
||||
embedTokens: {},
|
||||
hasAccessToFeature: ProjectHasAccessToFeatureArgs,
|
||||
id: {},
|
||||
invitableCollaborators: ProjectInvitableCollaboratorsArgs,
|
||||
@@ -8762,10 +8801,12 @@ export type ProjectMutationsFieldArgs = {
|
||||
automationMutations: ProjectMutationsAutomationMutationsArgs,
|
||||
batchDelete: ProjectMutationsBatchDeleteArgs,
|
||||
create: ProjectMutationsCreateArgs,
|
||||
createEmbedToken: ProjectMutationsCreateEmbedTokenArgs,
|
||||
createForOnboarding: {},
|
||||
delete: ProjectMutationsDeleteArgs,
|
||||
invites: {},
|
||||
leave: ProjectMutationsLeaveArgs,
|
||||
revokeEmbedToken: ProjectMutationsRevokeEmbedTokenArgs,
|
||||
update: ProjectMutationsUpdateArgs,
|
||||
updateRole: ProjectMutationsUpdateRoleArgs,
|
||||
}
|
||||
@@ -9529,6 +9570,7 @@ export type AllObjectFieldArgTypes = {
|
||||
CommitCollection: CommitCollectionFieldArgs,
|
||||
CountOnlyCollection: CountOnlyCollectionFieldArgs,
|
||||
CurrencyBasedPrices: CurrencyBasedPricesFieldArgs,
|
||||
EmbedToken: EmbedTokenFieldArgs,
|
||||
FileUpload: FileUploadFieldArgs,
|
||||
FileUploadCollection: FileUploadCollectionFieldArgs,
|
||||
FileUploadMutations: FileUploadMutationsFieldArgs,
|
||||
|
||||
@@ -6,8 +6,4 @@
|
||||
useHead({
|
||||
title: 'Connectors'
|
||||
})
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['auth']
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -24,6 +24,7 @@ export class ObjectLoader2 {
|
||||
#gathered: AsyncGeneratorQueue<Item>
|
||||
|
||||
#root?: Item = undefined
|
||||
#isRootStored = false
|
||||
|
||||
constructor(options: ObjectLoader2Options) {
|
||||
this.#rootId = options.rootId
|
||||
@@ -34,8 +35,8 @@ export class ObjectLoader2 {
|
||||
maxCacheReadSize: 10_000,
|
||||
maxCacheWriteSize: 10_000,
|
||||
maxWriteQueueSize: 40_000,
|
||||
maxCacheBatchWriteWait: 3_000,
|
||||
maxCacheBatchReadWait: 3_000
|
||||
maxCacheBatchWriteWait: 100, //100 ms, next to nothing!
|
||||
maxCacheBatchReadWait: 100 //100 ms, next to nothing!
|
||||
}
|
||||
|
||||
this.#gathered = new AsyncGeneratorQueue()
|
||||
@@ -56,10 +57,10 @@ export class ObjectLoader2 {
|
||||
await Promise.all([
|
||||
this.#gathered.disposeAsync(),
|
||||
this.#downloader.disposeAsync(),
|
||||
this.#cacheReader.disposeAsync(),
|
||||
this.#cacheWriter.disposeAsync()
|
||||
])
|
||||
this.#deferments.dispose()
|
||||
this.#cacheReader.dispose()
|
||||
}
|
||||
|
||||
async getRootObject(): Promise<Item | undefined> {
|
||||
@@ -67,6 +68,8 @@ export class ObjectLoader2 {
|
||||
this.#root = (await this.#database.getAll([this.#rootId]))[0]
|
||||
if (!this.#root) {
|
||||
this.#root = await this.#downloader.downloadSingle()
|
||||
} else {
|
||||
this.#isRootStored = true
|
||||
}
|
||||
}
|
||||
return this.#root
|
||||
@@ -114,6 +117,10 @@ export class ObjectLoader2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!this.#isRootStored) {
|
||||
await this.#database.saveBatch({ batch: [rootItem] })
|
||||
this.#isRootStored = true
|
||||
}
|
||||
}
|
||||
|
||||
static createFromObjects(objects: Base[]): ObjectLoader2 {
|
||||
|
||||
@@ -30,6 +30,6 @@ describe('CacheReader testing', () => {
|
||||
const base = await objPromise
|
||||
|
||||
expect(base).toMatchSnapshot()
|
||||
await cacheReader.disposeAsync()
|
||||
cacheReader.dispose()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -31,11 +31,12 @@ export class CacheReader {
|
||||
this.#notFoundQueue = notFoundQueue
|
||||
}
|
||||
|
||||
async getObject(params: { id: string }): Promise<Base> {
|
||||
if (!this.#defermentManager.isDeferred(params.id)) {
|
||||
getObject(params: { id: string }): Promise<Base> {
|
||||
const [p, b] = this.#defermentManager.defer({ id: params.id })
|
||||
if (!b) {
|
||||
this.#requestItem(params.id)
|
||||
}
|
||||
return await this.#defermentManager.defer({ id: params.id })
|
||||
return p
|
||||
}
|
||||
|
||||
#createReadQueue(): void {
|
||||
@@ -57,10 +58,15 @@ export class CacheReader {
|
||||
|
||||
requestAll(keys: string[]): void {
|
||||
this.#createReadQueue()
|
||||
for (const key of keys) {
|
||||
this.#defermentManager.trackDefermentRequest(key)
|
||||
}
|
||||
|
||||
this.#readQueue?.addAll(keys, keys)
|
||||
}
|
||||
|
||||
#processBatch = async (batch: string[]): Promise<void> => {
|
||||
const start = performance.now()
|
||||
const items = await this.#database.getAll(batch)
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i]
|
||||
@@ -71,9 +77,10 @@ export class CacheReader {
|
||||
this.#notFoundQueue?.add(batch[i])
|
||||
}
|
||||
}
|
||||
this.#logger('readBatch: left, time', items.length, performance.now() - start)
|
||||
}
|
||||
|
||||
async disposeAsync(): Promise<void> {
|
||||
await this.#readQueue?.disposeAsync()
|
||||
dispose(): void {
|
||||
this.#readQueue?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,17 +30,25 @@ export class CacheWriter implements Queue<Item> {
|
||||
this.#writeQueue = new BatchingQueue({
|
||||
batchSize: this.#options.maxCacheWriteSize,
|
||||
maxWaitTime: this.#options.maxCacheBatchWriteWait,
|
||||
processFunction: (batch: Item[]): Promise<void> =>
|
||||
this.#database.saveBatch({ batch })
|
||||
processFunction: async (batch: Item[]): Promise<void> => {
|
||||
await this.writeAll(batch)
|
||||
}
|
||||
})
|
||||
}
|
||||
this.#defermentManager.undefer(item)
|
||||
this.#writeQueue.add(item.baseId, item)
|
||||
}
|
||||
|
||||
async writeAll(items: Item[]): Promise<void> {
|
||||
const start = performance.now()
|
||||
await this.#database.saveBatch({ batch: items })
|
||||
this.#logger('writeBatch: left, time', items.length, performance.now() - start)
|
||||
}
|
||||
|
||||
async disposeAsync(): Promise<void> {
|
||||
await this.#writeQueue?.disposeAsync()
|
||||
this.#writeQueue?.dispose()
|
||||
this.#disposed = true
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
get isDisposed(): boolean {
|
||||
|
||||
@@ -121,7 +121,8 @@ export default class IndexedDatabase implements Database {
|
||||
async disposeAsync(): Promise<void> {
|
||||
this.#cacheDB?.close()
|
||||
this.#cacheDB = undefined
|
||||
await this.#writeQueue?.disposeAsync()
|
||||
this.#writeQueue?.dispose()
|
||||
this.#writeQueue = undefined
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface ServerDownloaderOptions {
|
||||
fetch?: Fetcher
|
||||
}
|
||||
|
||||
const MAX_SAFARI_DECODE_BYTES = 2 * 1024 * 1024 * 1024 - 1024 * 1024 // 2GB minus a margin
|
||||
|
||||
export default class ServerDownloader implements Downloader {
|
||||
#requestUrlRootObj: string
|
||||
#requestUrlChildren: string
|
||||
@@ -24,7 +26,8 @@ export default class ServerDownloader implements Downloader {
|
||||
#total?: number
|
||||
|
||||
#downloadQueue?: BatchedPool<string>
|
||||
#decoder = new TextDecoder()
|
||||
#decoder = new TextDecoder('utf-8', { fatal: true })
|
||||
#decodedBytesCount = 0
|
||||
|
||||
constructor(options: ServerDownloaderOptions) {
|
||||
this.#options = options
|
||||
@@ -89,6 +92,24 @@ export default class ServerDownloader implements Downloader {
|
||||
this.#getPool().add(id)
|
||||
}
|
||||
|
||||
/*
|
||||
This is the most frequently reported and confirmed reason for this error in Safari. There's a known bug in WebKit (Safari's rendering engine) where TextDecoder can fail or throw a RangeError after decoding around 2GB of data. Chrome and other browsers handle much larger amounts of data without this specific limitation.
|
||||
|
||||
Why it happens: It seems to be an internal memory or indexing limitation within Safari's TextDecoder implementation. After a certain threshold of data has been processed by a TextDecoder instance, it starts throwing this error.
|
||||
|
||||
Chrome's behavior: Chrome generally handles larger data sizes without this specific RangeError. It might become slow or run out of general memory, but not typically with this specific error.
|
||||
*/
|
||||
decodeChunk(chunkBuffer: Uint8Array): string {
|
||||
if (this.#decodedBytesCount + chunkBuffer.byteLength > MAX_SAFARI_DECODE_BYTES) {
|
||||
// Safari is approaching its limit, create a new decoder
|
||||
this.#decoder = new TextDecoder('utf-8', { fatal: true })
|
||||
this.#decodedBytesCount = 0 // Reset counter for the new decoder
|
||||
}
|
||||
const decodedText = this.#decoder.decode(chunkBuffer)
|
||||
this.#decodedBytesCount += chunkBuffer.byteLength
|
||||
return decodedText
|
||||
}
|
||||
|
||||
async disposeAsync(): Promise<void> {
|
||||
await this.#downloadQueue?.disposeAsync()
|
||||
}
|
||||
@@ -166,16 +187,16 @@ export default class ServerDownloader implements Downloader {
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
if (line[i] === 0x09) {
|
||||
//this is a tab
|
||||
const baseId = this.#decoder.decode(line.subarray(0, i))
|
||||
const baseId = this.decodeChunk(line.subarray(0, i))
|
||||
const json = line.subarray(i + 1)
|
||||
const base = this.#decoder.decode(json)
|
||||
const base = this.decodeChunk(json)
|
||||
const item = this.#processJson(baseId, base)
|
||||
item.size = json.length
|
||||
return item
|
||||
}
|
||||
}
|
||||
throw new ObjectLoaderRuntimeError(
|
||||
'Invalid line format: ' + this.#decoder.decode(line)
|
||||
'Invalid line format in response: ' + this.decodeChunk(line)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,14 +10,12 @@ describe('DefermentManager disposal', () => {
|
||||
base: { id, speckle_type: 'test' }
|
||||
})
|
||||
|
||||
it('should throw on get/defer/undefer after dispose', async () => {
|
||||
it('should throw on get/defer/undefer after dispose', () => {
|
||||
const manager = new DefermentManager(options)
|
||||
manager.dispose()
|
||||
expect(() => manager.get('a')).toThrow('DefermentManager is disposed')
|
||||
expect(() => manager.undefer(makeItem('a'))).toThrow('DefermentManager is disposed')
|
||||
await expect(manager.defer({ id: 'a' })).rejects.toThrow(
|
||||
'DefermentManager is disposed'
|
||||
)
|
||||
expect(() => manager.defer({ id: 'a' })).toThrow('DefermentManager is disposed')
|
||||
})
|
||||
|
||||
it('dispose is idempotent', () => {
|
||||
|
||||
@@ -6,7 +6,8 @@ import { DefermentManager } from './defermentManager.js'
|
||||
describe('deferments', () => {
|
||||
test('defer one', async () => {
|
||||
const deferments = new DefermentManager({ maxSizeInMb: 1, ttlms: 1 })
|
||||
const x = deferments.defer({ id: 'id' })
|
||||
const [x, alreadyDeferred] = deferments.defer({ id: 'id' })
|
||||
expect(alreadyDeferred).toBeFalsy()
|
||||
expect(x).toBeInstanceOf(Promise)
|
||||
deferments.undefer({ baseId: 'id', base: { id: 'id', speckle_type: 'type' } })
|
||||
const b = await x
|
||||
@@ -17,7 +18,8 @@ describe('deferments', () => {
|
||||
const now = 1
|
||||
const deferments = new DefermentManager({ maxSizeInMb: 1, ttlms: 1 })
|
||||
deferments['now'] = (): number => now
|
||||
const x = deferments.defer({ id: 'id' })
|
||||
const [x, alreadyDeferred] = deferments.defer({ id: 'id' })
|
||||
expect(alreadyDeferred).toBeFalsy()
|
||||
expect(x).toBeInstanceOf(Promise)
|
||||
const d = deferments.get('id')
|
||||
expect(d).toBeDefined()
|
||||
|
||||
@@ -22,23 +22,19 @@ export class DefermentManager {
|
||||
return Date.now()
|
||||
}
|
||||
|
||||
isDeferred(id: string): boolean {
|
||||
return this.deferments.has(id)
|
||||
}
|
||||
|
||||
get(id: string): DeferredBase | undefined {
|
||||
if (this.disposed) throw new Error('DefermentManager is disposed')
|
||||
return this.deferments.get(id)
|
||||
}
|
||||
|
||||
async defer(params: { id: string }): Promise<Base> {
|
||||
defer(params: { id: string }): [Promise<Base>, boolean] {
|
||||
if (this.disposed) throw new Error('DefermentManager is disposed')
|
||||
this.trackDefermentRequest(params.id)
|
||||
const now = this.now()
|
||||
const deferredBase = this.deferments.get(params.id)
|
||||
if (deferredBase) {
|
||||
deferredBase.setAccess(now)
|
||||
return deferredBase.getPromise()
|
||||
return [deferredBase.getPromise(), true]
|
||||
}
|
||||
const notYetFound = new DeferredBase(
|
||||
this.options.ttlms,
|
||||
@@ -46,10 +42,10 @@ export class DefermentManager {
|
||||
now + this.options.ttlms
|
||||
)
|
||||
this.deferments.set(params.id, notYetFound)
|
||||
return notYetFound.getPromise()
|
||||
return [notYetFound.getPromise(), false]
|
||||
}
|
||||
|
||||
private trackDefermentRequest(id: string): void {
|
||||
trackDefermentRequest(id: string): void {
|
||||
const request = this.totalDefermentRequests.get(id)
|
||||
if (request) {
|
||||
this.totalDefermentRequests.set(id, request + 1)
|
||||
@@ -131,7 +127,7 @@ export class DefermentManager {
|
||||
//we do not clean it up to allow the requests to resolve
|
||||
const requestCount = this.totalDefermentRequests.get(deferredBase.getId())
|
||||
if (requestCount && requestCount > 1) {
|
||||
return
|
||||
break
|
||||
}
|
||||
this.currentSize -= deferredBase.getSize() || 0
|
||||
this.deferments.delete(deferredBase.getId())
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import BatchingQueue from './batchingQueue.js'
|
||||
|
||||
describe('BatchingQueue', () => {
|
||||
let queue: BatchingQueue<string>
|
||||
|
||||
beforeEach(() => {
|
||||
queue = new BatchingQueue({
|
||||
batchSize: 3,
|
||||
maxWaitTime: 100,
|
||||
processFunction: async (): Promise<void> => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
queue.dispose()
|
||||
})
|
||||
|
||||
test('should add items and process them in batches', async () => {
|
||||
const processSpy = vi.fn()
|
||||
queue = new BatchingQueue({
|
||||
batchSize: 2,
|
||||
maxWaitTime: 100,
|
||||
processFunction: async (batch: string[]): Promise<void> => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
processSpy(batch)
|
||||
}
|
||||
})
|
||||
|
||||
queue.add('key1', 'item1')
|
||||
queue.add('key2', 'item2')
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
|
||||
expect(processSpy).toHaveBeenCalledTimes(1)
|
||||
expect(processSpy).toHaveBeenCalledWith(['item1', 'item2'])
|
||||
})
|
||||
|
||||
test('should process items after timeout if batch size is not reached', async () => {
|
||||
const processSpy = vi.fn()
|
||||
queue = new BatchingQueue({
|
||||
batchSize: 5,
|
||||
maxWaitTime: 100,
|
||||
processFunction: async (batch: string[]): Promise<void> => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
processSpy(batch)
|
||||
}
|
||||
})
|
||||
|
||||
queue.add('key1', 'item1')
|
||||
queue.add('key2', 'item2')
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
|
||||
expect(processSpy).toHaveBeenCalledTimes(1)
|
||||
expect(processSpy).toHaveBeenCalledWith(['item1', 'item2'])
|
||||
})
|
||||
|
||||
test('should not process items if disposed', async () => {
|
||||
const processSpy = vi.fn()
|
||||
queue = new BatchingQueue({
|
||||
batchSize: 2,
|
||||
maxWaitTime: 10000,
|
||||
processFunction: async (batch: string[]): Promise<void> => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
processSpy(batch)
|
||||
}
|
||||
})
|
||||
|
||||
queue.add('key1', 'item1')
|
||||
queue.dispose()
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
|
||||
expect(processSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('should handle multiple batches correctly', async () => {
|
||||
const processSpy = vi.fn()
|
||||
queue = new BatchingQueue({
|
||||
batchSize: 2,
|
||||
maxWaitTime: 100,
|
||||
processFunction: async (batch: string[]): Promise<void> => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
processSpy(batch)
|
||||
}
|
||||
})
|
||||
|
||||
queue.add('key1', 'item1')
|
||||
queue.add('key2', 'item2')
|
||||
queue.add('key3', 'item3')
|
||||
queue.add('key4', 'item4')
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
|
||||
expect(processSpy).toHaveBeenCalledTimes(2)
|
||||
expect(processSpy).toHaveBeenCalledWith(['item1', 'item2'])
|
||||
expect(processSpy).toHaveBeenCalledWith(['item3', 'item4'])
|
||||
})
|
||||
|
||||
test('should retrieve items by key', () => {
|
||||
queue.add('key1', 'item1')
|
||||
queue.add('key2', 'item2')
|
||||
|
||||
expect(queue.get('key1')).toBe('item1')
|
||||
expect(queue.get('key2')).toBe('item2')
|
||||
expect(queue.get('key3')).toBeUndefined()
|
||||
})
|
||||
|
||||
test('should return correct count of items', () => {
|
||||
expect(queue.count()).toBe(0)
|
||||
|
||||
queue.add('key1', 'item1')
|
||||
queue.add('key2', 'item2')
|
||||
|
||||
expect(queue.count()).toBe(2)
|
||||
})
|
||||
|
||||
test('should not process items if already processing', async () => {
|
||||
const processSpy = vi.fn()
|
||||
queue = new BatchingQueue({
|
||||
batchSize: 2,
|
||||
maxWaitTime: 100,
|
||||
processFunction: async (batch: string[]): Promise<void> => {
|
||||
processSpy(batch)
|
||||
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||
}
|
||||
})
|
||||
|
||||
queue.add('key1', 'item1')
|
||||
queue.add('key2', 'item2')
|
||||
queue.add('key3', 'item3')
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
|
||||
expect(processSpy).toHaveBeenCalledTimes(1)
|
||||
expect(processSpy).toHaveBeenCalledWith(['item1', 'item2'])
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
|
||||
expect(processSpy).toHaveBeenCalledTimes(2)
|
||||
expect(processSpy).toHaveBeenCalledWith(['item3'])
|
||||
})
|
||||
})
|
||||
@@ -1,41 +1,100 @@
|
||||
import { CustomLogger } from '../types/functions.js'
|
||||
import KeyedQueue from './keyedQueue.js'
|
||||
|
||||
export default class BatchingQueue<T> {
|
||||
#queue: KeyedQueue<string, T> = new KeyedQueue<string, T>()
|
||||
#batchSize: number
|
||||
#processFunction: (batch: T[]) => Promise<void>
|
||||
#timeoutId: ReturnType<typeof setTimeout> | null = null
|
||||
#isProcessing = false
|
||||
#logger: CustomLogger
|
||||
|
||||
#baseInterval: number
|
||||
#minInterval: number
|
||||
#maxInterval: number
|
||||
|
||||
#processingLoop: Promise<void>
|
||||
#disposed = false
|
||||
#batchTimeout: number
|
||||
|
||||
// Helper methods for cross-environment timeout handling
|
||||
#getSetTimeoutFn(): typeof setTimeout {
|
||||
// First check for window object (browser), then fallback to global (node), then just use setTimeout
|
||||
return typeof window !== 'undefined'
|
||||
? window.setTimeout.bind(window)
|
||||
: typeof global !== 'undefined'
|
||||
? global.setTimeout
|
||||
: setTimeout
|
||||
}
|
||||
|
||||
#getClearTimeoutFn(): typeof clearTimeout {
|
||||
// First check for window object (browser), then fallback to global (node), then just use clearTimeout
|
||||
return typeof window !== 'undefined'
|
||||
? window.clearTimeout.bind(window)
|
||||
: typeof global !== 'undefined'
|
||||
? global.clearTimeout
|
||||
: clearTimeout
|
||||
}
|
||||
|
||||
constructor(params: {
|
||||
batchSize: number
|
||||
maxWaitTime?: number
|
||||
maxWaitTime: number
|
||||
processFunction: (batch: T[]) => Promise<void>
|
||||
logger?: CustomLogger
|
||||
}) {
|
||||
this.#batchSize = params.batchSize
|
||||
this.#baseInterval = Math.min(params.maxWaitTime ?? 200, 200) // Initial batch time (ms)
|
||||
this.#minInterval = Math.min(params.maxWaitTime ?? 100, 100) // Minimum batch time
|
||||
this.#maxInterval = Math.min(params.maxWaitTime ?? 3000, 3000) // Maximum batch time
|
||||
this.#processFunction = params.processFunction
|
||||
this.#processingLoop = this.#loop()
|
||||
this.#batchTimeout = params.maxWaitTime
|
||||
this.#logger = params.logger || ((): void => {})
|
||||
}
|
||||
|
||||
async disposeAsync(): Promise<void> {
|
||||
dispose(): void {
|
||||
this.#disposed = true
|
||||
await this.#processingLoop
|
||||
if (this.#timeoutId) {
|
||||
this.#getClearTimeoutFn()(this.#timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
add(key: string, item: T): void {
|
||||
this.#queue.enqueue(key, item)
|
||||
this.#addCheck()
|
||||
}
|
||||
|
||||
addAll(keys: string[], items: T[]): void {
|
||||
this.#queue.enqueueAll(keys, items)
|
||||
this.#addCheck()
|
||||
}
|
||||
|
||||
#addCheck(): void {
|
||||
if (this.#queue.size >= this.#batchSize) {
|
||||
// Fire and forget, no need to await
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.#flush()
|
||||
} else {
|
||||
if (this.#timeoutId) {
|
||||
this.#getClearTimeoutFn()(this.#timeoutId)
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
this.#timeoutId = this.#getSetTimeoutFn()(() => this.#flush(), this.#batchTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
async #flush(): Promise<void> {
|
||||
if (this.#timeoutId) {
|
||||
this.#getClearTimeoutFn()(this.#timeoutId)
|
||||
this.#timeoutId = null
|
||||
}
|
||||
|
||||
if (this.#isProcessing || this.#queue.size === 0) {
|
||||
return
|
||||
}
|
||||
this.#isProcessing = true
|
||||
|
||||
const batchToProcess = this.#getBatch(this.#batchSize)
|
||||
|
||||
try {
|
||||
await this.#processFunction(batchToProcess)
|
||||
} catch (error) {
|
||||
this.#logger('Batch processing failed:', error)
|
||||
} finally {
|
||||
this.#isProcessing = false
|
||||
}
|
||||
this.#addCheck()
|
||||
}
|
||||
|
||||
get(id: string): T | undefined {
|
||||
@@ -53,37 +112,4 @@ export default class BatchingQueue<T> {
|
||||
#getBatch(batchSize: number): T[] {
|
||||
return this.#queue.spliceValues(0, Math.min(batchSize, this.#queue.size))
|
||||
}
|
||||
|
||||
async #loop(): Promise<void> {
|
||||
let interval = this.#baseInterval
|
||||
while (!this.#disposed || this.#queue.size > 0) {
|
||||
const startTime = performance.now()
|
||||
if (this.#queue.size > 0) {
|
||||
const batch = this.#getBatch(this.#batchSize)
|
||||
//console.log('running with queue size of ' + this.#queue.length)
|
||||
await this.#processFunction(batch)
|
||||
}
|
||||
if (this.#queue.size < this.#batchSize / 2) {
|
||||
//refigure interval
|
||||
const endTime = performance.now()
|
||||
const duration = endTime - startTime
|
||||
if (duration > interval) {
|
||||
interval = Math.min(interval * 1.5, this.#maxInterval) // Increase if slow or empty
|
||||
} else {
|
||||
interval = Math.max(interval * 0.8, this.#minInterval) // Decrease if fast
|
||||
}
|
||||
/*console.log(
|
||||
'queue is waiting ' +
|
||||
interval / 1000 +
|
||||
' with queue size of ' +
|
||||
this.#queue.length
|
||||
)*/
|
||||
await this.#delay(interval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import { Database } from '../core/interfaces.js'
|
||||
import { CacheOptions } from '../core/options.js'
|
||||
import { DefermentManager } from '../deferment/defermentManager.js'
|
||||
import { Item } from '../types/types.js'
|
||||
import BatchingQueue from './batchingQueue.js'
|
||||
import Queue from './queue.js'
|
||||
|
||||
export class CacheWriter implements Queue<Item> {
|
||||
#writeQueue: BatchingQueue<Item> | undefined
|
||||
#database: Database
|
||||
#defermentManager: DefermentManager
|
||||
#options: CacheOptions
|
||||
#disposed = false
|
||||
|
||||
constructor(
|
||||
database: Database,
|
||||
defermentManager: DefermentManager,
|
||||
options: CacheOptions
|
||||
) {
|
||||
this.#database = database
|
||||
this.#defermentManager = defermentManager
|
||||
this.#options = options
|
||||
}
|
||||
|
||||
add(item: Item): void {
|
||||
if (!this.#writeQueue) {
|
||||
this.#writeQueue = new BatchingQueue({
|
||||
batchSize: this.#options.maxCacheWriteSize,
|
||||
maxWaitTime: this.#options.maxCacheBatchWriteWait,
|
||||
processFunction: (batch: Item[]): Promise<void> =>
|
||||
this.#database.saveBatch({ batch })
|
||||
})
|
||||
}
|
||||
this.#defermentManager.undefer(item)
|
||||
this.#writeQueue.add(item.baseId, item)
|
||||
}
|
||||
|
||||
async disposeAsync(): Promise<void> {
|
||||
await this.#writeQueue?.disposeAsync()
|
||||
this.#disposed = true
|
||||
}
|
||||
|
||||
get isDisposed(): boolean {
|
||||
return this.#disposed
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,33 @@ input AppTokenCreateInput {
|
||||
limitResources: [TokenResourceIdentifierInput!]
|
||||
}
|
||||
|
||||
"""
|
||||
A token used to enable an embedded viewer for a private project
|
||||
"""
|
||||
type EmbedToken {
|
||||
tokenId: String!
|
||||
projectId: String!
|
||||
user: LimitedUser
|
||||
resourceIdString: String!
|
||||
createdAt: DateTime!
|
||||
lifespan: BigInt!
|
||||
lastUsed: DateTime!
|
||||
}
|
||||
|
||||
input EmbedTokenCreateInput {
|
||||
projectId: String!
|
||||
"""
|
||||
The model(s) and version(s) string used in the embed url
|
||||
"""
|
||||
resourceIdString: String!
|
||||
lifespan: BigInt
|
||||
}
|
||||
|
||||
type CreateEmbedTokenReturn {
|
||||
token: String!
|
||||
tokenMetadata: EmbedToken!
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
"""
|
||||
Creates an personal api token.
|
||||
@@ -77,3 +104,21 @@ extend type Mutation {
|
||||
@hasServerRole(role: SERVER_USER)
|
||||
@hasScope(scope: "tokens:write")
|
||||
}
|
||||
|
||||
extend type ProjectMutations {
|
||||
createEmbedToken(token: EmbedTokenCreateInput!): CreateEmbedTokenReturn!
|
||||
@hasScope(scope: "tokens:write")
|
||||
revokeEmbedToken(token: String!, projectId: String!): Boolean!
|
||||
@hasScope(scope: "tokens:write")
|
||||
revokeEmbedTokens(projectId: String!): Boolean! @hasScope(scope: "tokens:write")
|
||||
}
|
||||
|
||||
type EmbedTokenCollection {
|
||||
items: [EmbedToken!]!
|
||||
totalCount: Int!
|
||||
cursor: String
|
||||
}
|
||||
|
||||
extend type Project {
|
||||
embedTokens(cursor: String, limit: Int): EmbedTokenCollection!
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ type ProjectPermissionChecks {
|
||||
canPublish: PermissionCheckResult!
|
||||
canLoad: PermissionCheckResult!
|
||||
canInvite: PermissionCheckResult!
|
||||
canCreateEmbedTokens: PermissionCheckResult!
|
||||
canRevokeEmbedTokens: PermissionCheckResult!
|
||||
canReadEmbedTokens: PermissionCheckResult!
|
||||
}
|
||||
|
||||
type RootPermissionChecks {
|
||||
|
||||
@@ -24,6 +24,7 @@ generates:
|
||||
ProjectAccessRequestMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn'
|
||||
LimitedUser: '@/modules/core/helpers/graphTypes#LimitedUserGraphQLReturn'
|
||||
User: '@/modules/core/helpers/graphTypes#UserGraphQLReturn'
|
||||
EmbedToken: '@/modules/core/helpers/graphTypes#EmbedTokenGraphQLReturn'
|
||||
ActiveUserMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn'
|
||||
UserMetaMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn'
|
||||
UserEmailMutations: '@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn'
|
||||
|
||||
@@ -21,6 +21,7 @@ import { mapStreamRoleToValue } from '@/modules/core/helpers/graphTypes'
|
||||
import { Roles } from '@/modules/core/helpers/mainConstants'
|
||||
import {
|
||||
getStreamFactory,
|
||||
getStreamRolesFactory,
|
||||
grantStreamPermissionsFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import { getUserFactory } from '@/modules/core/repositories/users'
|
||||
@@ -69,6 +70,7 @@ const addOrUpdateStreamCollaborator = addOrUpdateStreamCollaboratorFactory({
|
||||
validateStreamAccess,
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ import {
|
||||
requestProjectAccessFactory
|
||||
} from '@/modules/accessrequests/services/stream'
|
||||
import { StreamActionTypes } from '@/modules/activitystream/helpers/types'
|
||||
import { getActivitiesFactory } from '@/modules/activitystream/repositories/index'
|
||||
import {
|
||||
Activity,
|
||||
ServerAccessRequests,
|
||||
StreamActivity,
|
||||
Streams,
|
||||
@@ -24,6 +26,7 @@ import { Roles } from '@/modules/core/helpers/mainConstants'
|
||||
import {
|
||||
getStreamCollaboratorsFactory,
|
||||
getStreamFactory,
|
||||
getStreamRolesFactory,
|
||||
grantStreamPermissionsFactory,
|
||||
revokeStreamPermissionsFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
@@ -79,15 +82,17 @@ const removeStreamCollaborator = removeStreamCollaboratorFactory({
|
||||
validateStreamAccess,
|
||||
isStreamCollaborator,
|
||||
revokeStreamPermissions: revokeStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
|
||||
const addOrUpdateStreamCollaborator = addOrUpdateStreamCollaboratorFactory({
|
||||
validateStreamAccess,
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
const getActivities = getActivitiesFactory({ db })
|
||||
|
||||
const isNotCollaboratorError = (e: unknown) =>
|
||||
e instanceof StreamAccessUpdateError &&
|
||||
@@ -406,7 +411,11 @@ describe('Project access requests', () => {
|
||||
let validReqId: string
|
||||
|
||||
beforeEach(async () => {
|
||||
await truncateTables([ServerAccessRequests.name, StreamActivity.name])
|
||||
await truncateTables([
|
||||
ServerAccessRequests.name,
|
||||
StreamActivity.name,
|
||||
Activity.name
|
||||
])
|
||||
await removeStreamCollaborator(
|
||||
myPrivateStream.id,
|
||||
otherGuy.id,
|
||||
@@ -464,8 +473,8 @@ describe('Project access requests', () => {
|
||||
|
||||
// activity stream item should be inserted
|
||||
if (accept) {
|
||||
const streamActivity = await getStreamActivities(myPrivateStream.id, {
|
||||
actionType: StreamActionTypes.Stream.PermissionsAdd,
|
||||
const streamActivity = await getActivities({
|
||||
projectId: myPrivateStream.id,
|
||||
userId: me.id
|
||||
})
|
||||
expect(streamActivity).to.have.lengthOf(1)
|
||||
|
||||
@@ -13,7 +13,9 @@ import {
|
||||
requestStreamAccessFactory
|
||||
} from '@/modules/accessrequests/services/stream'
|
||||
import { StreamActionTypes } from '@/modules/activitystream/helpers/types'
|
||||
import { getActivitiesFactory } from '@/modules/activitystream/repositories/index'
|
||||
import {
|
||||
Activity,
|
||||
ServerAccessRequests,
|
||||
StreamActivity,
|
||||
Streams,
|
||||
@@ -25,6 +27,7 @@ import { Roles } from '@/modules/core/helpers/mainConstants'
|
||||
import {
|
||||
getStreamCollaboratorsFactory,
|
||||
getStreamFactory,
|
||||
getStreamRolesFactory,
|
||||
grantStreamPermissionsFactory,
|
||||
revokeStreamPermissionsFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
@@ -82,6 +85,7 @@ const removeStreamCollaborator = removeStreamCollaboratorFactory({
|
||||
validateStreamAccess,
|
||||
isStreamCollaborator,
|
||||
revokeStreamPermissions: revokeStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
|
||||
@@ -89,8 +93,10 @@ const addOrUpdateStreamCollaborator = addOrUpdateStreamCollaboratorFactory({
|
||||
validateStreamAccess,
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
const getActivities = getActivitiesFactory({ db })
|
||||
|
||||
const isNotCollaboratorError = (e: unknown) =>
|
||||
e instanceof StreamAccessUpdateError &&
|
||||
@@ -375,7 +381,11 @@ describe('Stream access requests', () => {
|
||||
let validReqId: string
|
||||
|
||||
beforeEach(async () => {
|
||||
await truncateTables([ServerAccessRequests.name, StreamActivity.name])
|
||||
await truncateTables([
|
||||
ServerAccessRequests.name,
|
||||
StreamActivity.name,
|
||||
Activity.name
|
||||
])
|
||||
await removeStreamCollaborator(
|
||||
myPrivateStream.id,
|
||||
otherGuy.id,
|
||||
@@ -424,9 +434,10 @@ describe('Stream access requests', () => {
|
||||
|
||||
// activity stream item should be inserted
|
||||
if (accept) {
|
||||
const streamActivity = await getStreamActivities(myPrivateStream.id, {
|
||||
actionType: StreamActionTypes.Stream.PermissionsAdd,
|
||||
userId: me.id
|
||||
const streamActivity = await getActivities({
|
||||
projectId: myPrivateStream.id,
|
||||
userId: me.id,
|
||||
eventType: 'project_role_updated'
|
||||
})
|
||||
expect(streamActivity).to.have.lengthOf(1)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
Activity,
|
||||
ActivitySummary,
|
||||
CommentCreatedActivityInput,
|
||||
ReplyCreatedActivityInput,
|
||||
@@ -7,7 +8,6 @@ import {
|
||||
StreamActionType
|
||||
} from '@/modules/activitystream/domain/types'
|
||||
import {
|
||||
Activity,
|
||||
StreamActivityRecord,
|
||||
StreamScopeActivity
|
||||
} from '@/modules/activitystream/helpers/types'
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
} from '@/modules/core/helpers/types'
|
||||
import { Nullable } from '@speckle/shared'
|
||||
|
||||
export type GetActivity = (
|
||||
export type GetUserStreamActivity = (
|
||||
streamId: string,
|
||||
start: Date,
|
||||
end: Date,
|
||||
@@ -281,3 +281,12 @@ export type SaveActivity = <
|
||||
>(
|
||||
args: Omit<Activity<T, R>, 'createdAt' | 'id'>
|
||||
) => Promise<Activity<T, R>>
|
||||
|
||||
type GetActivitiesArgs = Partial<{
|
||||
workspaceId: string
|
||||
projectId: string
|
||||
eventType: string
|
||||
userId: string
|
||||
}>
|
||||
|
||||
export type GetActivities = (filters: GetActivitiesArgs) => Promise<Activity[]>
|
||||
|
||||
@@ -23,6 +23,23 @@ export type ResourceEventsToPayloadMap = {
|
||||
workspace_seat_updated: z.infer<typeof WorkspaceSeatUpdatedActivity>
|
||||
workspace_seat_deleted: z.infer<typeof WorkspaceSeatDeletedActivity>
|
||||
}
|
||||
project: {
|
||||
project_role_updated: z.infer<typeof ProjectRoleUpdatedActivity>
|
||||
project_role_deleted: z.infer<typeof ProjectRoleDeletedActivity>
|
||||
}
|
||||
}
|
||||
|
||||
export interface Activity<
|
||||
T extends keyof ResourceEventsToPayloadMap = keyof ResourceEventsToPayloadMap,
|
||||
R extends keyof ResourceEventsToPayloadMap[T] = keyof ResourceEventsToPayloadMap[T]
|
||||
> {
|
||||
id: string
|
||||
contextResourceId: string
|
||||
contextResourceType: T
|
||||
eventType: R
|
||||
userId: string | null
|
||||
payload: ResourceEventsToPayloadMap[T][R]
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
const workspacePlan = z.object({
|
||||
@@ -56,6 +73,12 @@ const workspaceSubscription = z.object({
|
||||
totalEditorSeats: z.number()
|
||||
})
|
||||
|
||||
const projectRole = z.union([
|
||||
z.literal('stream:owner'),
|
||||
z.literal('stream:contributor'),
|
||||
z.literal('stream:reviewer')
|
||||
])
|
||||
|
||||
export const WorkspacePlanCreatedActivity = z.object({
|
||||
version: z.literal('1'),
|
||||
new: workspacePlan
|
||||
@@ -84,6 +107,19 @@ export const WorkspaceSeatDeletedActivity = z.object({
|
||||
old: workspaceSeat
|
||||
})
|
||||
|
||||
export const ProjectRoleUpdatedActivity = z.object({
|
||||
version: z.literal('1'),
|
||||
userId: z.string(),
|
||||
new: projectRole,
|
||||
old: z.nullable(projectRole)
|
||||
})
|
||||
|
||||
export const ProjectRoleDeletedActivity = z.object({
|
||||
version: z.literal('1'),
|
||||
userId: z.string(),
|
||||
old: projectRole
|
||||
})
|
||||
|
||||
// Stream Activity
|
||||
|
||||
export type StreamActionType =
|
||||
|
||||
@@ -19,13 +19,13 @@ import {
|
||||
*/
|
||||
const addStreamAccessRequestedActivityFactory =
|
||||
({
|
||||
saveActivity
|
||||
saveStreamActivity
|
||||
}: {
|
||||
saveActivity: SaveStreamActivity
|
||||
saveStreamActivity: SaveStreamActivity
|
||||
}): AddStreamAccessRequestedActivity =>
|
||||
async (params: { streamId: string; requesterId: string }) => {
|
||||
const { streamId, requesterId } = params
|
||||
await saveActivity({
|
||||
await saveStreamActivity({
|
||||
streamId,
|
||||
resourceType: StreamResourceTypes.Stream,
|
||||
resourceId: streamId,
|
||||
@@ -41,13 +41,13 @@ const addStreamAccessRequestedActivityFactory =
|
||||
*/
|
||||
const addStreamAccessRequestDeclinedActivityFactory =
|
||||
({
|
||||
saveActivity
|
||||
saveStreamActivity
|
||||
}: {
|
||||
saveActivity: SaveStreamActivity
|
||||
saveStreamActivity: SaveStreamActivity
|
||||
}): AddStreamAccessRequestDeclinedActivity =>
|
||||
async (params: { streamId: string; requesterId: string; declinerId: string }) => {
|
||||
const { streamId, requesterId, declinerId } = params
|
||||
await saveActivity({
|
||||
await saveStreamActivity({
|
||||
streamId,
|
||||
resourceType: StreamResourceTypes.Stream,
|
||||
resourceId: streamId,
|
||||
@@ -104,7 +104,8 @@ const onServerAccessRequestFinalizedFactory =
|
||||
}
|
||||
|
||||
export const reportAccessRequestActivityFactory =
|
||||
(deps: { eventListen: EventBusListen; saveActivity: SaveStreamActivity }) => () => {
|
||||
(deps: { eventListen: EventBusListen; saveStreamActivity: SaveStreamActivity }) =>
|
||||
() => {
|
||||
const addStreamAccessRequestedActivity =
|
||||
addStreamAccessRequestedActivityFactory(deps)
|
||||
const addStreamAccessRequestDeclinedActivity =
|
||||
|
||||
@@ -16,11 +16,15 @@ import { EventBusListen } from '@/modules/shared/services/eventBus'
|
||||
* Save "branch created" activity
|
||||
*/
|
||||
const addBranchCreatedActivityFactory =
|
||||
({ saveActivity }: { saveActivity: SaveStreamActivity }): AddBranchCreatedActivity =>
|
||||
({
|
||||
saveStreamActivity
|
||||
}: {
|
||||
saveStreamActivity: SaveStreamActivity
|
||||
}): AddBranchCreatedActivity =>
|
||||
async (params) => {
|
||||
const { branch } = params
|
||||
|
||||
await saveActivity({
|
||||
await saveStreamActivity({
|
||||
streamId: branch.streamId,
|
||||
resourceType: StreamResourceTypes.Branch,
|
||||
resourceId: branch.id,
|
||||
@@ -32,12 +36,16 @@ const addBranchCreatedActivityFactory =
|
||||
}
|
||||
|
||||
const addBranchUpdatedActivityFactory =
|
||||
({ saveActivity }: { saveActivity: SaveStreamActivity }): AddBranchUpdatedActivity =>
|
||||
({
|
||||
saveStreamActivity
|
||||
}: {
|
||||
saveStreamActivity: SaveStreamActivity
|
||||
}): AddBranchUpdatedActivity =>
|
||||
async (params) => {
|
||||
const { update, userId, oldBranch } = params
|
||||
|
||||
const streamId = isBranchUpdateInput(update) ? update.streamId : update.projectId
|
||||
await saveActivity({
|
||||
await saveStreamActivity({
|
||||
streamId,
|
||||
resourceType: StreamResourceTypes.Branch,
|
||||
resourceId: update.id,
|
||||
@@ -49,13 +57,17 @@ const addBranchUpdatedActivityFactory =
|
||||
}
|
||||
|
||||
const addBranchDeletedActivityFactory =
|
||||
({ saveActivity }: { saveActivity: SaveStreamActivity }): AddBranchDeletedActivity =>
|
||||
({
|
||||
saveStreamActivity
|
||||
}: {
|
||||
saveStreamActivity: SaveStreamActivity
|
||||
}): AddBranchDeletedActivity =>
|
||||
async (params) => {
|
||||
const { input, userId, branchName } = params
|
||||
|
||||
const streamId = isBranchDeleteInput(input) ? input.streamId : input.projectId
|
||||
await Promise.all([
|
||||
saveActivity({
|
||||
saveStreamActivity({
|
||||
streamId,
|
||||
resourceType: StreamResourceTypes.Branch,
|
||||
resourceId: input.id,
|
||||
@@ -68,7 +80,8 @@ const addBranchDeletedActivityFactory =
|
||||
}
|
||||
|
||||
export const reportBranchActivityFactory =
|
||||
(deps: { eventListen: EventBusListen; saveActivity: SaveStreamActivity }) => () => {
|
||||
(deps: { eventListen: EventBusListen; saveStreamActivity: SaveStreamActivity }) =>
|
||||
() => {
|
||||
const addBranchCreatedActivity = addBranchCreatedActivityFactory(deps)
|
||||
const addBranchUpdatedActivity = addBranchUpdatedActivityFactory(deps)
|
||||
const addBranchDeletedActivity = addBranchDeletedActivityFactory(deps)
|
||||
|
||||
@@ -19,11 +19,15 @@ import { has } from 'lodash'
|
||||
import { OverrideProperties } from 'type-fest'
|
||||
|
||||
const addThreadCreatedActivityFactory =
|
||||
({ saveActivity }: { saveActivity: SaveStreamActivity }): AddThreadCreatedActivity =>
|
||||
({
|
||||
saveStreamActivity
|
||||
}: {
|
||||
saveStreamActivity: SaveStreamActivity
|
||||
}): AddThreadCreatedActivity =>
|
||||
async (params) => {
|
||||
const { input, comment } = params
|
||||
|
||||
await saveActivity({
|
||||
await saveStreamActivity({
|
||||
resourceId: comment.id,
|
||||
streamId: comment.streamId,
|
||||
resourceType: StreamResourceTypes.Comment,
|
||||
@@ -39,14 +43,18 @@ const isLegacyReplyCreateInput = (
|
||||
): i is ReplyCreateInput => has(i, 'streamId')
|
||||
|
||||
const addReplyAddedActivityFactory =
|
||||
({ saveActivity }: { saveActivity: SaveStreamActivity }): AddReplyAddedActivity =>
|
||||
({
|
||||
saveStreamActivity
|
||||
}: {
|
||||
saveStreamActivity: SaveStreamActivity
|
||||
}): AddReplyAddedActivity =>
|
||||
async (params) => {
|
||||
const { input, reply } = params
|
||||
|
||||
const parentCommentId = isLegacyReplyCreateInput(input)
|
||||
? input.parentComment
|
||||
: input.threadId
|
||||
await saveActivity({
|
||||
await saveStreamActivity({
|
||||
streamId: reply.streamId,
|
||||
resourceType: StreamResourceTypes.Comment,
|
||||
resourceId: parentCommentId,
|
||||
@@ -59,14 +67,14 @@ const addReplyAddedActivityFactory =
|
||||
|
||||
const addCommentArchivedActivityFactory =
|
||||
({
|
||||
saveActivity
|
||||
saveStreamActivity
|
||||
}: {
|
||||
saveActivity: SaveStreamActivity
|
||||
saveStreamActivity: SaveStreamActivity
|
||||
}): AddCommentArchivedActivity =>
|
||||
async (params) => {
|
||||
const { userId, input, comment } = params
|
||||
|
||||
await saveActivity({
|
||||
await saveStreamActivity({
|
||||
streamId: comment.streamId,
|
||||
resourceType: StreamResourceTypes.Comment,
|
||||
resourceId: comment.id,
|
||||
@@ -100,7 +108,8 @@ const isThreadCreatedPayload = (
|
||||
}
|
||||
|
||||
export const reportCommentActivityFactory =
|
||||
(deps: { eventListen: EventBusListen; saveActivity: SaveStreamActivity }) => () => {
|
||||
(deps: { eventListen: EventBusListen; saveStreamActivity: SaveStreamActivity }) =>
|
||||
() => {
|
||||
const addThreadCreatedActivity = addThreadCreatedActivityFactory(deps)
|
||||
const addReplyAddedActivity = addReplyAddedActivityFactory(deps)
|
||||
const addCommentArchivedActivity = addCommentArchivedActivityFactory(deps)
|
||||
|
||||
@@ -18,7 +18,11 @@ import { MaybeNullOrUndefined } from '@speckle/shared'
|
||||
* Save "new commit created" activity item
|
||||
*/
|
||||
const addCommitCreatedActivityFactory =
|
||||
({ saveActivity }: { saveActivity: SaveStreamActivity }): AddCommitCreatedActivity =>
|
||||
({
|
||||
saveStreamActivity
|
||||
}: {
|
||||
saveStreamActivity: SaveStreamActivity
|
||||
}): AddCommitCreatedActivity =>
|
||||
async (params: {
|
||||
commitId: string
|
||||
streamId: string
|
||||
@@ -29,7 +33,7 @@ const addCommitCreatedActivityFactory =
|
||||
commit: CommitRecord
|
||||
}) => {
|
||||
const { commitId, input, streamId, userId, branchName, commit, modelId } = params
|
||||
await saveActivity({
|
||||
await saveStreamActivity({
|
||||
streamId,
|
||||
resourceType: StreamResourceTypes.Commit,
|
||||
resourceId: commitId,
|
||||
@@ -49,11 +53,15 @@ const addCommitCreatedActivityFactory =
|
||||
}
|
||||
|
||||
const addCommitUpdatedActivityFactory =
|
||||
({ saveActivity }: { saveActivity: SaveStreamActivity }): AddCommitUpdatedActivity =>
|
||||
({
|
||||
saveStreamActivity
|
||||
}: {
|
||||
saveStreamActivity: SaveStreamActivity
|
||||
}): AddCommitUpdatedActivity =>
|
||||
async (params) => {
|
||||
const { commitId, streamId, userId, originalCommit, update } = params
|
||||
|
||||
await saveActivity({
|
||||
await saveStreamActivity({
|
||||
streamId,
|
||||
resourceType: StreamResourceTypes.Commit,
|
||||
resourceId: commitId,
|
||||
@@ -65,7 +73,7 @@ const addCommitUpdatedActivityFactory =
|
||||
}
|
||||
|
||||
const addCommitMovedActivityFactory =
|
||||
({ saveActivity }: { saveActivity: SaveStreamActivity }) =>
|
||||
({ saveStreamActivity }: { saveStreamActivity: SaveStreamActivity }) =>
|
||||
async (params: {
|
||||
commitId: string
|
||||
streamId: string
|
||||
@@ -75,7 +83,7 @@ const addCommitMovedActivityFactory =
|
||||
commit: CommitRecord
|
||||
}) => {
|
||||
const { commitId, streamId, userId, originalBranchId, newBranchId } = params
|
||||
await saveActivity({
|
||||
await saveStreamActivity({
|
||||
streamId,
|
||||
resourceType: StreamResourceTypes.Commit,
|
||||
resourceId: commitId,
|
||||
@@ -87,7 +95,11 @@ const addCommitMovedActivityFactory =
|
||||
}
|
||||
|
||||
const addCommitDeletedActivityFactory =
|
||||
({ saveActivity }: { saveActivity: SaveStreamActivity }): AddCommitDeletedActivity =>
|
||||
({
|
||||
saveStreamActivity
|
||||
}: {
|
||||
saveStreamActivity: SaveStreamActivity
|
||||
}): AddCommitDeletedActivity =>
|
||||
async (params: {
|
||||
commitId: string
|
||||
streamId: string
|
||||
@@ -96,7 +108,7 @@ const addCommitDeletedActivityFactory =
|
||||
branchId: string
|
||||
}) => {
|
||||
const { commitId, streamId, userId, commit } = params
|
||||
await saveActivity({
|
||||
await saveStreamActivity({
|
||||
streamId,
|
||||
resourceType: StreamResourceTypes.Commit,
|
||||
resourceId: commitId,
|
||||
@@ -108,7 +120,7 @@ const addCommitDeletedActivityFactory =
|
||||
}
|
||||
|
||||
const addCommitReceivedActivityFactory =
|
||||
({ saveActivity }: { saveActivity: SaveStreamActivity }) =>
|
||||
({ saveStreamActivity }: { saveStreamActivity: SaveStreamActivity }) =>
|
||||
async (params: {
|
||||
streamId: string
|
||||
commitId: string
|
||||
@@ -117,7 +129,7 @@ const addCommitReceivedActivityFactory =
|
||||
message: MaybeNullOrUndefined<string>
|
||||
}) => {
|
||||
const { streamId, commitId, userId, sourceApplication, message } = params
|
||||
await saveActivity({
|
||||
await saveStreamActivity({
|
||||
streamId,
|
||||
resourceType: StreamResourceTypes.Commit,
|
||||
resourceId: commitId,
|
||||
@@ -132,7 +144,8 @@ const addCommitReceivedActivityFactory =
|
||||
}
|
||||
|
||||
export const reportCommitActivityFactory =
|
||||
(deps: { eventListen: EventBusListen; saveActivity: SaveStreamActivity }) => () => {
|
||||
(deps: { eventListen: EventBusListen; saveStreamActivity: SaveStreamActivity }) =>
|
||||
() => {
|
||||
const addCommitCreatedActivity = addCommitCreatedActivityFactory(deps)
|
||||
const addCommitUpdatedActivity = addCommitUpdatedActivityFactory(deps)
|
||||
const addCommitMovedActivity = addCommitMovedActivityFactory(deps)
|
||||
|
||||
@@ -18,7 +18,7 @@ import { Roles } from '@speckle/shared'
|
||||
*/
|
||||
const addStreamInviteSentOutActivityFactory =
|
||||
(deps: {
|
||||
saveActivity: SaveStreamActivity
|
||||
saveStreamActivity: SaveStreamActivity
|
||||
getProjectInviteProject: GetProjectInviteProject
|
||||
}) =>
|
||||
async (payload: EventPayload<typeof ServerInvitesEvents.Created>) => {
|
||||
@@ -29,7 +29,7 @@ const addStreamInviteSentOutActivityFactory =
|
||||
const userTarget = resolveTarget(invite.target)
|
||||
const targetDisplay = userTarget.userId || userTarget.userEmail
|
||||
|
||||
await deps.saveActivity({
|
||||
await deps.saveStreamActivity({
|
||||
streamId: project.id,
|
||||
resourceType: StreamResourceTypes.Stream,
|
||||
resourceId: project.id,
|
||||
@@ -48,7 +48,7 @@ const addStreamInviteSentOutActivityFactory =
|
||||
*/
|
||||
const addStreamInviteAcceptedActivityFactory =
|
||||
(deps: {
|
||||
saveActivity: SaveStreamActivity
|
||||
saveStreamActivity: SaveStreamActivity
|
||||
getProjectInviteProject: GetProjectInviteProject
|
||||
}) =>
|
||||
async (payload: EventPayload<typeof ServerInvitesEvents.Finalized>) => {
|
||||
@@ -63,7 +63,7 @@ const addStreamInviteAcceptedActivityFactory =
|
||||
|
||||
const differentFinalizer = trueFinalizerUserId !== userTarget.userId
|
||||
|
||||
await deps.saveActivity({
|
||||
await deps.saveStreamActivity({
|
||||
streamId: project.id,
|
||||
resourceType: StreamResourceTypes.Stream,
|
||||
resourceId: project.id,
|
||||
@@ -81,7 +81,7 @@ const addStreamInviteAcceptedActivityFactory =
|
||||
*/
|
||||
const addStreamInviteDeclinedActivityFactory =
|
||||
(deps: {
|
||||
saveActivity: SaveStreamActivity
|
||||
saveStreamActivity: SaveStreamActivity
|
||||
getProjectInviteProject: GetProjectInviteProject
|
||||
}) =>
|
||||
async (payload: EventPayload<typeof ServerInvitesEvents.Finalized>) => {
|
||||
@@ -90,7 +90,7 @@ const addStreamInviteDeclinedActivityFactory =
|
||||
if (!project) return
|
||||
|
||||
const userTarget = resolveTarget(invite.target)
|
||||
await deps.saveActivity({
|
||||
await deps.saveStreamActivity({
|
||||
streamId: project.id,
|
||||
resourceType: StreamResourceTypes.Stream,
|
||||
resourceId: project.id,
|
||||
@@ -104,7 +104,7 @@ const addStreamInviteDeclinedActivityFactory =
|
||||
export const reportStreamInviteActivityFactory =
|
||||
(deps: {
|
||||
eventListen: EventBusListen
|
||||
saveActivity: SaveStreamActivity
|
||||
saveStreamActivity: SaveStreamActivity
|
||||
getProjectInviteProject: GetProjectInviteProject
|
||||
}) =>
|
||||
() => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
AddStreamDeletedActivity,
|
||||
AddStreamUpdatedActivity,
|
||||
SaveActivity,
|
||||
SaveStreamActivity
|
||||
} from '@/modules/activitystream/domain/operations'
|
||||
import {
|
||||
@@ -13,14 +14,54 @@ import {
|
||||
StreamCreateInput
|
||||
} from '@/modules/core/graph/generated/graphql'
|
||||
import { StreamRecord } from '@/modules/core/helpers/types'
|
||||
import { EventBusListen } from '@/modules/shared/services/eventBus'
|
||||
import { StreamRoles } from '@speckle/shared'
|
||||
import { EventBusListen, EventPayload } from '@/modules/shared/services/eventBus'
|
||||
|
||||
// Activity
|
||||
|
||||
const addProjectPermissionsAddedActivityFactory =
|
||||
({ saveActivity }: { saveActivity: SaveActivity }) =>
|
||||
async ({
|
||||
payload: { activityUserId, project, role, targetUserId, previousRole }
|
||||
}: EventPayload<typeof ProjectEvents.PermissionsAdded>) => {
|
||||
await saveActivity({
|
||||
contextResourceId: project.id,
|
||||
contextResourceType: 'project',
|
||||
eventType: 'project_role_updated',
|
||||
userId: activityUserId,
|
||||
payload: {
|
||||
version: '1',
|
||||
userId: targetUserId,
|
||||
new: role,
|
||||
old: previousRole
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const addProjectPermissionsRevokedActivityFactory =
|
||||
({ saveActivity }: { saveActivity: SaveActivity }) =>
|
||||
async ({
|
||||
payload: { activityUserId, project, removedUserId, role }
|
||||
}: EventPayload<typeof ProjectEvents.PermissionsRevoked>) => {
|
||||
await saveActivity({
|
||||
contextResourceId: project.id,
|
||||
contextResourceType: 'project',
|
||||
eventType: 'project_role_deleted',
|
||||
userId: activityUserId,
|
||||
payload: {
|
||||
version: '1',
|
||||
userId: removedUserId,
|
||||
old: role
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Stream activity
|
||||
|
||||
/**
|
||||
* Save "user created stream" activity item
|
||||
*/
|
||||
const addStreamCreatedActivityFactory =
|
||||
({ saveActivity }: { saveActivity: SaveStreamActivity }) =>
|
||||
({ saveStreamActivity }: { saveStreamActivity: SaveStreamActivity }) =>
|
||||
async (params: {
|
||||
streamId: string
|
||||
creatorId: string
|
||||
@@ -29,7 +70,7 @@ const addStreamCreatedActivityFactory =
|
||||
}) => {
|
||||
const { streamId, creatorId, input } = params
|
||||
|
||||
await saveActivity({
|
||||
await saveStreamActivity({
|
||||
streamId,
|
||||
resourceType: StreamResourceTypes.Stream,
|
||||
resourceId: streamId,
|
||||
@@ -44,11 +85,15 @@ const addStreamCreatedActivityFactory =
|
||||
* Save "stream updated" activity
|
||||
*/
|
||||
const addStreamUpdatedActivityFactory =
|
||||
({ saveActivity }: { saveActivity: SaveStreamActivity }): AddStreamUpdatedActivity =>
|
||||
({
|
||||
saveStreamActivity
|
||||
}: {
|
||||
saveStreamActivity: SaveStreamActivity
|
||||
}): AddStreamUpdatedActivity =>
|
||||
async (params) => {
|
||||
const { streamId, updaterId, oldStream, update } = params
|
||||
|
||||
await saveActivity({
|
||||
await saveStreamActivity({
|
||||
streamId,
|
||||
resourceType: StreamResourceTypes.Stream,
|
||||
resourceId: streamId,
|
||||
@@ -63,11 +108,15 @@ const addStreamUpdatedActivityFactory =
|
||||
* Save "stream deleted" activity
|
||||
*/
|
||||
const addStreamDeletedActivityFactory =
|
||||
({ saveActivity }: { saveActivity: SaveStreamActivity }): AddStreamDeletedActivity =>
|
||||
({
|
||||
saveStreamActivity
|
||||
}: {
|
||||
saveStreamActivity: SaveStreamActivity
|
||||
}): AddStreamDeletedActivity =>
|
||||
async (params) => {
|
||||
const { streamId, deleterId } = params
|
||||
|
||||
await saveActivity({
|
||||
await saveStreamActivity({
|
||||
streamId,
|
||||
resourceType: StreamResourceTypes.Stream,
|
||||
resourceId: streamId,
|
||||
@@ -82,7 +131,7 @@ const addStreamDeletedActivityFactory =
|
||||
* Save "user cloned stream X" activity item
|
||||
*/
|
||||
const addStreamClonedActivityFactory =
|
||||
({ saveActivity }: { saveActivity: SaveStreamActivity }) =>
|
||||
({ saveStreamActivity }: { saveStreamActivity: SaveStreamActivity }) =>
|
||||
async (params: {
|
||||
sourceStreamId: string
|
||||
newStream: StreamRecord
|
||||
@@ -91,7 +140,7 @@ const addStreamClonedActivityFactory =
|
||||
const { sourceStreamId, newStream, clonerId } = params
|
||||
const newStreamId = newStream.id
|
||||
|
||||
await saveActivity({
|
||||
await saveStreamActivity({
|
||||
streamId: newStreamId,
|
||||
resourceType: StreamResourceTypes.Stream,
|
||||
resourceId: newStreamId,
|
||||
@@ -102,68 +151,31 @@ const addStreamClonedActivityFactory =
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Save "stream permissions granted to user" activity item
|
||||
*/
|
||||
const addStreamPermissionsAddedActivityFactory =
|
||||
({ saveActivity }: { saveActivity: SaveStreamActivity }) =>
|
||||
async (params: {
|
||||
streamId: string
|
||||
activityUserId: string
|
||||
targetUserId: string
|
||||
role: StreamRoles
|
||||
}) => {
|
||||
const { streamId, activityUserId, targetUserId, role } = params
|
||||
await saveActivity({
|
||||
streamId,
|
||||
resourceType: StreamResourceTypes.Stream,
|
||||
resourceId: streamId,
|
||||
actionType: StreamActionTypes.Stream.PermissionsAdd,
|
||||
userId: activityUserId,
|
||||
info: { targetUser: targetUserId, role },
|
||||
message: `Permission granted to user ${targetUserId} (${role})`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Save "stream permissions revoked for user" activity item
|
||||
*/
|
||||
const addStreamPermissionsRevokedActivityFactory =
|
||||
({ saveActivity }: { saveActivity: SaveStreamActivity }) =>
|
||||
async (params: {
|
||||
streamId: string
|
||||
activityUserId: string
|
||||
removedUserId: string
|
||||
stream: StreamRecord
|
||||
}) => {
|
||||
const { streamId, activityUserId, removedUserId } = params
|
||||
const isVoluntaryLeave = activityUserId === removedUserId
|
||||
|
||||
await saveActivity({
|
||||
streamId,
|
||||
resourceType: StreamResourceTypes.Stream,
|
||||
resourceId: streamId,
|
||||
actionType: StreamActionTypes.Stream.PermissionsRemove,
|
||||
userId: activityUserId,
|
||||
info: { targetUser: removedUserId },
|
||||
message: isVoluntaryLeave
|
||||
? `User ${removedUserId} left the stream`
|
||||
: `Permission revoked for user ${removedUserId}`
|
||||
})
|
||||
}
|
||||
|
||||
export const reportStreamActivityFactory =
|
||||
(deps: { eventListen: EventBusListen; saveActivity: SaveStreamActivity }) => () => {
|
||||
(deps: {
|
||||
eventListen: EventBusListen
|
||||
saveActivity: SaveActivity
|
||||
saveStreamActivity: SaveStreamActivity
|
||||
}) =>
|
||||
() => {
|
||||
const addProjectPermissionsAddedActivity =
|
||||
addProjectPermissionsAddedActivityFactory(deps)
|
||||
const addProjectPermissionsRevokedActivity =
|
||||
addProjectPermissionsRevokedActivityFactory(deps)
|
||||
const addStreamCreatedActivity = addStreamCreatedActivityFactory(deps)
|
||||
const addStreamUpdatedActivity = addStreamUpdatedActivityFactory(deps)
|
||||
const addStreamDeletedActivity = addStreamDeletedActivityFactory(deps)
|
||||
const addStreamClonedActivity = addStreamClonedActivityFactory(deps)
|
||||
const addStreamPermissionsAddedActivity =
|
||||
addStreamPermissionsAddedActivityFactory(deps)
|
||||
const addStreamPermissionsRevokedActivity =
|
||||
addStreamPermissionsRevokedActivityFactory(deps)
|
||||
|
||||
const quitters = [
|
||||
deps.eventListen(
|
||||
ProjectEvents.PermissionsAdded,
|
||||
addProjectPermissionsAddedActivity
|
||||
),
|
||||
deps.eventListen(
|
||||
ProjectEvents.PermissionsRevoked,
|
||||
addProjectPermissionsRevokedActivity
|
||||
),
|
||||
deps.eventListen(ProjectEvents.Created, async ({ payload }) => {
|
||||
await addStreamCreatedActivity({
|
||||
stream: payload.project,
|
||||
@@ -194,22 +206,6 @@ export const reportStreamActivityFactory =
|
||||
newStream: payload.newProject,
|
||||
clonerId: payload.clonerId
|
||||
})
|
||||
}),
|
||||
deps.eventListen(ProjectEvents.PermissionsAdded, async ({ payload }) => {
|
||||
await addStreamPermissionsAddedActivity({
|
||||
streamId: payload.project.id,
|
||||
activityUserId: payload.activityUserId,
|
||||
targetUserId: payload.targetUserId,
|
||||
role: payload.role
|
||||
})
|
||||
}),
|
||||
deps.eventListen(ProjectEvents.PermissionsRevoked, async ({ payload }) => {
|
||||
await addStreamPermissionsRevokedActivity({
|
||||
streamId: payload.project.id,
|
||||
activityUserId: payload.activityUserId,
|
||||
removedUserId: payload.removedUserId,
|
||||
stream: payload.project
|
||||
})
|
||||
})
|
||||
]
|
||||
|
||||
|
||||
@@ -9,11 +9,11 @@ import { EventBusListen, EventPayload } from '@/modules/shared/services/eventBus
|
||||
import { UserEvents } from '@/modules/core/domain/users/events'
|
||||
|
||||
const addUserCreatedActivityFactory =
|
||||
({ saveActivity }: { saveActivity: SaveStreamActivity }) =>
|
||||
({ saveStreamActivity }: { saveStreamActivity: SaveStreamActivity }) =>
|
||||
async (payload: EventPayload<typeof UserEvents.Created>) => {
|
||||
const { user } = payload.payload
|
||||
|
||||
await saveActivity({
|
||||
await saveStreamActivity({
|
||||
streamId: null,
|
||||
resourceType: StreamResourceTypes.User,
|
||||
resourceId: user.id,
|
||||
@@ -25,7 +25,7 @@ const addUserCreatedActivityFactory =
|
||||
}
|
||||
|
||||
const addUserUpdatedActivityFactory =
|
||||
({ saveActivity }: { saveActivity: SaveStreamActivity }) =>
|
||||
({ saveStreamActivity }: { saveStreamActivity: SaveStreamActivity }) =>
|
||||
async (params: {
|
||||
oldUser: UserRecord
|
||||
update: UserUpdateInput
|
||||
@@ -33,7 +33,7 @@ const addUserUpdatedActivityFactory =
|
||||
}) => {
|
||||
const { oldUser, update, updaterId } = params
|
||||
|
||||
await saveActivity({
|
||||
await saveStreamActivity({
|
||||
streamId: null,
|
||||
resourceType: StreamResourceTypes.User,
|
||||
resourceId: oldUser.id,
|
||||
@@ -45,11 +45,11 @@ const addUserUpdatedActivityFactory =
|
||||
}
|
||||
|
||||
const addUserDeletedActivityFactory =
|
||||
(deps: { saveActivity: SaveStreamActivity }) =>
|
||||
(deps: { saveStreamActivity: SaveStreamActivity }) =>
|
||||
async (params: { targetUserId: string; invokerUserId: string }) => {
|
||||
const { targetUserId, invokerUserId } = params
|
||||
|
||||
await deps.saveActivity({
|
||||
await deps.saveStreamActivity({
|
||||
streamId: null,
|
||||
resourceType: 'user',
|
||||
resourceId: targetUserId,
|
||||
@@ -61,7 +61,8 @@ const addUserDeletedActivityFactory =
|
||||
}
|
||||
|
||||
export const reportUserActivityFactory =
|
||||
(deps: { eventListen: EventBusListen; saveActivity: SaveStreamActivity }) => () => {
|
||||
(deps: { eventListen: EventBusListen; saveStreamActivity: SaveStreamActivity }) =>
|
||||
() => {
|
||||
const addUserDeletedActivity = addUserDeletedActivityFactory(deps)
|
||||
const addUserUpdatedActivity = addUserUpdatedActivityFactory(deps)
|
||||
const addUserCreatedActivity = addUserCreatedActivityFactory(deps)
|
||||
|
||||
@@ -1,18 +1,4 @@
|
||||
import { Nullable } from '@/modules/shared/helpers/typeHelper'
|
||||
import { ResourceEventsToPayloadMap } from '@/modules/activitystream/domain/types'
|
||||
|
||||
export interface Activity<
|
||||
T extends keyof ResourceEventsToPayloadMap,
|
||||
R extends keyof ResourceEventsToPayloadMap[T]
|
||||
> {
|
||||
id: string
|
||||
contextResourceId: string
|
||||
contextResourceType: T
|
||||
eventType: R
|
||||
userId: string | null
|
||||
payload: ResourceEventsToPayloadMap[T][R]
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
export type StreamActivityRecord = {
|
||||
streamId: Nullable<string>
|
||||
|
||||
@@ -44,45 +44,47 @@ const initializeEventListeners = ({
|
||||
eventBus: EventBus
|
||||
db: Knex
|
||||
}) => {
|
||||
const saveActivity = saveActivityFactory({ db })
|
||||
const saveStreamActivity = saveStreamActivityFactory({ db })
|
||||
const reportUserActivity = reportUserActivityFactory({
|
||||
eventListen: eventBus.listen,
|
||||
saveActivity: saveStreamActivity
|
||||
saveStreamActivity
|
||||
})
|
||||
const reportAccessRequestActivity = reportAccessRequestActivityFactory({
|
||||
eventListen: eventBus.listen,
|
||||
saveActivity: saveStreamActivity
|
||||
saveStreamActivity
|
||||
})
|
||||
const reportBranchActivity = reportBranchActivityFactory({
|
||||
eventListen: eventBus.listen,
|
||||
saveActivity: saveStreamActivity
|
||||
saveStreamActivity
|
||||
})
|
||||
const reportCommitActivity = reportCommitActivityFactory({
|
||||
eventListen: eventBus.listen,
|
||||
saveActivity: saveStreamActivity
|
||||
saveStreamActivity
|
||||
})
|
||||
const reportCommentActivity = reportCommentActivityFactory({
|
||||
eventListen: eventBus.listen,
|
||||
saveActivity: saveStreamActivity
|
||||
saveStreamActivity
|
||||
})
|
||||
const reportStreamInviteActivity = reportStreamInviteActivityFactory({
|
||||
eventListen: eventBus.listen,
|
||||
saveActivity: saveStreamActivity,
|
||||
saveStreamActivity,
|
||||
getProjectInviteProject: getProjectInviteProjectFactory({
|
||||
getStream: getStreamFactory({ db })
|
||||
})
|
||||
})
|
||||
const reportStreamActivity = reportStreamActivityFactory({
|
||||
eventListen: eventBus.listen,
|
||||
saveActivity: saveStreamActivity
|
||||
saveActivity,
|
||||
saveStreamActivity
|
||||
})
|
||||
const reportGatekeeperActivity = reportGatekeeperActivityFactory({
|
||||
eventListen: eventBus.listen,
|
||||
saveActivity: saveActivityFactory({ db })
|
||||
saveActivity
|
||||
})
|
||||
const reportWorkspaceActivity = reportWorkspaceActivityFactory({
|
||||
eventListen: eventBus.listen,
|
||||
saveActivity: saveActivityFactory({ db })
|
||||
saveActivity
|
||||
})
|
||||
|
||||
const quitCbs = [
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
GetActiveUserStreams,
|
||||
GetActivities,
|
||||
GetActivityCountByResourceId,
|
||||
GetActivityCountByStreamId,
|
||||
GetActivityCountByUserId,
|
||||
@@ -15,7 +16,11 @@ import {
|
||||
StreamActivityRecord,
|
||||
StreamScopeActivity
|
||||
} from '@/modules/activitystream/helpers/types'
|
||||
import { Activity, StreamAcl, StreamActivity } from '@/modules/core/dbSchema'
|
||||
import {
|
||||
Activity as ActivityModel,
|
||||
StreamAcl,
|
||||
StreamActivity
|
||||
} from '@/modules/core/dbSchema'
|
||||
import { Roles } from '@/modules/core/helpers/mainConstants'
|
||||
import { StreamAclRecord } from '@/modules/core/helpers/types'
|
||||
import {
|
||||
@@ -29,6 +34,7 @@ import { getUserFactory } from '@/modules/core/repositories/users'
|
||||
import { getServerInfoFactory } from '@/modules/core/repositories/server'
|
||||
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { Activity } from '@/modules/activitystream/domain/types'
|
||||
|
||||
const tables = {
|
||||
streamActivity: <T extends object = StreamActivityRecord>(db: Knex) =>
|
||||
@@ -36,7 +42,7 @@ const tables = {
|
||||
streamAcl: (db: Knex) => db<StreamAclRecord>(StreamAcl.name)
|
||||
}
|
||||
|
||||
export const getActivityFactory =
|
||||
export const geUserStreamActivityFactory =
|
||||
({ db }: { db: Knex }) =>
|
||||
async (
|
||||
streamId: string,
|
||||
@@ -280,8 +286,40 @@ export const saveActivityFactory =
|
||||
const createdAt = new Date()
|
||||
|
||||
const [result] = await db<typeof activity & { id: string; createdAt: Date }>(
|
||||
Activity.name
|
||||
ActivityModel.name
|
||||
).insert({ ...activity, id, createdAt }, '*')
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export const getActivitiesFactory =
|
||||
({ db }: { db: Knex }): GetActivities =>
|
||||
async (filters = {}): Promise<Activity[]> => {
|
||||
const { workspaceId, projectId, eventType, userId } = filters
|
||||
|
||||
const q = db<Activity>(ActivityModel.name).select('*')
|
||||
|
||||
if (projectId) {
|
||||
q.where(ActivityModel.col.contextResourceId, projectId).andWhere(
|
||||
ActivityModel.col.contextResourceType,
|
||||
'project'
|
||||
)
|
||||
}
|
||||
|
||||
if (workspaceId) {
|
||||
q.where(ActivityModel.col.contextResourceId, workspaceId).andWhere(
|
||||
ActivityModel.col.contextResourceType,
|
||||
'workspace'
|
||||
)
|
||||
}
|
||||
|
||||
if (eventType) {
|
||||
q.andWhere(ActivityModel.col.eventType, eventType)
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
q.andWhere(ActivityModel.col.userId, userId)
|
||||
}
|
||||
|
||||
return await q
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
import {
|
||||
CreateActivitySummary,
|
||||
GetActiveUserStreams,
|
||||
GetActivity
|
||||
GetUserStreamActivity
|
||||
} from '@/modules/activitystream/domain/operations'
|
||||
import { GetStream } from '@/modules/core/domain/streams/operations'
|
||||
import { GetUser } from '@/modules/core/domain/users/operations'
|
||||
@@ -17,7 +17,7 @@ export const createActivitySummaryFactory =
|
||||
getUser
|
||||
}: {
|
||||
getStream: GetStream
|
||||
getActivity: GetActivity
|
||||
getActivity: GetUserStreamActivity
|
||||
getUser: GetUser
|
||||
}): CreateActivitySummary =>
|
||||
async ({
|
||||
|
||||
@@ -12,7 +12,10 @@ import {
|
||||
addOrUpdateStreamCollaboratorFactory
|
||||
} from '@/modules/core/services/streams/access'
|
||||
import { authorizeResolver } from '@/modules/shared'
|
||||
import { grantStreamPermissionsFactory } from '@/modules/core/repositories/streams'
|
||||
import {
|
||||
getStreamRolesFactory,
|
||||
grantStreamPermissionsFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import {
|
||||
getUserFactory,
|
||||
storeUserFactory,
|
||||
@@ -49,6 +52,7 @@ import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import type http from 'node:http'
|
||||
import { BasicTestStream, createTestStream } from '@/test/speckle-helpers/streamHelper'
|
||||
import { BasicTestBranch, createTestBranch } from '@/test/speckle-helpers/branchHelper'
|
||||
import { getActivitiesFactory } from '@/modules/activitystream/repositories/index'
|
||||
|
||||
const getUser = getUserFactory({ db })
|
||||
const getUserActivity = getUserActivityFactory({ db })
|
||||
@@ -57,6 +61,7 @@ const addOrUpdateStreamCollaborator = addOrUpdateStreamCollaboratorFactory({
|
||||
validateStreamAccess,
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
|
||||
@@ -100,6 +105,8 @@ const createObject = createObjectFactory({
|
||||
storeSingleObjectIfNotFoundFactory: storeSingleObjectIfNotFoundFactory({ db })
|
||||
})
|
||||
|
||||
const getActivities = getActivitiesFactory({ db })
|
||||
|
||||
let server: http.Server
|
||||
let sendRequest: Awaited<ReturnType<typeof initializeTestServer>>['sendRequest']
|
||||
|
||||
@@ -265,11 +272,37 @@ describe('Activity @activity', () => {
|
||||
expect(activityC.length).to.equal(3)
|
||||
expect(activityC[0].actionType).to.equal('commit_create')
|
||||
|
||||
const { items: activityI } = await getUserActivity({ userId: userIz.id })
|
||||
const activityI = await getUserActivity({ userId: userIz.id })
|
||||
|
||||
// iz1 to iz4 + user created + user added as collaborator
|
||||
expect(activityI.length).to.equal(6)
|
||||
expect(activityI[0].actionType).to.equal('stream_permissions_add')
|
||||
expect(activityI.items.length).to.equal(4)
|
||||
expect(activityI).to.nested.include({
|
||||
'items[0].actionType': 'commit_create',
|
||||
'items[1].actionType': 'branch_create',
|
||||
'items[2].actionType': 'stream_create',
|
||||
'items[3].actionType': 'user_create'
|
||||
})
|
||||
|
||||
const activity = { items: await getActivities({ userId: userIz.id }) }
|
||||
|
||||
expect(activity.items.length).to.equal(3)
|
||||
expect(activity).to.nested.include({
|
||||
'items[0].eventType': 'project_role_updated',
|
||||
'items[0].payload.new': 'stream:owner',
|
||||
'items[0].payload.old': null,
|
||||
'items[0].userId': userIz.id, // created branch
|
||||
|
||||
'items[1].eventType': 'project_role_updated',
|
||||
'items[1].payload.new': 'stream:reviewer',
|
||||
'items[1].payload.old': null,
|
||||
'items[1].payload.userId': userCr.id,
|
||||
'items[1].userId': userIz.id, // added user
|
||||
|
||||
'items[2].eventType': 'project_role_updated',
|
||||
'items[2].payload.new': 'stream:contributor',
|
||||
'items[2].payload.old': 'stream:reviewer',
|
||||
'items[2].payload.userId': userCr.id,
|
||||
'items[2].userId': userIz.id // made him a contibutor
|
||||
})
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
@@ -283,9 +316,9 @@ describe('Activity @activity', () => {
|
||||
expect(noErrors(res))
|
||||
const activity = res.body.data.activeUser.activity
|
||||
|
||||
expect(activity.items.length).to.equal(6)
|
||||
expect(activity.totalCount).to.equal(6)
|
||||
expect(activity.items[0].actionType).to.equal('stream_permissions_add')
|
||||
expect(activity.items.length).to.equal(4)
|
||||
expect(activity.totalCount).to.equal(4)
|
||||
expect(activity.items[0].actionType).to.equal('commit_create')
|
||||
expect(activity.items[activity.totalCount - 1].actionType).to.equal('user_create')
|
||||
})
|
||||
|
||||
@@ -303,8 +336,8 @@ describe('Activity @activity', () => {
|
||||
query: `query {otherUser(id:"${userCr.id}") { name timeline { totalCount items {id streamId resourceType resourceId actionType userId message time}}} }`
|
||||
})
|
||||
expect(noErrors(res))
|
||||
expect(res.body.data.otherUser.timeline.items.length).to.equal(7) // sum of all actions in before hook
|
||||
expect(res.body.data.otherUser.timeline.totalCount).to.equal(7)
|
||||
expect(res.body.data.otherUser.timeline.items.length).to.equal(5) // sum of all actions in before hook
|
||||
expect(res.body.data.otherUser.timeline.totalCount).to.equal(5)
|
||||
})
|
||||
|
||||
it("Should get a stream's activity", async () => {
|
||||
@@ -313,8 +346,8 @@ describe('Activity @activity', () => {
|
||||
})
|
||||
expect(noErrors(res))
|
||||
const activity = res.body.data.stream.activity
|
||||
expect(activity.items.length).to.equal(5)
|
||||
expect(activity.totalCount).to.equal(5)
|
||||
expect(activity.items.length).to.equal(3)
|
||||
expect(activity.totalCount).to.equal(3)
|
||||
expect(activity.items[activity.totalCount - 1].actionType).to.equal('stream_create')
|
||||
})
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
NotificationTypeMessageMap
|
||||
} from '@/modules/notifications/helpers/types'
|
||||
import {
|
||||
getActivityFactory,
|
||||
geUserStreamActivityFactory,
|
||||
saveStreamActivityFactory
|
||||
} from '@/modules/activitystream/repositories'
|
||||
import { db } from '@/db/knex'
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
createStreamFactory,
|
||||
deleteStreamFactory,
|
||||
getStreamFactory,
|
||||
getStreamRolesFactory,
|
||||
grantStreamPermissionsFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import {
|
||||
@@ -81,7 +82,7 @@ const getStream = getStreamFactory({ db })
|
||||
const saveActivity = saveStreamActivityFactory({ db })
|
||||
const createActivitySummary = createActivitySummaryFactory({
|
||||
getStream,
|
||||
getActivity: getActivityFactory({ db }),
|
||||
getActivity: geUserStreamActivityFactory({ db }),
|
||||
getUser
|
||||
})
|
||||
|
||||
@@ -97,6 +98,7 @@ const buildFinalizeProjectInvite = () =>
|
||||
validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }),
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -74,6 +74,13 @@ export type PersonalApiTokenRecord = {
|
||||
tokenId: string
|
||||
}
|
||||
|
||||
export type EmbedApiTokenRecord = {
|
||||
projectId: string
|
||||
tokenId: string
|
||||
userId: string
|
||||
resourceIdString: string
|
||||
}
|
||||
|
||||
export type TokenScopeRecord = {
|
||||
tokenId: string
|
||||
scopeName: ServerScope
|
||||
|
||||
@@ -22,7 +22,8 @@ import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/se
|
||||
import {
|
||||
getStreamFactory,
|
||||
createStreamFactory,
|
||||
grantStreamPermissionsFactory
|
||||
grantStreamPermissionsFactory,
|
||||
getStreamRolesFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents'
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
@@ -95,6 +96,7 @@ const buildFinalizeProjectInvite = () =>
|
||||
validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }),
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -48,7 +48,10 @@ import {
|
||||
validateStreamAccessFactory
|
||||
} from '@/modules/core/services/streams/access'
|
||||
import { authorizeResolver } from '@/modules/shared'
|
||||
import { grantStreamPermissionsFactory } from '@/modules/core/repositories/streams'
|
||||
import {
|
||||
getStreamRolesFactory,
|
||||
grantStreamPermissionsFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import { getUserFactory } from '@/modules/core/repositories/users'
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { AutomationEvents } from '@/modules/automate/domain/events'
|
||||
@@ -67,6 +70,7 @@ const addOrUpdateStreamCollaborator = addOrUpdateStreamCollaboratorFactory({
|
||||
validateStreamAccess,
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
getS3AccessKey,
|
||||
getS3BucketName,
|
||||
getS3Endpoint,
|
||||
getS3PublicEndpoint,
|
||||
getS3Region,
|
||||
getS3SecretKey
|
||||
} from '@/modules/shared/helpers/envHelper'
|
||||
@@ -51,9 +52,17 @@ export const getObjectStorage = (params: GetObjectStorageParams): ObjectStorage
|
||||
}
|
||||
|
||||
let mainObjectStorage: Optional<ObjectStorage> = undefined
|
||||
let publicMainObjectStorage: Optional<ObjectStorage> = undefined
|
||||
|
||||
/**
|
||||
* Get main object storage client
|
||||
*
|
||||
* This is used for connecting the server to the S3 host. Where the S3 host is
|
||||
* on the same private network as the server (e.g. in a Docker network),
|
||||
* the S3_ENDPOINT can use the private IP or DNS name of the S3 host.
|
||||
*
|
||||
* S3_PUBLIC_ENDPOINT can be used to connect to the S3 host via the
|
||||
* public internet (or localhost network if running locally or testing).
|
||||
*/
|
||||
export const getMainObjectStorage = (): ObjectStorage => {
|
||||
if (mainObjectStorage) return mainObjectStorage
|
||||
@@ -72,6 +81,37 @@ export const getMainObjectStorage = (): ObjectStorage => {
|
||||
return mainObjectStorage
|
||||
}
|
||||
|
||||
/**
|
||||
* (Optional) Used to connect to the S3 host via the public endpoint.
|
||||
* This is useful for clients that need to access the S3 bucket directly, e.g
|
||||
* during testing or when the S3 host is not on the same private network as the server.
|
||||
*
|
||||
* If `S3_PUBLIC_ENDPOINT` is not set, it will return the same object storage
|
||||
* as `getMainObjectStorage`.
|
||||
*/
|
||||
export const getPublicMainObjectStorage = (): ObjectStorage => {
|
||||
if (publicMainObjectStorage) return publicMainObjectStorage
|
||||
|
||||
const endpoint = getS3PublicEndpoint()
|
||||
if (!endpoint) {
|
||||
// If no public endpoint is set, return the main object storage
|
||||
return getMainObjectStorage()
|
||||
}
|
||||
|
||||
const mainParams: GetObjectStorageParams = {
|
||||
credentials: {
|
||||
accessKeyId: getS3AccessKey(),
|
||||
secretAccessKey: getS3SecretKey()
|
||||
},
|
||||
endpoint,
|
||||
region: getS3Region(),
|
||||
bucket: getS3BucketName()
|
||||
}
|
||||
|
||||
publicMainObjectStorage = getObjectStorage(mainParams)
|
||||
return publicMainObjectStorage
|
||||
}
|
||||
|
||||
export const getSignedUrlFactory = (deps: {
|
||||
objectStorage: ObjectStorage
|
||||
}): GetSignedUrl => {
|
||||
|
||||
@@ -124,7 +124,9 @@ export const blobStorageRouterFactory = (): Router => {
|
||||
|
||||
const getBlobMetadata = getBlobMetadataFactory({ db: projectDb })
|
||||
const getFileStream = getFileStreamFactory({ getBlobMetadata })
|
||||
const getObjectStream = getObjectStreamFactory({ storage: projectStorage })
|
||||
const getObjectStream = getObjectStreamFactory({
|
||||
storage: projectStorage.private
|
||||
})
|
||||
|
||||
const { fileName } = await getBlobMetadata({
|
||||
streamId: req.params.streamId,
|
||||
@@ -160,7 +162,7 @@ export const blobStorageRouterFactory = (): Router => {
|
||||
])
|
||||
|
||||
const getBlobMetadata = getBlobMetadataFactory({ db: projectDb })
|
||||
const deleteObject = deleteObjectFactory({ storage: projectStorage })
|
||||
const deleteObject = deleteObjectFactory({ storage: projectStorage.private })
|
||||
const deleteBlob = fullyDeleteBlobFactory({
|
||||
getBlobMetadata,
|
||||
deleteBlob: deleteBlobFactory({ db: projectDb }),
|
||||
|
||||
@@ -45,7 +45,7 @@ export const processNewFileStreamFactory = (): NewFileStreamProcessor => {
|
||||
getProjectObjectStorage({ projectId: streamId })
|
||||
])
|
||||
|
||||
const storeFileStream = storeFileStreamFactory({ storage: projectStorage })
|
||||
const storeFileStream = storeFileStreamFactory({ storage: projectStorage.private })
|
||||
const updateBlob = updateBlobFactory({ db: projectDb })
|
||||
const getBlobMetadata = getBlobMetadataFactory({ db: projectDb })
|
||||
|
||||
@@ -66,9 +66,9 @@ export const processNewFileStreamFactory = (): NewFileStreamProcessor => {
|
||||
})
|
||||
|
||||
const getObjectAttributes = getObjectAttributesFactory({
|
||||
storage: projectStorage
|
||||
storage: projectStorage.private
|
||||
})
|
||||
const deleteObject = deleteObjectFactory({ storage: projectStorage })
|
||||
const deleteObject = deleteObjectFactory({ storage: projectStorage.private })
|
||||
|
||||
busboy.on(
|
||||
'file',
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
import {
|
||||
getStreamFactory,
|
||||
createStreamFactory,
|
||||
grantStreamPermissionsFactory
|
||||
grantStreamPermissionsFactory,
|
||||
getStreamRolesFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import { db } from '@/db/knex'
|
||||
import {
|
||||
@@ -83,6 +84,7 @@ const buildFinalizeProjectInvite = () =>
|
||||
validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }),
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
}),
|
||||
|
||||
+6
-3
@@ -23,7 +23,10 @@ import { BasicTestStream, createTestStream } from '@/test/speckle-helpers/stream
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { BasicTestUser, createTestUser } from '@/test/authHelper'
|
||||
import { storeFileStreamFactory } from '@/modules/blobstorage/repositories/blobs'
|
||||
import { getMainObjectStorage } from '@/modules/blobstorage/clients/objectStorage'
|
||||
import {
|
||||
getMainObjectStorage,
|
||||
getPublicMainObjectStorage
|
||||
} from '@/modules/blobstorage/clients/objectStorage'
|
||||
import { expect } from 'chai'
|
||||
import { UploadFileStream } from '@/modules/blobstorage/domain/operations'
|
||||
import { BlobStorageItem } from '@/modules/blobstorage/domain/types'
|
||||
@@ -43,8 +46,8 @@ const buildUploadFileStream = async (params: { streamId: string | null }) => {
|
||||
|
||||
const storage = streamId
|
||||
? await getProjectObjectStorage({ projectId: streamId })
|
||||
: getMainObjectStorage()
|
||||
const storeFileStream = storeFileStreamFactory({ storage })
|
||||
: { private: getMainObjectStorage(), public: getPublicMainObjectStorage() }
|
||||
const storeFileStream = storeFileStreamFactory({ storage: storage.public })
|
||||
const uploadFileStream = uploadFileStreamFactory({
|
||||
upsertBlob,
|
||||
updateBlob,
|
||||
|
||||
@@ -52,7 +52,7 @@ const { FF_LARGE_FILE_IMPORTS_ENABLED } = getFeatureFlags()
|
||||
}
|
||||
|
||||
let projectDb: Knex
|
||||
let projectStorage: ObjectStorage
|
||||
let projectStorage: { private: ObjectStorage; public: ObjectStorage }
|
||||
let getBlobMetadata: GetBlobMetadata
|
||||
|
||||
before(async () => {
|
||||
@@ -77,7 +77,7 @@ const { FF_LARGE_FILE_IMPORTS_ENABLED } = getFeatureFlags()
|
||||
before(() => {
|
||||
SUT = generatePresignedUrlFactory({
|
||||
getSignedUrl: getSignedUrlFactory({
|
||||
objectStorage: projectStorage
|
||||
objectStorage: projectStorage.public
|
||||
}),
|
||||
upsertBlob: upsertBlobFactory({
|
||||
db: projectDb
|
||||
@@ -117,7 +117,7 @@ const { FF_LARGE_FILE_IMPORTS_ENABLED } = getFeatureFlags()
|
||||
before(() => {
|
||||
generatePresignedUrl = generatePresignedUrlFactory({
|
||||
getSignedUrl: getSignedUrlFactory({
|
||||
objectStorage: projectStorage
|
||||
objectStorage: projectStorage.public
|
||||
}),
|
||||
upsertBlob: upsertBlobFactory({
|
||||
db: projectDb
|
||||
@@ -126,7 +126,7 @@ const { FF_LARGE_FILE_IMPORTS_ENABLED } = getFeatureFlags()
|
||||
SUT = registerCompletedUploadFactory({
|
||||
getBlob: getBlobFactory({ db: projectDb }),
|
||||
getBlobMetadata: getBlobMetadataFromStorage({
|
||||
objectStorage: projectStorage
|
||||
objectStorage: projectStorage.public
|
||||
}),
|
||||
updateBlob: updateBlobFactory({
|
||||
db: projectDb
|
||||
|
||||
@@ -56,7 +56,8 @@ import {
|
||||
getStreamFactory,
|
||||
createStreamFactory,
|
||||
markCommitStreamUpdatedFactory,
|
||||
grantStreamPermissionsFactory
|
||||
grantStreamPermissionsFactory,
|
||||
getStreamRolesFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import {
|
||||
createCommitByBranchIdFactory,
|
||||
@@ -250,6 +251,7 @@ const buildFinalizeProjectInvite = () =>
|
||||
validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }),
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -406,6 +406,13 @@ export const PersonalApiTokens = buildTableHelper('personal_api_tokens', [
|
||||
'userId'
|
||||
])
|
||||
|
||||
export const EmbedApiTokens = buildTableHelper('embed_api_tokens', [
|
||||
'tokenId',
|
||||
'projectId',
|
||||
'userId',
|
||||
'resourceIdString'
|
||||
])
|
||||
|
||||
export const UserServerAppTokens = buildTableHelper('user_server_app_tokens', [
|
||||
'appId',
|
||||
'userId',
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
StreamUpdateInput
|
||||
} from '@/modules/core/graph/generated/graphql'
|
||||
import { ServerInviteRecord } from '@/modules/serverinvites/domain/types'
|
||||
import { StreamRoles } from '@speckle/shared'
|
||||
import { Nullable, StreamRoles } from '@speckle/shared'
|
||||
|
||||
export const projectEventsNamespace = 'projects' as const
|
||||
|
||||
@@ -54,10 +54,12 @@ export type ProjectEventsPayloads = {
|
||||
targetUserId: string
|
||||
role: StreamRoles
|
||||
project: Project
|
||||
previousRole: Nullable<StreamRoles>
|
||||
}
|
||||
[ProjectEvents.PermissionsRevoked]: {
|
||||
activityUserId: string
|
||||
removedUserId: string
|
||||
project: Project
|
||||
role: StreamRoles
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Project } from '@/modules/core/domain/streams/types'
|
||||
import { Project, StreamWithOptionalRole } from '@/modules/core/domain/streams/types'
|
||||
import { StreamAclRecord, StreamRecord } from '@/modules/core/helpers/types'
|
||||
import { MaybeNullOrUndefined, StreamRoles } from '@speckle/shared'
|
||||
|
||||
@@ -38,7 +38,7 @@ export type DeleteProjectRole = (args: {
|
||||
|
||||
export type DeleteProject = (args: { projectId: string }) => Promise<void>
|
||||
|
||||
export type GetRolesByUserId = ({
|
||||
export type GetUserProjectRoles = ({
|
||||
userId,
|
||||
workspaceId
|
||||
}: {
|
||||
@@ -73,3 +73,15 @@ export type WaitForRegionProject = (params: {
|
||||
regionKey: string
|
||||
maxAttempts?: number
|
||||
}) => Promise<void>
|
||||
|
||||
export type QueryAllProjects = (
|
||||
args:
|
||||
| {
|
||||
userId: string
|
||||
workspaceId?: string
|
||||
}
|
||||
| {
|
||||
userId?: string
|
||||
workspaceId: string
|
||||
}
|
||||
) => AsyncGenerator<StreamWithOptionalRole[], void, unknown>
|
||||
|
||||
@@ -258,7 +258,7 @@ export type GetStreamRoles = (
|
||||
userId: string,
|
||||
streamIds: string[]
|
||||
) => Promise<{
|
||||
[streamId: string]: Nullable<string>
|
||||
[streamId: string]: Nullable<StreamRoles>
|
||||
}>
|
||||
|
||||
export type GetUserStreamCounts = (params: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
ApiToken,
|
||||
EmbedApiToken,
|
||||
PersonalApiToken,
|
||||
TokenResourceAccessDefinition,
|
||||
TokenResourceIdentifierType,
|
||||
@@ -31,6 +32,8 @@ export type StorePersonalApiToken = (
|
||||
token: PersonalApiToken
|
||||
) => Promise<PersonalApiToken>
|
||||
|
||||
export type StoreEmbedApiToken = (token: EmbedApiToken) => Promise<EmbedApiToken>
|
||||
|
||||
export type GetUserPersonalAccessTokens = (userId: string) => Promise<
|
||||
{
|
||||
id: string
|
||||
@@ -43,10 +46,33 @@ export type GetUserPersonalAccessTokens = (userId: string) => Promise<
|
||||
}[]
|
||||
>
|
||||
|
||||
export type ListProjectEmbedTokens = (args: {
|
||||
projectId: string
|
||||
filter?: {
|
||||
limit?: number
|
||||
createdBefore?: string | null
|
||||
}
|
||||
}) => Promise<
|
||||
(EmbedApiToken & {
|
||||
createdAt: Date
|
||||
lastUsed: Date
|
||||
lifespan: number | bigint
|
||||
})[]
|
||||
>
|
||||
|
||||
export type CountProjectEmbedTokens = (args: { projectId: string }) => Promise<number>
|
||||
|
||||
export type RevokeTokenById = (tokenId: string) => Promise<boolean>
|
||||
|
||||
export type RevokeUserTokenById = (tokenId: string, userId: string) => Promise<boolean>
|
||||
|
||||
export type RevokeEmbedTokenById = (args: {
|
||||
tokenId: string
|
||||
projectId: string
|
||||
}) => Promise<boolean>
|
||||
|
||||
export type RevokeProjectEmbedTokens = (args: { projectId: string }) => Promise<void>
|
||||
|
||||
export type GetApiTokenById = (tokenId: string) => Promise<Optional<ApiToken>>
|
||||
|
||||
export type GetTokenScopesById = (tokenId: string) => Promise<TokenScope[]>
|
||||
@@ -86,4 +112,30 @@ export type CreateAndStorePersonalAccessToken = (
|
||||
lifespan?: number | bigint
|
||||
) => Promise<string>
|
||||
|
||||
export type CreateAndStoreEmbedToken = (args: {
|
||||
projectId: string
|
||||
userId: string
|
||||
/**
|
||||
* The models (and optional versions) included in the embed.
|
||||
* @example 'foo123,bar456@baz789'
|
||||
*/
|
||||
resourceIdString: string
|
||||
lifespan?: number | bigint
|
||||
}) => Promise<{
|
||||
token: string
|
||||
tokenMetadata: EmbedApiToken
|
||||
}>
|
||||
|
||||
export type GetPaginatedProjectEmbedTokens = (args: {
|
||||
projectId: string
|
||||
filter?: {
|
||||
limit?: number
|
||||
cursor?: string
|
||||
}
|
||||
}) => Promise<{
|
||||
items: EmbedApiToken[]
|
||||
totalCount: number
|
||||
cursor: string | null
|
||||
}>
|
||||
|
||||
export type ValidateToken = (tokenString: string) => Promise<TokenValidationResult>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
EmbedApiTokenRecord,
|
||||
PersonalApiTokenRecord,
|
||||
TokenScopeRecord,
|
||||
UserServerAppTokenRecord
|
||||
@@ -26,3 +27,5 @@ export type TokenResourceAccessDefinition = TokenResourceAccessRecord
|
||||
export type UserServerAppToken = UserServerAppTokenRecord
|
||||
|
||||
export type PersonalApiToken = PersonalApiTokenRecord
|
||||
|
||||
export type EmbedApiToken = EmbedApiTokenRecord
|
||||
|
||||
@@ -11,3 +11,9 @@ export class ProjectNotFoundError extends BaseError {
|
||||
static code = 'PROJECT_NOT_FOUND'
|
||||
static statusCode = 404
|
||||
}
|
||||
|
||||
export class ProjectQueryError extends BaseError {
|
||||
static defaultMessage = 'Unexpected error during query operation'
|
||||
static code = 'PROJECT_QUERY_ERROR'
|
||||
static statusCode = 500
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql';
|
||||
import { StreamGraphQLReturn, CommitGraphQLReturn, ProjectGraphQLReturn, ObjectGraphQLReturn, VersionGraphQLReturn, ServerInviteGraphQLReturnType, ModelGraphQLReturn, ModelsTreeItemGraphQLReturn, MutationsObjectGraphQLReturn, LimitedUserGraphQLReturn, UserGraphQLReturn, GraphQLEmptyReturn, StreamCollaboratorGraphQLReturn, ProjectCollaboratorGraphQLReturn, ServerInfoGraphQLReturn, BranchGraphQLReturn, UserMetaGraphQLReturn, ProjectPermissionChecksGraphQLReturn, ModelPermissionChecksGraphQLReturn, VersionPermissionChecksGraphQLReturn, RootPermissionChecksGraphQLReturn } from '@/modules/core/helpers/graphTypes';
|
||||
import { StreamGraphQLReturn, CommitGraphQLReturn, ProjectGraphQLReturn, ObjectGraphQLReturn, VersionGraphQLReturn, ServerInviteGraphQLReturnType, ModelGraphQLReturn, ModelsTreeItemGraphQLReturn, MutationsObjectGraphQLReturn, LimitedUserGraphQLReturn, UserGraphQLReturn, EmbedTokenGraphQLReturn, GraphQLEmptyReturn, StreamCollaboratorGraphQLReturn, ProjectCollaboratorGraphQLReturn, ServerInfoGraphQLReturn, BranchGraphQLReturn, UserMetaGraphQLReturn, ProjectPermissionChecksGraphQLReturn, ModelPermissionChecksGraphQLReturn, VersionPermissionChecksGraphQLReturn, RootPermissionChecksGraphQLReturn } from '@/modules/core/helpers/graphTypes';
|
||||
import { StreamAccessRequestGraphQLReturn, ProjectAccessRequestGraphQLReturn } from '@/modules/accessrequests/helpers/graphTypes';
|
||||
import { CommentReplyAuthorCollectionGraphQLReturn, CommentGraphQLReturn, CommentPermissionChecksGraphQLReturn } from '@/modules/comments/helpers/graphTypes';
|
||||
import { PendingStreamCollaboratorGraphQLReturn } from '@/modules/serverinvites/helpers/graphTypes';
|
||||
@@ -945,6 +945,12 @@ export type CreateCommentReplyInput = {
|
||||
threadId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type CreateEmbedTokenReturn = {
|
||||
__typename?: 'CreateEmbedTokenReturn';
|
||||
token: Scalars['String']['output'];
|
||||
tokenMetadata: EmbedToken;
|
||||
};
|
||||
|
||||
export type CreateModelInput = {
|
||||
description?: InputMaybe<Scalars['String']['input']>;
|
||||
name: Scalars['String']['input'];
|
||||
@@ -1023,6 +1029,32 @@ export type EmailVerificationRequestInput = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
/** A token used to enable an embedded viewer for a private project */
|
||||
export type EmbedToken = {
|
||||
__typename?: 'EmbedToken';
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
lastUsed: Scalars['DateTime']['output'];
|
||||
lifespan: Scalars['BigInt']['output'];
|
||||
projectId: Scalars['String']['output'];
|
||||
resourceIdString: Scalars['String']['output'];
|
||||
tokenId: Scalars['String']['output'];
|
||||
user?: Maybe<LimitedUser>;
|
||||
};
|
||||
|
||||
export type EmbedTokenCollection = {
|
||||
__typename?: 'EmbedTokenCollection';
|
||||
cursor?: Maybe<Scalars['String']['output']>;
|
||||
items: Array<EmbedToken>;
|
||||
totalCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type EmbedTokenCreateInput = {
|
||||
lifespan?: InputMaybe<Scalars['BigInt']['input']>;
|
||||
projectId: Scalars['String']['input'];
|
||||
/** The model(s) and version(s) string used in the embed url */
|
||||
resourceIdString: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type FileUpload = {
|
||||
__typename?: 'FileUpload';
|
||||
branchName: Scalars['String']['output'];
|
||||
@@ -2118,6 +2150,7 @@ export type Project = {
|
||||
description?: Maybe<Scalars['String']['output']>;
|
||||
/** Public project-level configuration for embedded viewer */
|
||||
embedOptions: ProjectEmbedOptions;
|
||||
embedTokens: EmbedTokenCollection;
|
||||
hasAccessToFeature: Scalars['Boolean']['output'];
|
||||
id: Scalars['ID']['output'];
|
||||
invitableCollaborators: WorkspaceCollaboratorCollection;
|
||||
@@ -2200,6 +2233,12 @@ export type ProjectCommentThreadsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type ProjectEmbedTokensArgs = {
|
||||
cursor?: InputMaybe<Scalars['String']['input']>;
|
||||
limit?: InputMaybe<Scalars['Int']['input']>;
|
||||
};
|
||||
|
||||
|
||||
export type ProjectHasAccessToFeatureArgs = {
|
||||
featureName: WorkspaceFeatureName;
|
||||
};
|
||||
@@ -2599,6 +2638,7 @@ export type ProjectMutations = {
|
||||
batchDelete: Scalars['Boolean']['output'];
|
||||
/** Create new project */
|
||||
create: Project;
|
||||
createEmbedToken: CreateEmbedTokenReturn;
|
||||
/**
|
||||
* Create onboarding/tutorial project. If one is already created for the active user, that
|
||||
* one will be returned instead.
|
||||
@@ -2610,6 +2650,8 @@ export type ProjectMutations = {
|
||||
invites: ProjectInviteMutations;
|
||||
/** Leave a project. Only possible if you're not the last remaining owner. */
|
||||
leave: Scalars['Boolean']['output'];
|
||||
revokeEmbedToken: Scalars['Boolean']['output'];
|
||||
revokeEmbedTokens: Scalars['Boolean']['output'];
|
||||
/** Updates an existing project */
|
||||
update: Project;
|
||||
/** Update role for a collaborator */
|
||||
@@ -2632,6 +2674,11 @@ export type ProjectMutationsCreateArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type ProjectMutationsCreateEmbedTokenArgs = {
|
||||
token: EmbedTokenCreateInput;
|
||||
};
|
||||
|
||||
|
||||
export type ProjectMutationsDeleteArgs = {
|
||||
id: Scalars['String']['input'];
|
||||
};
|
||||
@@ -2642,6 +2689,17 @@ export type ProjectMutationsLeaveArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type ProjectMutationsRevokeEmbedTokenArgs = {
|
||||
projectId: Scalars['String']['input'];
|
||||
token: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type ProjectMutationsRevokeEmbedTokensArgs = {
|
||||
projectId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type ProjectMutationsUpdateArgs = {
|
||||
update: ProjectUpdateInput;
|
||||
};
|
||||
@@ -2684,6 +2742,7 @@ export type ProjectPermissionChecks = {
|
||||
canBroadcastActivity: PermissionCheckResult;
|
||||
canCreateAutomation: PermissionCheckResult;
|
||||
canCreateComment: PermissionCheckResult;
|
||||
canCreateEmbedTokens: PermissionCheckResult;
|
||||
canCreateModel: PermissionCheckResult;
|
||||
canDelete: PermissionCheckResult;
|
||||
canInvite: PermissionCheckResult;
|
||||
@@ -2692,9 +2751,11 @@ export type ProjectPermissionChecks = {
|
||||
canMoveToWorkspace: PermissionCheckResult;
|
||||
canPublish: PermissionCheckResult;
|
||||
canRead: PermissionCheckResult;
|
||||
canReadEmbedTokens: PermissionCheckResult;
|
||||
canReadSettings: PermissionCheckResult;
|
||||
canReadWebhooks: PermissionCheckResult;
|
||||
canRequestRender: PermissionCheckResult;
|
||||
canRevokeEmbedTokens: PermissionCheckResult;
|
||||
canUpdate: PermissionCheckResult;
|
||||
canUpdateAllowPublicComments: PermissionCheckResult;
|
||||
};
|
||||
@@ -5429,6 +5490,7 @@ export type ResolversTypes = {
|
||||
CreateAutomateFunctionWithoutVersionInput: CreateAutomateFunctionWithoutVersionInput;
|
||||
CreateCommentInput: CreateCommentInput;
|
||||
CreateCommentReplyInput: CreateCommentReplyInput;
|
||||
CreateEmbedTokenReturn: ResolverTypeWrapper<Omit<CreateEmbedTokenReturn, 'tokenMetadata'> & { tokenMetadata: ResolversTypes['EmbedToken'] }>;
|
||||
CreateModelInput: CreateModelInput;
|
||||
CreateServerRegionInput: CreateServerRegionInput;
|
||||
CreateUserEmailInput: CreateUserEmailInput;
|
||||
@@ -5444,6 +5506,9 @@ export type ResolversTypes = {
|
||||
DiscoverableStreamsSortingInput: DiscoverableStreamsSortingInput;
|
||||
EditCommentInput: EditCommentInput;
|
||||
EmailVerificationRequestInput: EmailVerificationRequestInput;
|
||||
EmbedToken: ResolverTypeWrapper<EmbedTokenGraphQLReturn>;
|
||||
EmbedTokenCollection: ResolverTypeWrapper<Omit<EmbedTokenCollection, 'items'> & { items: Array<ResolversTypes['EmbedToken']> }>;
|
||||
EmbedTokenCreateInput: EmbedTokenCreateInput;
|
||||
FileUpload: ResolverTypeWrapper<FileUploadGraphQLReturn>;
|
||||
FileUploadCollection: ResolverTypeWrapper<Omit<FileUploadCollection, 'items'> & { items: Array<ResolversTypes['FileUpload']> }>;
|
||||
FileUploadMutations: ResolverTypeWrapper<MutationsObjectGraphQLReturn>;
|
||||
@@ -5773,6 +5838,7 @@ export type ResolversParentTypes = {
|
||||
CreateAutomateFunctionWithoutVersionInput: CreateAutomateFunctionWithoutVersionInput;
|
||||
CreateCommentInput: CreateCommentInput;
|
||||
CreateCommentReplyInput: CreateCommentReplyInput;
|
||||
CreateEmbedTokenReturn: Omit<CreateEmbedTokenReturn, 'tokenMetadata'> & { tokenMetadata: ResolversParentTypes['EmbedToken'] };
|
||||
CreateModelInput: CreateModelInput;
|
||||
CreateServerRegionInput: CreateServerRegionInput;
|
||||
CreateUserEmailInput: CreateUserEmailInput;
|
||||
@@ -5786,6 +5852,9 @@ export type ResolversParentTypes = {
|
||||
DiscoverableStreamsSortingInput: DiscoverableStreamsSortingInput;
|
||||
EditCommentInput: EditCommentInput;
|
||||
EmailVerificationRequestInput: EmailVerificationRequestInput;
|
||||
EmbedToken: EmbedTokenGraphQLReturn;
|
||||
EmbedTokenCollection: Omit<EmbedTokenCollection, 'items'> & { items: Array<ResolversParentTypes['EmbedToken']> };
|
||||
EmbedTokenCreateInput: EmbedTokenCreateInput;
|
||||
FileUpload: FileUploadGraphQLReturn;
|
||||
FileUploadCollection: Omit<FileUploadCollection, 'items'> & { items: Array<ResolversParentTypes['FileUpload']> };
|
||||
FileUploadMutations: MutationsObjectGraphQLReturn;
|
||||
@@ -6458,6 +6527,12 @@ export type CountOnlyCollectionResolvers<ContextType = GraphQLContext, ParentTyp
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type CreateEmbedTokenReturnResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['CreateEmbedTokenReturn'] = ResolversParentTypes['CreateEmbedTokenReturn']> = {
|
||||
token?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
tokenMetadata?: Resolver<ResolversTypes['EmbedToken'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type CurrencyBasedPricesResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['CurrencyBasedPrices'] = ResolversParentTypes['CurrencyBasedPrices']> = {
|
||||
gbp?: Resolver<ResolversTypes['WorkspacePaidPlanPrices'], ParentType, ContextType>;
|
||||
usd?: Resolver<ResolversTypes['WorkspacePaidPlanPrices'], ParentType, ContextType>;
|
||||
@@ -6468,6 +6543,24 @@ export interface DateTimeScalarConfig extends GraphQLScalarTypeConfig<ResolversT
|
||||
name: 'DateTime';
|
||||
}
|
||||
|
||||
export type EmbedTokenResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['EmbedToken'] = ResolversParentTypes['EmbedToken']> = {
|
||||
createdAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
|
||||
lastUsed?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
|
||||
lifespan?: Resolver<ResolversTypes['BigInt'], ParentType, ContextType>;
|
||||
projectId?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
resourceIdString?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
tokenId?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
user?: Resolver<Maybe<ResolversTypes['LimitedUser']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type EmbedTokenCollectionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['EmbedTokenCollection'] = ResolversParentTypes['EmbedTokenCollection']> = {
|
||||
cursor?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
items?: Resolver<Array<ResolversTypes['EmbedToken']>, ParentType, ContextType>;
|
||||
totalCount?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type FileUploadResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['FileUpload'] = ResolversParentTypes['FileUpload']> = {
|
||||
branchName?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
convertedCommitId?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
@@ -6820,6 +6913,7 @@ export type ProjectResolvers<ContextType = GraphQLContext, ParentType extends Re
|
||||
createdAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
|
||||
description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
embedOptions?: Resolver<ResolversTypes['ProjectEmbedOptions'], ParentType, ContextType>;
|
||||
embedTokens?: Resolver<ResolversTypes['EmbedTokenCollection'], ParentType, ContextType, Partial<ProjectEmbedTokensArgs>>;
|
||||
hasAccessToFeature?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<ProjectHasAccessToFeatureArgs, 'featureName'>>;
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
invitableCollaborators?: Resolver<ResolversTypes['WorkspaceCollaboratorCollection'], ParentType, ContextType, RequireFields<ProjectInvitableCollaboratorsArgs, 'limit'>>;
|
||||
@@ -6954,10 +7048,13 @@ export type ProjectMutationsResolvers<ContextType = GraphQLContext, ParentType e
|
||||
automationMutations?: Resolver<ResolversTypes['ProjectAutomationMutations'], ParentType, ContextType, RequireFields<ProjectMutationsAutomationMutationsArgs, 'projectId'>>;
|
||||
batchDelete?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<ProjectMutationsBatchDeleteArgs, 'ids'>>;
|
||||
create?: Resolver<ResolversTypes['Project'], ParentType, ContextType, Partial<ProjectMutationsCreateArgs>>;
|
||||
createEmbedToken?: Resolver<ResolversTypes['CreateEmbedTokenReturn'], ParentType, ContextType, RequireFields<ProjectMutationsCreateEmbedTokenArgs, 'token'>>;
|
||||
createForOnboarding?: Resolver<ResolversTypes['Project'], ParentType, ContextType>;
|
||||
delete?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<ProjectMutationsDeleteArgs, 'id'>>;
|
||||
invites?: Resolver<ResolversTypes['ProjectInviteMutations'], ParentType, ContextType>;
|
||||
leave?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<ProjectMutationsLeaveArgs, 'id'>>;
|
||||
revokeEmbedToken?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<ProjectMutationsRevokeEmbedTokenArgs, 'projectId' | 'token'>>;
|
||||
revokeEmbedTokens?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<ProjectMutationsRevokeEmbedTokensArgs, 'projectId'>>;
|
||||
update?: Resolver<ResolversTypes['Project'], ParentType, ContextType, RequireFields<ProjectMutationsUpdateArgs, 'update'>>;
|
||||
updateRole?: Resolver<ResolversTypes['Project'], ParentType, ContextType, RequireFields<ProjectMutationsUpdateRoleArgs, 'input'>>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
@@ -6981,6 +7078,7 @@ export type ProjectPermissionChecksResolvers<ContextType = GraphQLContext, Paren
|
||||
canBroadcastActivity?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canCreateAutomation?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canCreateComment?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canCreateEmbedTokens?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canCreateModel?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canDelete?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canInvite?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
@@ -6989,9 +7087,11 @@ export type ProjectPermissionChecksResolvers<ContextType = GraphQLContext, Paren
|
||||
canMoveToWorkspace?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType, Partial<ProjectPermissionChecksCanMoveToWorkspaceArgs>>;
|
||||
canPublish?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canRead?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canReadEmbedTokens?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canReadSettings?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canReadWebhooks?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canRequestRender?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canRevokeEmbedTokens?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canUpdate?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canUpdateAllowPublicComments?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
@@ -7911,8 +8011,11 @@ export type Resolvers<ContextType = GraphQLContext> = {
|
||||
Commit?: CommitResolvers<ContextType>;
|
||||
CommitCollection?: CommitCollectionResolvers<ContextType>;
|
||||
CountOnlyCollection?: CountOnlyCollectionResolvers<ContextType>;
|
||||
CreateEmbedTokenReturn?: CreateEmbedTokenReturnResolvers<ContextType>;
|
||||
CurrencyBasedPrices?: CurrencyBasedPricesResolvers<ContextType>;
|
||||
DateTime?: GraphQLScalarType;
|
||||
EmbedToken?: EmbedTokenResolvers<ContextType>;
|
||||
EmbedTokenCollection?: EmbedTokenCollectionResolvers<ContextType>;
|
||||
FileUpload?: FileUploadResolvers<ContextType>;
|
||||
FileUploadCollection?: FileUploadCollectionResolvers<ContextType>;
|
||||
FileUploadMutations?: FileUploadMutationsResolvers<ContextType>;
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { db } from '@/db/knex'
|
||||
import { Resolvers } from '@/modules/core/graph/generated/graphql'
|
||||
import {
|
||||
storeApiTokenFactory,
|
||||
storeTokenResourceAccessDefinitionsFactory,
|
||||
storeTokenScopesFactory
|
||||
} from '@/modules/core/repositories/tokens'
|
||||
import {
|
||||
countProjectEmbedTokensFactory,
|
||||
listProjectEmbedTokensFactory,
|
||||
revokeEmbedTokenByIdFactory,
|
||||
revokeProjectEmbedTokensFactory,
|
||||
storeEmbedApiTokenFactory
|
||||
} from '@/modules/core/repositories/embedTokens'
|
||||
import {
|
||||
createEmbedTokenFactory,
|
||||
createTokenFactory,
|
||||
getPaginatedProjectEmbedTokensFactory
|
||||
} from '@/modules/core/services/tokens'
|
||||
import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper'
|
||||
import { removeNullOrUndefinedKeys } from '@speckle/shared'
|
||||
import { getUserFactory } from '@/modules/core/repositories/users'
|
||||
import { throwIfResourceAccessNotAllowed } from '@/modules/core/helpers/token'
|
||||
|
||||
const resolvers: Resolvers = {
|
||||
EmbedToken: {
|
||||
user: async (parent) => {
|
||||
return await getUserFactory({ db })(parent.userId)
|
||||
}
|
||||
},
|
||||
Project: {
|
||||
embedTokens: async (parent, args, context) => {
|
||||
const canReadEmbedTokens = await context.authPolicies.project.canReadEmbedTokens({
|
||||
userId: context.userId,
|
||||
projectId: parent.id
|
||||
})
|
||||
throwIfAuthNotOk(canReadEmbedTokens)
|
||||
|
||||
return await getPaginatedProjectEmbedTokensFactory({
|
||||
listEmbedTokens: listProjectEmbedTokensFactory({ db }),
|
||||
countEmbedTokens: countProjectEmbedTokensFactory({ db })
|
||||
})({
|
||||
projectId: parent.id,
|
||||
filter: removeNullOrUndefinedKeys(args)
|
||||
})
|
||||
}
|
||||
},
|
||||
ProjectMutations: {
|
||||
createEmbedToken: async (_parent, args, context) => {
|
||||
const canCreateEmbedToken =
|
||||
await context.authPolicies.project.canUpdateEmbedTokens({
|
||||
userId: context.userId,
|
||||
projectId: args.token.projectId
|
||||
})
|
||||
throwIfAuthNotOk(canCreateEmbedToken)
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: args.token.projectId,
|
||||
resourceType: 'project',
|
||||
resourceAccessRules: context.resourceAccessRules
|
||||
})
|
||||
|
||||
return await createEmbedTokenFactory({
|
||||
createToken: createTokenFactory({
|
||||
storeApiToken: storeApiTokenFactory({ db }),
|
||||
storeTokenScopes: storeTokenScopesFactory({ db }),
|
||||
storeTokenResourceAccessDefinitions:
|
||||
storeTokenResourceAccessDefinitionsFactory({ db })
|
||||
}),
|
||||
storeEmbedToken: storeEmbedApiTokenFactory({ db })
|
||||
})({
|
||||
...removeNullOrUndefinedKeys(args.token),
|
||||
userId: context.userId!
|
||||
})
|
||||
},
|
||||
revokeEmbedToken: async (_parent, args, context) => {
|
||||
const canRevokeEmbedToken =
|
||||
await context.authPolicies.project.canUpdateEmbedTokens({
|
||||
userId: context.userId,
|
||||
projectId: args.projectId
|
||||
})
|
||||
throwIfAuthNotOk(canRevokeEmbedToken)
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: args.projectId,
|
||||
resourceType: 'project',
|
||||
resourceAccessRules: context.resourceAccessRules
|
||||
})
|
||||
|
||||
return await revokeEmbedTokenByIdFactory({ db })({
|
||||
tokenId: args.token,
|
||||
projectId: args.projectId
|
||||
})
|
||||
},
|
||||
revokeEmbedTokens: async (_parent, args, context) => {
|
||||
const canRevokeEmbedTokens =
|
||||
await context.authPolicies.project.canUpdateEmbedTokens({
|
||||
userId: context.userId,
|
||||
projectId: args.projectId
|
||||
})
|
||||
throwIfAuthNotOk(canRevokeEmbedTokens)
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: args.projectId,
|
||||
resourceType: 'project',
|
||||
resourceAccessRules: context.resourceAccessRules
|
||||
})
|
||||
|
||||
await revokeProjectEmbedTokensFactory({ db })({ projectId: args.projectId })
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default resolvers
|
||||
@@ -113,6 +113,27 @@ export default {
|
||||
userId: ctx.userId
|
||||
})
|
||||
return Authz.toGraphqlResult(canInvite)
|
||||
},
|
||||
canReadEmbedTokens: async (parent, _args, ctx) => {
|
||||
const canReadEmbedTokens = await ctx.authPolicies.project.canReadEmbedTokens({
|
||||
projectId: parent.projectId,
|
||||
userId: ctx.userId
|
||||
})
|
||||
return Authz.toGraphqlResult(canReadEmbedTokens)
|
||||
},
|
||||
canCreateEmbedTokens: async (parent, _args, ctx) => {
|
||||
const canCreateEmbedTokens = await ctx.authPolicies.project.canUpdateEmbedTokens({
|
||||
projectId: parent.projectId,
|
||||
userId: ctx.userId
|
||||
})
|
||||
return Authz.toGraphqlResult(canCreateEmbedTokens)
|
||||
},
|
||||
canRevokeEmbedTokens: async (parent, _args, ctx) => {
|
||||
const canUpdateEmbedTokens = await ctx.authPolicies.project.canUpdateEmbedTokens({
|
||||
projectId: parent.projectId,
|
||||
userId: ctx.userId
|
||||
})
|
||||
return Authz.toGraphqlResult(canUpdateEmbedTokens)
|
||||
}
|
||||
},
|
||||
ModelPermissionChecks: {
|
||||
|
||||
@@ -45,7 +45,8 @@ import {
|
||||
grantStreamPermissionsFactory,
|
||||
getOnboardingBaseStreamFactory,
|
||||
getUserStreamsPageFactory,
|
||||
getUserStreamsCountFactory
|
||||
getUserStreamsCountFactory,
|
||||
getStreamRolesFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import { getUserFactory, getUsersFactory } from '@/modules/core/repositories/users'
|
||||
import {
|
||||
@@ -136,6 +137,7 @@ const buildFinalizeProjectInvite = () =>
|
||||
validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }),
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
}),
|
||||
@@ -203,6 +205,7 @@ const removeStreamCollaborator = removeStreamCollaboratorFactory({
|
||||
validateStreamAccess,
|
||||
isStreamCollaborator,
|
||||
revokeStreamPermissions: revokeStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
const updateStreamRoleAndNotify = updateStreamRoleAndNotifyFactory({
|
||||
@@ -211,6 +214,7 @@ const updateStreamRoleAndNotify = updateStreamRoleAndNotifyFactory({
|
||||
validateStreamAccess,
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
}),
|
||||
removeStreamCollaborator
|
||||
|
||||
@@ -25,7 +25,8 @@ import {
|
||||
canUserFavoriteStreamFactory,
|
||||
setStreamFavoritedFactory,
|
||||
getUserStreamsPageFactory,
|
||||
getUserStreamsCountFactory
|
||||
getUserStreamsCountFactory,
|
||||
getStreamRolesFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import {
|
||||
createStreamReturnRecordFactory,
|
||||
@@ -125,6 +126,7 @@ const buildFinalizeProjectInvite = () =>
|
||||
validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }),
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
}),
|
||||
@@ -203,6 +205,7 @@ const removeStreamCollaborator = removeStreamCollaboratorFactory({
|
||||
validateStreamAccess,
|
||||
isStreamCollaborator,
|
||||
revokeStreamPermissions: revokeStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
const updateStreamRoleAndNotify = updateStreamRoleAndNotifyFactory({
|
||||
@@ -211,6 +214,7 @@ const updateStreamRoleAndNotify = updateStreamRoleAndNotifyFactory({
|
||||
validateStreamAccess,
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
}),
|
||||
removeStreamCollaborator
|
||||
|
||||
@@ -33,7 +33,8 @@ import {
|
||||
} from '@/modules/core/services/users/management'
|
||||
import {
|
||||
deleteStreamFactory,
|
||||
getUserDeletableStreamsFactory
|
||||
getUserDeletableStreamsFactory,
|
||||
legacyGetStreamsFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import { dbLogger } from '@/observability/logging'
|
||||
import { getAdminUsersListCollectionFactory } from '@/modules/core/services/users/legacyAdminUsersList'
|
||||
@@ -51,6 +52,8 @@ import { asOperation } from '@/modules/shared/command'
|
||||
import { setUserOnboardingChoicesFactory } from '@/modules/core/services/users/tracking'
|
||||
import { getMixpanelClient } from '@/modules/shared/utils/mixpanel'
|
||||
import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper'
|
||||
import { getUserWorkspaceSeatsFactory } from '@/modules/workspacesCore/repositories/workspaces'
|
||||
import { queryAllProjectsFactory } from '@/modules/core/services/projects'
|
||||
|
||||
const getUser = legacyGetUserFactory({ db })
|
||||
const getUserByEmail = legacyGetUserByEmailFactory({ db })
|
||||
@@ -67,6 +70,10 @@ const deleteUser = deleteUserFactory({
|
||||
logger: dbLogger,
|
||||
isLastAdminUser: isLastAdminUserFactory({ db }),
|
||||
getUserDeletableStreams: getUserDeletableStreamsFactory({ db }),
|
||||
queryAllProjects: queryAllProjectsFactory({
|
||||
getStreams: legacyGetStreamsFactory({ db })
|
||||
}),
|
||||
getUserWorkspaceSeats: getUserWorkspaceSeatsFactory({ db }),
|
||||
deleteAllUserInvites: deleteAllUserInvitesFactory({ db }),
|
||||
deleteUserRecord: deleteUserRecordFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
LegacyStreamCommit,
|
||||
LegacyUserCommit
|
||||
} from '@/modules/core/domain/commits/types'
|
||||
import { EmbedApiToken } from '@/modules/core/domain/tokens/types'
|
||||
import {
|
||||
LimitedUser,
|
||||
StreamRole,
|
||||
@@ -154,3 +155,5 @@ export type VersionPermissionChecksGraphQLReturn = {
|
||||
versionId: string
|
||||
projectId: string
|
||||
}
|
||||
|
||||
export type EmbedTokenGraphQLReturn = EmbedApiToken
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Knex } from 'knex'
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.createTable('embed_api_tokens', (table) => {
|
||||
table
|
||||
.string('tokenId')
|
||||
.notNullable()
|
||||
.references('id')
|
||||
.inTable('api_tokens')
|
||||
.onDelete('cascade')
|
||||
table
|
||||
.string('projectId')
|
||||
.notNullable()
|
||||
.references('id')
|
||||
.inTable('streams')
|
||||
.onDelete('cascade')
|
||||
table
|
||||
.string('userId')
|
||||
.notNullable()
|
||||
.references('id')
|
||||
.inTable('users')
|
||||
.onDelete('cascade')
|
||||
table.string('resourceIdString').notNullable()
|
||||
table.primary(['projectId', 'tokenId'])
|
||||
})
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists('embed_api_tokens')
|
||||
}
|
||||
@@ -351,12 +351,14 @@ export const getPaginatedBranchCommitsItemsFactory =
|
||||
if (cursor) {
|
||||
q.andWhere(Commits.col.createdAt, '<', cursor)
|
||||
}
|
||||
if (deps.limitsDate) {
|
||||
q.andWhere(Commits.col.createdAt, '>', deps.limitsDate)
|
||||
|
||||
let rows = await q
|
||||
|
||||
const limitsDate = deps.limitsDate
|
||||
if (limitsDate && rows.length > 0) {
|
||||
// the length check above makes the ! ok
|
||||
rows = [rows.shift()!, ...rows.filter((r) => r.createdAt > limitsDate)]
|
||||
}
|
||||
|
||||
const rows = await q
|
||||
|
||||
return {
|
||||
commits: rows,
|
||||
cursor: rows.length > 0 ? rows[rows.length - 1].createdAt.toISOString() : null
|
||||
@@ -701,11 +703,17 @@ export const legacyGetPaginatedStreamCommitsPageFactory =
|
||||
if (ignoreGlobalsBranch) query.andWhere('branches.name', '!=', 'globals')
|
||||
|
||||
if (cursor) query.andWhere('commits.createdAt', '<', cursor)
|
||||
if (deps.limitsDate) query.andWhere('commits.createdAt', '>', deps.limitsDate)
|
||||
// if (deps.limitsDate) query.andWhere('commits.createdAt', '>', deps.limitsDate)
|
||||
|
||||
query.orderBy('commits.createdAt', 'desc').limit(limit)
|
||||
|
||||
const rows = (await query) as LegacyStreamCommit[]
|
||||
let rows = (await query) as LegacyStreamCommit[]
|
||||
|
||||
const limitsDate = deps.limitsDate
|
||||
if (limitsDate && rows.length > 0) {
|
||||
// the length check above makes the ! ok
|
||||
rows = [rows.shift()!, ...rows.filter((r) => r.createdAt > limitsDate)]
|
||||
}
|
||||
return {
|
||||
commits: rows,
|
||||
cursor: rows.length > 0 ? rows[rows.length - 1].createdAt.toISOString() : null
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { EmbedApiTokenRecord } from '@/modules/auth/helpers/types'
|
||||
import { ApiTokenRecord } from '@/modules/auth/repositories'
|
||||
import { ApiTokens, EmbedApiTokens } from '@/modules/core/dbSchema'
|
||||
import {
|
||||
CountProjectEmbedTokens,
|
||||
ListProjectEmbedTokens,
|
||||
RevokeEmbedTokenById,
|
||||
RevokeProjectEmbedTokens,
|
||||
StoreEmbedApiToken
|
||||
} from '@/modules/core/domain/tokens/operations'
|
||||
import { UserInputError } from '@/modules/core/errors/userinput'
|
||||
import { Knex } from 'knex'
|
||||
import { clamp } from 'lodash'
|
||||
|
||||
const tables = {
|
||||
apiTokens: (db: Knex) => db<ApiTokenRecord>(ApiTokens.name),
|
||||
embedApiTokens: (db: Knex) => db<EmbedApiTokenRecord>(EmbedApiTokens.name)
|
||||
}
|
||||
|
||||
export const storeEmbedApiTokenFactory =
|
||||
(deps: { db: Knex }): StoreEmbedApiToken =>
|
||||
async (token) => {
|
||||
const [newToken] = await tables.embedApiTokens(deps.db).insert(token).returning('*')
|
||||
return newToken
|
||||
}
|
||||
|
||||
export const countProjectEmbedTokensFactory =
|
||||
(deps: { db: Knex }): CountProjectEmbedTokens =>
|
||||
async ({ projectId }) => {
|
||||
const [{ count }] = await tables
|
||||
.embedApiTokens(deps.db)
|
||||
.where(EmbedApiTokens.col.projectId, projectId)
|
||||
.count()
|
||||
return Number.parseInt(count as string)
|
||||
}
|
||||
|
||||
export const listProjectEmbedTokensFactory =
|
||||
(deps: { db: Knex }): ListProjectEmbedTokens =>
|
||||
async ({ projectId, filter = {} }) => {
|
||||
const { limit = 10, createdBefore } = filter
|
||||
|
||||
if (limit === 0) return []
|
||||
|
||||
const q = tables
|
||||
.embedApiTokens(deps.db)
|
||||
.select<
|
||||
(EmbedApiTokenRecord &
|
||||
Pick<ApiTokenRecord, 'createdAt' | 'lastUsed' | 'lifespan'>)[]
|
||||
>(
|
||||
...EmbedApiTokens.cols,
|
||||
ApiTokens.col.createdAt,
|
||||
ApiTokens.col.lastUsed,
|
||||
ApiTokens.col.lifespan
|
||||
)
|
||||
.orderBy(ApiTokens.col.createdAt, 'desc')
|
||||
.leftJoin(ApiTokens.name, ApiTokens.col.id, EmbedApiTokens.col.tokenId)
|
||||
.where(EmbedApiTokens.col.projectId, projectId)
|
||||
.limit(clamp(limit, 0, 50))
|
||||
|
||||
if (createdBefore) {
|
||||
q.andWhere(ApiTokens.col.createdAt, '<', createdBefore)
|
||||
}
|
||||
|
||||
return await q
|
||||
}
|
||||
|
||||
export const revokeEmbedTokenByIdFactory =
|
||||
(deps: { db: Knex }): RevokeEmbedTokenById =>
|
||||
async ({ tokenId: token, projectId }) => {
|
||||
const tokenId = token.slice(0, 10)
|
||||
const delCount = await tables
|
||||
.embedApiTokens(deps.db)
|
||||
.where({ tokenId, projectId })
|
||||
.delete()
|
||||
if (delCount === 0) throw new UserInputError('Embed token not found')
|
||||
await tables.apiTokens(deps.db).where(ApiTokens.col.id, tokenId).delete()
|
||||
return true
|
||||
}
|
||||
|
||||
export const revokeProjectEmbedTokensFactory =
|
||||
(deps: { db: Knex }): RevokeProjectEmbedTokens =>
|
||||
async ({ projectId }) => {
|
||||
await tables
|
||||
.apiTokens(deps.db)
|
||||
.whereIn(ApiTokens.col.id, (builder) => {
|
||||
return builder
|
||||
.select('tokenId')
|
||||
.from<EmbedApiTokenRecord>(EmbedApiTokens.name)
|
||||
.where('projectId', projectId)
|
||||
})
|
||||
.delete()
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { StreamAcl, Streams } from '@/modules/core/dbSchema'
|
||||
import {
|
||||
DeleteProject,
|
||||
GetProject,
|
||||
GetUserProjectRoles,
|
||||
StoreProject,
|
||||
StoreProjectRole,
|
||||
StoreProjectRoles
|
||||
@@ -51,3 +52,17 @@ export const storeProjectRolesFactory =
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
export const getUserProjectRolesFactory =
|
||||
({ db }: { db: Knex }): GetUserProjectRoles =>
|
||||
async ({ userId, workspaceId }) => {
|
||||
const query = db<StreamAclRecord>(StreamAcl.name).where({ userId })
|
||||
|
||||
if (workspaceId) {
|
||||
query
|
||||
.join(Streams.name, Streams.col.id, StreamAcl.col.resourceId)
|
||||
.where({ workspaceId })
|
||||
}
|
||||
|
||||
return await query
|
||||
}
|
||||
|
||||
@@ -63,7 +63,6 @@ import { removePrivateFields } from '@/modules/core/helpers/userHelper'
|
||||
import {
|
||||
DeleteProjectRole,
|
||||
UpdateProject,
|
||||
GetRolesByUserId,
|
||||
UpsertProjectRole
|
||||
} from '@/modules/core/domain/projects/operations'
|
||||
import {
|
||||
@@ -455,7 +454,7 @@ export const getStreamRolesFactory =
|
||||
async (userId: string, streamIds: string[]) => {
|
||||
const q = tables
|
||||
.streams(deps.db)
|
||||
.select<{ id: string; role: Nullable<string> }[]>([
|
||||
.select<{ id: string; role: Nullable<StreamRoles> }[]>([
|
||||
Streams.col.id,
|
||||
StreamAcl.col.role
|
||||
])
|
||||
@@ -1330,20 +1329,6 @@ export const getOnboardingBaseStreamFactory =
|
||||
return await q
|
||||
}
|
||||
|
||||
export const getRolesByUserIdFactory =
|
||||
({ db }: { db: Knex }): GetRolesByUserId =>
|
||||
async ({ userId, workspaceId }) => {
|
||||
const query = db<Pick<StreamAclRecord, 'role' | 'resourceId' | 'userId'>>(
|
||||
StreamAcl.name
|
||||
).where({ userId })
|
||||
if (workspaceId) {
|
||||
query
|
||||
.join(Streams.name, Streams.col.id, StreamAcl.col.resourceId)
|
||||
.where({ workspaceId })
|
||||
}
|
||||
return await query
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use getStreams() from the repository directly
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
EmbedApiTokenRecord,
|
||||
PersonalApiTokenRecord,
|
||||
TokenScopeRecord,
|
||||
UserServerAppTokenRecord
|
||||
@@ -6,6 +7,7 @@ import {
|
||||
import { ApiTokenRecord } from '@/modules/auth/repositories'
|
||||
import {
|
||||
ApiTokens,
|
||||
EmbedApiTokens,
|
||||
PersonalApiTokens,
|
||||
TokenResourceAccess,
|
||||
TokenScopes,
|
||||
@@ -38,7 +40,8 @@ const tables = {
|
||||
db<TokenResourceAccessRecord>(TokenResourceAccess.name),
|
||||
userServerAppTokens: (db: Knex) =>
|
||||
db<UserServerAppTokenRecord>(UserServerAppTokens.name),
|
||||
personalApiTokens: (db: Knex) => db<PersonalApiTokenRecord>(PersonalApiTokens.name)
|
||||
personalApiTokens: (db: Knex) => db<PersonalApiTokenRecord>(PersonalApiTokens.name),
|
||||
embedApiTokens: (db: Knex) => db<EmbedApiTokenRecord>(EmbedApiTokens.name)
|
||||
}
|
||||
|
||||
export const storeApiTokenFactory =
|
||||
|
||||
@@ -4,13 +4,17 @@ import {
|
||||
CreateProject,
|
||||
DeleteProject,
|
||||
GetProject,
|
||||
QueryAllProjects,
|
||||
StoreModel,
|
||||
StoreProject,
|
||||
StoreProjectRole,
|
||||
WaitForRegionProject
|
||||
} from '@/modules/core/domain/projects/operations'
|
||||
import { Project } from '@/modules/core/domain/streams/types'
|
||||
import { RegionalProjectCreationError } from '@/modules/core/errors/projects'
|
||||
import { Project, StreamWithOptionalRole } from '@/modules/core/domain/streams/types'
|
||||
import {
|
||||
ProjectQueryError,
|
||||
RegionalProjectCreationError
|
||||
} from '@/modules/core/errors/projects'
|
||||
import { StreamNotFoundError } from '@/modules/core/errors/stream'
|
||||
import { ProjectVisibility } from '@/modules/core/graph/generated/graphql'
|
||||
import { mapGqlToDbProjectVisibility } from '@/modules/core/helpers/project'
|
||||
@@ -19,6 +23,7 @@ import { EventBusEmit } from '@/modules/shared/services/eventBus'
|
||||
import { retry } from '@lifeomic/attempt'
|
||||
import { Roles, TIME_MS } from '@speckle/shared'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { LegacyGetStreams } from '@/modules/core/domain/streams/operations'
|
||||
|
||||
export const createNewProjectFactory =
|
||||
({
|
||||
@@ -68,6 +73,7 @@ export const createNewProjectFactory =
|
||||
projectId,
|
||||
authorId: ownerId
|
||||
})
|
||||
|
||||
await emitEvent({
|
||||
eventName: ProjectEvents.Created,
|
||||
payload: {
|
||||
@@ -80,6 +86,17 @@ export const createNewProjectFactory =
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await emitEvent({
|
||||
eventName: ProjectEvents.PermissionsAdded,
|
||||
payload: {
|
||||
project,
|
||||
activityUserId: ownerId,
|
||||
targetUserId: ownerId,
|
||||
role: Roles.Stream.Owner,
|
||||
previousRole: null
|
||||
}
|
||||
})
|
||||
return project
|
||||
}
|
||||
|
||||
@@ -109,3 +126,38 @@ export const waitForRegionProjectFactory =
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export const queryAllProjectsFactory = ({
|
||||
getStreams
|
||||
}: {
|
||||
getStreams: LegacyGetStreams
|
||||
}): QueryAllProjects =>
|
||||
async function* queryAllWorkspaceProjects({
|
||||
userId,
|
||||
workspaceId
|
||||
}): AsyncGenerator<StreamWithOptionalRole[], void, unknown> {
|
||||
let cursor: Date | null = null
|
||||
let iterationCount = 0
|
||||
|
||||
if (!userId && !workspaceId) throw new ProjectQueryError()
|
||||
|
||||
do {
|
||||
if (iterationCount > 500) throw new ProjectQueryError()
|
||||
|
||||
const { streams, cursorDate } = await getStreams({
|
||||
cursor,
|
||||
orderBy: null,
|
||||
limit: 100,
|
||||
visibility: null,
|
||||
searchQuery: null,
|
||||
streamIdWhitelist: null,
|
||||
workspaceIdWhitelist: workspaceId ? [workspaceId] : null,
|
||||
userId
|
||||
})
|
||||
|
||||
yield streams
|
||||
|
||||
cursor = cursorDate
|
||||
iterationCount++
|
||||
} while (!!cursor)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ProjectEvents } from '@/modules/core/domain/projects/events'
|
||||
import {
|
||||
AddOrUpdateStreamCollaborator,
|
||||
GetStream,
|
||||
GetStreamRoles,
|
||||
GrantStreamPermissions,
|
||||
IsStreamCollaborator,
|
||||
RemoveStreamCollaborator,
|
||||
@@ -92,6 +93,7 @@ export const removeStreamCollaboratorFactory =
|
||||
validateStreamAccess: ValidateStreamAccess
|
||||
isStreamCollaborator: IsStreamCollaborator
|
||||
revokeStreamPermissions: RevokeStreamPermissions
|
||||
getStreamRoles: GetStreamRoles
|
||||
emitEvent: EventBusEmit
|
||||
}): RemoveStreamCollaborator =>
|
||||
async (streamId, userId, removedById, removerResourceAccessRules, options) => {
|
||||
@@ -113,19 +115,23 @@ export const removeStreamCollaboratorFactory =
|
||||
}
|
||||
}
|
||||
|
||||
const { [streamId]: role } = await deps.getStreamRoles(userId, [streamId])
|
||||
const stream = await deps.revokeStreamPermissions({ streamId, userId }, options)
|
||||
if (!stream) {
|
||||
throw new LogicError('Stream not found')
|
||||
}
|
||||
|
||||
await deps.emitEvent({
|
||||
eventName: ProjectEvents.PermissionsRevoked,
|
||||
payload: {
|
||||
project: stream,
|
||||
activityUserId: removedById,
|
||||
removedUserId: userId
|
||||
}
|
||||
})
|
||||
if (role) {
|
||||
await deps.emitEvent({
|
||||
eventName: ProjectEvents.PermissionsRevoked,
|
||||
payload: {
|
||||
project: stream,
|
||||
activityUserId: removedById,
|
||||
removedUserId: userId,
|
||||
role
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return stream
|
||||
}
|
||||
@@ -145,6 +151,7 @@ export const addOrUpdateStreamCollaboratorFactory =
|
||||
validateStreamAccess: ValidateStreamAccess
|
||||
getUser: GetUser
|
||||
grantStreamPermissions: GrantStreamPermissions
|
||||
getStreamRoles: GetStreamRoles
|
||||
emitEvent: EventBusEmit
|
||||
}): AddOrUpdateStreamCollaborator =>
|
||||
async (
|
||||
@@ -194,6 +201,7 @@ export const addOrUpdateStreamCollaboratorFactory =
|
||||
}
|
||||
})
|
||||
|
||||
const { [streamId]: previousRole } = await deps.getStreamRoles(userId, [streamId])
|
||||
const stream = (await deps.grantStreamPermissions(
|
||||
{
|
||||
streamId,
|
||||
@@ -203,17 +211,16 @@ export const addOrUpdateStreamCollaboratorFactory =
|
||||
{ trackProjectUpdate }
|
||||
)) as StreamRecord // validateStreamAccess already checked that it exists
|
||||
|
||||
if (!fromInvite) {
|
||||
await deps.emitEvent({
|
||||
eventName: ProjectEvents.PermissionsAdded,
|
||||
payload: {
|
||||
project: stream,
|
||||
activityUserId: addedById,
|
||||
targetUserId: userId,
|
||||
role: role as StreamRoles
|
||||
}
|
||||
})
|
||||
}
|
||||
await deps.emitEvent({
|
||||
eventName: ProjectEvents.PermissionsAdded,
|
||||
payload: {
|
||||
project: stream,
|
||||
activityUserId: addedById,
|
||||
targetUserId: userId,
|
||||
role: role as StreamRoles,
|
||||
previousRole: previousRole || null
|
||||
}
|
||||
})
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
@@ -92,6 +92,17 @@ export const createStreamReturnRecordFactory =
|
||||
}
|
||||
})
|
||||
|
||||
await deps.emitEvent({
|
||||
eventName: ProjectEvents.PermissionsAdded,
|
||||
payload: {
|
||||
project: stream,
|
||||
activityUserId: ownerId,
|
||||
targetUserId: ownerId,
|
||||
role: Roles.Stream.Owner,
|
||||
previousRole: null
|
||||
}
|
||||
})
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
|
||||
@@ -4,16 +4,21 @@ import {
|
||||
TokenResourceAccessRecord,
|
||||
TokenValidationResult
|
||||
} from '@/modules/core/helpers/types'
|
||||
import { Optional, ServerScope } from '@speckle/shared'
|
||||
import { Optional, Scopes, ServerScope } from '@speckle/shared'
|
||||
import {
|
||||
CountProjectEmbedTokens,
|
||||
CreateAndStoreAppToken,
|
||||
CreateAndStoreEmbedToken,
|
||||
CreateAndStorePersonalAccessToken,
|
||||
CreateAndStoreUserToken,
|
||||
GetApiTokenById,
|
||||
GetPaginatedProjectEmbedTokens,
|
||||
GetTokenResourceAccessDefinitionsById,
|
||||
GetTokenScopesById,
|
||||
ListProjectEmbedTokens,
|
||||
RevokeUserTokenById,
|
||||
StoreApiToken,
|
||||
StoreEmbedApiToken,
|
||||
StorePersonalApiToken,
|
||||
StoreTokenResourceAccessDefinitions,
|
||||
StoreTokenScopes,
|
||||
@@ -24,6 +29,19 @@ import {
|
||||
import { GetTokenAppInfo } from '@/modules/auth/domain/operations'
|
||||
import { GetUserRole } from '@/modules/core/domain/users/operations'
|
||||
import { TokenCreateError } from '@/modules/core/errors/user'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import {
|
||||
EmbedApiToken,
|
||||
TokenResourceIdentifierType
|
||||
} from '@/modules/core/domain/tokens/types'
|
||||
import {
|
||||
createGetParamFromResources,
|
||||
parseUrlParameters
|
||||
} from '@speckle/shared/viewer/route'
|
||||
import {
|
||||
decodeIsoDateCursor,
|
||||
encodeIsoDateCursor
|
||||
} from '@/modules/shared/helpers/dbHelper'
|
||||
|
||||
/*
|
||||
Tokens
|
||||
@@ -124,6 +142,69 @@ export const createPersonalAccessTokenFactory =
|
||||
return token
|
||||
}
|
||||
|
||||
export const createEmbedTokenFactory =
|
||||
(deps: {
|
||||
createToken: CreateAndStoreUserToken
|
||||
storeEmbedToken: StoreEmbedApiToken
|
||||
}): CreateAndStoreEmbedToken =>
|
||||
async ({ projectId, userId, resourceIdString, lifespan }) => {
|
||||
const validatedResourceIdString = createGetParamFromResources(
|
||||
parseUrlParameters(resourceIdString)
|
||||
)
|
||||
|
||||
const { id, token } = await deps.createToken({
|
||||
userId,
|
||||
name: cryptoRandomString({ length: 10 }),
|
||||
scopes: [Scopes.Streams.Read],
|
||||
limitResources: [
|
||||
{
|
||||
id: projectId,
|
||||
type: TokenResourceIdentifierType.Project
|
||||
}
|
||||
],
|
||||
lifespan
|
||||
})
|
||||
|
||||
const tokenMetadata: EmbedApiToken = {
|
||||
projectId,
|
||||
tokenId: id,
|
||||
userId,
|
||||
resourceIdString: validatedResourceIdString
|
||||
}
|
||||
|
||||
await deps.storeEmbedToken(tokenMetadata)
|
||||
|
||||
return { token, tokenMetadata }
|
||||
}
|
||||
|
||||
export const getPaginatedProjectEmbedTokensFactory =
|
||||
(deps: {
|
||||
listEmbedTokens: ListProjectEmbedTokens
|
||||
countEmbedTokens: CountProjectEmbedTokens
|
||||
}): GetPaginatedProjectEmbedTokens =>
|
||||
async ({ projectId, filter = {} }) => {
|
||||
const cursor = filter.cursor ? decodeIsoDateCursor(filter.cursor) : null
|
||||
|
||||
const [items, totalCount] = await Promise.all([
|
||||
deps.listEmbedTokens({
|
||||
projectId,
|
||||
filter: {
|
||||
createdBefore: cursor,
|
||||
limit: filter.limit
|
||||
}
|
||||
}),
|
||||
deps.countEmbedTokens({ projectId })
|
||||
])
|
||||
|
||||
const lastItem = items.at(-1)
|
||||
|
||||
return {
|
||||
items,
|
||||
totalCount,
|
||||
cursor: lastItem ? encodeIsoDateCursor(lastItem.createdAt) : null
|
||||
}
|
||||
}
|
||||
|
||||
export const validateTokenFactory =
|
||||
(deps: {
|
||||
revokeUserTokenById: RevokeUserTokenById
|
||||
|
||||
@@ -54,6 +54,11 @@ import { GetServerInfo } from '@/modules/core/domain/server/operations'
|
||||
import { EventBusEmit } from '@/modules/shared/services/eventBus'
|
||||
import { UserEvents } from '@/modules/core/domain/users/events'
|
||||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
import { GetUserWorkspaceSeatsFactory } from '@/modules/workspacesCore/domain/operations'
|
||||
import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events'
|
||||
import { ProjectEvents } from '@/modules/core/domain/projects/events'
|
||||
import { QueryAllProjects } from '@/modules/core/domain/projects/operations'
|
||||
import { StreamWithOptionalRole } from '@/modules/core/repositories/streams'
|
||||
|
||||
const { FF_NO_PERSONAL_EMAILS_ENABLED } = getFeatureFlags()
|
||||
|
||||
@@ -80,6 +85,11 @@ export const updateUserAndNotifyFactory =
|
||||
const key = entry[0] as keyof typeof update
|
||||
let val = entry[1]
|
||||
|
||||
if (key === 'avatar' && val === '') {
|
||||
filteredUpdate[key] = null // avatar removal
|
||||
continue
|
||||
}
|
||||
|
||||
if (key === 'avatar') {
|
||||
val = sanitizeImageUrl(val)
|
||||
}
|
||||
@@ -288,7 +298,9 @@ export const deleteUserFactory =
|
||||
isLastAdminUser: IsLastAdminUser
|
||||
getUserDeletableStreams: GetUserDeletableStreams
|
||||
deleteAllUserInvites: DeleteAllUserInvites
|
||||
getUserWorkspaceSeats: GetUserWorkspaceSeatsFactory
|
||||
deleteUserRecord: DeleteUserRecord
|
||||
queryAllProjects: QueryAllProjects
|
||||
emitEvent: EventBusEmit
|
||||
}): DeleteUser =>
|
||||
async (id, invokerId) => {
|
||||
@@ -307,6 +319,39 @@ export const deleteUserFactory =
|
||||
// THIS REALLY SHOULD BE A REACTION TO THE USER DELETED EVENT EMITTED HER
|
||||
await deps.deleteAllUserInvites(id)
|
||||
|
||||
const workspaceSeats = await deps.getUserWorkspaceSeats({ userId: id })
|
||||
for (const seat of workspaceSeats) {
|
||||
await deps.emitEvent({
|
||||
eventName: WorkspaceEvents.SeatDeleted,
|
||||
payload: {
|
||||
updatedByUserId: id,
|
||||
previousSeat: seat
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const emitRevokeEventIfUserHasRole = async (project: StreamWithOptionalRole) => {
|
||||
if (!project.role) return
|
||||
|
||||
await deps.emitEvent({
|
||||
eventName: ProjectEvents.PermissionsRevoked,
|
||||
payload: {
|
||||
activityUserId: id,
|
||||
removedUserId: id,
|
||||
role: project.role,
|
||||
project
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for await (const projectsPage of deps.queryAllProjects({
|
||||
userId: id
|
||||
})) {
|
||||
for (const project of projectsPage) {
|
||||
await emitRevokeEventIfUserHasRole(project)
|
||||
}
|
||||
}
|
||||
|
||||
const deleted = await deps.deleteUserRecord(id)
|
||||
if (deleted) {
|
||||
await deps.emitEvent({
|
||||
|
||||
@@ -4,7 +4,10 @@ import { Commits, Streams, Users } from '@/modules/core/dbSchema'
|
||||
import { Roles } from '@/modules/core/helpers/mainConstants'
|
||||
import { createBranchFactory } from '@/modules/core/repositories/branches'
|
||||
import { getCommitsFactory } from '@/modules/core/repositories/commits'
|
||||
import { grantStreamPermissionsFactory } from '@/modules/core/repositories/streams'
|
||||
import {
|
||||
getStreamRolesFactory,
|
||||
grantStreamPermissionsFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import { getUserFactory } from '@/modules/core/repositories/users'
|
||||
import {
|
||||
addOrUpdateStreamCollaboratorFactory,
|
||||
@@ -39,6 +42,7 @@ const addOrUpdateStreamCollaborator = addOrUpdateStreamCollaboratorFactory({
|
||||
validateStreamAccess,
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
|
||||
|
||||
@@ -28,7 +28,8 @@ import {
|
||||
createStreamFactory,
|
||||
markBranchStreamUpdatedFactory,
|
||||
markCommitStreamUpdatedFactory,
|
||||
grantStreamPermissionsFactory
|
||||
grantStreamPermissionsFactory,
|
||||
getStreamRolesFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import {
|
||||
createCommitByBranchIdFactory,
|
||||
@@ -152,6 +153,7 @@ const buildFinalizeProjectInvite = () =>
|
||||
validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }),
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -38,7 +38,8 @@ import {
|
||||
getCommitStreamFactory,
|
||||
createStreamFactory,
|
||||
markCommitStreamUpdatedFactory,
|
||||
grantStreamPermissionsFactory
|
||||
grantStreamPermissionsFactory,
|
||||
getStreamRolesFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import {
|
||||
getObjectFactory,
|
||||
@@ -166,6 +167,7 @@ const buildFinalizeProjectInvite = () =>
|
||||
validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }),
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -2,7 +2,10 @@ import { buildApolloServer } from '@/app'
|
||||
import { db } from '@/db/knex'
|
||||
import { Commits, Streams, Users } from '@/modules/core/dbSchema'
|
||||
import { Roles } from '@/modules/core/helpers/mainConstants'
|
||||
import { grantStreamPermissionsFactory } from '@/modules/core/repositories/streams'
|
||||
import {
|
||||
getStreamRolesFactory,
|
||||
grantStreamPermissionsFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import { getUserFactory } from '@/modules/core/repositories/users'
|
||||
import {
|
||||
addOrUpdateStreamCollaboratorFactory,
|
||||
@@ -25,6 +28,7 @@ const addOrUpdateStreamCollaborator = addOrUpdateStreamCollaboratorFactory({
|
||||
validateStreamAccess,
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { TokenResourceIdentifierType } from '@/modules/core/domain/tokens/types'
|
||||
import { AllScopes } from '@/modules/core/helpers/mainConstants'
|
||||
import {
|
||||
createRandomEmail,
|
||||
createRandomPassword
|
||||
} from '@/modules/core/helpers/testHelpers'
|
||||
import {
|
||||
BasicTestWorkspace,
|
||||
createTestWorkspace
|
||||
} from '@/modules/workspaces/tests/helpers/creation'
|
||||
import { BasicTestUser, createTestUser } from '@/test/authHelper'
|
||||
import {
|
||||
CreateEmbedTokenDocument,
|
||||
GetActiveUserDocument,
|
||||
GetProjectDocument,
|
||||
GetWorkspaceDocument
|
||||
} from '@/test/graphql/generated/graphql'
|
||||
import {
|
||||
createTestContext,
|
||||
testApolloServer,
|
||||
TestApolloServer
|
||||
} from '@/test/graphqlHelper'
|
||||
import { BasicTestStream, createTestStream } from '@/test/speckle-helpers/streamHelper'
|
||||
import { Roles, Scopes } from '@speckle/shared'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('Embed tokens', () => {
|
||||
const adminUser: BasicTestUser = {
|
||||
id: '',
|
||||
name: 'John Speckle',
|
||||
email: createRandomEmail(),
|
||||
password: createRandomPassword()
|
||||
}
|
||||
|
||||
const workspace: BasicTestWorkspace = {
|
||||
id: '',
|
||||
ownerId: '',
|
||||
name: 'My Workspace',
|
||||
slug: ''
|
||||
}
|
||||
|
||||
const projectA: BasicTestStream = {
|
||||
id: '',
|
||||
ownerId: '',
|
||||
name: 'My Project'
|
||||
}
|
||||
const projectB: BasicTestStream = {
|
||||
id: '',
|
||||
ownerId: '',
|
||||
name: 'My Project 2'
|
||||
}
|
||||
|
||||
let apollo: TestApolloServer
|
||||
|
||||
before(async () => {
|
||||
await createTestUser(adminUser)
|
||||
|
||||
await createTestWorkspace(workspace, adminUser)
|
||||
|
||||
projectA.workspaceId = workspace.id
|
||||
projectB.workspaceId = workspace.id
|
||||
|
||||
await createTestStream(projectA, adminUser)
|
||||
await createTestStream(projectB, adminUser)
|
||||
|
||||
const adminApollo = await testApolloServer({
|
||||
context: await createTestContext({
|
||||
auth: true,
|
||||
userId: adminUser.id,
|
||||
role: Roles.Server.Admin,
|
||||
scopes: AllScopes,
|
||||
token: 'abc'
|
||||
})
|
||||
})
|
||||
|
||||
const res = await adminApollo.execute(CreateEmbedTokenDocument, {
|
||||
token: {
|
||||
projectId: projectA.id,
|
||||
resourceIdString: 'foo123'
|
||||
}
|
||||
})
|
||||
const token = res.data!.projectMutations.createEmbedToken.token
|
||||
|
||||
apollo = await testApolloServer({
|
||||
context: await createTestContext({
|
||||
auth: true,
|
||||
userId: adminUser.id,
|
||||
role: Roles.Server.Admin,
|
||||
scopes: [Scopes.Streams.Read],
|
||||
resourceAccessRules: [
|
||||
{ id: projectA.id, type: TokenResourceIdentifierType.Project }
|
||||
],
|
||||
token
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('can read associated project data', async () => {
|
||||
const res = await apollo.execute(GetProjectDocument, { id: projectA.id })
|
||||
expect(res).to.not.haveGraphQLErrors()
|
||||
expect(res.data?.project.name).to.equal(projectA.name)
|
||||
})
|
||||
|
||||
it('cannot read other project data, even if the source user has access', async () => {
|
||||
const res = await apollo.execute(GetProjectDocument, { id: projectB.id })
|
||||
expect(res).to.haveGraphQLErrors()
|
||||
})
|
||||
|
||||
it('cannot access source user profile', async () => {
|
||||
const res = await apollo.execute(GetActiveUserDocument, {})
|
||||
expect(res).to.haveGraphQLErrors()
|
||||
})
|
||||
|
||||
it('cannot access workspace data', async () => {
|
||||
const res = await apollo.execute(GetWorkspaceDocument, {
|
||||
workspaceId: workspace.id
|
||||
})
|
||||
expect(res).to.haveGraphQLErrors()
|
||||
})
|
||||
})
|
||||
@@ -15,7 +15,8 @@ import {
|
||||
import {
|
||||
getStreamFactory,
|
||||
createStreamFactory,
|
||||
grantStreamPermissionsFactory
|
||||
grantStreamPermissionsFactory,
|
||||
getStreamRolesFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import { db } from '@/db/knex'
|
||||
import {
|
||||
@@ -86,6 +87,7 @@ const buildFinalizeProjectInvite = () =>
|
||||
validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }),
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -11,7 +11,8 @@ import { ForbiddenError } from '@/modules/shared/errors'
|
||||
import {
|
||||
getStreamFactory,
|
||||
createStreamFactory,
|
||||
grantStreamPermissionsFactory
|
||||
grantStreamPermissionsFactory,
|
||||
getStreamRolesFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import { db } from '@/db/knex'
|
||||
import {
|
||||
@@ -78,6 +79,7 @@ const buildFinalizeProjectInvite = () =>
|
||||
validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }),
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -18,7 +18,8 @@ import { authorizeResolver } from '@/modules/shared'
|
||||
import {
|
||||
getStreamFactory,
|
||||
revokeStreamPermissionsFactory,
|
||||
grantStreamPermissionsFactory
|
||||
grantStreamPermissionsFactory,
|
||||
getStreamRolesFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import {
|
||||
getUserFactory,
|
||||
@@ -76,6 +77,7 @@ const removeStreamCollaborator = removeStreamCollaboratorFactory({
|
||||
validateStreamAccess,
|
||||
isStreamCollaborator,
|
||||
revokeStreamPermissions: revokeStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
|
||||
@@ -83,6 +85,7 @@ const addOrUpdateStreamCollaborator = addOrUpdateStreamCollaboratorFactory({
|
||||
validateStreamAccess,
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
const getUsers = legacyGetPaginatedUsersFactory({ db })
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
storeProjectFactory,
|
||||
storeProjectRoleFactory
|
||||
} from '@/modules/core/repositories/projects'
|
||||
import { getRolesByUserIdFactory } from '@/modules/core/repositories/streams'
|
||||
import { getUserProjectRolesFactory } from '@/modules/core/repositories/projects'
|
||||
import { expectToThrow } from '@/test/assertionHelper'
|
||||
import { createTestUser } from '@/test/authHelper'
|
||||
import { Roles } from '@speckle/shared'
|
||||
@@ -105,7 +105,9 @@ describe('project repositories @core', () => {
|
||||
userId: testUser.id
|
||||
}
|
||||
await storeProjectRole(role)
|
||||
const storedRoles = await getRolesByUserIdFactory({ db })({ userId: testUser.id })
|
||||
const storedRoles = await getUserProjectRolesFactory({ db })({
|
||||
userId: testUser.id
|
||||
})
|
||||
expect(storedRoles).deep.equalInAnyOrder([
|
||||
{ resourceId: project.id, role: role.role, userId: role.userId }
|
||||
])
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,8 @@ import { getAnIdForThisOnePlease } from '@/test/helpers'
|
||||
import {
|
||||
getStreamFactory,
|
||||
createStreamFactory,
|
||||
grantStreamPermissionsFactory
|
||||
grantStreamPermissionsFactory,
|
||||
getStreamRolesFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import { db } from '@/db/knex'
|
||||
import {
|
||||
@@ -119,6 +120,7 @@ const buildFinalizeProjectInvite = () =>
|
||||
validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }),
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -12,7 +12,8 @@ import { Scopes } from '@speckle/shared'
|
||||
import {
|
||||
getStreamFactory,
|
||||
createStreamFactory,
|
||||
grantStreamPermissionsFactory
|
||||
grantStreamPermissionsFactory,
|
||||
getStreamRolesFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import { db } from '@/db/knex'
|
||||
import {
|
||||
@@ -92,6 +93,7 @@ const buildFinalizeProjectInvite = () =>
|
||||
validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }),
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
createStreamFactory,
|
||||
deleteStreamFactory,
|
||||
getStreamFactory,
|
||||
getStreamRolesFactory,
|
||||
getStreamsCollaboratorsFactory,
|
||||
grantStreamPermissionsFactory,
|
||||
markBranchStreamUpdatedFactory,
|
||||
@@ -163,6 +164,7 @@ const buildFinalizeProjectInvite = () =>
|
||||
validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }),
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
}),
|
||||
@@ -245,6 +247,7 @@ const addOrUpdateStreamCollaborator = addOrUpdateStreamCollaboratorFactory({
|
||||
validateStreamAccess,
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
const isStreamCollaborator = isStreamCollaboratorFactory({
|
||||
|
||||
@@ -37,7 +37,9 @@ import {
|
||||
grantStreamPermissionsFactory,
|
||||
markCommitStreamUpdatedFactory,
|
||||
deleteStreamFactory,
|
||||
getUserDeletableStreamsFactory
|
||||
getUserDeletableStreamsFactory,
|
||||
getStreamRolesFactory,
|
||||
legacyGetStreamsFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import {
|
||||
getObjectFactory,
|
||||
@@ -128,6 +130,8 @@ import {
|
||||
validateStreamAccessFactory
|
||||
} from '@/modules/core/services/streams/access'
|
||||
import { authorizeResolver } from '@/modules/shared'
|
||||
import { getUserWorkspaceSeatsFactory } from '@/modules/workspacesCore/repositories/workspaces'
|
||||
import { queryAllProjectsFactory } from '@/modules/core/services/projects'
|
||||
|
||||
const getServerInfo = getServerInfoFactory({ db })
|
||||
const getUser = legacyGetUserFactory({ db })
|
||||
@@ -167,6 +171,7 @@ const buildFinalizeProjectInvite = () =>
|
||||
validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }),
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
}),
|
||||
@@ -279,6 +284,10 @@ const deleteUser = deleteUserFactory({
|
||||
logger: dbLogger,
|
||||
isLastAdminUser: isLastAdminUserFactory({ db }),
|
||||
getUserDeletableStreams: getUserDeletableStreamsFactory({ db }),
|
||||
queryAllProjects: queryAllProjectsFactory({
|
||||
getStreams: legacyGetStreamsFactory({ db })
|
||||
}),
|
||||
getUserWorkspaceSeats: getUserWorkspaceSeatsFactory({ db }),
|
||||
deleteAllUserInvites: deleteAllUserInvitesFactory({ db }),
|
||||
deleteUserRecord: deleteUserRecordFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
|
||||
@@ -38,12 +38,15 @@ import {
|
||||
} from '@/modules/serverinvites/repositories/serverInvites'
|
||||
import {
|
||||
deleteStreamFactory,
|
||||
getUserDeletableStreamsFactory
|
||||
getUserDeletableStreamsFactory,
|
||||
legacyGetStreamsFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import { dbLogger } from '@/observability/logging'
|
||||
import { getServerInfoFactory } from '@/modules/core/repositories/server'
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { expect } from 'chai'
|
||||
import { getUserWorkspaceSeatsFactory } from '@/modules/workspacesCore/repositories/workspaces'
|
||||
import { queryAllProjectsFactory } from '@/modules/core/services/projects'
|
||||
|
||||
const getUsers = legacyGetPaginatedUsersFactory({ db })
|
||||
const countUsers = legacyGetPaginatedUsersCountFactory({ db })
|
||||
@@ -81,6 +84,10 @@ const deleteUser = deleteUserFactory({
|
||||
logger: dbLogger,
|
||||
isLastAdminUser: isLastAdminUserFactory({ db }),
|
||||
getUserDeletableStreams: getUserDeletableStreamsFactory({ db }),
|
||||
queryAllProjects: queryAllProjectsFactory({
|
||||
getStreams: legacyGetStreamsFactory({ db })
|
||||
}),
|
||||
getUserWorkspaceSeats: getUserWorkspaceSeatsFactory({ db }),
|
||||
deleteAllUserInvites: deleteAllUserInvitesFactory({ db }),
|
||||
deleteUserRecord: deleteUserRecordFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
|
||||
@@ -12,6 +12,7 @@ import { createAuthedTestContext, ServerAndContext } from '@/test/graphqlHelper'
|
||||
import {
|
||||
createStreamFactory,
|
||||
getStreamFactory,
|
||||
getStreamRolesFactory,
|
||||
grantStreamPermissionsFactory
|
||||
} from '@/modules/core/repositories/streams'
|
||||
import { db } from '@/db/knex'
|
||||
@@ -86,6 +87,7 @@ const buildFinalizeProjectInvite = () =>
|
||||
validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }),
|
||||
getUser,
|
||||
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
|
||||
getStreamRoles: getStreamRolesFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -925,6 +925,12 @@ export type CreateCommentReplyInput = {
|
||||
threadId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type CreateEmbedTokenReturn = {
|
||||
__typename?: 'CreateEmbedTokenReturn';
|
||||
token: Scalars['String']['output'];
|
||||
tokenMetadata: EmbedToken;
|
||||
};
|
||||
|
||||
export type CreateModelInput = {
|
||||
description?: InputMaybe<Scalars['String']['input']>;
|
||||
name: Scalars['String']['input'];
|
||||
@@ -1003,6 +1009,32 @@ export type EmailVerificationRequestInput = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
/** A token used to enable an embedded viewer for a private project */
|
||||
export type EmbedToken = {
|
||||
__typename?: 'EmbedToken';
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
lastUsed: Scalars['DateTime']['output'];
|
||||
lifespan: Scalars['BigInt']['output'];
|
||||
projectId: Scalars['String']['output'];
|
||||
resourceIdString: Scalars['String']['output'];
|
||||
tokenId: Scalars['String']['output'];
|
||||
user?: Maybe<LimitedUser>;
|
||||
};
|
||||
|
||||
export type EmbedTokenCollection = {
|
||||
__typename?: 'EmbedTokenCollection';
|
||||
cursor?: Maybe<Scalars['String']['output']>;
|
||||
items: Array<EmbedToken>;
|
||||
totalCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type EmbedTokenCreateInput = {
|
||||
lifespan?: InputMaybe<Scalars['BigInt']['input']>;
|
||||
projectId: Scalars['String']['input'];
|
||||
/** The model(s) and version(s) string used in the embed url */
|
||||
resourceIdString: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type FileUpload = {
|
||||
__typename?: 'FileUpload';
|
||||
branchName: Scalars['String']['output'];
|
||||
@@ -2098,6 +2130,7 @@ export type Project = {
|
||||
description?: Maybe<Scalars['String']['output']>;
|
||||
/** Public project-level configuration for embedded viewer */
|
||||
embedOptions: ProjectEmbedOptions;
|
||||
embedTokens: EmbedTokenCollection;
|
||||
hasAccessToFeature: Scalars['Boolean']['output'];
|
||||
id: Scalars['ID']['output'];
|
||||
invitableCollaborators: WorkspaceCollaboratorCollection;
|
||||
@@ -2180,6 +2213,12 @@ export type ProjectCommentThreadsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type ProjectEmbedTokensArgs = {
|
||||
cursor?: InputMaybe<Scalars['String']['input']>;
|
||||
limit?: InputMaybe<Scalars['Int']['input']>;
|
||||
};
|
||||
|
||||
|
||||
export type ProjectHasAccessToFeatureArgs = {
|
||||
featureName: WorkspaceFeatureName;
|
||||
};
|
||||
@@ -2579,6 +2618,7 @@ export type ProjectMutations = {
|
||||
batchDelete: Scalars['Boolean']['output'];
|
||||
/** Create new project */
|
||||
create: Project;
|
||||
createEmbedToken: CreateEmbedTokenReturn;
|
||||
/**
|
||||
* Create onboarding/tutorial project. If one is already created for the active user, that
|
||||
* one will be returned instead.
|
||||
@@ -2590,6 +2630,8 @@ export type ProjectMutations = {
|
||||
invites: ProjectInviteMutations;
|
||||
/** Leave a project. Only possible if you're not the last remaining owner. */
|
||||
leave: Scalars['Boolean']['output'];
|
||||
revokeEmbedToken: Scalars['Boolean']['output'];
|
||||
revokeEmbedTokens: Scalars['Boolean']['output'];
|
||||
/** Updates an existing project */
|
||||
update: Project;
|
||||
/** Update role for a collaborator */
|
||||
@@ -2612,6 +2654,11 @@ export type ProjectMutationsCreateArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type ProjectMutationsCreateEmbedTokenArgs = {
|
||||
token: EmbedTokenCreateInput;
|
||||
};
|
||||
|
||||
|
||||
export type ProjectMutationsDeleteArgs = {
|
||||
id: Scalars['String']['input'];
|
||||
};
|
||||
@@ -2622,6 +2669,17 @@ export type ProjectMutationsLeaveArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type ProjectMutationsRevokeEmbedTokenArgs = {
|
||||
projectId: Scalars['String']['input'];
|
||||
token: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type ProjectMutationsRevokeEmbedTokensArgs = {
|
||||
projectId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type ProjectMutationsUpdateArgs = {
|
||||
update: ProjectUpdateInput;
|
||||
};
|
||||
@@ -2664,6 +2722,7 @@ export type ProjectPermissionChecks = {
|
||||
canBroadcastActivity: PermissionCheckResult;
|
||||
canCreateAutomation: PermissionCheckResult;
|
||||
canCreateComment: PermissionCheckResult;
|
||||
canCreateEmbedTokens: PermissionCheckResult;
|
||||
canCreateModel: PermissionCheckResult;
|
||||
canDelete: PermissionCheckResult;
|
||||
canInvite: PermissionCheckResult;
|
||||
@@ -2672,9 +2731,11 @@ export type ProjectPermissionChecks = {
|
||||
canMoveToWorkspace: PermissionCheckResult;
|
||||
canPublish: PermissionCheckResult;
|
||||
canRead: PermissionCheckResult;
|
||||
canReadEmbedTokens: PermissionCheckResult;
|
||||
canReadSettings: PermissionCheckResult;
|
||||
canReadWebhooks: PermissionCheckResult;
|
||||
canRequestRender: PermissionCheckResult;
|
||||
canRevokeEmbedTokens: PermissionCheckResult;
|
||||
canUpdate: PermissionCheckResult;
|
||||
canUpdateAllowPublicComments: PermissionCheckResult;
|
||||
};
|
||||
|
||||
@@ -69,11 +69,16 @@ export type ProcessFileImportResult = (params: {
|
||||
|
||||
export type UpdateFileStatus = (params: {
|
||||
fileId: string
|
||||
projectId: string
|
||||
status: FileUploadConvertedStatus
|
||||
convertedMessage: string
|
||||
convertedCommitId: string | null
|
||||
}) => Promise<FileUploadRecord>
|
||||
|
||||
export type UpdateFileStatusForProjectFactory = (params: {
|
||||
projectId: string
|
||||
}) => Promise<UpdateFileStatus>
|
||||
|
||||
export type UploadedFile = UploadResult & { userId: string }
|
||||
|
||||
export type FileImportMessage = Pick<
|
||||
|
||||
@@ -25,7 +25,9 @@ import {
|
||||
} from '@/modules/shared/errors'
|
||||
import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper'
|
||||
import {
|
||||
fileImportServiceShouldUsePrivateObjectsServerUrl,
|
||||
getFileUploadUrlExpiryMinutes,
|
||||
getPrivateObjectsServerOrigin,
|
||||
getServerOrigin,
|
||||
isFileUploadsEnabled
|
||||
} from '@/modules/shared/helpers/envHelper'
|
||||
@@ -68,6 +70,11 @@ import cryptoRandomString from 'crypto-random-string'
|
||||
import { getFeatureFlags } from '@speckle/shared/environment'
|
||||
import { throwIfResourceAccessNotAllowed } from '@/modules/core/helpers/token'
|
||||
import { TokenResourceIdentifierType } from '@/modules/core/domain/tokens/types'
|
||||
import { getModelUploadsFactory } from '@/modules/fileuploads/services/management'
|
||||
import {
|
||||
FileUploadRecord,
|
||||
FileUploadRecordV2
|
||||
} from '@/modules/fileuploads/helpers/types'
|
||||
|
||||
const { FF_LARGE_FILE_IMPORTS_ENABLED, FF_NEXT_GEN_FILE_IMPORTER_ENABLED } =
|
||||
getFeatureFlags()
|
||||
@@ -134,7 +141,7 @@ const fileUploadMutations: Resolvers['FileUploadMutations'] = {
|
||||
|
||||
const generatePresignedUrl = generatePresignedUrlFactory({
|
||||
getSignedUrl: getSignedUrlFactory({
|
||||
objectStorage: projectStorage
|
||||
objectStorage: projectStorage.public
|
||||
}),
|
||||
upsertBlob: upsertBlobFactory({
|
||||
db: projectDb
|
||||
@@ -189,7 +196,9 @@ const fileUploadMutations: Resolvers['FileUploadMutations'] = {
|
||||
])
|
||||
|
||||
const pushJobToFileImporter = pushJobToFileImporterFactory({
|
||||
getServerOrigin,
|
||||
getServerOrigin: fileImportServiceShouldUsePrivateObjectsServerUrl()
|
||||
? getPrivateObjectsServerOrigin
|
||||
: getServerOrigin,
|
||||
createAppToken: createAppTokenFactory({
|
||||
storeApiToken: storeApiTokenFactory({ db }),
|
||||
storeTokenScopes: storeTokenScopesFactory({ db }),
|
||||
@@ -221,7 +230,7 @@ const fileUploadMutations: Resolvers['FileUploadMutations'] = {
|
||||
db: projectDb
|
||||
}),
|
||||
getBlobMetadata: getBlobMetadataFromStorage({
|
||||
objectStorage: projectStorage
|
||||
objectStorage: projectStorage.private
|
||||
})
|
||||
}),
|
||||
insertNewUploadAndNotify: FF_NEXT_GEN_FILE_IMPORTER_ENABLED
|
||||
@@ -249,11 +258,6 @@ const fileUploadMutations: Resolvers['FileUploadMutations'] = {
|
||||
}
|
||||
}
|
||||
}
|
||||
import { getModelUploadsFactory } from '@/modules/fileuploads/services/management'
|
||||
import {
|
||||
FileUploadRecord,
|
||||
FileUploadRecordV2
|
||||
} from '@/modules/fileuploads/helpers/types'
|
||||
|
||||
export = {
|
||||
Stream: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user