UNPKG

@dbs-portal/tool-mock

Version:

API mocking toolkit using MSW for DBS Portal development workflows

334 lines 12 kB
/** * 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