Extensibility points for CRUD operations
@repo/crud
provides a powerful extensibility mechanism for CRUD operations. Plugins can intercept and modify the behavior of CRUD operations before and after they are executed.
import { engine } from '@repo/crud';
// Create a simple logging plugin
const loggingPlugin: engine.CrudPlugin = {
name: 'logging',
initialize: (engine) => {
console.log('Logging plugin initialized');
},
hooks: {
beforeGetList: (params) => {
console.log('Getting list', params);
return params;
},
afterGetList: (result, params) => {
console.log('Got list', result);
return result;
},
beforeGetOne: (params) => {
console.log('Getting one', params);
return params;
},
afterGetOne: (result, params) => {
console.log('Got one', result);
return result;
},
beforeCreate: (params) => {
console.log('Creating', params);
return params;
},
afterCreate: (result, params) => {
console.log('Created', result);
return result;
},
beforeUpdate: (params) => {
console.log('Updating', params);
return params;
},
afterUpdate: (result, params) => {
console.log('Updated', result);
return result;
},
beforeDelete: (params) => {
console.log('Deleting', params);
return params;
},
afterDelete: (result, params) => {
console.log('Deleted', result);
return result;
}
}
};
import { engine } from '@repo/crud';
import { providers } from '@repo/data';
// Create a data provider
const dataProvider = providers.supabase.createProvider({
url: process.env.SUPABASE_URL,
key: process.env.SUPABASE_KEY
});
// Create a CRUD engine with the plugin
const crudEngine = engine.createCrudEngine({
dataProvider,
plugins: [loggingPlugin]
});
// Now all CRUD operations will be logged
const result = await crudEngine.getList({
resource: 'users',
pagination: { page: 1, perPage: 10 }
});
import { plugins } from '@repo/crud';
// Create a logging plugin
const loggingPlugin = plugins.createLoggingPlugin({
level: 'info', // 'debug', 'info', 'warn', 'error'
logger: console, // Custom logger
resources: ['users', 'posts'], // Only log these resources
operations: ['create', 'update', 'delete'] // Only log these operations
});
// Use the plugin
const crudEngine = engine.createCrudEngine({
dataProvider,
plugins: [loggingPlugin]
});
import { plugins } from '@repo/crud';
// Create a validation plugin
const validationPlugin = plugins.createValidationPlugin({
validators: {
users: {
create: (data) => {
const errors = {};
if (!data.name) {
errors.name = 'Name is required';
}
if (!data.email) {
errors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
errors.email = 'Invalid email format';
}
return Object.keys(errors).length > 0 ? errors : null;
},
update: (data) => {
const errors = {};
if (data.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
errors.email = 'Invalid email format';
}
return Object.keys(errors).length > 0 ? errors : null;
}
},
posts: {
create: (data) => {
const errors = {};
if (!data.title) {
errors.title = 'Title is required';
}
if (!data.content) {
errors.content = 'Content is required';
}
return Object.keys(errors).length > 0 ? errors : null;
},
update: (data) => {
const errors = {};
if (data.title === '') {
errors.title = 'Title cannot be empty';
}
return Object.keys(errors).length > 0 ? errors : null;
}
}
}
});
// Use the plugin
const crudEngine = engine.createCrudEngine({
dataProvider,
plugins: [validationPlugin]
});
import { plugins } from '@repo/crud';
// Create a caching plugin
const cachingPlugin = plugins.createCachingPlugin({
ttl: 5 * 60 * 1000, // 5 minutes
resources: ['users', 'posts'], // Only cache these resources
operations: ['getList', 'getOne'], // Only cache these operations
keyGenerator: (params) => {
// Generate a cache key based on the parameters
if (params.resource === 'getList') {
return `${params.resource}:${JSON.stringify(params)}`;
}
return `${params.resource}:${params.id}`;
}
});
// Use the plugin
const crudEngine = engine.createCrudEngine({
dataProvider,
plugins: [cachingPlugin]
});
import { plugins } from '@repo/crud';
// Create a transformation plugin
const transformationPlugin = plugins.createTransformationPlugin({
transformers: {
users: {
// Transform data before creating a user
beforeCreate: (data) => ({
...data,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}),
// Transform data before updating a user
beforeUpdate: (data) => ({
...data,
updatedAt: new Date().toISOString()
}),
// Transform user data after fetching
afterGetOne: (data) => ({
...data,
fullName: `${data.firstName} ${data.lastName}`
}),
// Transform user list after fetching
afterGetList: (data) => data.map(user => ({
...user,
fullName: `${user.firstName} ${user.lastName}`
}))
}
}
});
// Use the plugin
const crudEngine = engine.createCrudEngine({
dataProvider,
plugins: [transformationPlugin]
});
import { plugins } from '@repo/crud';
// Create a soft delete plugin
const softDeletePlugin = plugins.createSoftDeletePlugin({
resources: ['users', 'posts'], // Enable soft delete for these resources
deletedField: 'deletedAt', // Field to store deletion timestamp
filterDeleted: true // Automatically filter out deleted resources
});
// Use the plugin
const crudEngine = engine.createCrudEngine({
dataProvider,
plugins: [softDeletePlugin]
});
// Now delete operations will set the deletedAt field instead of actually deleting
await crudEngine.delete({
resource: 'users',
id: '123'
});
// And getList operations will filter out deleted resources
const { data } = await crudEngine.getList({
resource: 'users',
pagination: { page: 1, perPage: 10 }
});
// data will only include users where deletedAt is null
import { plugins } from '@repo/crud';
// Create an internationalization plugin
const i18nPlugin = plugins.createI18nPlugin({
resources: ['posts'], // Enable translations for these resources
languages: ['en', 'fr', 'de', 'es', 'tr'], // Supported languages
defaultLanguage: 'en', // Default language
translatedFields: {
posts: ['title', 'content'] // Fields to translate
},
translationField: 'translations' // Field to store translations
});
// Use the plugin
const crudEngine = engine.createCrudEngine({
dataProvider,
plugins: [i18nPlugin]
});
// Create a post with translations
await crudEngine.create({
resource: 'posts',
data: {
title: 'Hello World', // Default language (en)
content: 'This is my first post',
translations: {
fr: {
title: 'Bonjour le Monde',
content: 'Ceci est mon premier post'
},
de: {
title: 'Hallo Welt',
content: 'Dies ist mein erster Beitrag'
}
}
}
});
// Get posts in French
const { data: frenchPosts } = await crudEngine.getList({
resource: 'posts',
pagination: { page: 1, perPage: 10 },
meta: { language: 'fr' }
});
// frenchPosts will have title and content in French
interface CrudPlugin {
/** Unique name of the plugin */
name: string;
/** Plugin initialization function */
initialize: (engine: CrudEngine) => void;
/** Hooks for different CRUD operations */
hooks?: {
beforeGetList?: (params: GetListParams) => GetListParams;
afterGetList?: (result: any, params: GetListParams) => any;
beforeGetOne?: (params: GetOneParams) => GetOneParams;
afterGetOne?: (result: any, params: GetOneParams) => any;
beforeCreate?: (params: CreateParams) => CreateParams;
afterCreate?: (result: any, params: CreateParams) => any;
beforeUpdate?: (params: UpdateParams) => UpdateParams;
afterUpdate?: (result: any, params: UpdateParams) => any;
beforeDelete?: (params: DeleteParams) => DeleteParams;
afterDelete?: (result: any, params: DeleteParams) => any;
};
}
import { engine } from '@repo/crud';
// Define plugin options
interface RateLimitPluginOptions {
limit: number;
windowMs: number;
resources?: string[];
operations?: ('getList' | 'getOne' | 'create' | 'update' | 'delete')[];
}
// Create a rate limiting plugin
function createRateLimitPlugin(options: RateLimitPluginOptions): engine.CrudPlugin {
const { limit, windowMs, resources, operations } = options;
// Track requests
const requests = new Map<string, { count: number, resetAt: number }>();
// Check if the operation should be rate limited
const shouldRateLimit = (resource: string, operation: string) => {
if (resources && !resources.includes(resource)) {
return false;
}
if (operations && !operations.includes(operation as any)) {
return false;
}
return true;
};
// Check rate limit
const checkRateLimit = (resource: string, operation: string) => {
const key = `${resource}:${operation}`;
const now = Date.now();
// Get or create request tracking
let tracking = requests.get(key);
if (!tracking || tracking.resetAt <= now) {
tracking = { count: 0, resetAt: now + windowMs };
requests.set(key, tracking);
}
// Check if limit is exceeded
if (tracking.count >= limit) {
throw new Error(`Rate limit exceeded for ${operation} on ${resource}`);
}
// Increment count
tracking.count++;
};
return {
name: 'rateLimit',
initialize: (engine) => {
console.log('Rate limit plugin initialized');
},
hooks: {
beforeGetList: (params) => {
if (shouldRateLimit(params.resource, 'getList')) {
checkRateLimit(params.resource, 'getList');
}
return params;
},
beforeGetOne: (params) => {
if (shouldRateLimit(params.resource, 'getOne')) {
checkRateLimit(params.resource, 'getOne');
}
return params;
},
beforeCreate: (params) => {
if (shouldRateLimit(params.resource, 'create')) {
checkRateLimit(params.resource, 'create');
}
return params;
},
beforeUpdate: (params) => {
if (shouldRateLimit(params.resource, 'update')) {
checkRateLimit(params.resource, 'update');
}
return params;
},
beforeDelete: (params) => {
if (shouldRateLimit(params.resource, 'delete')) {
checkRateLimit(params.resource, 'delete');
}
return params;
}
}
};
}
// Use the plugin
const rateLimitPlugin = createRateLimitPlugin({
limit: 100,
windowMs: 60 * 1000, // 1 minute
resources: ['users'],
operations: ['create', 'update', 'delete']
});
const crudEngine = engine.createCrudEngine({
dataProvider,
plugins: [rateLimitPlugin]
});
plugins
array:
const crudEngine = engine.createCrudEngine({
dataProvider,
plugins: [
loggingPlugin, // Executed first
validationPlugin, // Executed second
cachingPlugin // Executed third
]
});
getList
operation, the execution order would be:
loggingPlugin.hooks.beforeGetList
validationPlugin.hooks.beforeGetList
cachingPlugin.hooks.beforeGetList
getList
operationcachingPlugin.hooks.afterGetList
validationPlugin.hooks.afterGetList
loggingPlugin.hooks.afterGetList
import { engine } from '@repo/crud';
// Compose multiple plugins
function composePlugins(...plugins: engine.CrudPlugin[]): engine.CrudPlugin {
return {
name: 'composedPlugin',
initialize: (engine) => {
// Initialize all plugins
plugins.forEach(plugin => plugin.initialize(engine));
},
hooks: {
beforeGetList: (params) => {
// Apply all beforeGetList hooks in order
return plugins.reduce(
(result, plugin) => plugin.hooks?.beforeGetList?.(result) ?? result,
params
);
},
afterGetList: (result, params) => {
// Apply all afterGetList hooks in reverse order
return plugins.reduceRight(
(result, plugin) => plugin.hooks?.afterGetList?.(result, params) ?? result,
result
);
},
// Implement other hooks similarly
}
};
}
// Use the composed plugin
const composedPlugin = composePlugins(
loggingPlugin,
validationPlugin,
cachingPlugin
);
const crudEngine = engine.createCrudEngine({
dataProvider,
plugins: [composedPlugin]
});
import { engine } from '@repo/crud';
// Create a resource-specific plugin
function createResourcePlugin(
resourceName: string,
plugin: engine.CrudPlugin
): engine.CrudPlugin {
return {
name: `${resourceName}:${plugin.name}`,
initialize: plugin.initialize,
hooks: {
beforeGetList: (params) => {
if (params.resource === resourceName) {
return plugin.hooks?.beforeGetList?.(params) ?? params;
}
return params;
},
afterGetList: (result, params) => {
if (params.resource === resourceName) {
return plugin.hooks?.afterGetList?.(result, params) ?? result;
}
return result;
},
// Implement other hooks similarly
}
};
}
// Use the resource-specific plugin
const userValidationPlugin = createResourcePlugin('users', validationPlugin);
const postValidationPlugin = createResourcePlugin('posts', validationPlugin);
const crudEngine = engine.createCrudEngine({
dataProvider,
plugins: [userValidationPlugin, postValidationPlugin]
});
import { engine } from '@repo/crud';
// Create a dynamic plugin
function createDynamicPlugin(
plugin: engine.CrudPlugin,
isEnabled: () => boolean
): engine.CrudPlugin {
return {
name: `dynamic:${plugin.name}`,
initialize: plugin.initialize,
hooks: {
beforeGetList: (params) => {
if (isEnabled()) {
return plugin.hooks?.beforeGetList?.(params) ?? params;
}
return params;
},
afterGetList: (result, params) => {
if (isEnabled()) {
return plugin.hooks?.afterGetList?.(result, params) ?? result;
}
return result;
},
// Implement other hooks similarly
}
};
}
// Use the dynamic plugin
let loggingEnabled = true;
const dynamicLoggingPlugin = createDynamicPlugin(
loggingPlugin,
() => loggingEnabled
);
const crudEngine = engine.createCrudEngine({
dataProvider,
plugins: [dynamicLoggingPlugin]
});
// Later, you can disable the plugin
loggingEnabled = false;
import { engine } from '@repo/crud';
// Create a plugin with state
function createStatefulPlugin(): engine.CrudPlugin {
// Plugin state
const state = {
operationCount: 0,
lastOperation: null as string | null,
errors: [] as Error[]
};
return {
name: 'stateful',
initialize: (engine) => {
console.log('Stateful plugin initialized');
},
hooks: {
beforeGetList: (params) => {
state.operationCount++;
state.lastOperation = 'getList';
return params;
},
beforeGetOne: (params) => {
state.operationCount++;
state.lastOperation = 'getOne';
return params;
},
beforeCreate: (params) => {
state.operationCount++;
state.lastOperation = 'create';
return params;
},
beforeUpdate: (params) => {
state.operationCount++;
state.lastOperation = 'update';
return params;
},
beforeDelete: (params) => {
state.operationCount++;
state.lastOperation = 'delete';
return params;
},
afterGetList: (result, params) => {
return result;
},
afterGetOne: (result, params) => {
return result;
},
afterCreate: (result, params) => {
return result;
},
afterUpdate: (result, params) => {
return result;
},
afterDelete: (result, params) => {
return result;
}
},
// Expose methods to access state
getState: () => ({ ...state }),
resetState: () => {
state.operationCount = 0;
state.lastOperation = null;
state.errors = [];
}
};
}
// Use the stateful plugin
const statefulPlugin = createStatefulPlugin();
const crudEngine = engine.createCrudEngine({
dataProvider,
plugins: [statefulPlugin]
});
// Later, you can access the plugin state
console.log(statefulPlugin.getState());
// { operationCount: 5, lastOperation: 'getList', errors: [] }
// And reset the state
statefulPlugin.resetState();