feat(workspaces): add repository function implementations

Co-authored-by: Charles Driesler <chuck@speckle.systems>
This commit is contained in:
Gergő Jedlicska
2024-07-04 14:46:48 +02:00
committed by GitHub
parent 8ba0b09f45
commit 28ce7c757c
10 changed files with 201 additions and 27 deletions
@@ -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')
})
}