@dbs-portal/tool-mock
Version:
API mocking toolkit using MSW for DBS Portal development workflows
334 lines • 12 kB
JavaScript
/**
* CRUD handler factory for creating complete REST API handlers
*/
import { http, HttpResponse } from 'msw';
import { applyDelay } from '../utils/delay';
import { simulateError } from '../utils/error';
import { getMockConfig } from '../core/config';
import { createPaginatedResponse } from '../factories/pagination';
/**
* In-memory data store for CRUD operations
*/
class CrudDataStore {
options;
data = new Map();
nextId = 1;
constructor(options, initialData = []) {
this.options = options;
// Initialize with provided data
for (const item of initialData) {
const id = this.getId(item);
this.data.set(id, item);
// Update next ID if numeric
if (typeof id === 'string' && /^\d+$/.test(id)) {
this.nextId = Math.max(this.nextId, parseInt(id, 10) + 1);
}
}
}
getId(item) {
const idField = this.options.idField || 'id';
const id = item[idField];
return id ? String(id) : String(this.nextId++);
}
setId(item, id) {
const idField = this.options.idField || 'id';
return { ...item, [idField]: id };
}
getAll() {
const items = Array.from(this.data.values());
// Filter out soft-deleted items if soft delete is enabled
if (this.options.softDelete) {
const deletedField = this.options.deletedField || 'deleted';
return items.filter(item => !item[deletedField]);
}
return items;
}
getById(id) {
const item = this.data.get(id);
// Check if soft-deleted
if (item && this.options.softDelete) {
const deletedField = this.options.deletedField || 'deleted';
if (item[deletedField]) {
return undefined;
}
}
return item;
}
create(data) {
const id = String(this.nextId++);
const item = this.setId(this.options.dataFactory(data), id);
this.data.set(id, item);
return item;
}
update(id, data) {
const existing = this.getById(id);
if (!existing) {
return undefined;
}
const updated = { ...existing, ...data };
this.data.set(id, updated);
return updated;
}
delete(id) {
if (this.options.softDelete) {
const existing = this.getById(id);
if (!existing) {
return false;
}
const deletedField = this.options.deletedField || 'deleted';
const updated = { ...existing, [deletedField]: true };
this.data.set(id, updated);
return true;
}
else {
return this.data.delete(id);
}
}
search(query, fields) {
const allItems = this.getAll();
const lowerQuery = query.toLowerCase();
return allItems.filter(item => {
return fields.some(field => {
const value = item[field];
return value && String(value).toLowerCase().includes(lowerQuery);
});
});
}
filter(filters) {
const allItems = this.getAll();
return allItems.filter(item => {
return Object.entries(filters).every(([key, value]) => {
const itemValue = item[key];
if (Array.isArray(value)) {
return value.includes(itemValue);
}
return itemValue === value;
});
});
}
sort(items, sortBy, order = 'asc') {
return [...items].sort((a, b) => {
const aValue = a[sortBy];
const bValue = b[sortBy];
if (aValue < bValue)
return order === 'asc' ? -1 : 1;
if (aValue > bValue)
return order === 'asc' ? 1 : -1;
return 0;
});
}
}
/**
* Create CRUD handlers for a resource
*/
export function createCrudHandlers(options) {
const store = new CrudDataStore(options, options.initialData);
const config = getMockConfig();
// Helper function to apply delay and error simulation
const withMiddleware = async (request, method) => {
// Simulate errors if configured
const errorResponse = simulateError(request.url, method, config.errorSimulation);
if (errorResponse) {
return errorResponse;
}
// Apply delay
const delay = options.delay || config.delay;
if (delay) {
await applyDelay(delay);
}
return null;
};
// Helper function to validate data
const validateData = (data) => {
if (options.validate) {
return options.validate(data);
}
return null;
};
// Helper function to create response
const createResponse = (data, status = 200) => {
const headers = {
'Content-Type': 'application/json',
...options.headers,
};
return HttpResponse.json({
success: true,
data,
timestamp: new Date().toISOString(),
}, { status, headers });
};
// Helper function to create error response
const createErrorResponse = (error, status = 400) => {
return HttpResponse.json({
success: false,
error,
timestamp: new Date().toISOString(),
}, { status });
};
const handlers = [];
// GET /resource - List with pagination, filtering, and search
handlers.push(http.get(options.basePath, async ({ request }) => {
const middlewareResponse = await withMiddleware(request, 'GET');
if (middlewareResponse)
return middlewareResponse;
const url = new URL(request.url);
const page = parseInt(url.searchParams.get('page') || '1', 10);
const pageSize = Math.min(parseInt(url.searchParams.get('pageSize') || String(options.pagination?.defaultPageSize || 10), 10), options.pagination?.maxPageSize || 100);
const search = url.searchParams.get('search') || url.searchParams.get('q');
const sortBy = url.searchParams.get('sortBy');
const sortOrder = (url.searchParams.get('sortOrder') || 'asc');
let items = store.getAll();
// Apply search
if (search) {
// Simple search across all string fields
const searchFields = Object.keys(items[0] || {});
items = store.search(search, searchFields);
}
// Apply filters
const filters = {};
for (const [key, value] of url.searchParams.entries()) {
if (!['page', 'pageSize', 'search', 'q', 'sortBy', 'sortOrder'].includes(key)) {
filters[key] = value;
}
}
if (Object.keys(filters).length > 0) {
items = store.filter(filters);
}
// Apply sorting
if (sortBy) {
items = store.sort(items, sortBy, sortOrder);
}
// Apply pagination
const paginatedResponse = createPaginatedResponse({
items,
page,
pageSize,
total: items.length,
});
return createResponse(paginatedResponse.data, 200);
}));
// GET /resource/:id - Get single item
handlers.push(http.get(`${options.basePath}/:id`, async ({ request, params }) => {
const middlewareResponse = await withMiddleware(request, 'GET');
if (middlewareResponse)
return middlewareResponse;
const id = params['id'];
const item = store.getById(id);
if (!item) {
return createErrorResponse({
code: 'NOT_FOUND',
message: `Item with id ${id} not found`,
}, 404);
}
return createResponse(item);
}));
// POST /resource - Create new item
handlers.push(http.post(options.basePath, async ({ request }) => {
const middlewareResponse = await withMiddleware(request, 'POST');
if (middlewareResponse)
return middlewareResponse;
try {
const data = await request.json();
// Validate data
const validationErrors = validateData(data);
if (validationErrors) {
return createErrorResponse({
code: 'VALIDATION_ERROR',
message: 'Validation failed',
details: validationErrors,
}, 400);
}
const item = store.create(data);
return createResponse(item, 201);
}
catch (error) {
return createErrorResponse({
code: 'INVALID_JSON',
message: 'Invalid JSON in request body',
}, 400);
}
}));
// PUT /resource/:id - Update item (full replacement)
handlers.push(http.put(`${options.basePath}/:id`, async ({ request, params }) => {
const middlewareResponse = await withMiddleware(request, 'PUT');
if (middlewareResponse)
return middlewareResponse;
const id = params['id'];
try {
const data = await request.json();
// Validate data
const validationErrors = validateData(data);
if (validationErrors) {
return createErrorResponse({
code: 'VALIDATION_ERROR',
message: 'Validation failed',
details: validationErrors,
}, 400);
}
const item = store.update(id, data);
if (!item) {
return createErrorResponse({
code: 'NOT_FOUND',
message: `Item with id ${id} not found`,
}, 404);
}
return createResponse(item);
}
catch (error) {
return createErrorResponse({
code: 'INVALID_JSON',
message: 'Invalid JSON in request body',
}, 400);
}
}));
// PATCH /resource/:id - Partial update
handlers.push(http.patch(`${options.basePath}/:id`, async ({ request, params }) => {
const middlewareResponse = await withMiddleware(request, 'PATCH');
if (middlewareResponse)
return middlewareResponse;
const id = params['id'];
try {
const data = await request.json();
// Validate data (partial validation)
const validationErrors = validateData(data);
if (validationErrors) {
return createErrorResponse({
code: 'VALIDATION_ERROR',
message: 'Validation failed',
details: validationErrors,
}, 400);
}
const item = store.update(id, data);
if (!item) {
return createErrorResponse({
code: 'NOT_FOUND',
message: `Item with id ${id} not found`,
}, 404);
}
return createResponse(item);
}
catch (error) {
return createErrorResponse({
code: 'INVALID_JSON',
message: 'Invalid JSON in request body',
}, 400);
}
}));
// DELETE /resource/:id - Delete item
handlers.push(http.delete(`${options.basePath}/:id`, async ({ request, params }) => {
const middlewareResponse = await withMiddleware(request, 'DELETE');
if (middlewareResponse)
return middlewareResponse;
const id = params['id'];
const success = store.delete(id);
if (!success) {
return createErrorResponse({
code: 'NOT_FOUND',
message: `Item with id ${id} not found`,
}, 404);
}
return createResponse({ id, deleted: true }, 200);
}));
return handlers;
}
//# sourceMappingURL=crud.js.map