feat: personal project limits (#4759)
* base limits cleanup * history limit tests * canCreatePersonal & tests * canInvite block * WIP model check * fix tests * shared tests fix * lodash import fix * lint fix * help update * hopefully fixing test * CR comment
This commit is contained in:
committed by
GitHub
parent
d2f2d95bb5
commit
9998ed2586
@@ -715,8 +715,12 @@ jobs:
|
||||
command: yarn lint:ci
|
||||
working_directory: 'packages/shared'
|
||||
- run:
|
||||
name: Run tests
|
||||
command: yarn test:ci
|
||||
name: Run tests (all FFs)
|
||||
command: ENABLE_ALL_FFS=1 yarn test:ci
|
||||
working_directory: 'packages/shared'
|
||||
- run:
|
||||
name: Run tests (no FFs)
|
||||
command: DISABLE_ALL_FFS=1 yarn test:ci
|
||||
working_directory: 'packages/shared'
|
||||
- codecov/upload:
|
||||
files: packages/shared/coverage/coverage-final.json
|
||||
|
||||
@@ -120,6 +120,7 @@ generates:
|
||||
- 'test/graphql/*.{js,ts}'
|
||||
- 'modules/**/tests/helpers/graphql.ts'
|
||||
- 'modules/**/tests/helpers/*Graphql.ts'
|
||||
- 'modules/**/tests/helpers/graphql/*.ts'
|
||||
config:
|
||||
enumsAsConst: true
|
||||
scalars:
|
||||
|
||||
+64
-70
@@ -1,61 +1,54 @@
|
||||
/* istanbul ignore file */
|
||||
const expect = require('chai').expect
|
||||
import { expect } from 'chai'
|
||||
|
||||
const { beforeEachContext, initializeTestServer } = require('@/test/hooks')
|
||||
const { noErrors } = require('@/test/helpers')
|
||||
import { beforeEachContext, initializeTestServer } from '@/test/hooks'
|
||||
import { noErrors } from '@/test/helpers'
|
||||
|
||||
const { Roles, Scopes } = require('@speckle/shared')
|
||||
const { getUserActivityFactory } = require('@/modules/activitystream/repositories')
|
||||
const { db } = require('@/db/knex')
|
||||
const {
|
||||
import { Roles, Scopes } from '@speckle/shared'
|
||||
import { getUserActivityFactory } from '@/modules/activitystream/repositories'
|
||||
import { db } from '@/db/knex'
|
||||
import {
|
||||
validateStreamAccessFactory,
|
||||
addOrUpdateStreamCollaboratorFactory
|
||||
} = require('@/modules/core/services/streams/access')
|
||||
const { authorizeResolver } = require('@/modules/shared')
|
||||
const { grantStreamPermissionsFactory } = require('@/modules/core/repositories/streams')
|
||||
const {
|
||||
} from '@/modules/core/services/streams/access'
|
||||
import { authorizeResolver } from '@/modules/shared'
|
||||
import { grantStreamPermissionsFactory } from '@/modules/core/repositories/streams'
|
||||
import {
|
||||
getUserFactory,
|
||||
storeUserFactory,
|
||||
countAdminUsersFactory,
|
||||
storeUserAclFactory
|
||||
} = require('@/modules/core/repositories/users')
|
||||
const {
|
||||
} from '@/modules/core/repositories/users'
|
||||
import {
|
||||
findEmailFactory,
|
||||
createUserEmailFactory,
|
||||
ensureNoPrimaryEmailForUserFactory
|
||||
} = require('@/modules/core/repositories/userEmails')
|
||||
const {
|
||||
requestNewEmailVerificationFactory
|
||||
} = require('@/modules/emails/services/verification/request')
|
||||
const {
|
||||
deleteOldAndInsertNewVerificationFactory
|
||||
} = require('@/modules/emails/repositories')
|
||||
const { renderEmail } = require('@/modules/emails/services/emailRendering')
|
||||
const { sendEmail } = require('@/modules/emails/services/sending')
|
||||
const { createUserFactory } = require('@/modules/core/services/users/management')
|
||||
const {
|
||||
validateAndCreateUserEmailFactory
|
||||
} = require('@/modules/core/services/userEmails')
|
||||
const {
|
||||
finalizeInvitedServerRegistrationFactory
|
||||
} = require('@/modules/serverinvites/services/processing')
|
||||
const {
|
||||
} from '@/modules/core/repositories/userEmails'
|
||||
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
|
||||
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
|
||||
import { renderEmail } from '@/modules/emails/services/emailRendering'
|
||||
import { sendEmail } from '@/modules/emails/services/sending'
|
||||
import { createUserFactory } from '@/modules/core/services/users/management'
|
||||
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
|
||||
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
|
||||
import {
|
||||
deleteServerOnlyInvitesFactory,
|
||||
updateAllInviteTargetsFactory
|
||||
} = require('@/modules/serverinvites/repositories/serverInvites')
|
||||
const { createPersonalAccessTokenFactory } = require('@/modules/core/services/tokens')
|
||||
const {
|
||||
} from '@/modules/serverinvites/repositories/serverInvites'
|
||||
import { createPersonalAccessTokenFactory } from '@/modules/core/services/tokens'
|
||||
import {
|
||||
storePersonalApiTokenFactory,
|
||||
storeApiTokenFactory,
|
||||
storeTokenScopesFactory,
|
||||
storeTokenResourceAccessDefinitionsFactory
|
||||
} = require('@/modules/core/repositories/tokens')
|
||||
const { getServerInfoFactory } = require('@/modules/core/repositories/server')
|
||||
const { createObjectFactory } = require('@/modules/core/services/objects/management')
|
||||
const {
|
||||
storeSingleObjectIfNotFoundFactory
|
||||
} = require('@/modules/core/repositories/objects')
|
||||
const { getEventBus } = require('@/modules/shared/services/eventBus')
|
||||
} from '@/modules/core/repositories/tokens'
|
||||
import { getServerInfoFactory } from '@/modules/core/repositories/server'
|
||||
import { createObjectFactory } from '@/modules/core/services/objects/management'
|
||||
import { storeSingleObjectIfNotFoundFactory } from '@/modules/core/repositories/objects'
|
||||
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'
|
||||
|
||||
const getUser = getUserFactory({ db })
|
||||
const getUserActivity = getUserActivityFactory({ db })
|
||||
@@ -107,36 +100,40 @@ const createObject = createObjectFactory({
|
||||
storeSingleObjectIfNotFoundFactory: storeSingleObjectIfNotFoundFactory({ db })
|
||||
})
|
||||
|
||||
let server
|
||||
let sendRequest
|
||||
let server: http.Server
|
||||
let sendRequest: Awaited<ReturnType<typeof initializeTestServer>>['sendRequest']
|
||||
|
||||
describe('Activity @activity', () => {
|
||||
const userIz = {
|
||||
name: 'Izzy Lyseggen',
|
||||
email: 'izzybizzi@speckle.systems',
|
||||
password: 'sp0ckle sucks 9001',
|
||||
id: undefined
|
||||
id: '',
|
||||
token: ''
|
||||
}
|
||||
|
||||
const userCr = {
|
||||
name: 'Cristi Balas',
|
||||
email: 'cristib@speckle.systems',
|
||||
password: 'hack3r man 666',
|
||||
id: undefined
|
||||
id: '',
|
||||
token: ''
|
||||
}
|
||||
|
||||
const userX = {
|
||||
name: 'Mystery User',
|
||||
email: 'mysteriousDude@speckle.systems',
|
||||
password: 'super $ecret pw0rd',
|
||||
id: undefined
|
||||
id: '',
|
||||
token: ''
|
||||
}
|
||||
|
||||
const streamPublic = {
|
||||
const streamPublic: BasicTestStream = {
|
||||
name: 'a fun stream for sharing',
|
||||
description: 'for all to see!',
|
||||
isPublic: true,
|
||||
id: undefined
|
||||
id: '',
|
||||
ownerId: ''
|
||||
}
|
||||
|
||||
// const collaboratorTestStream = {
|
||||
@@ -146,24 +143,32 @@ describe('Activity @activity', () => {
|
||||
// id: undefined
|
||||
// }
|
||||
|
||||
const branchPublic = { name: '🍁maple branch' }
|
||||
const branchPublic: BasicTestBranch = {
|
||||
name: '🍁maple branch',
|
||||
id: '',
|
||||
streamId: '',
|
||||
authorId: ''
|
||||
}
|
||||
|
||||
const streamSecret = {
|
||||
const streamSecret: BasicTestStream = {
|
||||
name: 'a secret stream for me',
|
||||
description: 'for no one to see!',
|
||||
isPublic: false,
|
||||
id: undefined
|
||||
id: '',
|
||||
ownerId: ''
|
||||
}
|
||||
|
||||
const testObj = {
|
||||
hello: 'hallo',
|
||||
cool: 'kult',
|
||||
bunny: 'kanin'
|
||||
bunny: 'kanin',
|
||||
id: ''
|
||||
}
|
||||
const testObj2 = {
|
||||
goodbye: 'ha det bra',
|
||||
warm: 'varmt',
|
||||
bunny: 'kanin'
|
||||
bunny: 'kanin',
|
||||
id: ''
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
@@ -211,15 +216,10 @@ describe('Activity @activity', () => {
|
||||
// It's definitely not great that there's a full on test case in the before() hook, but that's because
|
||||
// these tests were originally written incorrectly - they depend on each other. So this is a temporary fix that
|
||||
// ensures tests can be ran in any order
|
||||
// + Avoiding GQL to bypass personal project limits, if any
|
||||
|
||||
// create stream (cr1)
|
||||
const resStream1 = await sendRequest(userCr.token, {
|
||||
query:
|
||||
'mutation createStream($myStream:StreamCreateInput!) { streamCreate(stream: $myStream) }',
|
||||
variables: { myStream: streamSecret }
|
||||
})
|
||||
expect(noErrors(resStream1))
|
||||
streamSecret.id = resStream1.body.data.streamCreate
|
||||
await createTestStream(streamSecret, userCr)
|
||||
|
||||
// create commit (cr2)
|
||||
testObj2.id = await createObject({ streamId: streamSecret.id, object: testObj2 })
|
||||
@@ -229,20 +229,14 @@ describe('Activity @activity', () => {
|
||||
expect(noErrors(resCommit1))
|
||||
|
||||
// create stream #2 (iz1)
|
||||
const resStream2 = await sendRequest(userIz.token, {
|
||||
query:
|
||||
'mutation createStream($myStream:StreamCreateInput!) { streamCreate(stream: $myStream) }',
|
||||
variables: { myStream: streamPublic }
|
||||
})
|
||||
expect(noErrors(resStream2))
|
||||
streamPublic.id = resStream2.body.data.streamCreate
|
||||
await createTestStream(streamPublic, userIz)
|
||||
|
||||
// create branch (iz2)
|
||||
const resBranch = await sendRequest(userIz.token, {
|
||||
query: `mutation { branchCreate(branch: { streamId: "${streamPublic.id}", name: "${branchPublic.name}" }) }`
|
||||
await createTestBranch({
|
||||
branch: branchPublic,
|
||||
stream: streamPublic,
|
||||
owner: userIz
|
||||
})
|
||||
expect(noErrors(resBranch))
|
||||
branchPublic.id = resBranch.body.data.branchCreate
|
||||
|
||||
// create commit #2 (iz3)
|
||||
testObj.id = await createObject({ streamId: streamPublic.id, object: testObj })
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { AllScopes } from '@/modules/core/helpers/mainConstants'
|
||||
import { updateServerInfoFactory } from '@/modules/core/repositories/server'
|
||||
import { findInviteFactory } from '@/modules/serverinvites/repositories/serverInvites'
|
||||
import { LogicError } from '@/modules/shared/errors'
|
||||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
import { expectToThrow, itEach } from '@/test/assertionHelper'
|
||||
import { BasicTestUser, createTestUsers } from '@/test/authHelper'
|
||||
@@ -34,7 +35,8 @@ import {
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { expect } from 'chai'
|
||||
|
||||
const { FF_NO_PERSONAL_EMAILS_ENABLED } = getFeatureFlags()
|
||||
const { FF_NO_PERSONAL_EMAILS_ENABLED, FF_PERSONAL_PROJECTS_LIMITS_ENABLED } =
|
||||
getFeatureFlags()
|
||||
|
||||
const updateServerInfo = updateServerInfoFactory({ db })
|
||||
|
||||
@@ -47,6 +49,10 @@ describe('Server registration', () => {
|
||||
) => {
|
||||
return await captureCreatedInvite(async () => {
|
||||
if ('projectId' in args) {
|
||||
if (FF_PERSONAL_PROJECTS_LIMITS_ENABLED) {
|
||||
throw new LogicError('Should not be invoked when personal limits are enabled')
|
||||
}
|
||||
|
||||
await apollo.execute(CreateProjectInviteDocument, args, {
|
||||
assertNoErrors: true
|
||||
})
|
||||
@@ -154,49 +160,51 @@ describe('Server registration', () => {
|
||||
)
|
||||
expect(e.message).to.contain('Code challenge mismatch')
|
||||
})
|
||||
;(FF_PERSONAL_PROJECTS_LIMITS_ENABLED ? it.skip : it)(
|
||||
'works with stream invite and allows joining stream afterwards',
|
||||
async () => {
|
||||
const params = generateRegistrationParams()
|
||||
|
||||
it('works with stream invite and allows joining stream afterwards', async () => {
|
||||
const params = generateRegistrationParams()
|
||||
const invite = await createInviteAsAdmin({
|
||||
input: {
|
||||
email: params.user.email,
|
||||
serverRole: Roles.Server.Admin
|
||||
},
|
||||
projectId: basicAdminStream.id
|
||||
})
|
||||
expect(invite.token).to.be.ok
|
||||
|
||||
const invite = await createInviteAsAdmin({
|
||||
input: {
|
||||
email: params.user.email,
|
||||
serverRole: Roles.Server.Admin
|
||||
},
|
||||
projectId: basicAdminStream.id
|
||||
})
|
||||
expect(invite.token).to.be.ok
|
||||
params.inviteToken = invite.token
|
||||
|
||||
params.inviteToken = invite.token
|
||||
const newUser = await restApi.register(params)
|
||||
expect(newUser.role).to.equal(Roles.Server.Admin)
|
||||
|
||||
const newUser = await restApi.register(params)
|
||||
expect(newUser.role).to.equal(Roles.Server.Admin)
|
||||
const res = await apollo.execute(
|
||||
UseStreamInviteDocument,
|
||||
{
|
||||
accept: true,
|
||||
token: invite.token,
|
||||
streamId: basicAdminStream.id
|
||||
},
|
||||
{
|
||||
context: await createTestContext({
|
||||
userId: newUser.id,
|
||||
auth: true,
|
||||
role: Roles.Server.User,
|
||||
token: 'asd',
|
||||
scopes: AllScopes
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const res = await apollo.execute(
|
||||
UseStreamInviteDocument,
|
||||
{
|
||||
accept: true,
|
||||
token: invite.token,
|
||||
streamId: basicAdminStream.id
|
||||
},
|
||||
{
|
||||
context: await createTestContext({
|
||||
userId: newUser.id,
|
||||
auth: true,
|
||||
role: Roles.Server.User,
|
||||
token: 'asd',
|
||||
scopes: AllScopes
|
||||
})
|
||||
}
|
||||
)
|
||||
expect(res).to.not.haveGraphQLErrors()
|
||||
expect(res.data?.streamInviteUse).to.be.ok
|
||||
expect(await findInviteFactory({ db })({ inviteId: invite.id })).to.be.not.ok
|
||||
|
||||
expect(res).to.not.haveGraphQLErrors()
|
||||
expect(res.data?.streamInviteUse).to.be.ok
|
||||
expect(await findInviteFactory({ db })({ inviteId: invite.id })).to.be.not.ok
|
||||
|
||||
const userStreamRole = await getUserStreamRole(newUser.id, basicAdminStream.id)
|
||||
expect(userStreamRole).to.be.ok
|
||||
})
|
||||
const userStreamRole = await getUserStreamRole(newUser.id, basicAdminStream.id)
|
||||
expect(userStreamRole).to.be.ok
|
||||
}
|
||||
)
|
||||
|
||||
const inviteOnlyModeSettings = [{ inviteOnly: true }, { inviteOnly: false }]
|
||||
|
||||
@@ -220,7 +228,10 @@ describe('Server registration', () => {
|
||||
}
|
||||
|
||||
itEach(
|
||||
[{ stream: true }, { stream: false }],
|
||||
[
|
||||
...(FF_PERSONAL_PROJECTS_LIMITS_ENABLED ? [] : [{ stream: true }]),
|
||||
{ stream: false }
|
||||
],
|
||||
({ stream }) =>
|
||||
`works with valid ${
|
||||
stream ? 'stream' : 'server'
|
||||
|
||||
@@ -1135,7 +1135,8 @@ const createAppToken = createAppTokenFactory({
|
||||
id: '',
|
||||
objectId: '',
|
||||
streamId: testUserStream.id,
|
||||
authorId: testUser.id
|
||||
authorId: testUser.id,
|
||||
branchId: ''
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1223,7 +1224,8 @@ const createAppToken = createAppTokenFactory({
|
||||
authorId: testUser.id,
|
||||
streamId: testUserStream.id,
|
||||
branchName: testUserStreamModel.name,
|
||||
objectId: ''
|
||||
objectId: '',
|
||||
branchId: ''
|
||||
}
|
||||
|
||||
await createTestCommit(testVersion)
|
||||
|
||||
@@ -63,6 +63,7 @@ const command: CommandModule<
|
||||
(i): BasicTestCommit => ({
|
||||
id: '',
|
||||
objectId: '',
|
||||
branchId: '',
|
||||
streamId,
|
||||
authorId,
|
||||
message: `#${i} - ${date} - Fake commit batch`
|
||||
|
||||
@@ -69,6 +69,8 @@ export type InsertCommentPayload = MarkNullableOptional<
|
||||
text: SmartTextEditorValueSchema
|
||||
archived?: boolean
|
||||
id?: string
|
||||
createdAt?: Date
|
||||
updatedAt?: Date
|
||||
}
|
||||
>
|
||||
|
||||
@@ -239,7 +241,13 @@ export type ConvertLegacyDataToState = (
|
||||
|
||||
export type CreateCommentThreadAndNotify = (
|
||||
input: CreateCommentInput,
|
||||
userId: string
|
||||
userId: string,
|
||||
options?: Partial<{
|
||||
/**
|
||||
* Used in tests: Override createdAt date
|
||||
*/
|
||||
createdAt: Date
|
||||
}>
|
||||
) => Promise<CommentRecord>
|
||||
|
||||
export type CreateCommentReplyAndNotify = (
|
||||
|
||||
@@ -81,7 +81,6 @@ import {
|
||||
getCommitsAndTheirBranchIdsFactory,
|
||||
getSpecificBranchCommitsFactory
|
||||
} from '@/modules/core/repositories/commits'
|
||||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
import {
|
||||
getBranchLatestCommitsFactory,
|
||||
getStreamBranchesByNameFactory
|
||||
@@ -92,15 +91,9 @@ import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
||||
import { Knex } from 'knex'
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { StreamNotFoundError } from '@/modules/core/errors/stream'
|
||||
import { isCreatedBeyondHistoryLimitCutoff, getProjectLimitDate } from '@speckle/shared'
|
||||
import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper'
|
||||
import { withOperationLogging } from '@/observability/domain/businessLogging'
|
||||
import { Authz } from '@speckle/shared'
|
||||
|
||||
const { FF_FORCE_PERSONAL_PROJECTS_LIMITS_ENABLED } = getFeatureFlags()
|
||||
const getPersonalProjectLimits = FF_FORCE_PERSONAL_PROJECTS_LIMITS_ENABLED
|
||||
? () => Promise.resolve(Authz.PersonalProjectsLimits)
|
||||
: () => Promise.resolve(null)
|
||||
import { isCreatedBeyondHistoryLimitCutoffFactory } from '@/modules/gatekeeperCore/utils/limits'
|
||||
|
||||
// We can use the main DB for these
|
||||
const getStream = getStreamFactory({ db })
|
||||
@@ -222,18 +215,16 @@ export = {
|
||||
})
|
||||
}
|
||||
|
||||
const isBeyondLimit = await isCreatedBeyondHistoryLimitCutoff({
|
||||
getProjectLimitDate: getProjectLimitDate({
|
||||
getWorkspaceLimits: ctx.authLoaders.getWorkspaceLimits,
|
||||
getPersonalProjectLimits
|
||||
})
|
||||
})({ entity: parent, limitType: 'commentHistory', project })
|
||||
const isBeyondLimit = await isCreatedBeyondHistoryLimitCutoffFactory({ ctx })({
|
||||
entity: parent,
|
||||
limitType: 'commentHistory',
|
||||
project
|
||||
})
|
||||
// null is for out of limits
|
||||
if (isBeyondLimit) return null
|
||||
// why is the text nullable in the DB record?
|
||||
if (!parent.text) return ''
|
||||
|
||||
return {
|
||||
...ensureCommentSchema(parent.text),
|
||||
...ensureCommentSchema(parent.text || ''),
|
||||
projectId: parent.streamId
|
||||
}
|
||||
},
|
||||
@@ -247,15 +238,14 @@ export = {
|
||||
})
|
||||
}
|
||||
|
||||
const isBeyondLimit = await isCreatedBeyondHistoryLimitCutoff({
|
||||
getProjectLimitDate: getProjectLimitDate({
|
||||
getWorkspaceLimits: ctx.authLoaders.getWorkspaceLimits,
|
||||
getPersonalProjectLimits
|
||||
})
|
||||
})({ entity: parent, limitType: 'commentHistory', project })
|
||||
const isBeyondLimit = await isCreatedBeyondHistoryLimitCutoffFactory({ ctx })({
|
||||
entity: parent,
|
||||
limitType: 'commentHistory',
|
||||
project
|
||||
})
|
||||
// null is for out of limits
|
||||
if (isBeyondLimit) return null
|
||||
// why us the text nullable in the DB?
|
||||
|
||||
const { doc } = ensureCommentSchema(parent.text || '')
|
||||
return documentToBasicString(doc)
|
||||
},
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ensureError, SpeckleViewer } from '@speckle/shared'
|
||||
import {
|
||||
CreateCommentInput,
|
||||
CreateCommentReplyInput,
|
||||
EditCommentInput
|
||||
} from '@/modules/core/graph/generated/graphql'
|
||||
@@ -44,7 +43,7 @@ export const createCommentThreadAndNotifyFactory =
|
||||
markCommentViewed: MarkCommentViewed
|
||||
emitEvent: EventBusEmit
|
||||
}): CreateCommentThreadAndNotify =>
|
||||
async (input: CreateCommentInput, userId: string) => {
|
||||
async (input, userId, options) => {
|
||||
const [resources] = await Promise.all([
|
||||
deps.getViewerResourceItemsUngrouped({ ...input, loadedVersionsOnly: true }),
|
||||
deps.validateInputAttachments(input.projectId, input.content.blobIds || [])
|
||||
@@ -68,7 +67,10 @@ export const createCommentThreadAndNotifyFactory =
|
||||
blobIds: input.content.blobIds || undefined
|
||||
}),
|
||||
screenshot: input.screenshot,
|
||||
data: dataStruct
|
||||
data: dataStruct,
|
||||
...(options?.createdAt
|
||||
? { createdAt: options.createdAt, updatedAt: options.createdAt }
|
||||
: {})
|
||||
}
|
||||
|
||||
let comment: CommentRecord
|
||||
|
||||
@@ -51,6 +51,7 @@ describe('Project Comments', () => {
|
||||
objectId: '',
|
||||
streamId: '',
|
||||
authorId: '',
|
||||
branchId: '',
|
||||
message: 'this is my nice commit :)))',
|
||||
branchName: myBranch.name
|
||||
}
|
||||
|
||||
@@ -70,6 +70,10 @@ export type CreateCommitByBranchId = (
|
||||
sourceApplication: Nullable<string>
|
||||
totalChildrenCount?: MaybeNullOrUndefined<number>
|
||||
parents: Nullable<string[]>
|
||||
/**
|
||||
* Only used in tests: Allows to set the createdAt date
|
||||
*/
|
||||
createdAt?: Nullable<Date>
|
||||
}>
|
||||
) => Promise<CommitWithStreamBranchId>
|
||||
|
||||
@@ -83,8 +87,12 @@ export type CreateCommitByBranchName = (
|
||||
sourceApplication: Nullable<string>
|
||||
totalChildrenCount?: MaybeNullOrUndefined<number>
|
||||
parents: Nullable<string[]>
|
||||
/**
|
||||
* Only used in tests: Allows to set the createdAt date
|
||||
*/
|
||||
createdAt?: Nullable<Date>
|
||||
}>
|
||||
) => Promise<Commit>
|
||||
) => Promise<CommitWithStreamBranchId>
|
||||
|
||||
export type InsertBranchCommits = (
|
||||
branchCommits: BranchCommitRecord[],
|
||||
|
||||
@@ -78,20 +78,9 @@ import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { isRateLimiterEnabled } from '@/modules/shared/helpers/envHelper'
|
||||
import { TokenResourceIdentifierType } from '@/modules/core/domain/tokens/types'
|
||||
import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper'
|
||||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
import { withOperationLogging } from '@/observability/domain/businessLogging'
|
||||
import {
|
||||
getProjectLimitDate,
|
||||
isNonNullable,
|
||||
MaybeNullOrUndefined,
|
||||
Roles
|
||||
} from '@speckle/shared'
|
||||
import { PersonalProjectsLimits } from '@speckle/shared/authz'
|
||||
|
||||
const { FF_FORCE_PERSONAL_PROJECTS_LIMITS_ENABLED } = getFeatureFlags()
|
||||
const getPersonalProjectLimits = FF_FORCE_PERSONAL_PROJECTS_LIMITS_ENABLED
|
||||
? () => Promise.resolve(PersonalProjectsLimits)
|
||||
: () => Promise.resolve(null)
|
||||
import { isNonNullable, MaybeNullOrUndefined, Roles } from '@speckle/shared'
|
||||
import { getProjectLimitDateFactory } from '@/modules/gatekeeperCore/utils/limits'
|
||||
|
||||
const getStreams = getStreamsFactory({ db })
|
||||
|
||||
@@ -221,10 +210,10 @@ export = {
|
||||
async commits(parent, args, ctx) {
|
||||
const projectDB = await getProjectDbClient({ projectId: parent.id })
|
||||
|
||||
const limitsDate = await getProjectLimitDate({
|
||||
getWorkspaceLimits: ctx.authLoaders.getWorkspaceLimits,
|
||||
getPersonalProjectLimits
|
||||
})({ limitType: 'versionsHistory', project: parent })
|
||||
const limitsDate = await getProjectLimitDateFactory({ ctx })({
|
||||
limitType: 'versionsHistory',
|
||||
project: parent
|
||||
})
|
||||
|
||||
const getCommitsByStreamId = legacyGetPaginatedStreamCommitsPageFactory({
|
||||
db: projectDB,
|
||||
@@ -347,10 +336,10 @@ export = {
|
||||
})
|
||||
}
|
||||
|
||||
const limitsDate = await getProjectLimitDate({
|
||||
getWorkspaceLimits: ctx.authLoaders.getWorkspaceLimits,
|
||||
getPersonalProjectLimits
|
||||
})({ limitType: 'versionsHistory', project })
|
||||
const limitsDate = await getProjectLimitDateFactory({ ctx })({
|
||||
limitType: 'versionsHistory',
|
||||
project
|
||||
})
|
||||
|
||||
const getPaginatedBranchCommits = getPaginatedBranchCommitsFactory({
|
||||
getSpecificBranchCommits: getSpecificBranchCommitsFactory({ db: projectDB }),
|
||||
|
||||
@@ -40,7 +40,8 @@ import { throwForNotHavingServerRole } from '@/modules/shared/authz'
|
||||
import {
|
||||
toProjectIdWhitelist,
|
||||
isResourceAllowed,
|
||||
throwIfResourceAccessNotAllowed
|
||||
throwIfResourceAccessNotAllowed,
|
||||
throwIfNewResourceNotAllowed
|
||||
} from '@/modules/core/helpers/token'
|
||||
import {
|
||||
Resolvers,
|
||||
@@ -101,6 +102,7 @@ import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repos
|
||||
import { renderEmail } from '@/modules/emails/services/emailRendering'
|
||||
import { sendEmail } from '@/modules/emails/services/sending'
|
||||
import { ProjectRecordVisibility } from '@/modules/core/helpers/types'
|
||||
import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper'
|
||||
|
||||
const getServerInfo = getServerInfoFactory({ db })
|
||||
const getUsers = getUsersFactory({ db })
|
||||
@@ -497,6 +499,15 @@ export = {
|
||||
source: context.userId!
|
||||
})
|
||||
|
||||
throwIfNewResourceNotAllowed({
|
||||
resourceAccessRules: context.resourceAccessRules,
|
||||
resourceType: TokenResourceIdentifierType.Project
|
||||
})
|
||||
const canCreate = await context.authPolicies.project.canCreatePersonal({
|
||||
userId: context.userId!
|
||||
})
|
||||
throwIfAuthNotOk(canCreate)
|
||||
|
||||
const { id } = await withOperationLogging(
|
||||
async () =>
|
||||
await createStreamReturnRecord({
|
||||
@@ -516,12 +527,18 @@ export = {
|
||||
|
||||
async streamUpdate(_, args, context) {
|
||||
const projectId = args.stream.id
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
args.stream.id,
|
||||
Roles.Stream.Owner,
|
||||
context.resourceAccessRules
|
||||
)
|
||||
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: projectId,
|
||||
resourceType: TokenResourceIdentifierType.Project,
|
||||
resourceAccessRules: context.resourceAccessRules
|
||||
})
|
||||
const canUpdate = await context.authPolicies.project.canUpdate({
|
||||
userId: context.userId!,
|
||||
projectId
|
||||
})
|
||||
throwIfAuthNotOk(canUpdate)
|
||||
|
||||
const logger = context.log.child({
|
||||
projectId,
|
||||
streamId: projectId //legacy
|
||||
@@ -540,12 +557,18 @@ export = {
|
||||
|
||||
async streamDelete(_, args, context) {
|
||||
const projectId = args.id
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
args.id,
|
||||
Roles.Stream.Owner,
|
||||
context.resourceAccessRules
|
||||
)
|
||||
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: projectId,
|
||||
resourceType: TokenResourceIdentifierType.Project,
|
||||
resourceAccessRules: context.resourceAccessRules
|
||||
})
|
||||
const canDelete = await context.authPolicies.project.canDelete({
|
||||
userId: context.userId!,
|
||||
projectId
|
||||
})
|
||||
throwIfAuthNotOk(canDelete)
|
||||
|
||||
const logger = context.log.child({
|
||||
projectId,
|
||||
streamId: projectId //legacy
|
||||
@@ -568,6 +591,17 @@ export = {
|
||||
async () =>
|
||||
await Promise.all(
|
||||
(args.ids || []).map(async (id) => {
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: id,
|
||||
resourceType: TokenResourceIdentifierType.Project,
|
||||
resourceAccessRules: context.resourceAccessRules
|
||||
})
|
||||
const canDelete = await context.authPolicies.project.canDelete({
|
||||
userId: context.userId!,
|
||||
projectId: id
|
||||
})
|
||||
throwIfAuthNotOk(canDelete)
|
||||
|
||||
return await deleteStreamAndNotify(id, context.userId!)
|
||||
})
|
||||
),
|
||||
@@ -582,6 +616,18 @@ export = {
|
||||
|
||||
async streamUpdatePermission(_, args, context) {
|
||||
const projectId = args.permissionParams.streamId
|
||||
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: projectId,
|
||||
resourceType: TokenResourceIdentifierType.Project,
|
||||
resourceAccessRules: context.resourceAccessRules
|
||||
})
|
||||
const canUpdate = await context.authPolicies.project.canUpdate({
|
||||
userId: context.userId!,
|
||||
projectId
|
||||
})
|
||||
throwIfAuthNotOk(canUpdate)
|
||||
|
||||
const logger = context.log.child({
|
||||
projectId,
|
||||
streamId: projectId //legacy
|
||||
@@ -604,6 +650,18 @@ export = {
|
||||
|
||||
async streamRevokePermission(_, args, context) {
|
||||
const projectId = args.permissionParams.streamId
|
||||
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: projectId,
|
||||
resourceType: TokenResourceIdentifierType.Project,
|
||||
resourceAccessRules: context.resourceAccessRules
|
||||
})
|
||||
const canUpdate = await context.authPolicies.project.canUpdate({
|
||||
userId: context.userId!,
|
||||
projectId
|
||||
})
|
||||
throwIfAuthNotOk(canUpdate)
|
||||
|
||||
const logger = context.log.child({
|
||||
projectId,
|
||||
streamId: projectId //legacy
|
||||
@@ -654,6 +712,17 @@ export = {
|
||||
const { streamId } = args
|
||||
const { userId } = ctx
|
||||
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: streamId,
|
||||
resourceType: TokenResourceIdentifierType.Project,
|
||||
resourceAccessRules: ctx.resourceAccessRules
|
||||
})
|
||||
const canLeave = await ctx.authPolicies.project.canLeave({
|
||||
userId: ctx.userId!,
|
||||
projectId: streamId
|
||||
})
|
||||
throwIfAuthNotOk(canLeave)
|
||||
|
||||
const logger = ctx.log.child({
|
||||
projectId: streamId,
|
||||
streamId //legacy
|
||||
@@ -719,6 +788,17 @@ export = {
|
||||
subscribe: filteredSubscribe(
|
||||
StreamSubscriptions.StreamUpdated,
|
||||
async (payload, variables, context) => {
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: payload.id,
|
||||
resourceType: TokenResourceIdentifierType.Project,
|
||||
resourceAccessRules: context.resourceAccessRules
|
||||
})
|
||||
const canRead = await context.authPolicies.project.canRead({
|
||||
userId: context.userId!,
|
||||
projectId: payload.id
|
||||
})
|
||||
throwIfAuthNotOk(canRead)
|
||||
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
payload.id,
|
||||
@@ -734,12 +814,17 @@ export = {
|
||||
subscribe: filteredSubscribe(
|
||||
StreamSubscriptions.StreamDeleted,
|
||||
async (payload, variables, context) => {
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
payload.streamId,
|
||||
Roles.Stream.Reviewer,
|
||||
context.resourceAccessRules
|
||||
)
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: payload.streamId,
|
||||
resourceType: TokenResourceIdentifierType.Project,
|
||||
resourceAccessRules: context.resourceAccessRules
|
||||
})
|
||||
const canRead = await context.authPolicies.project.canRead({
|
||||
userId: context.userId!,
|
||||
projectId: payload.streamId
|
||||
})
|
||||
throwIfAuthNotOk(canRead)
|
||||
|
||||
return payload.streamId === variables.streamId
|
||||
}
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
ProjectSubscriptions
|
||||
} from '@/modules/shared/utils/subscriptions'
|
||||
import {
|
||||
getFeatureFlags,
|
||||
getServerOrigin,
|
||||
isRateLimiterEnabled
|
||||
} from '@/modules/shared/helpers/envHelper'
|
||||
@@ -54,16 +53,7 @@ import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper'
|
||||
import { Version } from '@/modules/core/domain/commits/types'
|
||||
import { GraphQLResolveInfo } from 'graphql'
|
||||
import { withOperationLogging } from '@/observability/domain/businessLogging'
|
||||
import {
|
||||
Authz,
|
||||
getProjectLimitDate,
|
||||
isCreatedBeyondHistoryLimitCutoff
|
||||
} from '@speckle/shared'
|
||||
|
||||
const { FF_FORCE_PERSONAL_PROJECTS_LIMITS_ENABLED } = getFeatureFlags()
|
||||
const getPersonalProjectLimits = FF_FORCE_PERSONAL_PROJECTS_LIMITS_ENABLED
|
||||
? () => Promise.resolve(Authz.PersonalProjectsLimits)
|
||||
: () => Promise.resolve(null)
|
||||
import { isCreatedBeyondHistoryLimitCutoffFactory } from '@/modules/gatekeeperCore/utils/limits'
|
||||
|
||||
/**
|
||||
* Simple utility to check if version is inside a Model or a Project
|
||||
@@ -138,12 +128,12 @@ export = {
|
||||
})
|
||||
}
|
||||
|
||||
const isBeyondLimit = await isCreatedBeyondHistoryLimitCutoff({
|
||||
getProjectLimitDate: getProjectLimitDate({
|
||||
getWorkspaceLimits: ctx.authLoaders.getWorkspaceLimits,
|
||||
getPersonalProjectLimits
|
||||
})
|
||||
})({ entity: parent, limitType: 'versionsHistory', project })
|
||||
const isBeyondLimit = await isCreatedBeyondHistoryLimitCutoffFactory({ ctx })({
|
||||
entity: parent,
|
||||
limitType: 'versionsHistory',
|
||||
project
|
||||
})
|
||||
|
||||
let lastVersion: Version | null
|
||||
if (getTypeFromPath(info) === 'Model') {
|
||||
lastVersion = await ctx.loaders
|
||||
@@ -154,6 +144,7 @@ export = {
|
||||
.forRegion({ db: projectDB })
|
||||
.streams.getLastVersion.load(parent.streamId)
|
||||
}
|
||||
|
||||
if (lastVersion?.id === parent.id) return parent.referencedObject
|
||||
if (isBeyondLimit) return null
|
||||
return parent.referencedObject
|
||||
|
||||
@@ -101,7 +101,8 @@ export const createCommitByBranchIdFactory =
|
||||
authorId,
|
||||
message,
|
||||
sourceApplication,
|
||||
parents
|
||||
parents,
|
||||
createdAt
|
||||
} = params
|
||||
|
||||
// If no total children count is passed in, get it from the original object
|
||||
@@ -129,7 +130,8 @@ export const createCommitByBranchIdFactory =
|
||||
sourceApplication,
|
||||
totalChildrenCount,
|
||||
parents,
|
||||
message
|
||||
message,
|
||||
...(createdAt ? { createdAt } : {})
|
||||
})
|
||||
const id = commit.id
|
||||
|
||||
@@ -176,7 +178,8 @@ export const createCommitByBranchNameFactory =
|
||||
message,
|
||||
sourceApplication,
|
||||
parents,
|
||||
totalChildrenCount
|
||||
totalChildrenCount,
|
||||
createdAt
|
||||
} = params
|
||||
const branchName = params.branchName.toLowerCase()
|
||||
let myBranch = await deps.getStreamBranchByName(streamId, branchName)
|
||||
@@ -200,7 +203,8 @@ export const createCommitByBranchNameFactory =
|
||||
message,
|
||||
sourceApplication,
|
||||
totalChildrenCount,
|
||||
parents
|
||||
parents,
|
||||
createdAt
|
||||
})
|
||||
|
||||
return commit
|
||||
|
||||
@@ -119,6 +119,7 @@ describe('Batch commits', () => {
|
||||
return {
|
||||
id: '',
|
||||
objectId: '',
|
||||
branchId: '',
|
||||
streamId,
|
||||
authorId: me.id
|
||||
}
|
||||
@@ -128,6 +129,7 @@ describe('Batch commits', () => {
|
||||
(): BasicTestCommit => ({
|
||||
id: '',
|
||||
objectId: '',
|
||||
branchId: '',
|
||||
streamId: otherStream.id,
|
||||
authorId: otherGuy.id
|
||||
})
|
||||
@@ -208,6 +210,7 @@ describe('Batch commits', () => {
|
||||
streamId = i % 2 === 0 ? myStream.id : otherStream.id
|
||||
return {
|
||||
id: '',
|
||||
branchId: '',
|
||||
streamId,
|
||||
objectId: '',
|
||||
authorId: me.id
|
||||
@@ -245,6 +248,7 @@ describe('Batch commits', () => {
|
||||
streamId = i % 2 === 0 ? myStream.id : otherStream.id
|
||||
return {
|
||||
id: '',
|
||||
branchId: '',
|
||||
objectId: '',
|
||||
streamId,
|
||||
authorId: me.id
|
||||
|
||||
@@ -97,7 +97,8 @@ describe('Commits (GraphQL)', () => {
|
||||
id: '',
|
||||
objectId: '',
|
||||
streamId: usePrivateStream ? myStream.id : myPrivateStream.id,
|
||||
authorId: me.id
|
||||
authorId: me.id,
|
||||
branchId: ''
|
||||
})
|
||||
|
||||
// other guys commit
|
||||
@@ -105,7 +106,8 @@ describe('Commits (GraphQL)', () => {
|
||||
id: '',
|
||||
objectId: '',
|
||||
streamId: usePrivateStream ? myStream.id : myPrivateStream.id,
|
||||
authorId: otherGuy.id
|
||||
authorId: otherGuy.id,
|
||||
branchId: ''
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,116 @@
|
||||
import { gql } from 'graphql-tag'
|
||||
|
||||
/**
|
||||
* Commits/Versions
|
||||
*/
|
||||
|
||||
export const limitedPersonalProjectCommentFragment = gql`
|
||||
fragment LimitedPersonalProjectComment on Comment {
|
||||
id
|
||||
rawText
|
||||
createdAt
|
||||
text {
|
||||
doc
|
||||
type
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const limitedPersonalProjectVersionFragment = gql`
|
||||
fragment LimitedPersonalProjectVersion on Version {
|
||||
id
|
||||
createdAt
|
||||
message
|
||||
referencedObject
|
||||
commentThreads {
|
||||
totalCount
|
||||
items {
|
||||
...LimitedPersonalProjectComment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
${limitedPersonalProjectCommentFragment}
|
||||
`
|
||||
|
||||
export const getLimitedPersonalProjectVersionsQuery = gql`
|
||||
query GetLimitedPersonalProjectVersions($projectId: String!) {
|
||||
project(id: $projectId) {
|
||||
versions {
|
||||
totalCount
|
||||
items {
|
||||
...LimitedPersonalProjectVersion
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
${limitedPersonalProjectVersionFragment}
|
||||
`
|
||||
|
||||
export const getLimitedPersonalProjectVersionQuery = gql`
|
||||
query GetLimitedPersonalProjectVersion($projectId: String!, $versionId: String!) {
|
||||
project(id: $projectId) {
|
||||
version(id: $versionId) {
|
||||
...LimitedPersonalProjectVersion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
${limitedPersonalProjectVersionFragment}
|
||||
`
|
||||
|
||||
export const limitedPersonalStreamCommitFragment = gql`
|
||||
fragment LimitedPersonalStreamCommit on Commit {
|
||||
id
|
||||
message
|
||||
referencedObject
|
||||
createdAt
|
||||
}
|
||||
`
|
||||
|
||||
export const getLimitedPersonalStreamCommitsQuery = gql`
|
||||
query GetLimitedPersonalStreamCommits($streamId: String!) {
|
||||
stream(id: $streamId) {
|
||||
commits {
|
||||
totalCount
|
||||
items {
|
||||
...LimitedPersonalStreamCommit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
${limitedPersonalStreamCommitFragment}
|
||||
`
|
||||
|
||||
/**
|
||||
* Comments
|
||||
*/
|
||||
|
||||
export const getLimitedPersonalProjectCommentsQuery = gql`
|
||||
query GetLimitedPersonalProjectComments($projectId: String!) {
|
||||
project(id: $projectId) {
|
||||
commentThreads {
|
||||
totalCount
|
||||
items {
|
||||
...LimitedPersonalProjectComment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
${limitedPersonalProjectCommentFragment}
|
||||
`
|
||||
|
||||
export const getLimitedPersonalProjectCommentQuery = gql`
|
||||
query GetLimitedPersonalProjectComment($projectId: String!, $commentId: String!) {
|
||||
project(id: $projectId) {
|
||||
comment(id: $commentId) {
|
||||
...LimitedPersonalProjectComment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
${limitedPersonalProjectCommentFragment}
|
||||
`
|
||||
@@ -0,0 +1,295 @@
|
||||
import { CommentRecord } from '@/modules/comments/helpers/types'
|
||||
import { ProjectRecordVisibility } from '@/modules/core/helpers/types'
|
||||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
import { expectToThrow, itEach } from '@/test/assertionHelper'
|
||||
import { BasicTestUser, createTestUsers } from '@/test/authHelper'
|
||||
import {
|
||||
CreateProjectDocument,
|
||||
CreateProjectInviteDocument,
|
||||
CreateProjectModelDocument,
|
||||
GetLimitedPersonalProjectCommentDocument,
|
||||
GetLimitedPersonalProjectCommentsDocument,
|
||||
GetLimitedPersonalProjectVersionDocument,
|
||||
GetLimitedPersonalProjectVersionsDocument,
|
||||
GetLimitedPersonalStreamCommitsDocument,
|
||||
LimitedPersonalProjectCommentFragment,
|
||||
LimitedPersonalProjectVersionFragment,
|
||||
LimitedPersonalStreamCommitFragment
|
||||
} from '@/test/graphql/generated/graphql'
|
||||
import { testApolloServer, TestApolloServer } from '@/test/graphqlHelper'
|
||||
import { beforeEachContext } from '@/test/hooks'
|
||||
import { createTestComment } from '@/test/speckle-helpers/commentHelper'
|
||||
import { BasicTestCommit, createTestCommits } from '@/test/speckle-helpers/commitHelper'
|
||||
import { BasicTestStream, createTestStreams } from '@/test/speckle-helpers/streamHelper'
|
||||
import { expect } from 'chai'
|
||||
import dayjs from 'dayjs'
|
||||
import { flatten } from 'lodash'
|
||||
|
||||
const { FF_PERSONAL_PROJECTS_LIMITS_ENABLED } = getFeatureFlags()
|
||||
|
||||
;(FF_PERSONAL_PROJECTS_LIMITS_ENABLED ? describe : describe.skip)(
|
||||
'Personal project limits @graphql',
|
||||
() => {
|
||||
const me: BasicTestUser = {
|
||||
id: '',
|
||||
email: '',
|
||||
name: 'meeeeeee'
|
||||
}
|
||||
|
||||
const preexistingProject: BasicTestStream = {
|
||||
id: '',
|
||||
ownerId: '',
|
||||
name: 'preexisting project',
|
||||
description: 'preexisting project',
|
||||
visibility: ProjectRecordVisibility.Private
|
||||
}
|
||||
|
||||
let apollo: TestApolloServer
|
||||
|
||||
before(async () => {
|
||||
await beforeEachContext()
|
||||
await createTestUsers([me])
|
||||
await createTestStreams([[preexistingProject, me]])
|
||||
apollo = await testApolloServer({ authUserId: me.id })
|
||||
})
|
||||
|
||||
describe('history limits', () => {
|
||||
const oldVersion: BasicTestCommit = {
|
||||
id: '',
|
||||
objectId: '',
|
||||
streamId: '',
|
||||
authorId: '',
|
||||
branchId: '',
|
||||
createdAt: dayjs().subtract(8, 'day').toDate() // limit and -1 day
|
||||
}
|
||||
|
||||
const newVersion: BasicTestCommit = {
|
||||
id: '',
|
||||
objectId: '',
|
||||
streamId: '',
|
||||
authorId: '',
|
||||
branchId: ''
|
||||
}
|
||||
|
||||
let comments: CommentRecord[]
|
||||
|
||||
before(async () => {
|
||||
// old & new commit
|
||||
await createTestCommits([oldVersion, newVersion], {
|
||||
owner: me,
|
||||
stream: preexistingProject
|
||||
})
|
||||
|
||||
// old & new comment for each commit
|
||||
const commentCreateResults = await Promise.all(
|
||||
[newVersion, oldVersion].map(async (commit) => {
|
||||
const comments: CommentRecord[] = []
|
||||
|
||||
// Old
|
||||
const oldComment = await createTestComment({
|
||||
userId: me.id,
|
||||
projectId: preexistingProject.id,
|
||||
modelId: commit.branchId,
|
||||
versionId: commit.id,
|
||||
createdAt: dayjs().subtract(8, 'day').toDate() // limit and -1 day
|
||||
})
|
||||
comments.push(oldComment)
|
||||
|
||||
// New
|
||||
const newComment = await createTestComment({
|
||||
userId: me.id,
|
||||
projectId: preexistingProject.id,
|
||||
modelId: commit.branchId,
|
||||
versionId: commit.id
|
||||
})
|
||||
comments.push(newComment)
|
||||
|
||||
return comments
|
||||
})
|
||||
)
|
||||
comments = flatten(commentCreateResults)
|
||||
})
|
||||
|
||||
const checkComment = (comment: LimitedPersonalProjectCommentFragment) => {
|
||||
const isOldComment = dayjs(comment.createdAt).isBefore(
|
||||
dayjs().subtract(7, 'day')
|
||||
)
|
||||
|
||||
if (isOldComment) {
|
||||
expect(comment.text).to.not.be.ok
|
||||
expect(comment.rawText).to.not.be.ok
|
||||
} else {
|
||||
expect(comment.text).to.be.ok
|
||||
expect(comment.rawText).to.be.ok
|
||||
}
|
||||
}
|
||||
|
||||
const checkVersion = (
|
||||
version: BasicTestCommit,
|
||||
resultVersions: LimitedPersonalProjectVersionFragment[]
|
||||
) => {
|
||||
const isOld = dayjs(version.createdAt).isBefore(dayjs().subtract(7, 'day'))
|
||||
|
||||
const retVersion = resultVersions.find((v) => v.id === version.id)
|
||||
expect(retVersion).to.be.ok
|
||||
|
||||
if (isOld) {
|
||||
expect(retVersion?.referencedObject).to.not.be.ok
|
||||
} else {
|
||||
expect(retVersion?.referencedObject).to.be.ok
|
||||
}
|
||||
|
||||
const comments = retVersion?.commentThreads
|
||||
expect(comments).to.be.ok
|
||||
expect(comments?.totalCount).to.equal(2)
|
||||
comments?.items.forEach(checkComment)
|
||||
}
|
||||
|
||||
const checkCommit = (
|
||||
commit: BasicTestCommit,
|
||||
resultCommits: LimitedPersonalStreamCommitFragment[]
|
||||
) => {
|
||||
// const isOld = dayjs(commit.createdAt).isBefore(dayjs().subtract(7, 'day'))
|
||||
|
||||
const retCommit = resultCommits.find((v) => v.id === commit.id)
|
||||
expect(retCommit).to.be.ok
|
||||
|
||||
// if (isOld) {
|
||||
// expect(retCommit?.referencedObject).to.not.be.ok
|
||||
// } else {
|
||||
// expect(retCommit?.referencedObject).to.be.ok
|
||||
// }
|
||||
}
|
||||
|
||||
it('followed when querying for project versions', async () => {
|
||||
const res = await apollo.execute(
|
||||
GetLimitedPersonalProjectVersionsDocument,
|
||||
{
|
||||
projectId: preexistingProject.id
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
|
||||
const versions = res.data?.project.versions
|
||||
expect(versions?.items).to.be.ok
|
||||
expect(versions?.totalCount).to.equal(2)
|
||||
|
||||
checkVersion(oldVersion, versions!.items)
|
||||
checkVersion(newVersion, versions!.items)
|
||||
})
|
||||
|
||||
itEach(
|
||||
[{ old: true }, { old: false }],
|
||||
({ old }) =>
|
||||
`followed when querying for ${old ? 'old' : 'new'} project version`,
|
||||
async ({ old }) => {
|
||||
const baseVersion = old ? oldVersion : newVersion
|
||||
const res = await apollo.execute(
|
||||
GetLimitedPersonalProjectVersionDocument,
|
||||
{
|
||||
projectId: preexistingProject.id,
|
||||
versionId: baseVersion.id
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
|
||||
const version = res.data?.project.version
|
||||
expect(version).to.be.ok
|
||||
|
||||
checkVersion(baseVersion, [version!])
|
||||
}
|
||||
)
|
||||
|
||||
it('followed when querying for stream commits', async () => {
|
||||
const res = await apollo.execute(
|
||||
GetLimitedPersonalStreamCommitsDocument,
|
||||
{
|
||||
streamId: preexistingProject.id
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
|
||||
const commits = res.data?.stream?.commits
|
||||
expect(commits?.items).to.be.ok
|
||||
|
||||
checkCommit(newVersion, commits!.items!)
|
||||
await expectToThrow(() => checkCommit(oldVersion, commits!.items!)) // old commit should be filtered out
|
||||
})
|
||||
|
||||
it('followed when querying for project comments', async () => {
|
||||
const res = await apollo.execute(
|
||||
GetLimitedPersonalProjectCommentsDocument,
|
||||
{
|
||||
projectId: preexistingProject.id
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
|
||||
const comments = res.data?.project.commentThreads
|
||||
expect(comments?.items).to.be.ok
|
||||
expect(comments?.totalCount).to.equal(4) // 2 from each version
|
||||
comments?.items.forEach(checkComment)
|
||||
})
|
||||
|
||||
it('followed when quering for project comment', async () => {
|
||||
const test = async (comment: CommentRecord) => {
|
||||
const res = await apollo.execute(
|
||||
GetLimitedPersonalProjectCommentDocument,
|
||||
{
|
||||
projectId: preexistingProject.id,
|
||||
commentId: comment.id
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
|
||||
const resultComment = res.data?.project.comment
|
||||
expect(resultComment).to.be.ok
|
||||
checkComment(resultComment!)
|
||||
}
|
||||
|
||||
await Promise.all(comments.map(test))
|
||||
})
|
||||
})
|
||||
|
||||
it('prevent new personal project creation', async () => {
|
||||
const res = await apollo.execute(CreateProjectDocument, {
|
||||
input: {
|
||||
name: 'test personal project'
|
||||
}
|
||||
})
|
||||
|
||||
expect(res).to.haveGraphQLErrors(
|
||||
"Projects can't be created outside of workspaces"
|
||||
)
|
||||
expect(res.data?.projectMutations.create.id).to.not.be.ok
|
||||
})
|
||||
|
||||
it('prevent new invites to personal projects', async () => {
|
||||
const res = await apollo.execute(CreateProjectInviteDocument, {
|
||||
projectId: preexistingProject.id,
|
||||
input: {
|
||||
email: 'personalprojectlimitsinvite@example.com'
|
||||
}
|
||||
})
|
||||
|
||||
expect(res).to.haveGraphQLErrors(
|
||||
'No new collaborators can be added to personal projects'
|
||||
)
|
||||
expect(res.data?.projectMutations.invites.create.id).to.not.be.ok
|
||||
})
|
||||
|
||||
it('prevent new models in personal projects', async () => {
|
||||
const res = await apollo.execute(CreateProjectModelDocument, {
|
||||
input: {
|
||||
projectId: preexistingProject.id,
|
||||
name: 'test personal project model'
|
||||
}
|
||||
})
|
||||
|
||||
expect(res).to.haveGraphQLErrors(
|
||||
'No new models can be added to personal projects'
|
||||
)
|
||||
expect(res.data?.modelMutations.create.id).to.not.be.ok
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -39,7 +39,8 @@ describe('Object repository functions', () => {
|
||||
id: '',
|
||||
objectId: '',
|
||||
streamId: '',
|
||||
authorId: ''
|
||||
authorId: '',
|
||||
branchId: ''
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
@@ -54,6 +55,7 @@ describe('Object repository functions', () => {
|
||||
owner: adminUser
|
||||
})
|
||||
|
||||
testVersion.branchId = ''
|
||||
testVersion.branchName = testModel.name
|
||||
testVersion.objectId = await createTestObject({ projectId: testProject.id })
|
||||
|
||||
|
||||
@@ -7,37 +7,43 @@ import { Roles } from '@/modules/core/helpers/mainConstants'
|
||||
import { expect } from 'chai'
|
||||
import { beforeEachContext } from '@/test/hooks'
|
||||
import { TestApolloServer, testApolloServer } from '@/test/graphqlHelper'
|
||||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
|
||||
describe('Projects GraphQL @core (outside of workspaces)', () => {
|
||||
let apollo: TestApolloServer
|
||||
const me: BasicTestUser = {
|
||||
id: '',
|
||||
name: 'me',
|
||||
email: '',
|
||||
role: Roles.Server.Admin
|
||||
}
|
||||
const { FF_PERSONAL_PROJECTS_LIMITS_ENABLED } = getFeatureFlags()
|
||||
|
||||
before(async () => {
|
||||
await beforeEachContext()
|
||||
await createTestUser(me)
|
||||
apollo = await testApolloServer({ authUserId: me.id })
|
||||
})
|
||||
;(FF_PERSONAL_PROJECTS_LIMITS_ENABLED ? describe.skip : describe)(
|
||||
'Projects GraphQL @core (outside of workspaces)',
|
||||
() => {
|
||||
let apollo: TestApolloServer
|
||||
const me: BasicTestUser = {
|
||||
id: '',
|
||||
name: 'me',
|
||||
email: '',
|
||||
role: Roles.Server.Admin
|
||||
}
|
||||
|
||||
describe('when being created', () => {
|
||||
it('should use private visibility by default ', async () => {
|
||||
const res = await apollo.execute(
|
||||
CreateProjectDocument,
|
||||
{
|
||||
input: {
|
||||
name: 'Test Default Visibility Project'
|
||||
}
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
|
||||
const project = res.data?.projectMutations.create
|
||||
expect(project).to.be.ok
|
||||
expect(project?.visibility).to.eq(ProjectVisibility.Private)
|
||||
before(async () => {
|
||||
await beforeEachContext()
|
||||
await createTestUser(me)
|
||||
apollo = await testApolloServer({ authUserId: me.id })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when being created', () => {
|
||||
it('should use private visibility by default ', async () => {
|
||||
const res = await apollo.execute(
|
||||
CreateProjectDocument,
|
||||
{
|
||||
input: {
|
||||
name: 'Test Default Visibility Project'
|
||||
}
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
|
||||
const project = res.data?.projectMutations.create
|
||||
expect(project).to.be.ok
|
||||
expect(project?.visibility).to.eq(ProjectVisibility.Private)
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -688,6 +688,7 @@ describe('Core GraphQL Subscriptions (New)', () => {
|
||||
objectId: '',
|
||||
id: '',
|
||||
authorId: '',
|
||||
branchId: '',
|
||||
message
|
||||
}
|
||||
|
||||
@@ -708,6 +709,7 @@ describe('Core GraphQL Subscriptions (New)', () => {
|
||||
objectId: '',
|
||||
id: '',
|
||||
authorId: '',
|
||||
branchId: '',
|
||||
message: 'Commit to Delete'
|
||||
}
|
||||
await createTestCommits([commitToDelete], {
|
||||
@@ -761,6 +763,7 @@ describe('Core GraphQL Subscriptions (New)', () => {
|
||||
objectId: '',
|
||||
id: '',
|
||||
authorId: '',
|
||||
branchId: '',
|
||||
message: 'Commit to Update'
|
||||
}
|
||||
await createTestCommits([commitToUpdate], {
|
||||
@@ -846,6 +849,7 @@ describe('Core GraphQL Subscriptions (New)', () => {
|
||||
objectId: '',
|
||||
id: '',
|
||||
authorId: '',
|
||||
branchId: '',
|
||||
message: 'Random Commit'
|
||||
}
|
||||
await createTestCommits([commit], {
|
||||
|
||||
@@ -83,7 +83,7 @@ const createUser = createUserFactory({
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
|
||||
const { FF_BILLING_INTEGRATION_ENABLED, FF_FORCE_PERSONAL_PROJECTS_LIMITS_ENABLED } =
|
||||
const { FF_BILLING_INTEGRATION_ENABLED, FF_PERSONAL_PROJECTS_LIMITS_ENABLED } =
|
||||
getFeatureFlags()
|
||||
|
||||
describe('Versions graphql @core', () => {
|
||||
@@ -136,7 +136,7 @@ describe('Versions graphql @core', () => {
|
||||
}
|
||||
)
|
||||
})
|
||||
;(FF_FORCE_PERSONAL_PROJECTS_LIMITS_ENABLED ? describe : describe.skip)(
|
||||
;(FF_PERSONAL_PROJECTS_LIMITS_ENABLED ? describe : describe.skip)(
|
||||
'Version.referencedObject',
|
||||
() => {
|
||||
const tenDaysAgo = dayjs().subtract(10, 'day').toDate()
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
BasicTestWorkspace,
|
||||
createTestWorkspace
|
||||
} from '@/modules/workspaces/tests/helpers/creation'
|
||||
import { BasicTestUser, createTestUsers } from '@/test/authHelper'
|
||||
import {
|
||||
CreateModelInput,
|
||||
@@ -18,6 +22,13 @@ describe('Models', () => {
|
||||
id: ''
|
||||
}
|
||||
|
||||
const workspace: BasicTestWorkspace = {
|
||||
id: '',
|
||||
slug: '',
|
||||
ownerId: '',
|
||||
name: 'private workspace'
|
||||
}
|
||||
|
||||
const myPrivateStream: BasicTestStream = {
|
||||
name: 'this is my private stream #1',
|
||||
isPublic: false,
|
||||
@@ -28,6 +39,11 @@ describe('Models', () => {
|
||||
before(async () => {
|
||||
await beforeEachContext()
|
||||
await createTestUsers([me])
|
||||
|
||||
// workspace, to avoid personal project limits
|
||||
await createTestWorkspace(workspace, me)
|
||||
myPrivateStream.workspaceId = workspace.id
|
||||
|
||||
await createTestStreams([[myPrivateStream, me]])
|
||||
})
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ import {
|
||||
import { createTestObject } from '@/test/speckle-helpers/commitHelper'
|
||||
import { times } from 'lodash'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
|
||||
const { FF_PERSONAL_PROJECTS_LIMITS_ENABLED } = getFeatureFlags()
|
||||
|
||||
describe('Projects', () => {
|
||||
const me: BasicTestUser = {
|
||||
@@ -41,24 +44,28 @@ describe('Projects', () => {
|
||||
authUserId: me.id
|
||||
})
|
||||
})
|
||||
;(FF_PERSONAL_PROJECTS_LIMITS_ENABLED ? it.skip : it)(
|
||||
'can be created',
|
||||
async () => {
|
||||
const input: ProjectCreateInput = {
|
||||
name: 'my first project',
|
||||
description: 'ayyooo'
|
||||
}
|
||||
const res = await apollo.execute(CreateProjectDocument, {
|
||||
input
|
||||
})
|
||||
|
||||
it('can be created', async () => {
|
||||
const input: ProjectCreateInput = {
|
||||
name: 'my first project',
|
||||
description: 'ayyooo'
|
||||
expect(res).to.not.haveGraphQLErrors()
|
||||
expect(res.data?.projectMutations.create.id).to.be.ok
|
||||
expect(res.data?.projectMutations.create.name).to.equal(input.name)
|
||||
expect(res.data?.projectMutations.create.description).to.equal(
|
||||
input.description
|
||||
)
|
||||
expect(res.data?.projectMutations.create.visibility).to.equal(
|
||||
ProjectVisibility.Private // private by default
|
||||
)
|
||||
}
|
||||
const res = await apollo.execute(CreateProjectDocument, {
|
||||
input
|
||||
})
|
||||
|
||||
expect(res).to.not.haveGraphQLErrors()
|
||||
expect(res.data?.projectMutations.create.id).to.be.ok
|
||||
expect(res.data?.projectMutations.create.name).to.equal(input.name)
|
||||
expect(res.data?.projectMutations.create.description).to.equal(input.description)
|
||||
expect(res.data?.projectMutations.create.visibility).to.equal(
|
||||
ProjectVisibility.Private // private by default
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
describe('after creation', () => {
|
||||
const myStream: BasicTestStream = {
|
||||
|
||||
@@ -6,7 +6,12 @@ import {
|
||||
} from '@/modules/gatekeeper/repositories/workspaceSeat'
|
||||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
import { defineRequestDataloaders } from '@/modules/shared/helpers/graphqlHelper'
|
||||
import { WorkspacePlan } from '@speckle/shared'
|
||||
import {
|
||||
WorkspaceLimits,
|
||||
WorkspacePaidPlanConfigs,
|
||||
WorkspacePlan,
|
||||
WorkspaceUnpaidPlanConfigs
|
||||
} from '@speckle/shared'
|
||||
|
||||
const { FF_GATEKEEPER_MODULE_ENABLED } = getFeatureFlags()
|
||||
|
||||
@@ -74,6 +79,24 @@ const dataLoadersDefinition = defineRequestDataloaders(
|
||||
{
|
||||
cacheKeyFn: ({ workspaceId }) => workspaceId
|
||||
}
|
||||
),
|
||||
getWorkspaceLimits: createLoader<string, WorkspaceLimits | null>(
|
||||
async (workspaceIds) => {
|
||||
const workspacePlans = await getWorkspacePlansByWorkspaceId({
|
||||
workspaceIds: workspaceIds.slice()
|
||||
})
|
||||
|
||||
return workspaceIds.map((workspaceId) => {
|
||||
const plan = workspacePlans[workspaceId]
|
||||
if (!plan) return null
|
||||
|
||||
const config = {
|
||||
...WorkspacePaidPlanConfigs,
|
||||
...WorkspaceUnpaidPlanConfigs
|
||||
}
|
||||
return config[plan.name]?.limits || null
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,7 +298,8 @@ describe('Workspaces Billing', () => {
|
||||
authorId: user.id,
|
||||
objectId,
|
||||
streamId: project.id,
|
||||
branchName: modelWithVersions.name
|
||||
branchName: modelWithVersions.name,
|
||||
branchId: ''
|
||||
})
|
||||
|
||||
const session = await login(user)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { GraphQLContext } from '@/modules/shared/helpers/typeHelper'
|
||||
import {
|
||||
getProjectLimitDateFactory as getProjectLimitDateFactoryBase,
|
||||
isCreatedBeyondHistoryLimitCutoffFactory as isCreatedBeyondHistoryLimitCutoffFactoryBase,
|
||||
IsCreatedBeyondHistoryLimitCutoff,
|
||||
GetProjectLimitDate
|
||||
} from '@speckle/shared'
|
||||
import { PersonalProjectsLimits } from '@speckle/shared/authz'
|
||||
import { getFeatureFlags } from '@speckle/shared/environment'
|
||||
|
||||
const { FF_PERSONAL_PROJECTS_LIMITS_ENABLED } = getFeatureFlags()
|
||||
const getPersonalProjectLimits = FF_PERSONAL_PROJECTS_LIMITS_ENABLED
|
||||
? () => Promise.resolve(PersonalProjectsLimits)
|
||||
: () => Promise.resolve(null)
|
||||
|
||||
export const isCreatedBeyondHistoryLimitCutoffFactory = (deps: {
|
||||
ctx: GraphQLContext
|
||||
}): IsCreatedBeyondHistoryLimitCutoff => {
|
||||
const getProjectLimitDate = getProjectLimitDateFactory(deps)
|
||||
const isCreatedBeyondHistoryLimitCutoffFactory =
|
||||
isCreatedBeyondHistoryLimitCutoffFactoryBase({
|
||||
getProjectLimitDate
|
||||
})
|
||||
|
||||
return isCreatedBeyondHistoryLimitCutoffFactory
|
||||
}
|
||||
|
||||
export const getProjectLimitDateFactory = (deps: {
|
||||
ctx: GraphQLContext
|
||||
}): GetProjectLimitDate => {
|
||||
const getProjectLimitDate = getProjectLimitDateFactoryBase({
|
||||
getWorkspaceLimits: async ({ workspaceId }) =>
|
||||
(await deps.ctx.loaders.gatekeeper?.getWorkspaceLimits.load(workspaceId)) || null,
|
||||
getPersonalProjectLimits
|
||||
})
|
||||
|
||||
return getProjectLimitDate
|
||||
}
|
||||
@@ -119,7 +119,8 @@ isMultiRegionTestMode()
|
||||
id: '',
|
||||
objectId: '',
|
||||
streamId: '',
|
||||
authorId: ''
|
||||
authorId: '',
|
||||
branchId: ''
|
||||
}
|
||||
|
||||
let testAutomation: AutomationRecord
|
||||
@@ -162,6 +163,7 @@ isMultiRegionTestMode()
|
||||
owner: adminUser
|
||||
})
|
||||
|
||||
testVersion.branchId = testModel.id
|
||||
testVersion.branchName = testModel.name
|
||||
testVersion.objectId = await createTestObject({ projectId: testProject.id })
|
||||
|
||||
|
||||
@@ -43,6 +43,9 @@ import {
|
||||
import { ServerInviteRecord } from '@/modules/serverinvites/domain/types'
|
||||
import { reduce } from 'lodash'
|
||||
import { grantStreamPermissionsFactory } from '@/modules/core/repositories/streams'
|
||||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
|
||||
const { FF_PERSONAL_PROJECTS_LIMITS_ENABLED } = getFeatureFlags()
|
||||
|
||||
async function cleanup() {
|
||||
await truncateTables([ServerInvites.name, Streams.name, Users.name])
|
||||
@@ -189,268 +192,270 @@ describe('[Stream & Server Invites]', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
;(FF_PERSONAL_PROJECTS_LIMITS_ENABLED ? describe.skip : describe)(
|
||||
'and inviting to stream',
|
||||
() => {
|
||||
const otherGuyAlreadyInvitedStream: BasicTestStream = {
|
||||
name: 'Other guy is already here stream',
|
||||
isPublic: false,
|
||||
id: '',
|
||||
ownerId: ''
|
||||
}
|
||||
|
||||
describe('and inviting to stream', () => {
|
||||
const otherGuyAlreadyInvitedStream: BasicTestStream = {
|
||||
name: 'Other guy is already here stream',
|
||||
isPublic: false,
|
||||
id: '',
|
||||
ownerId: ''
|
||||
}
|
||||
const createInvite = (input: StreamInviteCreateInput) =>
|
||||
apollo.execute(CreateStreamInviteDocument, { input })
|
||||
|
||||
const createInvite = (input: StreamInviteCreateInput) =>
|
||||
apollo.execute(CreateStreamInviteDocument, { input })
|
||||
const createProjectInvite = (args: CreateProjectInviteMutationVariables) =>
|
||||
apollo.execute(CreateProjectInviteDocument, args)
|
||||
|
||||
const createProjectInvite = (args: CreateProjectInviteMutationVariables) =>
|
||||
apollo.execute(CreateProjectInviteDocument, args)
|
||||
|
||||
before(async () => {
|
||||
// Create a stream and make sure otherGuy is already a contributor there
|
||||
await createTestStream(otherGuyAlreadyInvitedStream, me)
|
||||
await grantStreamPermissions({
|
||||
streamId: otherGuyAlreadyInvitedStream.id,
|
||||
role: Roles.Stream.Contributor,
|
||||
userId: otherGuy.id
|
||||
})
|
||||
})
|
||||
|
||||
const alreadyInvitedUserDataSet = [
|
||||
{ display: 'by user id', userId: true },
|
||||
{ display: 'by email', userId: false }
|
||||
]
|
||||
|
||||
alreadyInvitedUserDataSet.forEach(({ display, userId }) => {
|
||||
it(`can't invite an already added user ${display}`, async () => {
|
||||
const { errors, data } = await createInvite({
|
||||
email: userId ? null : otherGuy.email,
|
||||
userId: userId ? otherGuy.id : null,
|
||||
message: 'hey dude come to my stream',
|
||||
streamId: otherGuyAlreadyInvitedStream.id
|
||||
before(async () => {
|
||||
// Create a stream and make sure otherGuy is already a contributor there
|
||||
await createTestStream(otherGuyAlreadyInvitedStream, me)
|
||||
await grantStreamPermissions({
|
||||
streamId: otherGuyAlreadyInvitedStream.id,
|
||||
role: Roles.Stream.Contributor,
|
||||
userId: otherGuy.id
|
||||
})
|
||||
|
||||
expect(data?.streamInviteCreate).to.be.not.ok
|
||||
expect(errors).to.be.ok
|
||||
expect(errors!.map((e) => e.message).join('|')).to.contain(
|
||||
'user is already a collaborator'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it("can't invite with an invalid role", async () => {
|
||||
const result = await createInvite({
|
||||
email: 'badroleguy@speckle.com',
|
||||
streamId: myPrivateStream.id,
|
||||
role: 'aaa'
|
||||
})
|
||||
|
||||
expect(result.data?.streamInviteCreate).to.be.not.ok
|
||||
expect(result.errors).to.be.ok
|
||||
expect((result.errors || []).map((e) => e.message).join('|')).to.contain(
|
||||
'Unexpected project invite role'
|
||||
)
|
||||
})
|
||||
|
||||
const createInviteTypes = [{ projectInvite: false }, { projectInvite: true }]
|
||||
|
||||
createInviteTypes.forEach(({ projectInvite }) => {
|
||||
const userTypesDataSet = [
|
||||
{
|
||||
display: 'registered user',
|
||||
user: otherGuy,
|
||||
stream: myPrivateStream,
|
||||
email: null
|
||||
},
|
||||
{
|
||||
display: 'registered user (with custom role)',
|
||||
user: otherGuy,
|
||||
stream: myPrivateStream,
|
||||
email: null,
|
||||
role: Roles.Stream.Owner
|
||||
},
|
||||
{
|
||||
display: 'unregistered user',
|
||||
user: null,
|
||||
stream: myPrivateStream,
|
||||
email: 'randomer22@lool.com'
|
||||
},
|
||||
{
|
||||
display: 'unregistered user (with custom role)',
|
||||
user: null,
|
||||
stream: myPrivateStream,
|
||||
email: 'randomer22@lool.com',
|
||||
Role: Roles.Stream.Reviewer
|
||||
}
|
||||
const alreadyInvitedUserDataSet = [
|
||||
{ display: 'by user id', userId: true },
|
||||
{ display: 'by email', userId: false }
|
||||
]
|
||||
|
||||
userTypesDataSet.forEach(({ display, user, stream, email, role }) => {
|
||||
it(`can ${
|
||||
projectInvite ? 'project' : 'stream'
|
||||
} invite a ${display}`, async () => {
|
||||
const messagePart1 = '1234hiiiiduuuuude'
|
||||
const messagePart2 = 'yepppppp'
|
||||
const unsanitaryMessage = `<a href="https://google.com">${messagePart1}</a> <script>${messagePart2}</script>`
|
||||
const targetEmail = email || user?.email
|
||||
alreadyInvitedUserDataSet.forEach(({ display, userId }) => {
|
||||
it(`can't invite an already added user ${display}`, async () => {
|
||||
const { errors, data } = await createInvite({
|
||||
email: userId ? null : otherGuy.email,
|
||||
userId: userId ? otherGuy.id : null,
|
||||
message: 'hey dude come to my stream',
|
||||
streamId: otherGuyAlreadyInvitedStream.id
|
||||
})
|
||||
|
||||
const sendEmailInvocations = mailerMock.hijackFunction(
|
||||
'sendEmail',
|
||||
async () => true
|
||||
expect(data?.streamInviteCreate).to.be.not.ok
|
||||
expect(errors).to.be.ok
|
||||
expect(errors!.map((e) => e.message).join('|')).to.contain(
|
||||
'user is already a collaborator'
|
||||
)
|
||||
|
||||
if (projectInvite) {
|
||||
const result = await createProjectInvite({
|
||||
projectId: stream.id,
|
||||
input: {
|
||||
email,
|
||||
userId: user?.id || null,
|
||||
role: role || null
|
||||
}
|
||||
})
|
||||
|
||||
// Check that operation was successful
|
||||
expect(result.data?.projectMutations.invites.create).to.be.ok
|
||||
expect(result.errors).to.be.not.ok
|
||||
} else {
|
||||
const result = await createInvite({
|
||||
email,
|
||||
message: unsanitaryMessage,
|
||||
userId: user?.id || null,
|
||||
streamId: stream.id,
|
||||
role: role || null
|
||||
})
|
||||
|
||||
// Check that operation was successful
|
||||
expect(result.data?.streamInviteCreate).to.be.ok
|
||||
expect(result.errors).to.be.not.ok
|
||||
}
|
||||
|
||||
// Check that email was sent out
|
||||
const emailParams = sendEmailInvocations.args[0][0]
|
||||
expect(emailParams).to.be.ok
|
||||
expect(emailParams.to).to.eq(targetEmail)
|
||||
expect(emailParams.subject).to.be.ok
|
||||
|
||||
// Check that message was sanitized
|
||||
if (!projectInvite) {
|
||||
expect(emailParams.text).to.contain(messagePart1)
|
||||
expect(emailParams.text).to.not.contain(messagePart2)
|
||||
expect(emailParams.html).to.contain(messagePart1)
|
||||
expect(emailParams.html).to.not.contain(messagePart2)
|
||||
}
|
||||
|
||||
// Validate that invite exists
|
||||
const invite = await validateInviteExistanceFromEmail(emailParams)
|
||||
expect(invite).to.be.ok
|
||||
expect(invite?.resource.role).to.eq(role || Roles.Stream.Contributor)
|
||||
})
|
||||
})
|
||||
|
||||
it(`can't ${
|
||||
projectInvite ? 'project' : 'stream'
|
||||
} invite user to a nonexistant stream`, async () => {
|
||||
const params = {
|
||||
email: 'whocares@really.com',
|
||||
streamId: 'ayoooooooo'
|
||||
}
|
||||
it("can't invite with an invalid role", async () => {
|
||||
const result = await createInvite({
|
||||
email: 'badroleguy@speckle.com',
|
||||
streamId: myPrivateStream.id,
|
||||
role: 'aaa'
|
||||
})
|
||||
|
||||
const result = projectInvite
|
||||
? await createProjectInvite({
|
||||
projectId: params.streamId,
|
||||
input: {
|
||||
email: params.email
|
||||
}
|
||||
})
|
||||
: await createInvite({
|
||||
email: params.email,
|
||||
streamId: params.streamId
|
||||
})
|
||||
|
||||
expect(result.data).to.not.be.ok
|
||||
expect(result.data?.streamInviteCreate).to.be.not.ok
|
||||
expect(result.errors).to.be.ok
|
||||
expect((result.errors || []).map((e) => e.message).join('|')).to.contain(
|
||||
'not found'
|
||||
'Unexpected project invite role'
|
||||
)
|
||||
})
|
||||
|
||||
it(`can't ${
|
||||
projectInvite ? 'project' : 'stream'
|
||||
} invite user w/ broken stream identifier`, async () => {
|
||||
const params = {
|
||||
email: 'whocares@really.com',
|
||||
streamId: ''
|
||||
}
|
||||
const createInviteTypes = [{ projectInvite: false }, { projectInvite: true }]
|
||||
|
||||
const result = projectInvite
|
||||
? await createProjectInvite({
|
||||
projectId: params.streamId,
|
||||
input: {
|
||||
email: params.email
|
||||
}
|
||||
})
|
||||
: await createInvite({
|
||||
email: params.email,
|
||||
streamId: params.streamId
|
||||
})
|
||||
createInviteTypes.forEach(({ projectInvite }) => {
|
||||
const userTypesDataSet = [
|
||||
{
|
||||
display: 'registered user',
|
||||
user: otherGuy,
|
||||
stream: myPrivateStream,
|
||||
email: null
|
||||
},
|
||||
{
|
||||
display: 'registered user (with custom role)',
|
||||
user: otherGuy,
|
||||
stream: myPrivateStream,
|
||||
email: null,
|
||||
role: Roles.Stream.Owner
|
||||
},
|
||||
{
|
||||
display: 'unregistered user',
|
||||
user: null,
|
||||
stream: myPrivateStream,
|
||||
email: 'randomer22@lool.com'
|
||||
},
|
||||
{
|
||||
display: 'unregistered user (with custom role)',
|
||||
user: null,
|
||||
stream: myPrivateStream,
|
||||
email: 'randomer22@lool.com',
|
||||
Role: Roles.Stream.Reviewer
|
||||
}
|
||||
]
|
||||
|
||||
expect(result.data).to.not.be.ok
|
||||
expect((result.errors || []).map((e) => e.message).join('|')).to.contain(
|
||||
projectInvite ? 'Project not found' : 'Invalid project ID specified'
|
||||
)
|
||||
userTypesDataSet.forEach(({ display, user, stream, email, role }) => {
|
||||
it(`can ${
|
||||
projectInvite ? 'project' : 'stream'
|
||||
} invite a ${display}`, async () => {
|
||||
const messagePart1 = '1234hiiiiduuuuude'
|
||||
const messagePart2 = 'yepppppp'
|
||||
const unsanitaryMessage = `<a href="https://google.com">${messagePart1}</a> <script>${messagePart2}</script>`
|
||||
const targetEmail = email || user?.email
|
||||
|
||||
const sendEmailInvocations = mailerMock.hijackFunction(
|
||||
'sendEmail',
|
||||
async () => true
|
||||
)
|
||||
|
||||
if (projectInvite) {
|
||||
const result = await createProjectInvite({
|
||||
projectId: stream.id,
|
||||
input: {
|
||||
email,
|
||||
userId: user?.id || null,
|
||||
role: role || null
|
||||
}
|
||||
})
|
||||
|
||||
// Check that operation was successful
|
||||
expect(result.data?.projectMutations.invites.create).to.be.ok
|
||||
expect(result.errors).to.be.not.ok
|
||||
} else {
|
||||
const result = await createInvite({
|
||||
email,
|
||||
message: unsanitaryMessage,
|
||||
userId: user?.id || null,
|
||||
streamId: stream.id,
|
||||
role: role || null
|
||||
})
|
||||
|
||||
// Check that operation was successful
|
||||
expect(result.data?.streamInviteCreate).to.be.ok
|
||||
expect(result.errors).to.be.not.ok
|
||||
}
|
||||
|
||||
// Check that email was sent out
|
||||
const emailParams = sendEmailInvocations.args[0][0]
|
||||
expect(emailParams).to.be.ok
|
||||
expect(emailParams.to).to.eq(targetEmail)
|
||||
expect(emailParams.subject).to.be.ok
|
||||
|
||||
// Check that message was sanitized
|
||||
if (!projectInvite) {
|
||||
expect(emailParams.text).to.contain(messagePart1)
|
||||
expect(emailParams.text).to.not.contain(messagePart2)
|
||||
expect(emailParams.html).to.contain(messagePart1)
|
||||
expect(emailParams.html).to.not.contain(messagePart2)
|
||||
}
|
||||
|
||||
// Validate that invite exists
|
||||
const invite = await validateInviteExistanceFromEmail(emailParams)
|
||||
expect(invite).to.be.ok
|
||||
expect(invite?.resource.role).to.eq(role || Roles.Stream.Contributor)
|
||||
})
|
||||
})
|
||||
|
||||
it(`can't ${
|
||||
projectInvite ? 'project' : 'stream'
|
||||
} invite user to a nonexistant stream`, async () => {
|
||||
const params = {
|
||||
email: 'whocares@really.com',
|
||||
streamId: 'ayoooooooo'
|
||||
}
|
||||
|
||||
const result = projectInvite
|
||||
? await createProjectInvite({
|
||||
projectId: params.streamId,
|
||||
input: {
|
||||
email: params.email
|
||||
}
|
||||
})
|
||||
: await createInvite({
|
||||
email: params.email,
|
||||
streamId: params.streamId
|
||||
})
|
||||
|
||||
expect(result.data).to.not.be.ok
|
||||
expect((result.errors || []).map((e) => e.message).join('|')).to.contain(
|
||||
'not found'
|
||||
)
|
||||
})
|
||||
|
||||
it(`can't ${
|
||||
projectInvite ? 'project' : 'stream'
|
||||
} invite user w/ broken stream identifier`, async () => {
|
||||
const params = {
|
||||
email: 'whocares@really.com',
|
||||
streamId: ''
|
||||
}
|
||||
|
||||
const result = projectInvite
|
||||
? await createProjectInvite({
|
||||
projectId: params.streamId,
|
||||
input: {
|
||||
email: params.email
|
||||
}
|
||||
})
|
||||
: await createInvite({
|
||||
email: params.email,
|
||||
streamId: params.streamId
|
||||
})
|
||||
|
||||
expect(result.data).to.not.be.ok
|
||||
expect((result.errors || []).map((e) => e.message).join('|')).to.contain(
|
||||
projectInvite ? 'Project not found' : 'Invalid project ID specified'
|
||||
)
|
||||
})
|
||||
|
||||
it(`can't ${
|
||||
projectInvite ? 'project' : 'stream'
|
||||
} invite user to a stream, if not its owner`, async () => {
|
||||
const params = {
|
||||
email: 'whocares@really.com',
|
||||
streamId: otherGuysStream.id
|
||||
}
|
||||
|
||||
const result = projectInvite
|
||||
? await createProjectInvite({
|
||||
projectId: params.streamId,
|
||||
input: {
|
||||
email: params.email
|
||||
}
|
||||
})
|
||||
: await createInvite({
|
||||
email: params.email,
|
||||
streamId: params.streamId
|
||||
})
|
||||
|
||||
expect(result.data).to.not.be.ok
|
||||
expect((result.errors || []).map((e) => e.message).join('|')).to.contain(
|
||||
projectInvite
|
||||
? 'You do not have access to the project'
|
||||
: "Inviter doesn't have owner access to"
|
||||
)
|
||||
})
|
||||
|
||||
it(`can't ${
|
||||
projectInvite ? 'project' : 'stream'
|
||||
} invite a nonexistant user ID to a stream`, async () => {
|
||||
const params = {
|
||||
userId: 'bababooey',
|
||||
streamId: myPrivateStream.id
|
||||
}
|
||||
|
||||
const result = projectInvite
|
||||
? await createProjectInvite({
|
||||
projectId: params.streamId,
|
||||
input: {
|
||||
userId: params.userId
|
||||
}
|
||||
})
|
||||
: await createInvite({
|
||||
userId: params.userId,
|
||||
streamId: params.streamId
|
||||
})
|
||||
|
||||
expect(result.data).to.not.be.ok
|
||||
expect((result.errors || []).map((e) => e.message).join('|')).to.contain(
|
||||
'Attempting to invite an invalid user'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it(`can't ${
|
||||
projectInvite ? 'project' : 'stream'
|
||||
} invite user to a stream, if not its owner`, async () => {
|
||||
const params = {
|
||||
email: 'whocares@really.com',
|
||||
streamId: otherGuysStream.id
|
||||
}
|
||||
|
||||
const result = projectInvite
|
||||
? await createProjectInvite({
|
||||
projectId: params.streamId,
|
||||
input: {
|
||||
email: params.email
|
||||
}
|
||||
})
|
||||
: await createInvite({
|
||||
email: params.email,
|
||||
streamId: params.streamId
|
||||
})
|
||||
|
||||
expect(result.data).to.not.be.ok
|
||||
expect((result.errors || []).map((e) => e.message).join('|')).to.contain(
|
||||
projectInvite
|
||||
? 'You do not have access to the project'
|
||||
: "Inviter doesn't have owner access to"
|
||||
)
|
||||
})
|
||||
|
||||
it(`can't ${
|
||||
projectInvite ? 'project' : 'stream'
|
||||
} invite a nonexistant user ID to a stream`, async () => {
|
||||
const params = {
|
||||
userId: 'bababooey',
|
||||
streamId: myPrivateStream.id
|
||||
}
|
||||
|
||||
const result = projectInvite
|
||||
? await createProjectInvite({
|
||||
projectId: params.streamId,
|
||||
input: {
|
||||
userId: params.userId
|
||||
}
|
||||
})
|
||||
: await createInvite({
|
||||
userId: params.userId,
|
||||
streamId: params.streamId
|
||||
})
|
||||
|
||||
expect(result.data).to.not.be.ok
|
||||
expect((result.errors || []).map((e) => e.message).join('|')).to.contain(
|
||||
'Attempting to invite an invalid user'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
describe('and administrating invites', () => {
|
||||
const serverInvite1 = {
|
||||
|
||||
@@ -62,6 +62,8 @@ export const mapAuthToServerError = (e: Authz.AllAuthErrors): BaseError => {
|
||||
case Authz.ModelNotFoundError.code:
|
||||
case Authz.VersionNotFoundError.code:
|
||||
return new NotFoundError(e.message)
|
||||
case Authz.PersonalProjectsLimitedError.code:
|
||||
return new BadRequestError(e.message)
|
||||
default:
|
||||
throwUncoveredError(e)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { ConditionalKeys, SetRequired } from 'type-fest'
|
||||
import type { Logger } from 'pino'
|
||||
import type { BaseContext } from '@apollo/server'
|
||||
import type { Registry } from 'prom-client'
|
||||
import { AuthCheckContextLoaders, AuthPolicies } from '@speckle/shared/authz'
|
||||
import { AuthPolicies } from '@speckle/shared/authz'
|
||||
|
||||
export type MarkNullableOptional<T> = SetRequired<
|
||||
Partial<T>,
|
||||
@@ -62,10 +62,6 @@ export type GraphQLContext = BaseContext &
|
||||
*/
|
||||
loaders: RequestDataLoaders
|
||||
log: Logger
|
||||
/**
|
||||
* @deprecated Should be cleaned up soon, just use dataloaders
|
||||
*/
|
||||
authLoaders: AuthCheckContextLoaders
|
||||
/**
|
||||
* Clear dataloader, auth policy loader etc. caches. Usually necessary after mutations
|
||||
* are done in resolvers
|
||||
|
||||
@@ -223,7 +223,6 @@ export async function buildContext(params?: {
|
||||
authLoaders.clearCache()
|
||||
}
|
||||
},
|
||||
authLoaders: authLoaders.loaders,
|
||||
clearCache: async () => {
|
||||
authLoaders.clearCache()
|
||||
dataLoaders.clearAll()
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
import { getWorkspaceRoleForUserFactory } from '@/modules/workspaces/repositories/workspaces'
|
||||
import { queryAllWorkspaceProjectsFactory } from '@/modules/workspaces/services/projects'
|
||||
import { getWorkspaceModelCountFactory } from '@/modules/workspaces/services/workspaceLimits'
|
||||
import { WorkspacePaidPlanConfigs, WorkspaceUnpaidPlanConfigs } from '@speckle/shared'
|
||||
|
||||
// TODO: Move everything to use dataLoaders
|
||||
export default defineModuleLoaders(async () => {
|
||||
@@ -72,11 +71,8 @@ export default defineModuleLoaders(async () => {
|
||||
getWorkspacePlan: async ({ workspaceId }) => {
|
||||
return await getWorkspacePlan({ workspaceId })
|
||||
},
|
||||
getWorkspaceLimits: async ({ workspaceId }) => {
|
||||
const plan = await getWorkspacePlan({ workspaceId })
|
||||
if (!plan) return null
|
||||
const config = { ...WorkspacePaidPlanConfigs, ...WorkspaceUnpaidPlanConfigs }
|
||||
return config[plan.name]?.limits ?? null
|
||||
getWorkspaceLimits: async ({ workspaceId }, { dataLoaders }) => {
|
||||
return await dataLoaders.gatekeeper!.getWorkspaceLimits.load(workspaceId)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -68,12 +68,15 @@ import {
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { WorkspaceSeatType } from '@/modules/workspacesCore/domain/types'
|
||||
import { ProjectRecordVisibility } from '@/modules/core/helpers/types'
|
||||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
|
||||
enum InviteByTarget {
|
||||
Email = 'email',
|
||||
Id = 'id'
|
||||
}
|
||||
|
||||
const { FF_PERSONAL_PROJECTS_LIMITS_ENABLED } = getFeatureFlags()
|
||||
|
||||
const validateStreamAccess = validateStreamAccessFactory({ authorizeResolver })
|
||||
|
||||
const getUser = getUserFactory({ db })
|
||||
@@ -598,22 +601,24 @@ describe('Workspaces Invites GQL', () => {
|
||||
expect(res).to.haveGraphQLErrors('Target project belongs to a workspace')
|
||||
expect(res.data?.projectMutations.invites.create.id).to.not.be.ok
|
||||
})
|
||||
;(FF_PERSONAL_PROJECTS_LIMITS_ENABLED ? it.skip : it)(
|
||||
'can invite to non-workspace project through workspace project invite resolver',
|
||||
async () => {
|
||||
const res = await gqlHelpers.createWorkspaceProjectInvite({
|
||||
projectId: myProjectInviteTargetBasicProject.id,
|
||||
inputs: [
|
||||
{
|
||||
userId: otherGuy.id,
|
||||
role: Roles.Stream.Owner,
|
||||
workspaceRole: Roles.Workspace.Admin // should be ignored
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
it('can invite to non-workspace project through workspace project invite resolver', async () => {
|
||||
const res = await gqlHelpers.createWorkspaceProjectInvite({
|
||||
projectId: myProjectInviteTargetBasicProject.id,
|
||||
inputs: [
|
||||
{
|
||||
userId: otherGuy.id,
|
||||
role: Roles.Stream.Owner,
|
||||
workspaceRole: Roles.Workspace.Admin // should be ignored
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
expect(res).to.not.haveGraphQLErrors()
|
||||
expect(res.data?.projectMutations.invites.createForWorkspace.id).to.be.ok
|
||||
})
|
||||
expect(res).to.not.haveGraphQLErrors()
|
||||
expect(res.data?.projectMutations.invites.createForWorkspace.id).to.be.ok
|
||||
}
|
||||
)
|
||||
|
||||
it("can't indirectly invite to workspace if not workspace admin", async () => {
|
||||
const res = await gqlHelpers.createWorkspaceProjectInvite(
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
import {
|
||||
ActiveUserProjectsDocument,
|
||||
ActiveUserProjectsWorkspaceDocument,
|
||||
CreateProjectDocument,
|
||||
CreateWorkspaceProjectDocument,
|
||||
GetProjectDocument,
|
||||
GetWorkspaceDocument,
|
||||
@@ -1019,13 +1018,14 @@ describe('Workspace project GQL CRUD', () => {
|
||||
)
|
||||
expect(createProjectInWorkspaceRes).to.not.haveGraphQLErrors()
|
||||
|
||||
const createProjectNonInWorkspaceRes = await session.execute(
|
||||
CreateProjectDocument,
|
||||
{ input: { name: 'project' } }
|
||||
)
|
||||
expect(createProjectNonInWorkspaceRes).to.not.haveGraphQLErrors()
|
||||
const projectNonInWorkspace =
|
||||
createProjectNonInWorkspaceRes.data!.projectMutations.create
|
||||
// create w/o GQL, to not mess w/ personal project limits
|
||||
const projectNonInWorkspace: BasicTestStream = {
|
||||
id: '',
|
||||
name: 'project',
|
||||
ownerId: '',
|
||||
isPublic: false
|
||||
}
|
||||
await createTestStream(projectNonInWorkspace, testAdminUser)
|
||||
|
||||
const userProjectsRes = await session.execute(ActiveUserProjectsDocument, {
|
||||
filter: { personalOnly: true }
|
||||
@@ -1071,11 +1071,14 @@ describe('Workspace project GQL CRUD', () => {
|
||||
const projectInWorkspace =
|
||||
createProjectInWorkspaceRes.data!.workspaceMutations.projects.create
|
||||
|
||||
const createProjectNonInWorkspaceRes = await session.execute(
|
||||
CreateProjectDocument,
|
||||
{ input: { name: 'project' } }
|
||||
)
|
||||
expect(createProjectNonInWorkspaceRes).to.not.haveGraphQLErrors()
|
||||
// create w/o GQL, to not mess w/ personal project limits
|
||||
const projectNonInWorkspace: BasicTestStream = {
|
||||
id: '',
|
||||
name: 'project',
|
||||
ownerId: '',
|
||||
isPublic: false
|
||||
}
|
||||
await createTestStream(projectNonInWorkspace, testAdminUser)
|
||||
|
||||
const userProjectsRes = await session.execute(ActiveUserProjectsDocument, {
|
||||
filter: { workspaceId }
|
||||
@@ -1119,11 +1122,14 @@ describe('Workspace project GQL CRUD', () => {
|
||||
)
|
||||
expect(createProjectInWorkspaceRes).to.not.haveGraphQLErrors()
|
||||
|
||||
const createProjectNonInWorkspaceRes = await session.execute(
|
||||
CreateProjectDocument,
|
||||
{ input: { name: 'project' } }
|
||||
)
|
||||
expect(createProjectNonInWorkspaceRes).to.not.haveGraphQLErrors()
|
||||
// create w/o GQL, to not mess w/ personal project limits
|
||||
const projectNonInWorkspace: BasicTestStream = {
|
||||
id: '',
|
||||
name: 'project',
|
||||
ownerId: '',
|
||||
isPublic: false
|
||||
}
|
||||
await createTestStream(projectNonInWorkspace, testAdminUser)
|
||||
|
||||
const userProjectsRes = await session.execute(ActiveUserProjectsDocument, {
|
||||
filter: {}
|
||||
|
||||
@@ -1084,7 +1084,8 @@ describe('Workspaces GQL CRUD', () => {
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
streamId: workspaceProject.id,
|
||||
objectId: '',
|
||||
authorId: ''
|
||||
authorId: '',
|
||||
branchId: ''
|
||||
}
|
||||
|
||||
await createTestCommit(testVersion, {
|
||||
|
||||
@@ -5299,6 +5299,49 @@ export type SetLegacyProjectsExplainerCollapsedMutationVariables = Exact<{
|
||||
|
||||
export type SetLegacyProjectsExplainerCollapsedMutation = { __typename?: 'Mutation', activeUserMutations: { __typename?: 'ActiveUserMutations', meta: { __typename?: 'UserMetaMutations', setLegacyProjectsExplainerCollapsed: boolean } } };
|
||||
|
||||
export type LimitedPersonalProjectCommentFragment = { __typename?: 'Comment', id: string, rawText?: string | null, createdAt: string, text?: { __typename?: 'SmartTextEditorValue', doc?: Record<string, unknown> | null, type: string } | null };
|
||||
|
||||
export type LimitedPersonalProjectVersionFragment = { __typename?: 'Version', id: string, createdAt: string, message?: string | null, referencedObject?: string | null, commentThreads: { __typename?: 'CommentCollection', totalCount: number, items: Array<{ __typename?: 'Comment', id: string, rawText?: string | null, createdAt: string, text?: { __typename?: 'SmartTextEditorValue', doc?: Record<string, unknown> | null, type: string } | null }> } };
|
||||
|
||||
export type GetLimitedPersonalProjectVersionsQueryVariables = Exact<{
|
||||
projectId: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetLimitedPersonalProjectVersionsQuery = { __typename?: 'Query', project: { __typename?: 'Project', versions: { __typename?: 'VersionCollection', totalCount: number, items: Array<{ __typename?: 'Version', id: string, createdAt: string, message?: string | null, referencedObject?: string | null, commentThreads: { __typename?: 'CommentCollection', totalCount: number, items: Array<{ __typename?: 'Comment', id: string, rawText?: string | null, createdAt: string, text?: { __typename?: 'SmartTextEditorValue', doc?: Record<string, unknown> | null, type: string } | null }> } }> } } };
|
||||
|
||||
export type GetLimitedPersonalProjectVersionQueryVariables = Exact<{
|
||||
projectId: Scalars['String']['input'];
|
||||
versionId: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetLimitedPersonalProjectVersionQuery = { __typename?: 'Query', project: { __typename?: 'Project', version: { __typename?: 'Version', id: string, createdAt: string, message?: string | null, referencedObject?: string | null, commentThreads: { __typename?: 'CommentCollection', totalCount: number, items: Array<{ __typename?: 'Comment', id: string, rawText?: string | null, createdAt: string, text?: { __typename?: 'SmartTextEditorValue', doc?: Record<string, unknown> | null, type: string } | null }> } } } };
|
||||
|
||||
export type LimitedPersonalStreamCommitFragment = { __typename?: 'Commit', id: string, message?: string | null, referencedObject: string, createdAt?: string | null };
|
||||
|
||||
export type GetLimitedPersonalStreamCommitsQueryVariables = Exact<{
|
||||
streamId: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetLimitedPersonalStreamCommitsQuery = { __typename?: 'Query', stream?: { __typename?: 'Stream', commits?: { __typename?: 'CommitCollection', totalCount: number, items?: Array<{ __typename?: 'Commit', id: string, message?: string | null, referencedObject: string, createdAt?: string | null }> | null } | null } | null };
|
||||
|
||||
export type GetLimitedPersonalProjectCommentsQueryVariables = Exact<{
|
||||
projectId: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetLimitedPersonalProjectCommentsQuery = { __typename?: 'Query', project: { __typename?: 'Project', commentThreads: { __typename?: 'ProjectCommentCollection', totalCount: number, items: Array<{ __typename?: 'Comment', id: string, rawText?: string | null, createdAt: string, text?: { __typename?: 'SmartTextEditorValue', doc?: Record<string, unknown> | null, type: string } | null }> } } };
|
||||
|
||||
export type GetLimitedPersonalProjectCommentQueryVariables = Exact<{
|
||||
projectId: Scalars['String']['input'];
|
||||
commentId: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetLimitedPersonalProjectCommentQuery = { __typename?: 'Query', project: { __typename?: 'Project', comment?: { __typename?: 'Comment', id: string, rawText?: string | null, createdAt: string, text?: { __typename?: 'SmartTextEditorValue', doc?: Record<string, unknown> | null, type: string } | null } | null } };
|
||||
|
||||
export type BasicWorkspaceFragment = { __typename?: 'Workspace', id: string, name: string, slug: string, updatedAt: string, createdAt: string, role?: string | null, readOnly: boolean };
|
||||
|
||||
export type BasicPendingWorkspaceCollaboratorFragment = { __typename?: 'PendingWorkspaceCollaborator', id: string, inviteId: string, workspaceId: string, workspaceName: string, title: string, role: string, token?: string | null, invitedBy: { __typename?: 'LimitedUser', id: string, name: string }, user?: { __typename?: 'LimitedUser', id: string, name: string } | null };
|
||||
@@ -6327,6 +6370,9 @@ export type ProjectEmbedOptionsQueryVariables = Exact<{
|
||||
|
||||
export type ProjectEmbedOptionsQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, embedOptions: { __typename?: 'ProjectEmbedOptions', hideSpeckleBranding: boolean } } };
|
||||
|
||||
export const LimitedPersonalProjectCommentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedPersonalProjectComment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rawText"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"text"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"doc"}},{"kind":"Field","name":{"kind":"Name","value":"type"}}]}}]}}]} as unknown as DocumentNode<LimitedPersonalProjectCommentFragment, unknown>;
|
||||
export const LimitedPersonalProjectVersionFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedPersonalProjectVersion"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Version"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}},{"kind":"Field","name":{"kind":"Name","value":"commentThreads"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedPersonalProjectComment"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedPersonalProjectComment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rawText"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"text"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"doc"}},{"kind":"Field","name":{"kind":"Name","value":"type"}}]}}]}}]} as unknown as DocumentNode<LimitedPersonalProjectVersionFragment, unknown>;
|
||||
export const LimitedPersonalStreamCommitFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedPersonalStreamCommit"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Commit"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode<LimitedPersonalStreamCommitFragment, unknown>;
|
||||
export const BasicWorkspaceFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}}]} as unknown as DocumentNode<BasicWorkspaceFragment, unknown>;
|
||||
export const BasicPendingWorkspaceCollaboratorFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"token"}}]}}]} as unknown as DocumentNode<BasicPendingWorkspaceCollaboratorFragment, unknown>;
|
||||
export const WorkspaceProjectsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkspaceProjects"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectCollection"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]} as unknown as DocumentNode<WorkspaceProjectsFragment, unknown>;
|
||||
@@ -6373,6 +6419,11 @@ export const GetNewWorkspaceExplainerDismissedDocument = {"kind":"Document","def
|
||||
export const SetNewWorkspaceExplainerDismissedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetNewWorkspaceExplainerDismissed"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUserMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"meta"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setNewWorkspaceExplainerDismissed"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"value"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]}}]} as unknown as DocumentNode<SetNewWorkspaceExplainerDismissedMutation, SetNewWorkspaceExplainerDismissedMutationVariables>;
|
||||
export const GetLegacyProjectsExplainerCollapsedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetLegacyProjectsExplainerCollapsed"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"meta"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"legacyProjectsExplainerCollapsed"}}]}}]}}]}}]} as unknown as DocumentNode<GetLegacyProjectsExplainerCollapsedQuery, GetLegacyProjectsExplainerCollapsedQueryVariables>;
|
||||
export const SetLegacyProjectsExplainerCollapsedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetLegacyProjectsExplainerCollapsed"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUserMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"meta"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setLegacyProjectsExplainerCollapsed"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"value"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]}}]} as unknown as DocumentNode<SetLegacyProjectsExplainerCollapsedMutation, SetLegacyProjectsExplainerCollapsedMutationVariables>;
|
||||
export const GetLimitedPersonalProjectVersionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetLimitedPersonalProjectVersions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"versions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedPersonalProjectVersion"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedPersonalProjectComment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rawText"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"text"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"doc"}},{"kind":"Field","name":{"kind":"Name","value":"type"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedPersonalProjectVersion"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Version"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}},{"kind":"Field","name":{"kind":"Name","value":"commentThreads"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedPersonalProjectComment"}}]}}]}}]}}]} as unknown as DocumentNode<GetLimitedPersonalProjectVersionsQuery, GetLimitedPersonalProjectVersionsQueryVariables>;
|
||||
export const GetLimitedPersonalProjectVersionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetLimitedPersonalProjectVersion"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"versionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"version"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"versionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedPersonalProjectVersion"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedPersonalProjectComment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rawText"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"text"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"doc"}},{"kind":"Field","name":{"kind":"Name","value":"type"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedPersonalProjectVersion"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Version"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}},{"kind":"Field","name":{"kind":"Name","value":"commentThreads"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedPersonalProjectComment"}}]}}]}}]}}]} as unknown as DocumentNode<GetLimitedPersonalProjectVersionQuery, GetLimitedPersonalProjectVersionQueryVariables>;
|
||||
export const GetLimitedPersonalStreamCommitsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetLimitedPersonalStreamCommits"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stream"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"commits"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedPersonalStreamCommit"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedPersonalStreamCommit"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Commit"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode<GetLimitedPersonalStreamCommitsQuery, GetLimitedPersonalStreamCommitsQueryVariables>;
|
||||
export const GetLimitedPersonalProjectCommentsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetLimitedPersonalProjectComments"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"commentThreads"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedPersonalProjectComment"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedPersonalProjectComment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rawText"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"text"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"doc"}},{"kind":"Field","name":{"kind":"Name","value":"type"}}]}}]}}]} as unknown as DocumentNode<GetLimitedPersonalProjectCommentsQuery, GetLimitedPersonalProjectCommentsQueryVariables>;
|
||||
export const GetLimitedPersonalProjectCommentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetLimitedPersonalProjectComment"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"commentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"comment"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"commentId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedPersonalProjectComment"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedPersonalProjectComment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rawText"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"text"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"doc"}},{"kind":"Field","name":{"kind":"Name","value":"type"}}]}}]}}]} as unknown as DocumentNode<GetLimitedPersonalProjectCommentQuery, GetLimitedPersonalProjectCommentQueryVariables>;
|
||||
export const CreateWorkspaceInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateWorkspaceInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceInviteCreateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"invites"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}},{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"invitedTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"token"}}]}}]} as unknown as DocumentNode<CreateWorkspaceInviteMutation, CreateWorkspaceInviteMutationVariables>;
|
||||
export const BatchCreateWorkspaceInvitesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"BatchCreateWorkspaceInvites"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceInviteCreateInput"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"invites"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"batchCreate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}},{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"invitedTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"token"}}]}}]} as unknown as DocumentNode<BatchCreateWorkspaceInvitesMutation, BatchCreateWorkspaceInvitesMutationVariables>;
|
||||
export const GetWorkspaceWithTeamDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspaceWithTeam"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"invitedTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"token"}}]}}]} as unknown as DocumentNode<GetWorkspaceWithTeamQuery, GetWorkspaceWithTeamQueryVariables>;
|
||||
|
||||
@@ -21,14 +21,17 @@ import {
|
||||
getViewerResourceItemsUngroupedFactory
|
||||
} from '@/modules/core/services/commit/viewerResources'
|
||||
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
||||
import { resourceBuilder } from '@speckle/shared/viewer/route'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
|
||||
export const createTestComment = async (params: {
|
||||
userId: string
|
||||
projectId: string
|
||||
objectId: string
|
||||
}): Promise<CommentRecord> => {
|
||||
const { userId, projectId, objectId } = params
|
||||
export const createTestComment = async (
|
||||
params: {
|
||||
userId: string
|
||||
projectId: string
|
||||
createdAt?: Date
|
||||
} & ({ objectId: string } | { modelId: string; versionId?: string })
|
||||
): Promise<CommentRecord> => {
|
||||
const { userId, projectId } = params
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId })
|
||||
|
||||
@@ -51,6 +54,13 @@ export const createTestComment = async (params: {
|
||||
emitEvent: async () => {}
|
||||
})
|
||||
|
||||
const resourceIdStringBuilder = resourceBuilder()
|
||||
if ('objectId' in params) {
|
||||
resourceIdStringBuilder.addObject(params.objectId)
|
||||
} else {
|
||||
resourceIdStringBuilder.addModel(params.modelId, params.versionId)
|
||||
}
|
||||
|
||||
return await createComment(
|
||||
{
|
||||
content: {
|
||||
@@ -70,8 +80,11 @@ export const createTestComment = async (params: {
|
||||
}
|
||||
},
|
||||
projectId,
|
||||
resourceIdString: objectId
|
||||
resourceIdString: resourceIdStringBuilder.toString()
|
||||
},
|
||||
userId
|
||||
userId,
|
||||
{
|
||||
createdAt: params.createdAt
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { BasicTestUser } from '@/test/authHelper'
|
||||
import { BasicTestStream } from '@/test/speckle-helpers/streamHelper'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
|
||||
export type BasicTestCommit = {
|
||||
/**
|
||||
@@ -40,6 +41,10 @@ export type BasicTestCommit = {
|
||||
* Can be left empty, will be filled on creation if owner passed in
|
||||
*/
|
||||
authorId: string
|
||||
/**
|
||||
* Can be left empty, will be filled on creation. Takes precedence over branchName
|
||||
*/
|
||||
branchId: string
|
||||
/**
|
||||
* Defaults to 'main'
|
||||
*/
|
||||
@@ -53,6 +58,11 @@ export type BasicTestCommit = {
|
||||
* Empty array by default
|
||||
*/
|
||||
parents?: string[]
|
||||
|
||||
/**
|
||||
* Optionally override the createdAt date
|
||||
*/
|
||||
createdAt?: Date
|
||||
}
|
||||
|
||||
export async function createTestObject(params: { projectId: string }) {
|
||||
@@ -85,7 +95,7 @@ async function ensureObjects(commits: BasicTestCommit[]) {
|
||||
|
||||
return createObject({
|
||||
streamId: c.streamId,
|
||||
object: { foo: 'bar' }
|
||||
object: { foo: cryptoRandomString({ length: 256 }) }
|
||||
}).then((oid) => (c.objectId = oid))
|
||||
})
|
||||
)
|
||||
@@ -128,7 +138,7 @@ export async function createTestCommits(
|
||||
getBranchById: getBranchByIdFactory({ db: projectDb })
|
||||
})
|
||||
|
||||
return createCommitByBranchName({
|
||||
const baseArgs = {
|
||||
streamId: c.streamId,
|
||||
branchName: c.branchName || 'main',
|
||||
message: c.message || 'this message is auto generated',
|
||||
@@ -136,8 +146,20 @@ export async function createTestCommits(
|
||||
objectId: c.objectId,
|
||||
authorId: c.authorId,
|
||||
totalChildrenCount: 0,
|
||||
parents: c.parents || []
|
||||
}).then((newCommit) => (c.id = newCommit.id))
|
||||
parents: c.parents || [],
|
||||
createdAt: c.createdAt
|
||||
}
|
||||
|
||||
const commit = await (c.branchId?.length
|
||||
? createCommitByBranchId({
|
||||
...baseArgs,
|
||||
branchId: c.branchId
|
||||
})
|
||||
: createCommitByBranchName(baseArgs))
|
||||
c.id = commit.id
|
||||
c.branchId = commit.branchId
|
||||
|
||||
return c
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@@ -97,6 +97,8 @@
|
||||
"./workers": "./src/workers/index.ts",
|
||||
"./workers/previews": "./src/workers/previews/index.ts",
|
||||
"./workers/fileimport": "./src/workers/fileimport/index.ts",
|
||||
"./viewer": "./src/viewer/index.ts",
|
||||
"./viewer/route": "./src/viewer/helpers/route.ts",
|
||||
"./dist/*": "./dist/*"
|
||||
},
|
||||
"exclude": [
|
||||
@@ -233,6 +235,26 @@
|
||||
"default": "./dist/commonjs/workers/fileimport/index.js"
|
||||
}
|
||||
},
|
||||
"./viewer": {
|
||||
"import": {
|
||||
"types": "./dist/esm/viewer/index.d.ts",
|
||||
"default": "./dist/esm/viewer/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/commonjs/viewer/index.d.ts",
|
||||
"default": "./dist/commonjs/viewer/index.js"
|
||||
}
|
||||
},
|
||||
"./viewer/route": {
|
||||
"import": {
|
||||
"types": "./dist/esm/viewer/helpers/route.d.ts",
|
||||
"default": "./dist/esm/viewer/helpers/route.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/commonjs/viewer/helpers/route.d.ts",
|
||||
"default": "./dist/commonjs/viewer/helpers/route.js"
|
||||
}
|
||||
},
|
||||
"./dist/*": "./dist/*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,11 @@ export const ProjectNoAccessError = defineAuthError({
|
||||
message: 'You do not have access to the project'
|
||||
})
|
||||
|
||||
export const PersonalProjectsLimitedError = defineAuthError({
|
||||
code: 'PersonalProjectsLimited',
|
||||
message: 'Non-workspaced/personal projects are limited'
|
||||
})
|
||||
|
||||
export const ProjectNotEnoughPermissionsError = defineAuthError({
|
||||
code: 'ProjectNotEnoughPermissions',
|
||||
message: 'You do not have enough permissions in the project to perform this action'
|
||||
|
||||
@@ -36,7 +36,10 @@ describe('ensureMinimumProjectRoleFragment', () => {
|
||||
getWorkspaceRole: async () => null,
|
||||
getServerRole: async () => Roles.Server.User,
|
||||
getProjectRole: async () => Roles.Stream.Contributor,
|
||||
getEnv: async () => parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'true' }),
|
||||
getEnv: async () =>
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
}),
|
||||
...overrides
|
||||
})
|
||||
|
||||
@@ -205,7 +208,10 @@ describe('checkIfPubliclyReadableProjectFragment', () => {
|
||||
id: 'projectId',
|
||||
workspaceId: null
|
||||
}),
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getEnv: async () =>
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
}),
|
||||
...overrides
|
||||
})
|
||||
|
||||
@@ -423,7 +429,10 @@ describe('ensureImplicitProjectMemberWithReadAccessFragment', async () => {
|
||||
getAdminOverrideEnabled: async () => false,
|
||||
getServerRole: async () => Roles.Server.User,
|
||||
getProjectRole: async () => Roles.Stream.Contributor,
|
||||
getEnv: async () => parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'true' }),
|
||||
getEnv: async () =>
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
}),
|
||||
getWorkspace: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
|
||||
@@ -9,8 +9,8 @@ import { canReadProjectSettingsPolicy } from './project/canReadSettings.js'
|
||||
import { canReadProjectWebhooksPolicy } from './project/canReadWebhooks.js'
|
||||
import { canUpdateProjectAllowPublicCommentsPolicy } from './project/canUpdateAllowPublicComments.js'
|
||||
import { canLeaveProjectPolicy } from './project/canLeave.js'
|
||||
import { canInvitePolicy as canInviteToWorkspacePolicy } from './workspace/canInvite.js'
|
||||
import { canInvitePolicy as canInviteToProjectPolicy } from './project/canInvite.js'
|
||||
import { canInviteToWorkspacePolicy } from './workspace/canInvite.js'
|
||||
import { canInviteToProjectPolicy } from './project/canInvite.js'
|
||||
import { canBroadcastProjectActivityPolicy } from './project/canBroadcastActivity.js'
|
||||
import { canCreateProjectCommentPolicy } from './project/comment/canCreate.js'
|
||||
import { canArchiveProjectCommentPolicy } from './project/comment/canArchive.js'
|
||||
|
||||
@@ -20,7 +20,10 @@ describe('canBroadcastProjectActivityPolicy', () => {
|
||||
overrides?: OverridesOf<typeof canBroadcastProjectActivityPolicy>
|
||||
) =>
|
||||
canBroadcastProjectActivityPolicy({
|
||||
getEnv: async () => parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'true' }),
|
||||
getEnv: async () =>
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
}),
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null
|
||||
|
||||
@@ -2,25 +2,25 @@ import { describe, expect, it } from 'vitest'
|
||||
import { canCreatePersonalProjectPolicy } from './canCreatePersonal.js'
|
||||
import { parseFeatureFlags } from '../../../environment/index.js'
|
||||
import {
|
||||
ProjectNoAccessError,
|
||||
PersonalProjectsLimitedError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
ServerNotEnoughPermissionsError
|
||||
} from '../../domain/authErrors.js'
|
||||
import { OverridesOf } from '../../../tests/helpers/types.js'
|
||||
|
||||
const buildSUT = (
|
||||
overrides?: Partial<Parameters<typeof canCreatePersonalProjectPolicy>[0]>
|
||||
) =>
|
||||
const buildSUT = (overrides?: OverridesOf<typeof canCreatePersonalProjectPolicy>) =>
|
||||
canCreatePersonalProjectPolicy({
|
||||
getEnv: async () =>
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'false'
|
||||
FF_PERSONAL_PROJECTS_LIMITS_ENABLED: 'false',
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
}),
|
||||
getServerRole: async () => 'server:user',
|
||||
...(overrides || {})
|
||||
})
|
||||
|
||||
describe('canCreateProject', () => {
|
||||
describe('canCreatePersonalProject', () => {
|
||||
it('returns error if user is not logged in', async () => {
|
||||
const canCreateProject = buildSUT()
|
||||
|
||||
@@ -30,15 +30,15 @@ describe('canCreateProject', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: Re-enable when ready
|
||||
it.skip('returns error if workspaces module is enabled', async () => {
|
||||
it('returns error if personal project limits enabled', async () => {
|
||||
const canCreateProject = buildSUT({
|
||||
getEnv: async () => parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'true' })
|
||||
getEnv: async () =>
|
||||
parseFeatureFlags({ FF_PERSONAL_PROJECTS_LIMITS_ENABLED: 'true' })
|
||||
})
|
||||
|
||||
const result = await canCreateProject({ userId: 'user-id' })
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: ProjectNoAccessError.code
|
||||
code: PersonalProjectsLimitedError.code
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { AuthPolicy } from '../../domain/policies.js'
|
||||
import { Roles } from '../../../core/constants.js'
|
||||
import { ensureMinimumServerRoleFragment } from '../../fragments/server.js'
|
||||
import {
|
||||
PersonalProjectsLimitedError,
|
||||
ServerNoAccessError,
|
||||
ServerNoSessionError,
|
||||
ServerNotEnoughPermissionsError
|
||||
@@ -17,18 +18,18 @@ export const canCreatePersonalProjectPolicy: AuthPolicy<
|
||||
| typeof ServerNoAccessError
|
||||
| typeof ServerNoSessionError
|
||||
| typeof ServerNotEnoughPermissionsError
|
||||
| typeof PersonalProjectsLimitedError
|
||||
>
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId }) => {
|
||||
const env = await loaders.getEnv()
|
||||
if (env.FF_WORKSPACES_MODULE_ENABLED) {
|
||||
// TODO: We're not ready to enforce this yet, there's a bunch of tests that would break
|
||||
// return err(
|
||||
// new ProjectNoAccessError({
|
||||
// message: "Projects can't be created outside of workspaces"
|
||||
// })
|
||||
// )
|
||||
if (env.FF_PERSONAL_PROJECTS_LIMITS_ENABLED) {
|
||||
return err(
|
||||
new PersonalProjectsLimitedError({
|
||||
message: "Projects can't be created outside of workspaces"
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const ensuredServerRole = await ensureMinimumServerRoleFragment(loaders)({
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { OverridesOf } from '../../../tests/helpers/types.js'
|
||||
import { canInviteToProjectPolicy } from './canInvite.js'
|
||||
import { parseFeatureFlags } from '../../../environment/index.js'
|
||||
import { Roles } from '../../../core/constants.js'
|
||||
import { getProjectFake } from '../../../tests/fakes.js'
|
||||
import { PersonalProjectsLimitedError } from '../../domain/authErrors.js'
|
||||
|
||||
const buildSUT = (overrides?: OverridesOf<typeof canInviteToProjectPolicy>) =>
|
||||
canInviteToProjectPolicy({
|
||||
getEnv: async () =>
|
||||
parseFeatureFlags({
|
||||
FF_PERSONAL_PROJECTS_LIMITS_ENABLED: 'false',
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
}),
|
||||
getServerRole: async () => Roles.Server.User,
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id'
|
||||
}),
|
||||
getProjectRole: async () => Roles.Stream.Owner,
|
||||
getWorkspace: async () => null,
|
||||
getWorkspaceRole: async () => null,
|
||||
getWorkspaceSsoProvider: async () => null,
|
||||
getWorkspaceSsoSession: async () => null,
|
||||
...(overrides || {})
|
||||
})
|
||||
|
||||
describe('canInviteToProjectPolicy', () => {
|
||||
it('succeeds for project owner', async () => {
|
||||
const canInvite = buildSUT()
|
||||
|
||||
const result = await canInvite({ userId: 'user-id', projectId: 'project-id' })
|
||||
expect(result).toBeOKResult()
|
||||
})
|
||||
|
||||
it('fails if personal projects are disabled', async () => {
|
||||
const canInvite = buildSUT({
|
||||
getEnv: async () =>
|
||||
parseFeatureFlags({
|
||||
FF_PERSONAL_PROJECTS_LIMITS_ENABLED: 'true'
|
||||
})
|
||||
})
|
||||
|
||||
const result = await canInvite({ userId: 'user-id', projectId: 'project-id' })
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: PersonalProjectsLimitedError.code
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
ProjectNotFoundError,
|
||||
ServerNotEnoughPermissionsError,
|
||||
ProjectNotEnoughPermissionsError,
|
||||
WorkspaceNotEnoughPermissionsError
|
||||
WorkspaceNotEnoughPermissionsError,
|
||||
PersonalProjectsLimitedError
|
||||
} from '../../domain/authErrors.js'
|
||||
import { ensureMinimumServerRoleFragment } from '../../fragments/server.js'
|
||||
import { Roles } from '../../../core/constants.js'
|
||||
@@ -39,8 +40,13 @@ type PolicyErrors =
|
||||
| InstanceType<typeof WorkspaceSsoSessionNoAccessError>
|
||||
| InstanceType<typeof WorkspaceNoAccessError>
|
||||
| InstanceType<typeof WorkspaceNotEnoughPermissionsError>
|
||||
| InstanceType<typeof PersonalProjectsLimitedError>
|
||||
|
||||
export const canInvitePolicy: AuthPolicy<PolicyLoaderKeys, PolicyArgs, PolicyErrors> =
|
||||
export const canInviteToProjectPolicy: AuthPolicy<
|
||||
PolicyLoaderKeys,
|
||||
PolicyArgs,
|
||||
PolicyErrors
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, projectId }) => {
|
||||
const ensuredServerRole = await ensureMinimumServerRoleFragment(loaders)({
|
||||
@@ -59,5 +65,16 @@ export const canInvitePolicy: AuthPolicy<PolicyLoaderKeys, PolicyArgs, PolicyErr
|
||||
|
||||
if (ensuredProjectRole.isErr) return err(ensuredProjectRole.error)
|
||||
|
||||
const env = await loaders.getEnv()
|
||||
const project = await loaders.getProject({ projectId })
|
||||
if (project && !project.workspaceId && env.FF_PERSONAL_PROJECTS_LIMITS_ENABLED) {
|
||||
// Prevent inviting collaborators to personal projects
|
||||
return err(
|
||||
new PersonalProjectsLimitedError({
|
||||
message: 'No new collaborators can be added to personal projects'
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return ok()
|
||||
}
|
||||
|
||||
@@ -33,7 +33,10 @@ describe('canReadProjectPolicy creates a function, that handles ', () => {
|
||||
const result = canReadProjectPolicy({
|
||||
getWorkspace,
|
||||
getAdminOverrideEnabled: async () => false,
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getEnv: async () =>
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
}),
|
||||
getProject: async () => null,
|
||||
getProjectRole: () => {
|
||||
assert.fail()
|
||||
|
||||
@@ -17,7 +17,10 @@ import { ProjectVisibility } from '../../domain/projects/types.js'
|
||||
describe('canReadProjectSettingsPolicy', () => {
|
||||
const buildSUT = (overrides?: OverridesOf<typeof canReadProjectSettingsPolicy>) =>
|
||||
canReadProjectSettingsPolicy({
|
||||
getEnv: async () => parseFeatureFlags({ FF_WORKSPACES_MODULE_ENABLED: 'true' }),
|
||||
getEnv: async () =>
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
}),
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null
|
||||
|
||||
@@ -12,7 +12,10 @@ describe('canUpdateProjectAllowPublicCommentsPolicy', () => {
|
||||
overrides?: OverridesOf<typeof canUpdateProjectAllowPublicCommentsPolicy>
|
||||
) =>
|
||||
canUpdateProjectAllowPublicCommentsPolicy({
|
||||
getEnv: async () => parseFeatureFlags({}),
|
||||
getEnv: async () =>
|
||||
parseFeatureFlags({
|
||||
FF_WORKSPACES_MODULE_ENABLED: 'true'
|
||||
}),
|
||||
getProject: getProjectFake({
|
||||
id: 'project-id',
|
||||
workspaceId: null,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Workspace } from '../../../domain/workspaces/types.js'
|
||||
import { WorkspacePlan } from '../../../../workspaces/index.js'
|
||||
import { Project } from '../../../domain/projects/types.js'
|
||||
import {
|
||||
PersonalProjectsLimitedError,
|
||||
ProjectNoAccessError,
|
||||
ProjectNotEnoughPermissionsError,
|
||||
ServerNoAccessError,
|
||||
@@ -115,15 +116,38 @@ describe('canCreateModelPolicy returns a function, that', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('forbids if personal project limits are enabled', async () => {
|
||||
const sut = buildCanCreateModelPolicy({
|
||||
getEnv: async () =>
|
||||
parseFeatureFlags({ FF_PERSONAL_PROJECTS_LIMITS_ENABLED: 'true' }),
|
||||
getProject: getProjectFake({
|
||||
workspaceId: null
|
||||
})
|
||||
})
|
||||
const result = await sut(canCreateArgs())
|
||||
|
||||
expect(result).toBeAuthErrorResult({
|
||||
code: PersonalProjectsLimitedError.code
|
||||
})
|
||||
})
|
||||
|
||||
it('allows stream contributors to create personal projects when project is not in a workspace', async () => {
|
||||
const result = await buildCanCreateModelPolicy({
|
||||
getProject: async () => {
|
||||
return {} as Project
|
||||
}
|
||||
},
|
||||
getEnv: async () =>
|
||||
parseFeatureFlags(
|
||||
{
|
||||
FF_PERSONAL_PROJECTS_LIMITS_ENABLED: 'false'
|
||||
},
|
||||
{ forceInputs: true }
|
||||
)
|
||||
})(canCreateArgs())
|
||||
|
||||
expect(result).toBeAuthOKResult()
|
||||
})
|
||||
|
||||
// Hold the workspace to a higher standard than myself
|
||||
it('requires the workspace to have a plan', async () => {
|
||||
const result = await buildCanCreateModelPolicy({
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
WorkspaceReadOnlyError,
|
||||
WorkspaceNotEnoughPermissionsError,
|
||||
ProjectNotEnoughPermissionsError,
|
||||
ServerNotEnoughPermissionsError
|
||||
ServerNotEnoughPermissionsError,
|
||||
PersonalProjectsLimitedError
|
||||
} from '../../../domain/authErrors.js'
|
||||
import { MaybeUserContext, ProjectContext } from '../../../domain/context.js'
|
||||
import { AuthCheckContextLoaderKeys } from '../../../domain/loaders.js'
|
||||
@@ -46,6 +47,7 @@ type PolicyErrors =
|
||||
| typeof WorkspaceNotEnoughPermissionsError
|
||||
| typeof ProjectNotEnoughPermissionsError
|
||||
| typeof ServerNotEnoughPermissionsError
|
||||
| typeof PersonalProjectsLimitedError
|
||||
>
|
||||
|
||||
export const canCreateModelPolicy: AuthPolicy<
|
||||
@@ -75,5 +77,16 @@ export const canCreateModelPolicy: AuthPolicy<
|
||||
return err(ensuredModelsAccepted.error)
|
||||
}
|
||||
|
||||
// Prevent personal project models, if personal projects limited
|
||||
const project = await loaders.getProject({ projectId })
|
||||
const env = await loaders.getEnv()
|
||||
if (project && !project.workspaceId && env.FF_PERSONAL_PROJECTS_LIMITS_ENABLED) {
|
||||
return err(
|
||||
new PersonalProjectsLimitedError(
|
||||
'No new models can be added to personal projects'
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return ok()
|
||||
}
|
||||
|
||||
@@ -37,7 +37,11 @@ type PolicyErrors =
|
||||
| InstanceType<typeof WorkspaceNoAccessError>
|
||||
| InstanceType<typeof WorkspaceNotEnoughPermissionsError>
|
||||
|
||||
export const canInvitePolicy: AuthPolicy<PolicyLoaderKeys, PolicyArgs, PolicyErrors> =
|
||||
export const canInviteToWorkspacePolicy: AuthPolicy<
|
||||
PolicyLoaderKeys,
|
||||
PolicyArgs,
|
||||
PolicyErrors
|
||||
> =
|
||||
(loaders) =>
|
||||
async ({ userId, workspaceId }) => {
|
||||
const ensuredWorkspacesEnabled = await ensureWorkspacesEnabledFragment(loaders)({})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { has } from '#lodash'
|
||||
import { parseEnv } from 'znv'
|
||||
import { z } from 'zod'
|
||||
|
||||
@@ -13,9 +14,17 @@ const isEnableAllFFsMode = () =>
|
||||
['true', '1'].includes(process.env.ENABLE_ALL_FFS || '')
|
||||
|
||||
export const parseFeatureFlags = (
|
||||
input: // | Record<string, string | undefined>
|
||||
Partial<Record<keyof FeatureFlags, 'true' | 'false' | undefined>>
|
||||
input: Partial<Record<keyof FeatureFlags, 'true' | 'false' | undefined>>,
|
||||
options?: Partial<{
|
||||
/**
|
||||
* Whether to prevent inputs from being overriden by disable/enable all
|
||||
* Default: true
|
||||
*/
|
||||
forceInputs: boolean
|
||||
}>
|
||||
): FeatureFlags => {
|
||||
const { forceInputs = true } = options || {}
|
||||
|
||||
//INFO
|
||||
// As a convention all feature flags should be prefixed with a FF_
|
||||
const res = parseEnv(input, {
|
||||
@@ -88,7 +97,7 @@ export const parseFeatureFlags = (
|
||||
defaults: { _: false }
|
||||
},
|
||||
// Enable limits on personal projects
|
||||
FF_FORCE_PERSONAL_PROJECTS_LIMITS_ENABLED: {
|
||||
FF_PERSONAL_PROJECTS_LIMITS_ENABLED: {
|
||||
schema: z.boolean(),
|
||||
description:
|
||||
'Enables limits on personal projects. Requires FF_GATEKEEPER_MODULE_ENABLED and FF_WORKSPACES_MODULE_ENABLED to be true. This requires a valid Speckle Enterprise Edition license in order to be enabled, see https://github.com/specklesystems/speckle-server?tab=License-1-ov-file#readme',
|
||||
@@ -105,6 +114,10 @@ export const parseFeatureFlags = (
|
||||
// Can be used to disable/enable all feature flags for testing purposes
|
||||
if (isDisableAllFFsMode() || isEnableAllFFsMode()) {
|
||||
for (const key of Object.keys(res)) {
|
||||
if (forceInputs && has(input, key)) {
|
||||
continue // skip if we are forcing inputs
|
||||
}
|
||||
|
||||
;(res as Record<string, boolean>)[key] = !isDisableAllFFsMode() // disable takes precedence
|
||||
}
|
||||
}
|
||||
@@ -125,12 +138,12 @@ export type FeatureFlags = {
|
||||
FF_FORCE_ONBOARDING: boolean
|
||||
FF_MOVE_PROJECT_REGION_ENABLED: boolean
|
||||
FF_NO_PERSONAL_EMAILS_ENABLED: boolean
|
||||
FF_FORCE_PERSONAL_PROJECTS_LIMITS_ENABLED: boolean
|
||||
FF_PERSONAL_PROJECTS_LIMITS_ENABLED: boolean
|
||||
FF_NEXT_GEN_FILE_IMPORTER_ENABLED: boolean
|
||||
}
|
||||
|
||||
export function getFeatureFlags(): FeatureFlags {
|
||||
//@ts-expect-error this way, the parse function typing is a lot better
|
||||
if (!parsedFlags) parsedFlags = parseFeatureFlags(process.env)
|
||||
if (!parsedFlags) parsedFlags = parseFeatureFlags(process.env, { forceInputs: false })
|
||||
return parsedFlags
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
calculateLimitCutoffDate,
|
||||
getProjectLimitDate,
|
||||
isCreatedBeyondHistoryLimitCutoff
|
||||
getProjectLimitDateFactory,
|
||||
isCreatedBeyondHistoryLimitCutoffFactory
|
||||
} from './utils.js'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('Limits utils', () => {
|
||||
})
|
||||
describe('getProjectLimitDate', () => {
|
||||
it('returns workspaceLimits for workspace projects', async () => {
|
||||
const cutoffDate = await getProjectLimitDate({
|
||||
const cutoffDate = await getProjectLimitDateFactory({
|
||||
getPersonalProjectLimits: () => {
|
||||
expect.fail()
|
||||
},
|
||||
@@ -39,7 +39,7 @@ describe('Limits utils', () => {
|
||||
expect(cutoffDate).toBeNull()
|
||||
})
|
||||
it('returns projectLimits for non workspaceProjects', async () => {
|
||||
const cutoffDate = await getProjectLimitDate({
|
||||
const cutoffDate = await getProjectLimitDateFactory({
|
||||
getPersonalProjectLimits: async () => null,
|
||||
getWorkspaceLimits: async () => {
|
||||
expect.fail()
|
||||
@@ -50,40 +50,43 @@ describe('Limits utils', () => {
|
||||
})
|
||||
describe('isCreatedBeyondHistoryLimitCutoff', () => {
|
||||
it('returns false if there are no limits', async () => {
|
||||
const isCreatedBeyondHistoryLimit = await isCreatedBeyondHistoryLimitCutoff({
|
||||
getProjectLimitDate: async () => null
|
||||
})({
|
||||
entity: { createdAt: new Date() },
|
||||
limitType: 'commentHistory',
|
||||
project: { workspaceId: null }
|
||||
})
|
||||
const isCreatedBeyondHistoryLimit =
|
||||
await isCreatedBeyondHistoryLimitCutoffFactory({
|
||||
getProjectLimitDate: async () => null
|
||||
})({
|
||||
entity: { createdAt: new Date() },
|
||||
limitType: 'commentHistory',
|
||||
project: { workspaceId: null }
|
||||
})
|
||||
expect(isCreatedBeyondHistoryLimit).to.be.toBeFalsy()
|
||||
})
|
||||
it('returns false if entity is newer than the limit cutoff date', async () => {
|
||||
const isCreatedBeyondHistoryLimit = await isCreatedBeyondHistoryLimitCutoff({
|
||||
getProjectLimitDate: async () => new Date(1999)
|
||||
})({
|
||||
entity: { createdAt: new Date() },
|
||||
limitType: 'commentHistory',
|
||||
project: { workspaceId: null }
|
||||
})
|
||||
const isCreatedBeyondHistoryLimit =
|
||||
await isCreatedBeyondHistoryLimitCutoffFactory({
|
||||
getProjectLimitDate: async () => new Date(1999)
|
||||
})({
|
||||
entity: { createdAt: new Date() },
|
||||
limitType: 'commentHistory',
|
||||
project: { workspaceId: null }
|
||||
})
|
||||
expect(isCreatedBeyondHistoryLimit).to.be.toBeFalsy()
|
||||
})
|
||||
it('returns false if entity is right on the limit cutoff date', async () => {
|
||||
const date = new Date()
|
||||
const isCreatedBeyondHistoryLimit = await isCreatedBeyondHistoryLimitCutoff({
|
||||
getProjectLimitDate: async () => date
|
||||
})({
|
||||
entity: { createdAt: date },
|
||||
limitType: 'commentHistory',
|
||||
project: { workspaceId: null }
|
||||
})
|
||||
const isCreatedBeyondHistoryLimit =
|
||||
await isCreatedBeyondHistoryLimitCutoffFactory({
|
||||
getProjectLimitDate: async () => date
|
||||
})({
|
||||
entity: { createdAt: date },
|
||||
limitType: 'commentHistory',
|
||||
project: { workspaceId: null }
|
||||
})
|
||||
expect(isCreatedBeyondHistoryLimit).to.be.toBeFalsy()
|
||||
})
|
||||
})
|
||||
it('returns true of entity is older than the limit cutoff date', async () => {
|
||||
const date = new Date()
|
||||
const isCreatedBeyondHistoryLimit = await isCreatedBeyondHistoryLimitCutoff({
|
||||
const isCreatedBeyondHistoryLimit = await isCreatedBeyondHistoryLimitCutoffFactory({
|
||||
getProjectLimitDate: async () => date
|
||||
})({
|
||||
entity: { createdAt: new Date(1999) },
|
||||
|
||||
@@ -3,7 +3,7 @@ import { GetWorkspaceLimits } from '../authz/domain/workspaces/operations.js'
|
||||
import { GetHistoryLimits, HistoryLimitTypes, HistoryLimits } from './domain.js'
|
||||
import { Project } from '../authz/domain/projects/types.js'
|
||||
|
||||
export const isCreatedBeyondHistoryLimitCutoff =
|
||||
export const isCreatedBeyondHistoryLimitCutoffFactory =
|
||||
({ getProjectLimitDate }: { getProjectLimitDate: GetProjectLimitDate }) =>
|
||||
async ({
|
||||
entity,
|
||||
@@ -21,6 +21,10 @@ export const isCreatedBeyondHistoryLimitCutoff =
|
||||
return limitDate ? dayjs(limitDate).isAfter(entity.createdAt) : false
|
||||
}
|
||||
|
||||
export type IsCreatedBeyondHistoryLimitCutoff = ReturnType<
|
||||
typeof isCreatedBeyondHistoryLimitCutoffFactory
|
||||
>
|
||||
|
||||
export const calculateLimitCutoffDate = (
|
||||
historyLimits: HistoryLimits | null,
|
||||
limitType: HistoryLimitTypes
|
||||
@@ -32,12 +36,12 @@ export const calculateLimitCutoffDate = (
|
||||
.toDate()
|
||||
}
|
||||
|
||||
type GetProjectLimitDate = (args: {
|
||||
export type GetProjectLimitDate = (args: {
|
||||
project: Pick<Project, 'workspaceId'>
|
||||
limitType: HistoryLimitTypes
|
||||
}) => Promise<Date | null>
|
||||
|
||||
export const getProjectLimitDate =
|
||||
export const getProjectLimitDateFactory =
|
||||
({
|
||||
getWorkspaceLimits,
|
||||
getPersonalProjectLimits
|
||||
|
||||
@@ -5,6 +5,8 @@ import { nanoid } from 'nanoid'
|
||||
import { Model } from '../authz/domain/models/types.js'
|
||||
import { Version } from '../authz/domain/versions/types.js'
|
||||
import { Workspace } from '../authz/domain/workspaces/types.js'
|
||||
import { FeatureFlags, parseFeatureFlags } from '../environment/index.js'
|
||||
import { mapValues } from 'lodash'
|
||||
|
||||
export const fakeGetFactory =
|
||||
<T extends Record<string, unknown>>(defaults: () => T) =>
|
||||
@@ -47,3 +49,6 @@ export const getVersionFake = fakeGetFactory<Version>(() => ({
|
||||
projectId: nanoid(10),
|
||||
authorId: nanoid(10)
|
||||
}))
|
||||
|
||||
export const getEnvFake = (overrides?: Partial<FeatureFlags>) =>
|
||||
parseFeatureFlags(mapValues(overrides || {}, (v) => `${v}` as 'true' | 'false'))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './errors/index.js'
|
||||
export * from './helpers/plans.js'
|
||||
export * from './helpers/features.js'
|
||||
export * from './helpers/limits.js'
|
||||
|
||||
@@ -566,6 +566,9 @@ Generate the environment variables for Speckle server and Speckle objects deploy
|
||||
- name: FF_WORKSPACES_MODULE_ENABLED
|
||||
value: {{ .Values.featureFlags.workspacesModuleEnabled | quote }}
|
||||
|
||||
- name: FF_PERSONAL_PROJECTS_LIMITS_ENABLED
|
||||
value: {{ .Values.featureFlags.personalProjectLimitsEnabled | quote }}
|
||||
|
||||
- name: FF_WORKSPACES_SSO_ENABLED
|
||||
value: {{ .Values.featureFlags.workspacesSSOEnabled | quote }}
|
||||
|
||||
|
||||
@@ -50,6 +50,11 @@
|
||||
"description": "High level flag fully toggles the workspaces module",
|
||||
"default": false
|
||||
},
|
||||
"personalProjectLimitsEnabled": {
|
||||
"type": "boolean",
|
||||
"description": "High level flag toggles personal (non-workspace) project limits",
|
||||
"default": false
|
||||
},
|
||||
"workspacesSSOEnabled": {
|
||||
"type": "boolean",
|
||||
"description": "High level flag fully toggles the workspaces dynamic sso",
|
||||
|
||||
@@ -41,6 +41,8 @@ featureFlags:
|
||||
gendoAIModuleEnabled: false
|
||||
## @param featureFlags.workspacesModuleEnabled High level flag fully toggles the workspaces module
|
||||
workspacesModuleEnabled: false
|
||||
## @param featureFlags.personalProjectLimitsEnabled High level flag toggles personal (non-workspace) project limits
|
||||
personalProjectLimitsEnabled: false
|
||||
## @param featureFlags.workspacesSSOEnabled High level flag fully toggles the workspaces dynamic sso
|
||||
workspacesSSOEnabled: false
|
||||
## @param featureFlags.multipleEmailsModuleEnabled High level flag fully toggles multiple emails
|
||||
|
||||
@@ -133,8 +133,10 @@
|
||||
},
|
||||
"vue.complete.casing.props": "kebab",
|
||||
"vue.inlayHints.missingProps": true,
|
||||
"circleci.persistedProjectSelection": ["gh/specklesystems/speckle-server"],
|
||||
"eslint.experimental.useFlatConfig": true
|
||||
"circleci.persistedProjectSelection": ["gh/specklesystems/speckle-server"]
|
||||
// "vitest.nodeEnv": {
|
||||
// "DISABLE_ALL_FFS": "1"
|
||||
// }
|
||||
},
|
||||
"extensions": {
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
|
||||
|
||||
Reference in New Issue
Block a user