A framework for real-time applications and REST API with JavaScript and TypeScript
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Middleware system for intercepting and modifying service method calls with support for before, after, error, and around hooks.
Register hooks that run for all services in the application.
/**
* Register application level hooks
* @param map - The application hook settings
* @returns The application instance for chaining
*/
hooks(map: ApplicationHookOptions<this>): this;
type ApplicationHookOptions<A> = HookOptions<A, any> | ApplicationHookMap<A>;
interface ApplicationHookMap<A> {
setup?: ApplicationHookFunction<A>[];
teardown?: ApplicationHookFunction<A>[];
}
type ApplicationHookFunction<A> = (
context: ApplicationHookContext<A>,
next: NextFunction
) => Promise<void>;
interface ApplicationHookContext<A = Application> {
app: A;
server: any;
}Usage Examples:
// Application-wide hooks for all services
app.hooks({
before: {
all: [
async (context) => {
console.log(`Calling ${context.method} on ${context.path}`);
}
],
create: [
async (context) => {
context.data.createdAt = new Date();
}
]
},
after: {
all: [
async (context) => {
console.log(`Finished ${context.method} on ${context.path}`);
}
]
}
});
// Application lifecycle hooks
app.hooks({
setup: [
async (context) => {
console.log("Application is setting up");
}
],
teardown: [
async (context) => {
console.log("Application is tearing down");
}
]
});Register hooks for specific services.
/**
* Register service-level hooks
* @param options - Hook configuration
* @returns The service instance for chaining
*/
hooks(options: HookOptions<A, S>): this;
type HookOptions<A, S> = AroundHookMap<A, S> | AroundHookFunction<A, S>[] | HookMap<A, S>;
interface HookMap<A, S> {
around?: AroundHookMap<A, S>;
before?: HookTypeMap<A, S>;
after?: HookTypeMap<A, S>;
error?: HookTypeMap<A, S>;
}
type HookTypeMap<A, S> = SelfOrArray<HookFunction<A, S>> | HookMethodMap<A, S>;
type HookMethodMap<A, S> = {
[L in keyof S]?: SelfOrArray<HookFunction<A, S>>;
} & { all?: SelfOrArray<HookFunction<A, S>> };Usage Examples:
const messageService = app.service("messages");
// Service hooks using different hook types
messageService.hooks({
before: {
all: [
async (context) => {
// Runs before all methods
console.log(`Before ${context.method}`);
}
],
create: [
async (context) => {
// Validate data before creating
if (!context.data.text) {
throw new Error("Text is required");
}
context.data.createdAt = new Date();
}
]
},
after: {
create: [
async (context) => {
// Send notification after creating
console.log("Message created:", context.result);
}
]
},
error: {
all: [
async (context) => {
// Log all errors
console.error(`Error in ${context.method}:`, context.error);
}
]
}
});
// Around hooks for more control
messageService.hooks({
around: {
all: [
async (context, next) => {
const start = Date.now();
await next();
const duration = Date.now() - start;
console.log(`${context.method} took ${duration}ms`);
}
]
}
});Different types of hook functions for various use cases.
/**
* Regular hook function that can modify context
*/
type HookFunction<A = Application, S = Service> = (
this: S,
context: HookContext<A, S>
) => Promise<HookContext<Application, S> | void> | HookContext<Application, S> | void;
/**
* Around hook function that controls execution flow
*/
type AroundHookFunction<A = Application, S = Service> = (
context: HookContext<A, S>,
next: NextFunction
) => Promise<void>;
/**
* Alias for HookFunction
*/
type Hook<A = Application, S = Service> = HookFunction<A, S>;
/**
* Hook types available
*/
type HookType = 'before' | 'after' | 'error' | 'around';The context object passed to all hooks containing request and response data.
interface HookContext<A = Application, S = any> {
/**
* A read only property that contains the Feathers application object
*/
readonly app: A;
/**
* A read only property with the name of the service method
*/
readonly method: string;
/**
* A read only property and contains the service name (or path)
*/
path: string;
/**
* A read only property and contains the service this hook currently runs on
*/
readonly service: S;
/**
* A read only property with the hook type
*/
readonly type: HookType;
/**
* The list of method arguments. Should not be modified
*/
readonly arguments: any[];
/**
* A writeable property containing the data of a create, update and patch service method call
*/
data?: any;
/**
* A writeable property with the error object that was thrown in a failed method call
*/
error?: any;
/**
* A writeable property and the id for a get, remove, update and patch service method call
*/
id?: Id;
/**
* A writeable property that contains the service method parameters
*/
params: Params;
/**
* A writeable property containing the result of the successful service method call
*/
result?: any;
/**
* A writeable property containing a 'safe' version of the data that should be sent to any client
*/
dispatch?: any;
/**
* A writeable, optional property with options specific to HTTP transports
*/
http?: Http;
/**
* The event emitted by this method. Can be set to null to skip event emitting
*/
event: string | null;
}Usage Examples:
// Before hook modifying data
const validateAndEnhance = async (context: HookContext) => {
if (context.method === "create") {
// Validate required fields
if (!context.data.title) {
throw new Error("Title is required");
}
// Add timestamps
context.data.createdAt = new Date();
context.data.updatedAt = new Date();
// Add user info from params
if (context.params.user) {
context.data.userId = context.params.user.id;
}
}
};
// After hook modifying result
const addComputedFields = async (context: HookContext) => {
if (context.result) {
const results = Array.isArray(context.result) ? context.result : [context.result];
results.forEach(item => {
if (item.createdAt) {
item.age = Date.now() - new Date(item.createdAt).getTime();
}
});
}
};
// Error hook for logging
const logErrors = async (context: HookContext) => {
console.error(`Error in ${context.path}.${context.method}:`, {
error: context.error.message,
data: context.data,
params: context.params
});
};
// Around hook for caching
const cacheResults = async (context: HookContext, next: NextFunction) => {
if (context.method === "find") {
const cacheKey = `${context.path}-${JSON.stringify(context.params.query)}`;
const cached = cache.get(cacheKey);
if (cached) {
context.result = cached;
return; // Skip calling next()
}
await next();
if (context.result) {
cache.set(cacheKey, context.result, 60000); // Cache for 1 minute
}
} else {
await next();
}
};Utilities for managing and creating hooks.
/**
* Create a hook context for a service method
* @param service - The service instance
* @param method - The method name
* @param data - Optional context data
* @returns Hook context object
*/
function createContext(service: Service, method: string, data: HookContextData = {}): HookContext;
/**
* Enable hooks functionality on an object
* @param object - The object to enhance with hooks
* @returns Function to register hooks
*/
function enableHooks(object: any): Function;
/**
* Hook manager class for handling hook execution
*/
class FeathersHookManager<A> extends HookManager {
constructor(app: A, method: string);
collectMiddleware(self: any, args: any[]): Middleware[];
initializeContext(self: any, args: any[], context: HookContext): HookContext;
middleware(mw: Middleware[]): this;
}
/**
* Mixin to add hook functionality to services
*/
function hookMixin<A>(
this: A,
service: FeathersService<A>,
path: string,
options: ServiceOptions
): FeathersService<A>;// Method-specific hooks
messageService.hooks({
before: {
find: [hookFunction1, hookFunction2],
create: [validateData, addTimestamp]
}
});
// All methods hooks
messageService.hooks({
before: {
all: [authenticate, authorize]
}
});
// Multiple hook types
messageService.hooks({
before: {
create: [validateData]
},
after: {
create: [sendNotification]
},
error: {
all: [logError]
}
});
// Around hooks only
messageService.hooks([
cacheHook,
timingHook
]);
// Mixed registration
messageService.hooks({
around: {
find: [cacheHook],
all: [timingHook]
},
before: {
create: [validateData]
}
});Authentication Hook:
const authenticate = async (context: HookContext) => {
const token = context.params.headers?.authorization;
if (!token) {
throw new Error("Authentication required");
}
context.params.user = await verifyToken(token);
};Validation Hook:
const validateSchema = (schema: any) => async (context: HookContext) => {
if (context.data) {
const validation = await schema.validate(context.data);
if (!validation.isValid) {
throw new Error(`Validation failed: ${validation.errors.join(", ")}`);
}
}
};Pagination Hook:
const paginate = async (context: HookContext) => {
if (context.method === "find") {
const { $limit = 10, $skip = 0 } = context.params.query || {};
context.params.query = {
...context.params.query,
$limit: Math.min($limit, 100), // Max 100 items
$skip
};
}
};