diff --git a/packages/server/assets/workspaces/workspaces.graphql b/packages/server/assets/workspaces/workspaces.graphql index fa130bd6b..7d34895e6 100644 --- a/packages/server/assets/workspaces/workspaces.graphql +++ b/packages/server/assets/workspaces/workspaces.graphql @@ -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. """ diff --git a/packages/server/modules/workspaces/domain/operations.ts b/packages/server/modules/workspaces/domain/operations.ts index 4e317663a..48b36498e 100644 --- a/packages/server/modules/workspaces/domain/operations.ts +++ b/packages/server/modules/workspaces/domain/operations.ts @@ -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 +/** Workspace */ + +type UpsertWorkspaceArgs = { + workspace: Workspace +} + +export type UpsertWorkspace = (args: UpsertWorkspaceArgs) => Promise + +type GetWorkspaceArgs = { + workspaceId: string +} + +export type GetWorkspace = (args: GetWorkspaceArgs) => Promise + +/** WorkspaceRole */ + export type UpsertWorkspaceRole = (args: WorkspaceAcl) => Promise +type GetWorkspaceRoleArgs = { + workspaceId: string + userId: string +} + +export type GetWorkspaceRole = ( + args: GetWorkspaceRoleArgs +) => Promise + +/** Blob */ + export type StoreBlob = (args: string) => Promise +/** Events */ + export type EmitWorkspaceEvent = (args: { event: TEvent payload: WorkspaceEventsPayloads[TEvent] diff --git a/packages/server/modules/workspaces/domain/types.ts b/packages/server/modules/workspaces/domain/types.ts index 1e3176f29..fbb617a15 100644 --- a/packages/server/modules/workspaces/domain/types.ts +++ b/packages/server/modules/workspaces/domain/types.ts @@ -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 } diff --git a/packages/server/modules/workspaces/events/emitter.ts b/packages/server/modules/workspaces/events/emitter.ts new file mode 100644 index 000000000..8406a7974 --- /dev/null +++ b/packages/server/modules/workspaces/events/emitter.ts @@ -0,0 +1,11 @@ +import { initializeModuleEventEmitter } from '@/modules/shared/services/moduleEventEmitterSetup' +import { + WorkspaceEvents, + WorkspaceEventsPayloads +} from '@/modules/workspacesCore/domain/events' + +const { emit, listen } = initializeModuleEventEmitter({ + moduleName: 'workspaces' +}) + +export const WorkspacesEmitter = { emit, listen, events: WorkspaceEvents } diff --git a/packages/server/modules/workspaces/repositories/workspaces.ts b/packages/server/modules/workspaces/repositories/workspaces.ts index ff9e304b4..b0327ff3e 100644 --- a/packages/server/modules/workspaces/repositories/workspaces.ts +++ b/packages/server/modules/workspaces/repositories/workspaces.ts @@ -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('workspaces'), workspacesAcl: (db: Knex) => db('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']) } diff --git a/packages/server/modules/workspaces/services/workspaceCreation.ts b/packages/server/modules/workspaces/services/workspaceCreation.ts index 94672ab9f..43e026a5e 100644 --- a/packages/server/modules/workspaces/services/workspaceCreation.ts +++ b/packages/server/modules/workspaces/services/workspaceCreation.ts @@ -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 diff --git a/packages/server/modules/workspaces/tests/integration/repositories.spec.ts b/packages/server/modules/workspaces/tests/integration/repositories.spec.ts new file mode 100644 index 000000000..4a832c9ec --- /dev/null +++ b/packages/server/modules/workspaces/tests/integration/repositories.spec.ts @@ -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 => { + 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)) + }) + }) +}) diff --git a/packages/server/modules/workspaces/tests/unit/workspaceCreation.spec.ts b/packages/server/modules/workspaces/tests/unit/workspaceCreation.spec.ts index 93bd0087b..e4ca19244 100644 --- a/packages/server/modules/workspaces/tests/unit/workspaceCreation.spec.ts +++ b/packages/server/modules/workspaces/tests/unit/workspaceCreation.spec.ts @@ -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 diff --git a/packages/server/modules/workspacesCore/domain/events.ts b/packages/server/modules/workspacesCore/domain/events.ts index 975a73c5d..cacbe1a4d 100644 --- a/packages/server/modules/workspacesCore/domain/events.ts +++ b/packages/server/modules/workspacesCore/domain/events.ts @@ -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 diff --git a/packages/server/modules/workspacesCore/migrations/20240628112300_dropCreatorId.ts b/packages/server/modules/workspacesCore/migrations/20240628112300_dropCreatorId.ts new file mode 100644 index 000000000..05b5b115c --- /dev/null +++ b/packages/server/modules/workspacesCore/migrations/20240628112300_dropCreatorId.ts @@ -0,0 +1,13 @@ +import { Knex } from 'knex' + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable('workspaces', (table) => { + table.dropColumn('createdByUserId') + }) +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable('workspaces', (table) => { + table.text('createdByUserId').references('id').inTable('users').onDelete('set null') + }) +}