From cbdb3e62d5508d4cac60d8117e015cbe0a7f5af9 Mon Sep 17 00:00:00 2001 From: Many0nne Date: Sat, 7 Mar 2026 18:52:02 +0100 Subject: [PATCH 1/2] handle cache data and add force reload button on swagger --- src/core/cache.ts | 49 ++++++++++++++++++++++++++++++++++++++ src/core/router.ts | 26 ++++++++++++-------- src/core/swagger.ts | 32 +++++++++++++++++++++++++ src/server.ts | 58 ++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 154 insertions(+), 11 deletions(-) diff --git a/src/core/cache.ts b/src/core/cache.ts index 704fbd7..c42671d 100644 --- a/src/core/cache.ts +++ b/src/core/cache.ts @@ -119,3 +119,52 @@ export class SchemaCache { // Global cache instance export const schemaCache = new SchemaCache(); + +/** + * Always-on data store for stable mock data across requests. + * Caches both single object mocks and array pools independently of config.cache. + */ +interface MockEntry { + data: T; + createdAt: number; +} + +export class MockDataStore { + private singles: Map>> = new Map(); + private pools: Map[]>> = new Map(); + + private key(typeName: string, filePath: string): string { + return `${filePath}::${typeName}`; + } + + getSingle(typeName: string, filePath: string): Record | undefined { + return this.singles.get(this.key(typeName, filePath))?.data; + } + + setSingle(typeName: string, filePath: string, data: Record): void { + this.singles.set(this.key(typeName, filePath), { data, createdAt: Date.now() }); + } + + getPool(typeName: string, filePath: string): Record[] | undefined { + return this.pools.get(this.key(typeName, filePath))?.data; + } + + setPool(typeName: string, filePath: string, data: Record[]): void { + this.pools.set(this.key(typeName, filePath), { data, createdAt: Date.now() }); + } + + clear(): { singles: number; pools: number } { + const singles = this.singles.size; + const pools = this.pools.size; + this.singles.clear(); + this.pools.clear(); + logger.info(`MockDataStore cleared: ${singles} single(s), ${pools} pool(s)`); + return { singles, pools }; + } + + getStats(): { singles: number; pools: number } { + return { singles: this.singles.size, pools: this.pools.size }; + } +} + +export const mockDataStore = new MockDataStore(); diff --git a/src/core/router.ts b/src/core/router.ts index 4642645..949eda8 100644 --- a/src/core/router.ts +++ b/src/core/router.ts @@ -2,7 +2,7 @@ import { Request, Response } from 'express'; import { ServerConfig } from '../types/config'; import { findTypeForUrl } from '../utils/typeMapping'; import { generateMockFromInterface, generateMockArray } from './parser'; -import { schemaCache } from './cache'; +import { schemaCache, mockDataStore } from './cache'; import { logger } from '../utils/logger'; import { parseQueryParams, @@ -91,10 +91,12 @@ export function dynamicRouteHandler(config: ServerConfig) { return; } - // Generate a fixed pool to simulate a full dataset - const pool = generateMockArray(filePath, mapping.typeName, { - arrayLength: POOL_SIZE, - }); + // Use stable pool from the data store; generate and cache on first request + let pool = mockDataStore.getPool(mapping.typeName, filePath); + if (!pool) { + pool = generateMockArray(filePath, mapping.typeName, { arrayLength: POOL_SIZE }); + mockDataStore.setPool(mapping.typeName, filePath, pool); + } // Validate sort fields against schema keys if (parsed.sort.length > 0 && pool.length > 0) { @@ -109,13 +111,17 @@ export function dynamicRouteHandler(config: ServerConfig) { res.status(forcedStatus || 200).json(applyPagination(pool, parsed)); return; } else { - mockData = generateMockFromInterface( - filePath, - mapping.typeName - ); + // Use stable single from the data store; generate and cache on first request + const stored = mockDataStore.getSingle(mapping.typeName, filePath); + if (stored) { + res.status(forcedStatus || 200).json(stored); + return; + } + mockData = generateMockFromInterface(filePath, mapping.typeName); + mockDataStore.setSingle(mapping.typeName, filePath, mockData as Record); } - // Store in cache if enabled + // Store in schema cache too if enabled if (config.cache && !mapping.isArray) { schemaCache.set( mapping.typeName, diff --git a/src/core/swagger.ts b/src/core/swagger.ts index 1ecae8a..4135b65 100644 --- a/src/core/swagger.ts +++ b/src/core/swagger.ts @@ -297,6 +297,38 @@ export function generateOpenAPISpec(config: ServerConfig): Record { + res.type('js').send(` +window.addEventListener('load', function () { + var interval = setInterval(function () { + var container = document.getElementById('swagger-ui'); + if (!container) return; + clearInterval(interval); + + var toolbar = document.createElement('div'); + toolbar.style.cssText = 'background:#1b1b1b;padding:8px 20px;display:flex;align-items:center;gap:12px;'; + + var label = document.createElement('span'); + label.textContent = 'TS Mock API'; + label.style.cssText = 'color:#fff;font-family:sans-serif;font-size:15px;font-weight:700;flex:1;'; + + var btn = document.createElement('button'); + btn.textContent = 'Rebuild Data'; + btn.title = 'Clear all cached mock data and regenerate on next requests'; + btn.style.cssText = 'background:#49cc90;color:#fff;border:none;padding:6px 18px;border-radius:4px;cursor:pointer;font-size:13px;font-weight:700;font-family:sans-serif;'; + + btn.addEventListener('click', function () { + btn.disabled = true; + btn.textContent = 'Resetting\u2026'; + fetch('/mock-reset', { method: 'POST' }) + .then(function (r) { return r.json(); }) + .then(function () { + btn.textContent = 'Done! Reloading\u2026'; + setTimeout(function () { location.reload(); }, 600); + }) + .catch(function () { + btn.style.background = '#f93e3e'; + btn.textContent = 'Error \u2014 try again'; + btn.disabled = false; + setTimeout(function () { + btn.style.background = '#49cc90'; + btn.textContent = 'Rebuild Data'; + }, 2500); + }); + }); + + toolbar.appendChild(label); + toolbar.appendChild(btn); + container.parentNode.insertBefore(toolbar, container); + }, 100); +}); +`); + }); + + // Mock data reset endpoint + app.post('/mock-reset', (_req, res) => { + const cleared = mockDataStore.clear(); + res.json({ message: 'Mock data store cleared', cleared }); + }); + // Swagger documentation - use app.locals.swaggerSpec for dynamic updates app.use('/api-docs', swaggerUi.serve, (req: Request, res: Response, next: NextFunction) => { const spec = app.locals.swaggerSpec || swaggerSpec; swaggerUi.setup(spec, { customCss: '.swagger-ui .topbar { display: none }', customSiteTitle: 'TS Mock Proxy API Docs', + customJs: '/swagger-rebuild.js', })(req, res, next); }); From 0c8b64a37cd41ef8915190896a938f9976824889 Mon Sep 17 00:00:00 2001 From: Many0nne Date: Sat, 7 Mar 2026 19:06:06 +0100 Subject: [PATCH 2/2] fix after reviews --- review | 104 +++++++++++++++++++++++++++++++++++++++ src/core/cache.ts | 19 +++++++ src/server.ts | 5 +- src/utils/fileWatcher.ts | 4 +- tests/core/cache.test.ts | 88 ++++++++++++++++++++++++++++++++- 5 files changed, 216 insertions(+), 4 deletions(-) create mode 100644 review diff --git a/review b/review new file mode 100644 index 0000000..440b5d5 --- /dev/null +++ b/review @@ -0,0 +1,104 @@ +Pull request overview +This PR introduces an always-on mock data caching layer (MockDataStore) so that generated mock data remains stable across requests (instead of being regenerated each time), and adds a "Rebuild Data" button to the Swagger UI that allows users to clear the cached mock data and regenerate it on the next request. + +Changes: + +Added MockDataStore class in cache.ts that caches both single-object mocks and array pools, independent of the config.cache setting. +Integrated mockDataStore into the route handler (router.ts) to serve cached mock data on subsequent requests and regenerate only on first access. +Added a /mock-reset POST endpoint and a custom Swagger UI toolbar with a "Rebuild Data" button that triggers cache clearing. +Reviewed changes +Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments. + +File Description +src/core/cache.ts New MockDataStore class with singles/pools Maps, get/set/clear/getStats methods, and a global mockDataStore singleton. +src/core/router.ts Updated dynamic route handler to check mockDataStore before generating new mock data for both array and single-object endpoints. +src/server.ts Added /swagger-rebuild.js endpoint serving custom JS for the toolbar button, /mock-reset POST endpoint, and wired custom JS into Swagger UI setup. +src/core/swagger.ts Added OpenAPI spec entry for the /mock-reset endpoint. +💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started. + +src/server.ts +Comment on lines +107 to +108 + const cleared = mockDataStore.clear(); + res.json({ message: 'Mock data store cleared', cleared }); +Copilot AI +5 minutes ago +Bug: When config.cache is enabled, the schemaCache is checked before mockDataStore for single-object endpoints (lines 65-72 of router.ts). The /mock-reset endpoint only clears mockDataStore but does not clear schemaCache. This means after clicking "Rebuild Data", single-object endpoints will continue to serve the old cached data from schemaCache, making the reset appear broken for those endpoints. + +You should also call schemaCache.clear() in the /mock-reset handler. + +Suggested change + const cleared = mockDataStore.clear(); + res.json({ message: 'Mock data store cleared', cleared }); + const mockCleared = mockDataStore.clear(); + const schemaCleared = schemaCache.clear(); + res.json({ + message: 'Mock data store cleared', + cleared: { + mockDataStore: mockCleared, + schemaCache: schemaCleared, + }, + }); +Copilot uses AI. Check for mistakes. +@Many0nne Reply... +src/core/router.ts +Comment on lines +94 to +99 + // Use stable pool from the data store; generate and cache on first request + let pool = mockDataStore.getPool(mapping.typeName, filePath); + if (!pool) { + pool = generateMockArray(filePath, mapping.typeName, { arrayLength: POOL_SIZE }); + mockDataStore.setPool(mapping.typeName, filePath, pool); + } +Copilot AI +5 minutes ago +Bug: The file watcher invalidates schemaCache on file changes (see src/utils/fileWatcher.ts:32,50) but does not invalidate mockDataStore. When hot-reload is enabled and a type file is modified, mockDataStore will continue serving stale mock data because the mockDataStore cache hit (line 95-99 for arrays, line 115-118 for singles) happens before any generation, and the stale entries are never removed. + +mockDataStore should also be invalidated when type files change. Consider adding an invalidateFile method to MockDataStore (similar to SchemaCache.invalidateFile) and calling it from the file watcher. + +Copilot uses AI. Check for mistakes. +@Many0nne Reply... +src/core/cache.ts +Comment on lines +132 to +170 +export class MockDataStore { + private singles: Map>> = new Map(); + private pools: Map[]>> = new Map(); + + private key(typeName: string, filePath: string): string { + return `${filePath}::${typeName}`; + } + + getSingle(typeName: string, filePath: string): Record | undefined { + return this.singles.get(this.key(typeName, filePath))?.data; + } + + setSingle(typeName: string, filePath: string, data: Record): void { + this.singles.set(this.key(typeName, filePath), { data, createdAt: Date.now() }); + } + + getPool(typeName: string, filePath: string): Record[] | undefined { + return this.pools.get(this.key(typeName, filePath))?.data; + } + + setPool(typeName: string, filePath: string, data: Record[]): void { + this.pools.set(this.key(typeName, filePath), { data, createdAt: Date.now() }); + } + + clear(): { singles: number; pools: number } { + const singles = this.singles.size; + const pools = this.pools.size; + this.singles.clear(); + this.pools.clear(); + logger.info(`MockDataStore cleared: ${singles} single(s), ${pools} pool(s)`); + return { singles, pools }; + } + + getStats(): { singles: number; pools: number } { + return { singles: this.singles.size, pools: this.pools.size }; + } +} + +export const mockDataStore = new MockDataStore(); +Copilot AI +5 minutes ago +The MockDataStore class is missing tests. The existing tests/core/cache.test.ts comprehensively tests SchemaCache (get/set, invalidateFile, clear, size, getStats). Since MockDataStore is in the same file and follows a similar pattern, it should have analogous test coverage—especially for getSingle/setSingle, getPool/setPool, clear (including its return value), and getStats. + +Copilot uses AI. Check for mistakes. \ No newline at end of file diff --git a/src/core/cache.ts b/src/core/cache.ts index c42671d..e79e039 100644 --- a/src/core/cache.ts +++ b/src/core/cache.ts @@ -153,6 +153,25 @@ export class MockDataStore { this.pools.set(this.key(typeName, filePath), { data, createdAt: Date.now() }); } + invalidateFile(filePath: string): void { + let count = 0; + for (const key of this.singles.keys()) { + if (key.startsWith(`${filePath}::`)) { + this.singles.delete(key); + count++; + } + } + for (const key of this.pools.keys()) { + if (key.startsWith(`${filePath}::`)) { + this.pools.delete(key); + count++; + } + } + if (count > 0) { + logger.info(`MockDataStore invalidated: ${count} entry/entries from ${filePath}`); + } + } + clear(): { singles: number; pools: number } { const singles = this.singles.size; const pools = this.pools.size; diff --git a/src/server.ts b/src/server.ts index 56ddf41..632fa28 100644 --- a/src/server.ts +++ b/src/server.ts @@ -104,8 +104,9 @@ window.addEventListener('load', function () { // Mock data reset endpoint app.post('/mock-reset', (_req, res) => { - const cleared = mockDataStore.clear(); - res.json({ message: 'Mock data store cleared', cleared }); + const mockCleared = mockDataStore.clear(); + schemaCache.clear(); + res.json({ message: 'Mock data store cleared', cleared: mockCleared }); }); // Swagger documentation - use app.locals.swaggerSpec for dynamic updates diff --git a/src/utils/fileWatcher.ts b/src/utils/fileWatcher.ts index 4ed87c7..2d558dd 100644 --- a/src/utils/fileWatcher.ts +++ b/src/utils/fileWatcher.ts @@ -1,7 +1,7 @@ import chokidar from 'chokidar'; import * as path from 'path'; import { logger } from './logger'; -import { schemaCache } from '../core/cache'; +import { schemaCache, mockDataStore } from '../core/cache'; /** * Configures and starts the file watcher for hot-reload @@ -30,6 +30,7 @@ export function startFileWatcher( // Invalidate the cache for this file schemaCache.invalidateFile(filePath); + mockDataStore.invalidateFile(filePath); // Call the callback if provided if (onReload) { @@ -48,6 +49,7 @@ export function startFileWatcher( // Invalidate the cache for this deleted file schemaCache.invalidateFile(filePath); + mockDataStore.invalidateFile(filePath); }) .on('error', (error) => { logger.error(`File watcher error: ${error.message}`); diff --git a/tests/core/cache.test.ts b/tests/core/cache.test.ts index 19084d4..6538c06 100644 --- a/tests/core/cache.test.ts +++ b/tests/core/cache.test.ts @@ -1,4 +1,4 @@ -import { SchemaCache } from '../../src/core/cache'; +import { SchemaCache, MockDataStore } from '../../src/core/cache'; describe('SchemaCache', () => { let cache: SchemaCache; @@ -159,3 +159,89 @@ describe('SchemaCache', () => { }); }); }); + +describe('MockDataStore', () => { + let store: MockDataStore; + + beforeEach(() => { + store = new MockDataStore(); + }); + + describe('getSingle and setSingle', () => { + it('should store and retrieve a single', () => { + const data = { id: 1, name: 'Alice' }; + store.setSingle('User', '/path/user.ts', data); + expect(store.getSingle('User', '/path/user.ts')).toEqual(data); + }); + + it('should return undefined for non-existent single', () => { + expect(store.getSingle('User', '/path/user.ts')).toBeUndefined(); + }); + + it('should scope by filePath and typeName', () => { + store.setSingle('User', '/path/a.ts', { id: 1 }); + store.setSingle('User', '/path/b.ts', { id: 2 }); + expect(store.getSingle('User', '/path/a.ts')).toEqual({ id: 1 }); + expect(store.getSingle('User', '/path/b.ts')).toEqual({ id: 2 }); + }); + }); + + describe('getPool and setPool', () => { + it('should store and retrieve a pool', () => { + const pool = [{ id: 1 }, { id: 2 }]; + store.setPool('User', '/path/user.ts', pool); + expect(store.getPool('User', '/path/user.ts')).toEqual(pool); + }); + + it('should return undefined for non-existent pool', () => { + expect(store.getPool('User', '/path/user.ts')).toBeUndefined(); + }); + }); + + describe('clear', () => { + it('should clear all entries and return counts', () => { + store.setSingle('User', '/path/user.ts', { id: 1 }); + store.setPool('Product', '/path/product.ts', [{ id: 2 }]); + + const result = store.clear(); + + expect(result).toEqual({ singles: 1, pools: 1 }); + expect(store.getSingle('User', '/path/user.ts')).toBeUndefined(); + expect(store.getPool('Product', '/path/product.ts')).toBeUndefined(); + }); + + it('should return zero counts when already empty', () => { + expect(store.clear()).toEqual({ singles: 0, pools: 0 }); + }); + }); + + describe('invalidateFile', () => { + it('should remove singles and pools for a given file', () => { + store.setSingle('User', '/path/types.ts', { id: 1 }); + store.setPool('User', '/path/types.ts', [{ id: 1 }]); + store.setSingle('Product', '/path/other.ts', { id: 2 }); + + store.invalidateFile('/path/types.ts'); + + expect(store.getSingle('User', '/path/types.ts')).toBeUndefined(); + expect(store.getPool('User', '/path/types.ts')).toBeUndefined(); + expect(store.getSingle('Product', '/path/other.ts')).toEqual({ id: 2 }); + }); + + it('should not throw when file has no entries', () => { + expect(() => store.invalidateFile('/path/nonexistent.ts')).not.toThrow(); + }); + }); + + describe('getStats', () => { + it('should return correct counts', () => { + store.setSingle('User', '/path/user.ts', { id: 1 }); + store.setPool('Product', '/path/product.ts', [{ id: 2 }]); + expect(store.getStats()).toEqual({ singles: 1, pools: 1 }); + }); + + it('should return zeros when empty', () => { + expect(store.getStats()).toEqual({ singles: 0, pools: 0 }); + }); + }); +});