decoupled inter-module communication & third party (NPM) module support

This commit is contained in:
Kristaps Fabians Geikins 2024-07-12 13:37:37 +03:00
Родитель 4da196ec48
Коммит 18d826a176
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 16D20A16730A0111
3 изменённых файлов: 162 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,66 @@
import { SpeckleModule } from '@/modules/shared/helpers/typeHelper'
export type SharedAppApi = {}
// Each speckle module (even coming from NPM package)
// can augment this type to add the new API it offers
declare module '@speckle/server/api' {
interface SharedAppApi {
newFunc: (a: number) => void
findWorkspaceById: (id: string) => Workspace
}
}
// Each module sets up this API in its init() function
// With this kind of approach I find the `domain` folder to make more sense, as it essentially could
// describe the "shared API" of the module, accessible by other modules
const fooModule: SpeckleModule = {
async init(app, isInitial, sharedApi) {
sharedApi.newFunc = (a: number) => {
// ...
}
sharedApi.findWorkspaceById = (id: string) => {
// ...
}
}
}
/**
* IMAGINE THIS IS THE SERVERINVITES RESOLVER
* Note that there's no JS Imports from fooModule - they're fully decoupled - and yet
* they can still interact with each other through the shared API
*
* The global EventBus implementation we're working on could also be injected the same way,
* to avoid having to import it: ctx.eventBus
*/
const serverInviteCreate = async (_parent, args, ctx) => {
const createAndSendInvite = createAndSendInviteFactory({
findResource: findResourceFactory(),
findUserByTarget: findUserByTargetFactory(),
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
// Shared API is attached to ctx OR it could be a global singleton, since it is essentially
// the same object for the entire runtime for the server
findWorkspaceById: ctx.sharedApi.findWorkspaceById
})
}
/**
* IMAGINE THIS IS serverinvites/services/inviteCreationService.ts
*/
export const createAndSendInviteFactory = (deps) => (params) => {
/**
* Approach 1: The service does explicitly work with workspaces logic, but
* its not coupled to the module, cause its injected through deps
*
* I don't think its possible to avoid one of them having to "know"
* about the logic of another. Either ServerInvites has to know about Workspaces logic,
* or Workspaces has to know about ServerInvites logic. I don't think thats a problem,
* as long as the code is decoupled.
*/
if (isWorkspaceInvite) {
const worksace = deps.findWorkspaceById(params.workspaceId)
await validateWorkspaceInvite(params, workspace)
}
}

Просмотреть файл

@ -0,0 +1,89 @@
import { SpeckleModule } from '@/modules/shared/helpers/typeHelper'
/**
* Based on Wordpress filters: https://developer.wordpress.org/plugins/hooks/filters/
* A module defines a filter type, and other modules can register their own filters on top of that,
* kind of like middlewares.
*
* The filter defining module can then invoke the filter, without even knowing what kind of filters
* were registered by other modules (e.g. workspaces or streams)
*
* Wordpress gets away with just having Actions (Events w/o a return type) and Filters (Middlewares w/ return type),
* maybe that's all we need too? Gergo & Chuck are already working on a global EventBus implementation,
* the filters could be the other part of the equation.
*/
export type SharedAppApi = {
addFilterMiddleware: (
filter: string,
middleware: (data: unknown, next: () => void) => unknown
) => void
invokeFilter: (filter: string, data: unknown) => unknown
}
// Each speckle module (even coming from NPM package)
// can augment this type to add its own filters
// serverInvites.ts:
declare module '@speckle/server/api' {
interface SharedAppApi {
addFilterMiddleware: (
filter: 'inviteTargetBuilder',
middleware: (invite: Invite, currentTargets: InviteTarget[]) => InviteTarget[]
) => void
invokeFilter: (
filter: 'inviteTargetBuilder',
invite: Invite,
currentTargets: InviteTarget[]
) => InviteTarget[]
}
}
// workspaces.ts - adds filter that can handle workspaces logic
const fooModule: SpeckleModule = {
async init(app, isInitial, sharedApi) {
sharedApi.addFilterMiddleware('inviteTargetBuilder', (invite, currentTargets) => {
if (invite.type === 'workspace') {
const workspace = findWorkspaceById(invite.targetId)
return [...currentTargets, ...buildWorkspaceTargets(workspace)]
}
return currentTargets
})
}
}
/**
* IMAGINE THIS IS THE SERVERINVITES RESOLVER
*/
const serverInviteCreate = async (_parent, args, ctx) => {
const createAndSendInvite = createAndSendInviteFactory({
findUserByTarget: findUserByTargetFactory(),
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
/**
* We pass in the invokeFilter method
*/
invokeFilter: ctx.sharedApi.invokeFilter,
/**
* OR abstract that away like so:
*/
buildTargets: (invite) => {
return ctx.sharedApi.invokeFilter('inviteTargetBuilder', invite)
}
})
}
/**
* IMAGINE THIS IS serverinvites/services/inviteCreationService.ts
*/
export const createAndSendInviteFactory = (deps) => (params) => {
/**
* Approach 2: The service doesn't know about what kind of filters are attached, it
* just invokes it and gets back the result built by workspaces, streams and other future modules
*/
const resourceTargets: ResourceTargets[] = deps.invokeFilter(
'inviteTargetBuilder',
params.invite
)
}

Просмотреть файл

@ -0,0 +1,7 @@
# How I suggest we move forward within the scope of this ticket
1. First build out workspace invites the old way - with direct imports and coupling serverInvites
to workspaces
2. Once that's done and we know what the logic is supposed to be, we refactor all of that to use
the approach that we decide on, whether its the Shared API or Events+Filters or a combination of both
or something else entirely