Middleware System
Техническое руководство из исходного проекта Notty. Примеры, параметры и эксплуатационные ограничения.
Notty CMS поддерживает гибкую систему middleware/хуков для перехвата запросов и событий.
Generated App Contract
your-project/
├── notty.config.ts
└── src/
├── plugins/
│ └── app.ts
└── middlewares/
└── index.ts
Generated apps регистрируют middleware через src/plugins/app.ts, который вызывает registerAppMiddlewares() из src/middlewares/index.ts.
Admin API Middleware
Перехват запросов к административному API.
// src/middlewares/index.ts
import { defineAdminMiddleware } from '@notty/core';
export function registerAppMiddlewares(ctx) {
ctx.middleware.addAdmin(
defineAdminMiddleware({
name: 'log-admin-actions',
description: 'Логирование действий админов',
order: 10, // Выполняется раньше (меньше = раньше)
// Паттерны маршрутов (glob-like)
routes: 'admin:*', // Все admin маршруты
// routes: ['admin:users:*', 'admin:roles:*'], // Только users и roles
// HTTP методы (опционально)
methods: ['POST', 'PUT', 'DELETE'],
async handler(ctx, input, next) {
console.log(`[Admin] ${ctx.adminUser?.username} -> ${ctx.routeName}`);
// Вызываем следующий middleware/handler
const result = await next();
console.log(`[Admin] ${ctx.routeName} completed`);
return result;
},
})
);
}
Route Names
Admin маршруты используют формат admin:{resource}:{action}:
admin:users:list- GET /api/admin/usersadmin:users:get- GET /api/admin/users/:idadmin:users:create- POST /api/admin/usersadmin:users:update- PUT /api/admin/users/:idadmin:users:delete- DELETE /api/admin/users/:idadmin:auth:login- POST /api/admin/auth/loginadmin:settings:get- GET /api/admin/settings
Content API Middleware
Перехват CRUD операций над контентом.
// src/middlewares/index.ts
import { defineContentMiddleware } from '@notty/core';
export function registerAppMiddlewares(ctx) {
ctx.middleware.addContent(
defineContentMiddleware({
name: 'validate-slug',
description: 'Автогенерация slug из title',
// Фильтр по content types
contentTypes: '*', // Все типы
// contentTypes: ['article', 'page'], // Только article и page
// Фильтр по операциям
operations: ['create', 'update'],
async handler(ctx, input, next) {
const data = input as Record<string, unknown>;
// Автогенерация slug если не указан
if (data.title && !data.slug) {
data.slug = (data.title as string)
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
}
return next();
},
})
);
}
Операции Content API
create- POST /api/content/:typeread- GET /api/content/:type/:idupdate- PUT /api/content/:type/:iddelete- DELETE /api/content/:type/:idlist- GET /api/content/:typepublish- PUT /api/content/:type/:id/publishunpublish- PUT /api/content/:type/:id/unpublishbulk-update- PATCH /api/content/:type/bulk-updatebulk-delete- DELETE /api/content/:type/bulk-delete
System Event Handlers
Реакция на системные события.
// src/middlewares/index.ts
import { defineSystemMiddleware } from '@notty/core';
export function registerAppMiddlewares(ctx) {
ctx.middleware.addSystem(
defineSystemMiddleware({
name: 'notify-on-publish',
description: 'Уведомление при публикации',
events: ['content:afterPublish', 'content:afterCreate'],
async handler(ctx) {
if (ctx.event === 'content:afterPublish') {
const { contentType, id, entry } = ctx.payload;
console.log(`Published: ${contentType}#${id}`);
// Отправка webhook, email и т.д.
}
},
})
);
}
Legacy Compatibility
Root-level middlewares/ discovery remains available for older projects as a compatibility path.
For new generated apps use src/middlewares/index.ts together with src/plugins/app.ts.
Доступные события
Lifecycle:
app:start- Приложение запущеноapp:stop- Приложение остановлено
Content CRUD:
content:beforeCreate- Перед созданием записиcontent:afterCreate- После создания записиcontent:beforeUpdate- Перед обновлениемcontent:afterUpdate- После обновленияcontent:beforeDelete- Перед удалениемcontent:afterDelete- После удаленияcontent:beforePublish- Перед публикациейcontent:afterPublish- После публикацииcontent:beforeUnpublish- Перед снятия с публикацииcontent:afterUnpublish- После снятия с публикации
Content Read:
content:beforeFind- Перед получением одной записиcontent:afterFind- После получения одной записиcontent:beforeFindMany- Перед получением списка записейcontent:afterFindMany- После получения списка записейcontent:beforeCount- Перед подсчётом записейcontent:afterCount- После подсчёта записейcontent:beforeQuery- Универсальный хук перед любым read-запросомcontent:afterQuery- Универсальный хук после любого read-запроса
Content Validation & Save:
content:beforeValidate- Перед валидацией данныхcontent:afterValidate- После валидации данныхcontent:beforeSave- Перед сохранением (create или update)content:afterSave- После сохранения (create или update)
Schema:
schema:create- Схема созданаschema:update- Схема обновленаschema:delete- Схема удаленаschema:sync- Синхронизация схем
Media:
media:upload- Файл загруженmedia:delete- Файл удалён
Auth (Content API):
auth:beforeLogin- Перед попыткой входаauth:afterLogin- Успешный вход пользователяauth:loginFailed- Неудачная попытка входаauth:logout- Выход пользователяauth:register- Регистрация пользователяauth:tokenRefresh- Обновление токенаauth:passwordReset- Запрос (stage: requested) / завершение (stage: completed) сброса пароля (/api/auth/forgot-password,/api/auth/reset-password)
Admin Auth:
admin:beforeLogin- Перед попыткой входа админаadmin:afterLogin- Успешный вход админаadmin:loginFailed- Неудачная попытка входа админаadmin:logout- Выход админаadmin:tokenRefresh- Обновление токена админа
Контексты
AdminMiddlewareContext
interface AdminMiddlewareContext {
path: string; // URL path
method: 'GET' | 'POST' | ...; // HTTP метод
headers: Record<string, string>; // Заголовки
query: Record<string, string>; // Query параметры
params: Record<string, string>; // Route параметры
adminUser: AdminUserContext | null; // Текущий админ
routeName: string; // Имя маршрута (admin:users:list)
}
ContentMiddlewareContext
interface ContentMiddlewareContext {
path: string;
method: 'GET' | 'POST' | ...;
headers: Record<string, string>;
query: Record<string, string>;
params: Record<string, string>;
user: ContentUserContext | null; // Content user (если есть)
adminUser: AdminUserContext | null; // Admin user (если запрос от админа)
contentType: string; // Тип контента
operation: ContentOperation; // Операция (create, read, etc.)
entryId?: string | number; // ID записи (для read/update/delete)
}
Прерывание запроса
Middleware может прервать цепочку и вернуть свой ответ:
export default defineContentMiddleware({
name: 'block-delete',
operations: ['delete'],
async handler(ctx, input, next) {
// Запретить удаление опубликованных записей
if (ctx.entryId) {
// Проверка...
return {
continue: false,
response: {
statusCode: 403,
body: {
success: false,
message: 'Cannot delete published content',
},
},
};
}
return next();
},
});
Модификация данных
before* события позволяют модифицировать данные:
import { defineSystemMiddleware } from '@notty/core';
export function registerAppMiddlewares(ctx) {
ctx.middleware.addSystem(
defineSystemMiddleware({
name: 'add-author',
events: ['content:beforeCreate'],
async handler(ctx) {
const { data } = ctx.payload;
// Добавляем автора автоматически
data.createdBy = 'system';
// Возвращаем модифицированный payload
return { payload: { ...ctx.payload, data } };
},
})
);
}
Порядок выполнения
Middleware выполняются в порядке order (по умолчанию 100):
// Выполнится первым
defineAdminMiddleware({ name: 'first', order: 10, ... });
// Выполнится вторым
defineAdminMiddleware({ name: 'second', order: 50, ... });
// Выполнится последним
defineAdminMiddleware({ name: 'last', order: 200, ... });
Отключение middleware
Для generated apps обычно достаточно удалить регистрацию из src/middlewares/index.ts.
Файлы с префиксом _ игнорируются только в legacy root-level loader:
middlewares/
└── admin/
├── active.ts # Загружается
└── _disabled.ts # Игнорируется
Или через enabled: false:
defineAdminMiddleware({
name: 'temporarily-disabled',
enabled: false,
...
});
Единый конфиг файл
Для generated apps используйте единый registration entrypoint:
// src/middlewares/index.ts
import { defineAdminMiddleware, defineContentMiddleware } from '@notty/core';
export function registerAppMiddlewares(ctx) {
ctx.middleware.addAdmin(defineAdminMiddleware({ name: 'admin-1', ... }));
ctx.middleware.addAdmin(defineAdminMiddleware({ name: 'admin-2', ... }));
ctx.middleware.addContent(defineContentMiddleware({ name: 'content-1', ... }));
}
Observability (Наблюдаемость)
Система middleware поддерживает observability для мониторинга и отладки.
RequestId и Tracing
Каждый запрос получает уникальный requestId, который прокидывается через всю цепочку middleware:
defineContentMiddleware({
name: 'log-request',
async handler(ctx, input, next) {
console.log(`[${ctx.meta?.requestId}] Starting ${ctx.operation}`);
const result = await next();
console.log(`[${ctx.meta?.requestId}] Completed ${ctx.operation}`);
return result;
},
});
Заголовки трассировки
Поддерживаются стандартные заголовки:
X-Request-Id— уникальный ID запросаX-Trace-Id— ID трассировки (для распределённых систем)X-Parent-Span-Id— ID родительского spanX-Correlation-Id— альтернатива для Request-Id
Заголовки автоматически добавляются в ответ.
Middleware Observer
Для сбора метрик и логирования можно зарегистрировать observer:
import { setMiddlewareObserver } from '@notty/core';
setMiddlewareObserver({
// Вызывается при завершении цепочки middleware
onChainComplete(result) {
console.log(`Chain completed in ${result.totalDuration}ms`);
console.log(`Middlewares executed: ${result.middlewaresExecuted}`);
console.log(`Request ID: ${result.requestId}`);
// Детальные тайминги по каждому middleware
for (const timing of result.timings) {
console.log(` ${timing.name}: ${timing.duration}ms`);
}
},
// Вызывается при ошибке в middleware
onMiddlewareError(error) {
console.error(`Error in ${error.middlewareName}:`, error.message);
console.error(`Request ID: ${error.requestId}`);
},
// Вызывается при завершении каждого middleware
onMiddlewareComplete(timing, requestId) {
if (timing.duration > 100) {
console.warn(`Slow middleware: ${timing.name} took ${timing.duration}ms`);
}
},
});
Тайминги
В development режиме автоматически собираются тайминги каждого middleware:
interface MiddlewareTiming {
name: string; // Имя middleware
startTime: number; // Timestamp начала
endTime: number; // Timestamp окончания
duration: number; // Длительность в мс
error?: boolean; // Была ли ошибка
aborted?: boolean; // Была ли цепочка прервана
}
Error Boundaries
Ошибки в middleware обрабатываются и логируются автоматически:
- Для Admin/Content middleware — ошибка прерывает цепочку и прокидывается дальше
- Для System event handlers — ошибка логируется, но цепочка продолжает выполнение
Унифицированный формат ошибки:
interface MiddlewareError {
middlewareName: string; // Имя middleware
middlewareType: 'admin' | 'content' | 'system';
message: string; // Сообщение ошибки
stack?: string; // Stack trace (только в dev)
code?: string; // Код ошибки
statusCode?: number; // HTTP статус
timestamp: Date; // Время ошибки
requestId: string; // ID запроса
metadata?: Record<string, unknown>; // Дополнительные данные
}
Интеграция с OpenTelemetry (опционально)
Для интеграции с OTEL используйте observer:
import { trace } from '@opentelemetry/api';
import { setMiddlewareObserver } from '@notty/core';
const tracer = trace.getTracer('notty-middleware');
setMiddlewareObserver({
onChainComplete(result) {
const span = tracer.startSpan('middleware-chain', {
attributes: {
'middleware.request_id': result.requestId,
'middleware.duration_ms': result.totalDuration,
'middleware.count': result.middlewaresExecuted,
'middleware.has_error': result.hasError,
},
});
span.end();
},
onMiddlewareError(error) {
const span = tracer.startSpan('middleware-error', {
attributes: {
'middleware.name': error.middlewareName,
'middleware.type': error.middlewareType,
'error.message': error.message,
request_id: error.requestId,
},
});
span.setStatus({ code: 2, message: error.message });
span.end();
},
});
Источник: docs/middleware.md. Снимок документации исходного проекта. Технический справочник сохраняет язык оригинала.