Files
2025-12-29 19:04:38 +05:00

411 lines
11 KiB
TypeScript

/* eslint-disable @typescript-eslint/unbound-method */
import card from '@hcengineering/card'
import {
DOMAIN_BENCHMARK,
DOMAIN_BLOB,
DOMAIN_MODEL,
DOMAIN_TRANSIENT,
DOMAIN_TX,
Hierarchy,
ModelDb,
systemAccountUuid,
type Branding,
type Class,
type Doc,
type MeasureContext,
type Ref,
type Tx,
type WorkspaceIds
} from '@hcengineering/core'
import {
ApplyTxMiddleware,
BroadcastMiddleware,
ConfigurationMiddleware,
ContextNameMiddleware,
DBAdapterInitMiddleware,
DBAdapterMiddleware,
DomainFindMiddleware,
DomainTxMiddleware,
FindSecurityMiddleware,
FullTextMiddleware,
GuestPermissionsMiddleware,
IdentityMiddleware,
LiveQueryMiddleware,
LookupMiddleware,
LowLevelMiddleware,
MarkDerivedEntryMiddleware,
ModelMiddleware,
ModifiedMiddleware,
IdentifierMiddleware,
NormalizeTxMiddleware,
PluginConfigurationMiddleware,
PrivateMiddleware,
QueryJoinMiddleware,
QueueMiddleware,
SpacePermissionsMiddleware,
SpaceSecurityMiddleware,
VersioningMiddleware,
TriggersMiddleware,
TxMiddleware,
TxOrderingMiddleware,
UserStatusMiddleware
} from '@hcengineering/middleware'
import {
createBenchmarkAdapter,
createInMemoryAdapter,
createNullAdapter,
createPipeline,
type BroadcastOps,
type DbAdapterFactory,
type DbConfiguration,
type Middleware,
type MiddlewareCreator,
type Pipeline,
type PipelineContext,
type PipelineFactory,
type PlatformQueue,
type StorageAdapter,
type WorkspaceDestroyAdapter
} from '@hcengineering/server-core'
import { generateToken } from '@hcengineering/server-token'
import { createStorageDataAdapter } from './blobStorage'
import { CommunicationMiddleware, type CommunicationApiFactory } from './communication'
import { RatingMiddleware } from '@hcengineering/server-rating'
/**
* @public
*/
export function getTxAdapterFactory (
metrics: MeasureContext,
dbUrl: string,
workspace: WorkspaceIds,
branding: Branding | null,
opt: {
disableTriggers?: boolean
usePassedCtx?: boolean
externalStorage: StorageAdapter
},
extensions?: Partial<DbConfiguration>
): DbAdapterFactory {
const conf = getConfig(metrics, dbUrl, metrics, opt, extensions)
const adapterName = conf.domains[DOMAIN_TX] ?? conf.defaultAdapter
const adapter = conf.adapters[adapterName]
return adapter.factory
}
function addMessagesToFullText (fulltext: MiddlewareCreator): MiddlewareCreator {
return async (ctx: MeasureContext, context: PipelineContext, next?: Middleware) => {
const result: FullTextMiddleware = (await fulltext(ctx, context, next)) as FullTextMiddleware
result.addExtraFind = (baseClass, childClasses) => {
if (context.hierarchy.isDerived(baseClass, card.class.Card)) {
// Using Card as base class because messages are the same for any card subclass
childClasses.add(`${card.class.Card}%message` as Ref<Class<Doc>>)
}
}
return result
}
}
/**
* @public
*/
export function createServerPipeline (
metrics: MeasureContext,
dbUrl: string,
model: Tx[],
opt: {
fulltextUrl?: string
disableTriggers?: boolean
usePassedCtx?: boolean
adapterSecurity?: boolean
externalStorage: StorageAdapter
queue?: PlatformQueue
extraLogging?: boolean // If passed, will log every request/etc.
pipelineContextVars?: Record<string, any>
communicationApiFactory?: CommunicationApiFactory
},
extensions?: Partial<DbConfiguration>
): PipelineFactory {
return (ctx, workspace, broadcast, branding) => {
const metricsCtx = opt.usePassedCtx === true ? ctx : metrics
const wsMetrics = metricsCtx.newChild('🧲 session', {}, { span: false })
const conf = getConfig(metrics, dbUrl, wsMetrics, opt, extensions)
const middlewares: MiddlewareCreator[] = [
LookupMiddleware.create,
NormalizeTxMiddleware.create,
IdentityMiddleware.create,
ModifiedMiddleware.create,
FindSecurityMiddleware.create,
PluginConfigurationMiddleware.create,
PrivateMiddleware.create,
(ctx: MeasureContext, context: PipelineContext, next?: Middleware) =>
SpaceSecurityMiddleware.create(opt.adapterSecurity ?? false, ctx, context, next),
SpacePermissionsMiddleware.create,
GuestPermissionsMiddleware.create,
ConfigurationMiddleware.create,
ContextNameMiddleware.create,
MarkDerivedEntryMiddleware.create,
...(opt.communicationApiFactory !== undefined
? [CommunicationMiddleware.create(opt.communicationApiFactory)]
: []),
UserStatusMiddleware.create,
ApplyTxMiddleware.create, // Extract apply
VersioningMiddleware.create,
IdentifierMiddleware.create, // After ApplyTx to ensure that it pass
RatingMiddleware.create, // Rating editing restrictions
TxMiddleware.create, // Store tx into transaction domain
...(opt.disableTriggers === true ? [] : [TriggersMiddleware.create]),
...(opt.fulltextUrl !== undefined
? [
addMessagesToFullText(
FullTextMiddleware.create(
opt.fulltextUrl,
generateToken(systemAccountUuid, workspace.uuid, { service: 'transactor' })
)
)
]
: []),
LowLevelMiddleware.create,
TxOrderingMiddleware.create(),
QueryJoinMiddleware.create,
LiveQueryMiddleware.create,
DomainFindMiddleware.create,
DomainTxMiddleware.create,
...(opt.queue !== undefined ? [QueueMiddleware.create(opt.queue)] : []),
DBAdapterInitMiddleware.create,
ModelMiddleware.create(model),
DBAdapterMiddleware.create(conf), // Configure DB adapters
BroadcastMiddleware.create(broadcast)
]
const hierarchy = new Hierarchy()
const modelDb = new ModelDb(hierarchy)
const context: PipelineContext = {
workspace,
branding,
modelDb,
hierarchy,
queue: opt.queue,
storageAdapter: opt.externalStorage,
contextVars: opt.pipelineContextVars ?? {}
}
return createPipeline(ctx, middlewares, context)
}
}
/**
* @public
*/
export function createBackupPipeline (
metrics: MeasureContext,
dbUrl: string,
systemTx: Tx[],
opt: {
usePassedCtx?: boolean
adapterSecurity?: boolean
externalStorage: StorageAdapter
}
): PipelineFactory {
return (ctx, workspace, broadcast, branding) => {
const metricsCtx = opt.usePassedCtx === true ? ctx : metrics
const wsMetrics = metricsCtx.newChild('🧲 backup', {}, { span: false })
const conf = getConfig(metrics, dbUrl, wsMetrics, {
...opt,
disableTriggers: true
})
const middlewares: MiddlewareCreator[] = [
LowLevelMiddleware.create,
ContextNameMiddleware.create,
// ConnectionMgrMiddleware.create,
DomainFindMiddleware.create,
DBAdapterInitMiddleware.create,
ModelMiddleware.create(systemTx),
DBAdapterMiddleware.create(conf)
]
const hierarchy = new Hierarchy()
const modelDb = new ModelDb(hierarchy)
const context: PipelineContext = {
workspace,
branding,
modelDb,
hierarchy,
storageAdapter: opt.externalStorage,
contextVars: {}
}
return createPipeline(ctx, middlewares, context)
}
}
export function createEmptyBroadcastOps (): BroadcastOps {
return {
broadcast: (): void => {},
broadcastSessions: (): void => {}
}
}
export async function getServerPipeline (
ctx: MeasureContext,
model: Tx[],
dbUrl: string,
wsUrl: WorkspaceIds,
storageAdapter: StorageAdapter,
opt?: {
queue?: PlatformQueue
disableTriggers?: boolean
communicationApiFactory?: CommunicationApiFactory
}
): Promise<Pipeline> {
const pipelineFactory = createServerPipeline(ctx, dbUrl, model, {
externalStorage: storageAdapter,
usePassedCtx: true,
disableTriggers: opt?.disableTriggers ?? false,
adapterSecurity: isAdapterSecurity(dbUrl),
queue: opt?.queue,
communicationApiFactory: opt?.communicationApiFactory
})
return await pipelineFactory(ctx, wsUrl, createEmptyBroadcastOps(), null)
}
const txAdapterFactories: Record<string, DbAdapterFactory> = {}
const adapterFactories: Record<string, DbAdapterFactory> = {}
const destroyFactories: Record<string, (url: string) => WorkspaceDestroyAdapter> = {}
const adapterSecurityState = new Set<string>()
export function isAdapterSecurity (name: string): boolean {
for (const it of adapterSecurityState) {
if (name.startsWith(it)) {
return true
}
}
return false
}
export function setAdapterSecurity (name: string, state: boolean): void {
if (state) {
adapterSecurityState.add(name)
} else {
adapterSecurityState.delete(name)
}
}
export function registerTxAdapterFactory (name: string, factory: DbAdapterFactory, useAsDefault: boolean = true): void {
txAdapterFactories[name] = factory
if (useAsDefault) {
txAdapterFactories[''] = factory
}
}
export function registerAdapterFactory (name: string, factory: DbAdapterFactory, useAsDefault: boolean = true): void {
adapterFactories[name] = factory
if (useAsDefault) {
adapterFactories[''] = factory
}
}
export function registerDestroyFactory (
name: string,
factory: (url: string) => WorkspaceDestroyAdapter,
useAsDefault: boolean = true
): void {
destroyFactories[name] = factory
if (useAsDefault) {
destroyFactories[''] = factory
}
}
function matchTxAdapterFactory (dbUrl: string): DbAdapterFactory {
for (const [k, v] of Object.entries(txAdapterFactories)) {
if (k !== '' && dbUrl.startsWith(k)) {
return v
}
}
return txAdapterFactories['']
}
function matchAdapterFactory (dbUrl: string): DbAdapterFactory {
for (const [k, v] of Object.entries(adapterFactories)) {
if (k !== '' && dbUrl.startsWith(k)) {
return v
}
}
return adapterFactories['']
}
export function getWorkspaceDestroyAdapter (dbUrl: string): WorkspaceDestroyAdapter {
for (const [k, v] of Object.entries(destroyFactories)) {
if (dbUrl.startsWith(k)) {
return v(dbUrl)
}
}
return destroyFactories[''](dbUrl)
}
export function getConfig (
metrics: MeasureContext,
dbUrl: string,
ctx: MeasureContext,
opt: {
disableTriggers?: boolean
usePassedCtx?: boolean
externalStorage: StorageAdapter
},
extensions?: Partial<DbConfiguration>
): DbConfiguration {
const metricsCtx = opt.usePassedCtx === true ? ctx : metrics
const wsMetrics = metricsCtx.newChild('🧲 session', {}, { span: false })
const conf: DbConfiguration = {
domains: {
[DOMAIN_TX]: 'Tx',
[DOMAIN_TRANSIENT]: 'InMemory',
[DOMAIN_BLOB]: 'StorageData',
[DOMAIN_MODEL]: 'Null',
[DOMAIN_BENCHMARK]: 'Benchmark',
...extensions?.domains
},
metrics: wsMetrics,
defaultAdapter: extensions?.defaultAdapter ?? 'Main',
adapters: {
Tx: {
factory: matchTxAdapterFactory(dbUrl),
url: dbUrl
},
Main: {
factory: matchAdapterFactory(dbUrl),
url: dbUrl
},
Null: {
factory: createNullAdapter,
url: ''
},
InMemory: {
factory: createInMemoryAdapter,
url: ''
},
StorageData: {
factory: createStorageDataAdapter,
url: ''
},
Benchmark: {
factory: createBenchmarkAdapter,
url: ''
},
...extensions?.adapters
},
serviceAdapters: extensions?.serviceAdapters ?? {}
}
return conf
}