mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2026-02-06 09:56:03 +00:00
refactor(backend): middleware before/after (#9128)
This commit is contained in:
parent
b28cdada6d
commit
753a300c3f
@ -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: { } })
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
223
backend/src/middleware/index.spec.ts
Normal file
223
backend/src/middleware/index.spec.ts
Normal 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"')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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.`)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user