refactor(backend): middleware before/after (#9128)

This commit is contained in:
Ulf Gebhardt 2026-02-03 14:20:19 +01:00 committed by GitHub
parent b28cdada6d
commit 753a300c3f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 296 additions and 35 deletions

View File

@ -1,6 +1,5 @@
// eslint-disable-next-line import/no-cycle
import { MiddlewareOrder } from '@middleware/index'
// import { addMiddleware } from '@middleware/index'
export default (): MiddlewareOrder[] => {
return []
export default () => {
// addMiddleware({ name: 'myMW', middleware: myMW, position: { } })
}

View File

@ -1,18 +1,20 @@
import { GraphQLResolveInfo } from 'graphql'
import type { Context } from '@src/context'
type Resolver = (
root: unknown,
args: unknown,
context: Context,
resolveInfo: unknown,
resolveInfo: GraphQLResolveInfo,
) => Promise<unknown>
const checkCategoriesActive = (
const checkCategoriesActive = async (
resolve: Resolver,
root: unknown,
args: unknown,
context: Context,
resolveInfo: unknown,
) => {
resolveInfo: GraphQLResolveInfo,
): Promise<unknown> => {
if (context.config.CATEGORIES_ACTIVE) {
return resolve(root, args, context, resolveInfo)
}

View File

@ -0,0 +1,223 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable n/global-require */
// Unit tests for addMiddleware testing append, prepend, before, after, and error cases.
// Each test uses jest.isolateModules + jest.doMock to get a fresh ocelotMiddlewares array.
interface MiddlewareModule {
addMiddleware: (mw: { name: string; middleware: unknown; position: unknown }) => void
default: (schema: unknown) => unknown
}
interface MockOptions {
extraMocks?: Record<string, unknown>
disabledMiddlewares?: string[]
}
const middlewareModules = [
'./categories',
'./chatMiddleware',
'./excerptMiddleware',
'./hashtags/hashtagsMiddleware',
'./includedFieldsMiddleware',
'./languages/languages',
'./login/loginMiddleware',
'./notifications/notificationsMiddleware',
'./orderByMiddleware',
'./permissionsMiddleware',
'./sentryMiddleware',
'./sluggifyMiddleware',
'./softDelete/softDeleteMiddleware',
'./userInteractions',
'./validation/validationMiddleware',
'./xssMiddleware',
]
const setupMocks = ({ extraMocks, disabledMiddlewares = [] }: MockOptions = {}) => {
jest.doMock('./branding/brandingMiddlewares', () => jest.fn())
jest.doMock('@config/index', () => ({ DISABLED_MIDDLEWARES: disabledMiddlewares }))
// Mock all middlewares and allow to override its mock
for (const mod of middlewareModules) {
// eslint-disable-next-line security/detect-object-injection
jest.doMock(mod, () => extraMocks?.[mod] ?? {})
}
}
const loadModule = (
options?: MockOptions,
): { mod: MiddlewareModule; getCapturedMiddlewares: () => unknown[] } => {
let capturedArgs: unknown[] = []
jest.doMock('graphql-middleware', () => ({
applyMiddleware: (_schema: unknown, ...middlewares: unknown[]) => {
capturedArgs = middlewares
return _schema
},
}))
setupMocks(options)
// eslint-disable-next-line n/no-missing-require
const mod = require('./index') as MiddlewareModule
return {
mod,
getCapturedMiddlewares: () => {
mod.default({})
return capturedArgs
},
}
}
describe('default', () => {
it('registers the 16 default middlewares', () => {
jest.isolateModules(() => {
const { getCapturedMiddlewares } = loadModule()
expect(getCapturedMiddlewares()).toHaveLength(16)
})
})
it('calls brandingMiddlewares', () => {
jest.isolateModules(() => {
const { mod } = loadModule()
// eslint-disable-next-line n/no-missing-require
const brandingMiddlewares = require('./branding/brandingMiddlewares') as jest.Mock
mod.default({})
expect(brandingMiddlewares).toHaveBeenCalledTimes(1)
})
})
it('filters out disabled middlewares', () => {
jest.isolateModules(() => {
const sentryMarker = { __test: 'sentry' }
const xssMarker = { __test: 'xss' }
const { getCapturedMiddlewares } = loadModule({
extraMocks: {
'./sentryMiddleware': sentryMarker,
'./xssMiddleware': xssMarker,
},
disabledMiddlewares: ['sentry', 'xss'],
})
const consoleSpy = jest.spyOn(console, 'log').mockImplementation()
const middlewares = getCapturedMiddlewares()
expect(middlewares).toHaveLength(14)
expect(middlewares).not.toContain(sentryMarker)
expect(middlewares).not.toContain(xssMarker)
expect(consoleSpy).toHaveBeenCalledWith('Warning: Disabled "sentry, xss" middleware.')
consoleSpy.mockRestore()
})
})
})
describe('addMiddleware', () => {
describe('append', () => {
it('adds middleware at the end', () => {
jest.isolateModules(() => {
const { mod, getCapturedMiddlewares } = loadModule()
const m = { __test: 'appended' }
mod.addMiddleware({ name: 'test-append', middleware: m, position: 'append' })
const middlewares = getCapturedMiddlewares()
expect(middlewares).toHaveLength(17)
expect(middlewares[16]).toBe(m)
})
})
})
describe('prepend', () => {
it('adds middleware at the beginning', () => {
jest.isolateModules(() => {
const { mod, getCapturedMiddlewares } = loadModule()
const m = { __test: 'prepended' }
mod.addMiddleware({ name: 'test-prepend', middleware: m, position: 'prepend' })
const middlewares = getCapturedMiddlewares()
expect(middlewares).toHaveLength(17)
expect(middlewares[0]).toBe(m)
})
})
})
describe('before', () => {
it('inserts middleware directly before the named anchor', () => {
jest.isolateModules(() => {
const sentryMarker = { __test: 'sentry' }
const permissionsMarker = { __test: 'permissions' }
const { mod, getCapturedMiddlewares } = loadModule({
extraMocks: {
'./sentryMiddleware': sentryMarker,
'./permissionsMiddleware': permissionsMarker,
},
})
const m = { __test: 'before-permissions' }
mod.addMiddleware({
name: 'test-before-permissions',
middleware: m,
position: { before: 'permissions' },
})
const middlewares = getCapturedMiddlewares()
const idxSentry = middlewares.indexOf(sentryMarker)
const idxNew = middlewares.indexOf(m)
const idxPermissions = middlewares.indexOf(permissionsMarker)
expect(idxSentry).toBeLessThan(idxNew)
expect(idxNew).toBe(idxPermissions - 1)
})
})
})
describe('after', () => {
it('inserts middleware directly after the named anchor', () => {
jest.isolateModules(() => {
const sentryMarker = { __test: 'sentry' }
const permissionsMarker = { __test: 'permissions' }
const { mod, getCapturedMiddlewares } = loadModule({
extraMocks: {
'./sentryMiddleware': sentryMarker,
'./permissionsMiddleware': permissionsMarker,
},
})
const m = { __test: 'after-sentry' }
mod.addMiddleware({
name: 'test-after-sentry',
middleware: m,
position: { after: 'sentry' },
})
const middlewares = getCapturedMiddlewares()
const idxSentry = middlewares.indexOf(sentryMarker)
const idxNew = middlewares.indexOf(m)
const idxPermissions = middlewares.indexOf(permissionsMarker)
expect(idxNew).toBe(idxSentry + 1)
expect(idxNew).toBeLessThan(idxPermissions)
})
})
})
describe('unknown anchor', () => {
it('throws when "before" anchor does not exist', () => {
jest.isolateModules(() => {
const { mod } = loadModule()
expect(() =>
mod.addMiddleware({
name: 'failure',
middleware: {},
position: { before: 'nonexistent' },
}),
).toThrow('Could not find middleware "nonexistent" to append the middleware "failure"')
})
})
it('throws when "after" anchor does not exist', () => {
jest.isolateModules(() => {
const { mod } = loadModule()
expect(() =>
mod.addMiddleware({
name: 'failure',
middleware: {},
position: { after: 'nonexistent' },
}),
).toThrow('Could not find middleware "nonexistent" to append the middleware "failure"')
})
})
})
})

View File

@ -1,11 +1,10 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { applyMiddleware, IMiddleware } from 'graphql-middleware'
import { applyMiddleware, IMiddleware, IMiddlewareGenerator } from 'graphql-middleware'
import CONFIG from '@config/index'
// eslint-disable-next-line import/no-cycle
import brandingMiddlewares from './branding/brandingMiddlewares'
import categories from './categories'
import chatMiddleware from './chatMiddleware'
@ -25,41 +24,79 @@ import validation from './validation/validationMiddleware'
import xss from './xssMiddleware'
export interface MiddlewareOrder {
order: number
position: 'prepend' | 'append' | { before: string } | { after: string }
name: string
middleware: IMiddleware
// eslint-disable-next-line @typescript-eslint/no-explicit-any
middleware: IMiddleware | IMiddlewareGenerator<any, any, any>
}
const ocelotMiddlewares: MiddlewareOrder[] = [
{ order: -200, name: 'sentry', middleware: sentry },
{ order: -190, name: 'permissions', middleware: permissions },
{ order: -180, name: 'xss', middleware: xss },
{ order: -170, name: 'validation', middleware: validation },
{ order: -160, name: 'userInteractions', middleware: userInteractions },
{ order: -150, name: 'sluggify', middleware: sluggify },
{ order: -140, name: 'languages', middleware: languages },
{ order: -130, name: 'excerpt', middleware: excerpt },
{ order: -120, name: 'login', middleware: login },
{ order: -110, name: 'notifications', middleware: notifications },
{ order: -100, name: 'hashtags', middleware: hashtags },
{ order: -90, name: 'softDelete', middleware: softDelete },
{ order: -80, name: 'includedFields', middleware: includedFields },
{ order: -70, name: 'orderBy', middleware: orderBy },
{ order: -60, name: 'chatMiddleware', middleware: chatMiddleware },
{ order: -50, name: 'categories', middleware: categories },
]
const ocelotMiddlewares: MiddlewareOrder[] = []
export const addMiddleware = (middleware: MiddlewareOrder) => {
switch (middleware.position) {
case 'append':
ocelotMiddlewares.push(middleware)
break
case 'prepend':
ocelotMiddlewares.unshift(middleware)
break
default: {
const anchor =
'before' in middleware.position ? middleware.position.before : middleware.position.after
const appendMiddlewareAt = ocelotMiddlewares.findIndex((m) => m.name === anchor)
if (appendMiddlewareAt === -1) {
throw new Error(
`Could not find middleware "${anchor}" to append the middleware "${middleware.name}"`,
)
}
ocelotMiddlewares.splice(
appendMiddlewareAt + ('before' in middleware.position ? 0 : 1),
0,
middleware,
)
}
}
}
addMiddleware({ name: 'sentry', middleware: sentry, position: 'append' })
addMiddleware({ name: 'permissions', middleware: permissions, position: { after: 'sentry' } })
addMiddleware({ name: 'xss', middleware: xss, position: { after: 'permissions' } })
addMiddleware({ name: 'validation', middleware: validation, position: { after: 'xss' } })
addMiddleware({
name: 'userInteractions',
middleware: userInteractions,
position: { after: 'validation' },
})
addMiddleware({ name: 'sluggify', middleware: sluggify, position: { after: 'userInteractions' } })
addMiddleware({ name: 'languages', middleware: languages, position: { after: 'sluggify' } })
addMiddleware({ name: 'excerpt', middleware: excerpt, position: { after: 'languages' } })
addMiddleware({ name: 'login', middleware: login, position: { after: 'excerpt' } })
addMiddleware({ name: 'notifications', middleware: notifications, position: { after: 'login' } })
addMiddleware({ name: 'hashtags', middleware: hashtags, position: { after: 'notifications' } })
addMiddleware({ name: 'softDelete', middleware: softDelete, position: { after: 'hashtags' } })
addMiddleware({
name: 'includedFields',
middleware: includedFields,
position: { after: 'softDelete' },
})
addMiddleware({ name: 'orderBy', middleware: orderBy, position: { after: 'includedFields' } })
addMiddleware({
name: 'chatMiddleware',
middleware: chatMiddleware,
position: { after: 'orderBy' },
})
addMiddleware({ name: 'categories', middleware: categories, position: { after: 'chatMiddleware' } })
export default (schema) => {
const middlewares = ocelotMiddlewares
.concat(brandingMiddlewares())
.sort((a, b) => a.order - b.order)
// execute branding middleware function
brandingMiddlewares()
const filteredMiddlewares = middlewares.filter(
const filteredMiddlewares = ocelotMiddlewares.filter(
(middleware) => !CONFIG.DISABLED_MIDDLEWARES.includes(middleware.name),
)
// Warn if we filtered
if (middlewares.length < filteredMiddlewares.length) {
if (ocelotMiddlewares.length !== filteredMiddlewares.length) {
// eslint-disable-next-line no-console
console.log(`Warning: Disabled "${CONFIG.DISABLED_MIDDLEWARES.join(', ')}" middleware.`)
}