feat(workspaces): add repository function implementations
Co-authored-by: Charles Driesler <chuck@speckle.systems>
This commit is contained in:
@@ -4,7 +4,6 @@ type Workspace {
|
||||
description: String
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
createdBy: LimitedUser
|
||||
"""
|
||||
Active user's role for this workspace. `null` if request is not authenticated, or the workspace is not explicitly shared with you.
|
||||
"""
|
||||
|
||||
@@ -4,11 +4,39 @@ import {
|
||||
} from '@/modules/workspacesCore/domain/events'
|
||||
import { Workspace, WorkspaceAcl } from '@/modules/workspaces/domain/types'
|
||||
|
||||
export type StoreWorkspace = (args: { workspace: Workspace }) => Promise<void>
|
||||
/** Workspace */
|
||||
|
||||
type UpsertWorkspaceArgs = {
|
||||
workspace: Workspace
|
||||
}
|
||||
|
||||
export type UpsertWorkspace = (args: UpsertWorkspaceArgs) => Promise<void>
|
||||
|
||||
type GetWorkspaceArgs = {
|
||||
workspaceId: string
|
||||
}
|
||||
|
||||
export type GetWorkspace = (args: GetWorkspaceArgs) => Promise<Workspace | null>
|
||||
|
||||
/** WorkspaceRole */
|
||||
|
||||
export type UpsertWorkspaceRole = (args: WorkspaceAcl) => Promise<void>
|
||||
|
||||
type GetWorkspaceRoleArgs = {
|
||||
workspaceId: string
|
||||
userId: string
|
||||
}
|
||||
|
||||
export type GetWorkspaceRole = (
|
||||
args: GetWorkspaceRoleArgs
|
||||
) => Promise<WorkspaceAcl | null>
|
||||
|
||||
/** Blob */
|
||||
|
||||
export type StoreBlob = (args: string) => Promise<string>
|
||||
|
||||
/** Events */
|
||||
|
||||
export type EmitWorkspaceEvent = <TEvent extends WorkspaceEvents>(args: {
|
||||
event: TEvent
|
||||
payload: WorkspaceEventsPayloads[TEvent]
|
||||
|
||||
@@ -6,8 +6,6 @@ export type Workspace = {
|
||||
description: string | null
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
// the user who created it, might not be a server user any more
|
||||
createdByUserId: string | null
|
||||
logoUrl: string | null
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { initializeModuleEventEmitter } from '@/modules/shared/services/moduleEventEmitterSetup'
|
||||
import {
|
||||
WorkspaceEvents,
|
||||
WorkspaceEventsPayloads
|
||||
} from '@/modules/workspacesCore/domain/events'
|
||||
|
||||
const { emit, listen } = initializeModuleEventEmitter<WorkspaceEventsPayloads>({
|
||||
moduleName: 'workspaces'
|
||||
})
|
||||
|
||||
export const WorkspacesEmitter = { emit, listen, events: WorkspaceEvents }
|
||||
@@ -1,24 +1,53 @@
|
||||
import { Workspace, WorkspaceAcl } from '@/modules/workspaces/domain/types'
|
||||
import {
|
||||
StoreWorkspace,
|
||||
GetWorkspace,
|
||||
GetWorkspaceRole,
|
||||
UpsertWorkspace,
|
||||
UpsertWorkspaceRole
|
||||
} from '@/modules/workspaces/domain/operations'
|
||||
import { Workspace, WorkspaceAcl } from '@/modules/workspaces/domain/types'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { Knex } from 'knex'
|
||||
import { Roles } from '@speckle/shared'
|
||||
|
||||
const tables = {
|
||||
workspaces: (db: Knex) => db<Workspace>('workspaces'),
|
||||
workspacesAcl: (db: Knex) => db<WorkspaceAcl>('workspace_acl')
|
||||
}
|
||||
|
||||
export const storeWorkspaceFactory =
|
||||
({ db }: { db: Knex }): StoreWorkspace =>
|
||||
async ({ workspace }) => {
|
||||
await tables.workspaces(db).insert(workspace)
|
||||
export const getWorkspaceFactory =
|
||||
({ db }: { db: Knex }): GetWorkspace =>
|
||||
async ({ workspaceId }) => {
|
||||
const workspace = await tables
|
||||
.workspaces(db)
|
||||
.select('*')
|
||||
.where('id', '=', workspaceId)
|
||||
.first()
|
||||
|
||||
return workspace || null
|
||||
}
|
||||
|
||||
// TODO: Authorise requester for given role change operation?
|
||||
export const upsertWorkspaceRole =
|
||||
export const upsertWorkspaceFactory =
|
||||
({ db }: { db: Knex }): UpsertWorkspace =>
|
||||
async ({ workspace }) => {
|
||||
await tables
|
||||
.workspaces(db)
|
||||
.insert(workspace)
|
||||
.onConflict('id')
|
||||
.merge(['description', 'logoUrl', 'name', 'updatedAt'])
|
||||
}
|
||||
|
||||
export const getWorkspaceRoleFactory =
|
||||
({ db }: { db: Knex }): GetWorkspaceRole =>
|
||||
async ({ userId, workspaceId }) => {
|
||||
const acl = await tables
|
||||
.workspacesAcl(db)
|
||||
.select('*')
|
||||
.where({ userId, workspaceId })
|
||||
.first()
|
||||
|
||||
return acl || null
|
||||
}
|
||||
|
||||
export const upsertWorkspaceRoleFactory =
|
||||
({ db }: { db: Knex }): UpsertWorkspaceRole =>
|
||||
async ({ userId, workspaceId, role }) => {
|
||||
const validRoles = Object.values(Roles.Workspace)
|
||||
@@ -26,5 +55,9 @@ export const upsertWorkspaceRole =
|
||||
throw new Error(`Unexpected workspace role provided: ${role}`)
|
||||
}
|
||||
|
||||
await tables.workspacesAcl(db).insert({ userId, workspaceId, role })
|
||||
await tables
|
||||
.workspacesAcl(db)
|
||||
.insert({ userId, workspaceId, role })
|
||||
.onConflict(['userId', 'workspaceId'])
|
||||
.merge(['role'])
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events'
|
||||
import {
|
||||
EmitWorkspaceEvent,
|
||||
StoreBlob,
|
||||
StoreWorkspace,
|
||||
UpsertWorkspace,
|
||||
UpsertWorkspaceRole
|
||||
} from '@/modules/workspaces/domain/operations'
|
||||
import { Workspace } from '@/modules/workspaces/domain/types'
|
||||
@@ -15,12 +16,12 @@ type WorkspaceCreateArgs = {
|
||||
|
||||
export const createWorkspaceFactory =
|
||||
({
|
||||
storeWorkspace,
|
||||
upsertWorkspace,
|
||||
upsertWorkspaceRole,
|
||||
emitWorkspaceEvent,
|
||||
storeBlob
|
||||
}: {
|
||||
storeWorkspace: StoreWorkspace
|
||||
upsertWorkspace: UpsertWorkspace
|
||||
upsertWorkspaceRole: UpsertWorkspaceRole
|
||||
storeBlob: StoreBlob
|
||||
emitWorkspaceEvent: EmitWorkspaceEvent
|
||||
@@ -36,10 +37,9 @@ export const createWorkspaceFactory =
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
createdByUserId: userId,
|
||||
logoUrl
|
||||
}
|
||||
await storeWorkspace({ workspace })
|
||||
await upsertWorkspace({ workspace })
|
||||
// assign the creator as workspace administrator
|
||||
await upsertWorkspaceRole({
|
||||
userId,
|
||||
@@ -47,7 +47,7 @@ export const createWorkspaceFactory =
|
||||
workspaceId: workspace.id
|
||||
})
|
||||
|
||||
await emitWorkspaceEvent({ event: 'created', payload: workspace })
|
||||
await emitWorkspaceEvent({ event: WorkspaceEvents.Created, payload: workspace })
|
||||
// emit a workspace created event
|
||||
|
||||
return workspace
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
getWorkspaceFactory,
|
||||
upsertWorkspaceFactory,
|
||||
upsertWorkspaceRoleFactory
|
||||
} from '@/modules/workspaces/repositories/workspaces'
|
||||
import db from '@/db/knex'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { expect } from 'chai'
|
||||
import { Workspace, WorkspaceAcl } from '@/modules/workspaces/domain/types'
|
||||
import { expectToThrow } from '@/test/assertionHelper'
|
||||
|
||||
const getWorkspace = getWorkspaceFactory({ db })
|
||||
const upsertWorkspace = upsertWorkspaceFactory({ db })
|
||||
const upsertWorkspaceRole = upsertWorkspaceRoleFactory({ db })
|
||||
|
||||
const createAndStoreTestWorkspace = async (): Promise<Workspace> => {
|
||||
const workspace: Workspace = {
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
name: cryptoRandomString({ length: 10 }),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
description: null,
|
||||
logoUrl: null
|
||||
}
|
||||
|
||||
await upsertWorkspace({ workspace })
|
||||
|
||||
return workspace
|
||||
}
|
||||
|
||||
describe('Workspace repositories', () => {
|
||||
describe('getWorkspaceFactory creates a function, that', () => {
|
||||
it('returns null if the workspace is not found', async () => {
|
||||
const workspace = await getWorkspace({
|
||||
workspaceId: cryptoRandomString({ length: 10 })
|
||||
})
|
||||
expect(workspace).to.be.null
|
||||
})
|
||||
// not testing get here, we're going to use that for testing upsert
|
||||
})
|
||||
|
||||
describe('upsertWorkspaceFactory creates a function, that', () => {
|
||||
it('upserts the workspace', async () => {
|
||||
const testWorkspace = await createAndStoreTestWorkspace()
|
||||
const storedWorkspace = await getWorkspace({ workspaceId: testWorkspace.id })
|
||||
expect(storedWorkspace).to.deep.equal(testWorkspace)
|
||||
|
||||
const modifiedTestWorkspace: Workspace = {
|
||||
...testWorkspace,
|
||||
description: 'now im adding a description to the workspace'
|
||||
}
|
||||
|
||||
await upsertWorkspace({ workspace: modifiedTestWorkspace })
|
||||
|
||||
const modifiedStoredWorkspace = await getWorkspace({
|
||||
workspaceId: testWorkspace.id
|
||||
})
|
||||
|
||||
expect(modifiedStoredWorkspace).to.deep.equal(modifiedTestWorkspace)
|
||||
})
|
||||
it('updates only relevant workspace fields', async () => {
|
||||
const testWorkspace = await createAndStoreTestWorkspace()
|
||||
const storedWorkspace = await getWorkspace({ workspaceId: testWorkspace.id })
|
||||
expect(storedWorkspace).to.deep.equal(testWorkspace)
|
||||
|
||||
await upsertWorkspace({
|
||||
workspace: {
|
||||
...testWorkspace,
|
||||
id: cryptoRandomString({ length: 13 }),
|
||||
createdAt: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
const modifiedStoredWorkspace = await getWorkspace({
|
||||
workspaceId: testWorkspace.id
|
||||
})
|
||||
|
||||
expect(modifiedStoredWorkspace).to.deep.equal(testWorkspace)
|
||||
})
|
||||
})
|
||||
|
||||
describe('upsertWorkspaceRoleFactory creates a function, that', () => {
|
||||
it('throws if an unknown role is provided', async () => {
|
||||
const role: WorkspaceAcl = {
|
||||
// @ts-expect-error type asserts valid values for `role`
|
||||
role: 'fake-role',
|
||||
userId: '',
|
||||
workspaceId: ''
|
||||
}
|
||||
|
||||
await expectToThrow(() => upsertWorkspaceRole(role))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -4,12 +4,12 @@ import { Roles } from '@speckle/shared'
|
||||
import { expect } from 'chai'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
|
||||
describe('Workspace creation', () => {
|
||||
describe('Workspace services', () => {
|
||||
describe('createWorkspaceFactory creates a function, that', () => {
|
||||
it('stores the workspace', async () => {
|
||||
const storedWorkspaces: Workspace[] = []
|
||||
const createWorkspace = createWorkspaceFactory({
|
||||
storeWorkspace: async ({ workspace }: { workspace: Workspace }) => {
|
||||
upsertWorkspace: async ({ workspace }: { workspace: Workspace }) => {
|
||||
storedWorkspaces.push(workspace)
|
||||
},
|
||||
upsertWorkspaceRole: async () => {},
|
||||
@@ -32,7 +32,7 @@ describe('Workspace creation', () => {
|
||||
it('makes the workspace creator becomes a workspace:admin', async () => {
|
||||
const storedRole: WorkspaceAcl[] = []
|
||||
const createWorkspace = createWorkspaceFactory({
|
||||
storeWorkspace: async () => {},
|
||||
upsertWorkspace: async () => {},
|
||||
upsertWorkspaceRole: async (workspaceAcl: WorkspaceAcl) => {
|
||||
storedRole.push(workspaceAcl)
|
||||
},
|
||||
@@ -64,7 +64,7 @@ describe('Workspace creation', () => {
|
||||
payload: {}
|
||||
}
|
||||
const createWorkspace = createWorkspaceFactory({
|
||||
storeWorkspace: async () => {},
|
||||
upsertWorkspace: async () => {},
|
||||
upsertWorkspaceRole: async () => {},
|
||||
emitWorkspaceEvent: async ({ event, payload }) => {
|
||||
eventData.isCalled = true
|
||||
|
||||
@@ -6,9 +6,7 @@ export const WorkspaceEvents = {
|
||||
|
||||
export type WorkspaceEvents = (typeof WorkspaceEvents)[keyof typeof WorkspaceEvents]
|
||||
|
||||
type WorkspaceCreatedPayload = Workspace & {
|
||||
createdByUserId: string
|
||||
}
|
||||
type WorkspaceCreatedPayload = Workspace
|
||||
|
||||
export type WorkspaceEventsPayloads = {
|
||||
[WorkspaceEvents.Created]: WorkspaceCreatedPayload
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Knex } from 'knex'
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('workspaces', (table) => {
|
||||
table.dropColumn('createdByUserId')
|
||||
})
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('workspaces', (table) => {
|
||||
table.text('createdByUserId').references('id').inTable('users').onDelete('set null')
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user