decoupled inter-module communication & third party (NPM) module support
This commit is contained in:
Родитель
4da196ec48
Коммит
18d826a176
|
@ -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
|
Загрузка…
Ссылка в новой задаче