diff --git a/.changesets/fix-vite-20k-seed-population.json b/.changesets/fix-vite-20k-seed-population.json new file mode 100644 index 00000000..e13b366a --- /dev/null +++ b/.changesets/fix-vite-20k-seed-population.json @@ -0,0 +1,127 @@ +{ + "branch": "fix/vite-20k-seed-population", + "bump": "patch", + "environments": [ + "production" + ], + "packages": [ + "@websublime/vite-plugin-open-api-core", + "@websublime/vite-plugin-open-api-server" + ], + "changes": [ + "7112339a6d1364052507d94381ea1cc08a8b2537", + "005682693dbf2d60bc9fbfa55926d80282fcdd0a", + "f345067c1cfbbce92a17a61f210ae2aebabec801", + "f317b1e0bd9e0b92f0caf6cdf046c2140a3384f2", + "639c7f60d27cf785bb6f3095117beb468e18e977", + "a50939d0d62e74c2d3b7d574c55c73c2554db66a", + "931844244915688d7a6e08b07194cd36356dade1", + "f640c756200b35ecb4e445248aa53232dbfa6e0e", + "45dd3f52c2482b1fb568f03308b3f7a8b9e39e56", + "5e9846a8cab0496ba2c4bde4f11b174339e86cbd", + "a07388cabe203c4e27f83a695fcd2d1d7539cde3", + "787dac6fb47fc6f036b426dbbb25ef020b7bcfe0", + "c225d6e8b8f72940ce225f5b58d03c769262a334", + "091c686e51c4804f08c24c5f664b947679c6f0cc", + "040e0b984f3b77beeabf152b1e378e26d19337f7", + "b12d89cf3e1a06ffcefb57096924b9e6b2e5b6e0", + "29ba36ba13df5d2e17e1eb81ac79d4580f363912", + "093d17ff5801983d722de39e61e7aceefa4d7019", + "97fbbbb019e2ce2c90485b771c435895baf32e53", + "8627a3ce86ea82a19d262bf0d652ff4f7b323ca8", + "cc048f0e7a87ec53f3e111f9f8c1cec56442c2ed", + "66c478f8092986fa498201587483fc8f61416e04", + "23bff103646176f2e616cd0869d36b09747cfbb4", + "7e17215aad7d66083d28e4208cc0ce391b02606d", + "94b75edfc3211765de2310fdce71ce4b6115b7ee", + "48eceee100d4dbe4b02fd337f8cc874f6ea4b71b", + "ad9c68de36d4c1893609aa068772b73655b1daaf", + "0fee4175f28427489fa728a474eefb2aeae21cc3", + "15a33dffaa291ee14b245a347f9c1ceb73c6e0f1", + "0cedc49ce1c778f7e5fde8f49c6c98a7de8fb58b", + "01da7c175e18c1386b66a0dbbd4d0320ba816ee6", + "22462b7f49c90a9b14973b4cdd21ae4e3f62009b", + "4d0f6aa43f6d6253aa5c6fa5a5125afe5cc8ff9e", + "5c47242e78fff9836bb29e43519e66131adb850e", + "9574aa95cc12cc67c5f9cacc155a1202ced1a849", + "518b43d954656f3a197608e18c3266d8fabaa3c4", + "680627ef2af228d96d35cf7838f7cddd568a1d5a", + "d6decf078d5dc393d60b66403a044ecf4af420ff", + "215db55b796bd6091d96f2d8f32deaf1af556d6a", + "10fa877bdf22b4c93d47d0837d5c1d42df9f8158", + "a9c295c4a4675d84dacadc6f551eb4e7c68aa700", + "18bc8ca00a1ad65329a2d67563545a2cd5ff0498", + "ab44398be43fce2958d2fa4cafa93e5ff9e368cd", + "c5bb469bcb5f4948a673245f913988af1947a6fd", + "e1ef500bc2752f84eff9cc5509d2d822c0eac2cf", + "791eab9254570db82f289dd0fc9caf2fa3f084e2", + "0e90ba397e1829434bcbbca745067aeb6bdf06a9", + "fcc263a2d327f7cdbf966e256b281cd6f24b6794", + "b8440e2c62192c61918522ec76eda6d9fe9e70f7", + "b9ecee1b9ffef1189e990ac550d453b1d868898c", + "ff3585e45a7cde8cce8e69ca8320ec467d703948", + "920aa364b763d38f0e323ed0d1fabf1777c781b1", + "bcdc4dbd10982df6a9d69badb8213d8902f0100c", + "0292cdc76820ea1544d7293d0935081a5529a86b", + "8cb0333889f6d747f28b12146a2d23a2f7a73ce9", + "a89a68e658c2062721a9fc612ee3ce7c0665deb8", + "151032eab051c407f2e84b82faaf71a0e5e0b9dc", + "1f054333c23135e66b785ce50008259c39615b91", + "9bd537dd8a166f28b7270ed63e7c4e923ec52b3c", + "b6a295595031941b3ec3324fb5bcbccfa52daf52", + "5bf5071294f924796190c374dc11051cc5c83004", + "ff82f49fdfa86d8f0b8604947cc84f54791638cd", + "ebdf0ff8d663b4058fd2b8e49c07239cf4e89924", + "06907a1353cbd356c7e557b014965d2d72a9d326", + "4227d7b03cbc1e2333d8edad4a1ac095897aacca", + "ee08e7795372772e0b0291049f4d7b128110f9d0", + "53ba344618dac42d6bafaffe7376d9f9a71523fa", + "fad30a6ce21cc7f0ca33de01ccd629e1b78fb2b7", + "b9bc6877589e8cc7bfe1de7c2ecce9b0298daaf5", + "7a7faa5397ea879470be32bbc3e6ab4c8ca100ab", + "8bac4ee2cb20752e03d2bf272c065e6627a7eb3b", + "aaaf52f62bc528969a79028fda9a6bd86c3070b8", + "22d6b50a70be0c58f1d24634933ac894434f51e2", + "1639cd27351289f3631b02cb9a70170d07255a99", + "6557578b51104b707d71b5cb2c138728edcee7c6", + "eed669cceafdd0a009d6c5b4fff52e12e80e9678", + "4b43b616d8b997c46c021074aca693135dba120a", + "c58f9a837ce0e9c5b0e31f3d0ab7ef61dadbe295", + "125d4467b11e0a582060d372c8157a8f3a584655", + "c0d28394a953a1a2a06c45c208549ffd84ef4ee9", + "b86ea75f2daef1b4275f2446ee4998b9f9f80547", + "532f03cc4ddf573612495f9332772274188a97ea", + "8fe89e21dd636c26ad81870cb8971c77d219b5b7", + "2f037ec4798018a1c1ff895cab3515850ad7c1c2", + "c831756f6e730541e5d3fa61cd58477428880dcc", + "9f4a6a3a52e9e9abe0ac3c6c33830a19d7524dbe", + "9288e85283c51732263f8dcca64f8acdb8f9d748", + "b5f0b1437718dd778876b9811268170912181b5d", + "f91ae5a1922d3f0252e289139b5342622246bd9b", + "f523c7caf298316f9635fb99a3a7aa23c6d7551f", + "e2880dba7e3acea4736a2d4136fc6baee81a5188", + "f6754a98833bbe36d3c86b28f7a942b79aab5d52", + "10a0c94368758ef7af9b1a53de154283b3d1e433", + "8ecc053864fac36e4feb59b4f71b4a66d038d9f1", + "eb6b05824e319eb78ccc491f8a4047dba1b313ef", + "f38dfa00bdc6f07e884a9dd5f2a27710535f77c8", + "467655ee544a9fc63eb716256427622b617af307", + "35e5eccdb05d578ed380432c03f4adfe3f27b8d7", + "4ddc0c638d19a806b78d6fde45edca5af1f356ea", + "f717863091bee302c5059e179ab2778f1431febb", + "f37cec1abcd62534f0a272258f2a0695565d4fcc", + "13343a22069816019df34b572562d53bdb5610ac", + "fbb4f222a05d641f556b42ba37e3f49f1e9613c9", + "bab5bf886c4058380058eb47ac3b6560773923b2", + "b6f231eef5e8d3ed6b77ef01f1c97b3cc6ccee60", + "e3a5927c964daa308cf67616c7bd317323b4344a", + "c7405dd8b54a772f610eeaec0b448f21d5e83c17", + "48cbda095a78af6d81035fc496d808493909a3c9", + "f2a89db2ea685da89c7d6b5602f6fb1840ada766", + "4b4bf3212f09bb69a3d249117df385712a96a353", + "e6b9993129cccb056c310dccd610cbf32f8b5f66", + "fb407897a645ceb716d0c62ada83e961c281a8af" + ], + "created_at": "2026-03-03T14:44:38.222145Z", + "updated_at": "2026-03-03T14:45:03.731242Z" +} \ No newline at end of file diff --git a/packages/core/src/server.ts b/packages/core/src/server.ts index 646530e9..6d48018c 100644 --- a/packages/core/src/server.ts +++ b/packages/core/src/server.ts @@ -269,13 +269,14 @@ export async function createOpenApiServer(config: OpenApiServerConfig): Promise< } } - // Current handlers (mutable for hot reload). - // IMPORTANT: Route closures in buildRoutes capture this Map by reference. - // updateHandlers() mutates it in-place (clear + re-populate) so that existing - // route closures see the updated entries. Never reassign this variable — doing - // so would break the reference chain and silently stop handler dispatch. + // Current handlers and seeds (mutable for hot reload). + // IMPORTANT: Route closures in buildRoutes capture these Maps by reference. + // updateHandlers()/updateSeeds() mutate them in-place (clear + re-populate) so + // that existing route closures see the updated entries. Never reassign these + // variables — doing so would break the reference chain and silently stop + // handler/seed dispatch. const currentHandlers = handlers; - let currentSeeds = seeds; + const currentSeeds = seeds; // Build routes from OpenAPI document. // IMPORTANT: buildRoutes must receive the exact same Map instances stored in @@ -557,15 +558,30 @@ export async function createOpenApiServer(config: OpenApiServerConfig): Promise< /** * Update seed data at runtime (for hot reload) * + * Repopulates the store with the new seed data and syncs the route + * builder's seed map so that the seed response priority path sees + * the updated entries. + * * @remarks - * **Warning**: This method clears ALL data in the store before repopulating - * with the new seeds. Any manually added data (including data in schemas - * not present in the new seeds) will be permanently lost. + * **Mutation contract**: Route closures capture `currentSeeds` by + * reference at build time. This method mutates the Map in-place + * (`.clear()` + `.set()`) so closures see the updates. Never replace + * the Map — that breaks the reference chain. + * + * **Warning**: This method clears ALL data in the store before + * repopulating with the new seeds. Any manually added data (including + * data in schemas not present in the new seeds) will be permanently lost. * * @param newSeeds - New seeds map (schema name -> array of items) */ updateSeeds(newSeeds: Map): void { - currentSeeds = newSeeds; + // Mutate the existing Map in-place so route closures see the updates. + // IMPORTANT: buildRoutes captures this Map by reference — never replace it. + // This mirrors the pattern used by updateHandlers(). + currentSeeds.clear(); + for (const [key, value] of newSeeds) { + currentSeeds.set(key, value); + } // Re-populate store with new seeds // Note: clearAll() removes ALL schemas, not just the ones being updated diff --git a/packages/server/src/__tests__/per-spec-reload.test.ts b/packages/server/src/__tests__/per-spec-reload.test.ts index 9efeb3e8..f3d4b417 100644 --- a/packages/server/src/__tests__/per-spec-reload.test.ts +++ b/packages/server/src/__tests__/per-spec-reload.test.ts @@ -35,10 +35,14 @@ vi.mock('../handlers.js', () => ({ getHandlerFiles: vi.fn(), })); -vi.mock('../seeds.js', () => ({ - loadSeeds: vi.fn(), - getSeedFiles: vi.fn(), -})); +vi.mock('../seeds.js', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadSeeds: vi.fn(), + getSeedFiles: vi.fn(), + }; +}); // Mock executeSeeds from core (called by reloadSpecSeeds) vi.mock('@websublime/vite-plugin-open-api-core', async (importOriginal) => { @@ -352,7 +356,7 @@ describe('Per-Spec Reload Isolation', () => { expect(specB.server.store.clearAll).not.toHaveBeenCalled(); }); - it('should only broadcast to the targeted spec wsHub', async () => { + it('should call updateSeeds on the targeted spec only', async () => { mockedLoadSeeds.mockResolvedValue({ seeds: new Map(), fileCount: 0, @@ -361,31 +365,43 @@ describe('Per-Spec Reload Isolation', () => { await reloadSpecSeeds(specA, mockVite, cwd, options); - // Spec A wsHub should have broadcast called - expect(specA.server.wsHub.broadcast).toHaveBeenCalledTimes(1); - expect(specA.server.wsHub.broadcast).toHaveBeenCalledWith({ - type: 'seeds:updated', - data: { count: 0 }, - }); + // Spec A should have updateSeeds called (which handles broadcast internally) + expect(specA.server.updateSeeds).toHaveBeenCalledTimes(1); + expect(specA.server.updateSeeds).toHaveBeenCalledWith(new Map()); - // Spec B wsHub should NOT have broadcast called - expect(specB.server.wsHub.broadcast).not.toHaveBeenCalled(); + // Spec B should NOT have updateSeeds called + expect(specB.server.updateSeeds).not.toHaveBeenCalled(); }); - it('should print reload notification with seeds.size (not fileCount)', async () => { + it('should print reload notification with seed schema count from store', async () => { const newSeeds = new Map([['Pet', [{ id: 1, name: 'Rex' }]]]); // fileCount deliberately differs from seeds.size to ensure - // the implementation uses seeds.size for the notification + // the implementation uses the store's schema count for the notification mockedLoadSeeds.mockResolvedValue({ seeds: newSeeds, fileCount: 3, files: ['pets.seeds.ts', 'owners.seeds.ts', 'admin.seeds.ts'], }); + // After executeSeeds, the store should have the seeded schemas + const mockStore = specA.server.store as unknown as MockStore; + mockedExecuteSeeds.mockImplementation(async () => { + // Simulate store being populated by executeSeeds + mockStore.getSchemas.mockReturnValue(['Pet']); + mockStore.list.mockReturnValue([{ id: 1, name: 'Rex' }]); + return { + schemaCount: 1, + totalItems: 1, + itemsPerSchema: { Pet: 1 }, + skippedSchemas: [], + warnings: [], + }; + }); + await reloadSpecSeeds(specA, mockVite, cwd, options); - // Should report seeds.size (1), not fileCount (3) + // Should report seed map size (1), not fileCount (3) expect(mockedPrintReloadNotification).toHaveBeenCalledWith('seeds', 1, options); }); @@ -398,6 +414,8 @@ describe('Per-Spec Reload Isolation', () => { await reloadSpecSeeds(specA, mockVite, cwd, options); + // updateSeeds should still be called (with empty map) to clear stale seed data + expect(specA.server.updateSeeds).toHaveBeenCalledWith(new Map()); expect(mockedPrintReloadNotification).not.toHaveBeenCalled(); }); @@ -435,11 +453,11 @@ describe('Per-Spec Reload Isolation', () => { // Verify spec B is completely untouched expect(specB.server.store.clearAll).not.toHaveBeenCalled(); - expect(specB.server.wsHub.broadcast).not.toHaveBeenCalled(); + expect(specB.server.updateSeeds).not.toHaveBeenCalled(); expect(specB.server.updateHandlers).not.toHaveBeenCalled(); }); - it('should clear store even when no seeds are loaded', async () => { + it('should clear store and sync empty seeds when no seeds are loaded', async () => { mockedLoadSeeds.mockResolvedValue({ seeds: new Map(), fileCount: 0, @@ -450,6 +468,9 @@ describe('Per-Spec Reload Isolation', () => { // Store should still be cleared (empty seeds = clear all data) expect(specA.server.store.clearAll).toHaveBeenCalledTimes(1); + + // updateSeeds should be called with empty map to clear stale seed data in route builder + expect(specA.server.updateSeeds).toHaveBeenCalledWith(new Map()); }); it('should handle errors without affecting other specs', async () => { @@ -467,12 +488,12 @@ describe('Per-Spec Reload Isolation', () => { // No reload notification on error expect(mockedPrintReloadNotification).not.toHaveBeenCalled(); - // No broadcast on seed-load failure (store was never modified) - expect(specA.server.wsHub.broadcast).not.toHaveBeenCalled(); + // No updateSeeds on seed-load failure (store was never modified) + expect(specA.server.updateSeeds).not.toHaveBeenCalled(); // Even with an error, spec B should be untouched expect(specB.server.store.clearAll).not.toHaveBeenCalled(); - expect(specB.server.wsHub.broadcast).not.toHaveBeenCalled(); + expect(specB.server.updateSeeds).not.toHaveBeenCalled(); }); it('should reload two specs independently', async () => { @@ -482,6 +503,20 @@ describe('Per-Spec Reload Isolation', () => { ['Category', [{ id: 1, name: 'Tools' }]], ]); + // Configure spec A mock store to return data after executeSeeds + const mockStoreA = specA.server.store as unknown as MockStore; + mockedExecuteSeeds.mockImplementationOnce(async () => { + mockStoreA.getSchemas.mockReturnValue(['Pet']); + mockStoreA.list.mockReturnValue([{ id: 1, name: 'Rex' }]); + return { + schemaCount: 1, + totalItems: 1, + itemsPerSchema: { Pet: 1 }, + skippedSchemas: [], + warnings: [], + }; + }); + // Reload spec A mockedLoadSeeds.mockResolvedValueOnce({ seeds: specASeeds, @@ -490,6 +525,24 @@ describe('Per-Spec Reload Isolation', () => { }); await reloadSpecSeeds(specA, mockVite, cwd, options); + // Configure spec B mock store to return data after executeSeeds + const mockStoreB = specB.server.store as unknown as MockStore; + mockedExecuteSeeds.mockImplementationOnce(async () => { + mockStoreB.getSchemas.mockReturnValue(['Item', 'Category']); + mockStoreB.list.mockImplementation((schema: string) => { + if (schema === 'Item') return [{ id: 1, sku: 'A001' }]; + if (schema === 'Category') return [{ id: 1, name: 'Tools' }]; + return []; + }); + return { + schemaCount: 2, + totalItems: 2, + itemsPerSchema: { Item: 1, Category: 1 }, + skippedSchemas: [], + warnings: [], + }; + }); + // Reload spec B mockedLoadSeeds.mockResolvedValueOnce({ seeds: specBSeeds, @@ -502,15 +555,16 @@ describe('Per-Spec Reload Isolation', () => { expect(specA.server.store.clearAll).toHaveBeenCalledTimes(1); expect(specB.server.store.clearAll).toHaveBeenCalledTimes(1); - // Each spec broadcast independently - expect(specA.server.wsHub.broadcast).toHaveBeenCalledWith({ - type: 'seeds:updated', - data: { count: 1 }, - }); - expect(specB.server.wsHub.broadcast).toHaveBeenCalledWith({ - type: 'seeds:updated', - data: { count: 2 }, - }); + // Each spec updateSeeds called independently with data from store + expect(specA.server.updateSeeds).toHaveBeenCalledWith( + new Map([['Pet', [{ id: 1, name: 'Rex' }]]]), + ); + expect(specB.server.updateSeeds).toHaveBeenCalledWith( + new Map([ + ['Item', [{ id: 1, sku: 'A001' }]], + ['Category', [{ id: 1, name: 'Tools' }]], + ]), + ); }); }); @@ -576,11 +630,8 @@ describe('Per-Spec Reload Isolation', () => { options, ); - // Should broadcast with count 0 to reflect the cleared state - expect(specA.server.wsHub.broadcast).toHaveBeenCalledWith({ - type: 'seeds:updated', - data: { count: 0 }, - }); + // updateSeeds should be called with empty map to clear stale seed data + expect(specA.server.updateSeeds).toHaveBeenCalledWith(new Map()); // No reload notification on executeSeeds failure expect(mockedPrintReloadNotification).not.toHaveBeenCalled(); @@ -598,7 +649,7 @@ describe('Per-Spec Reload Isolation', () => { // Spec B should be completely untouched expect(specB.server.store.clearAll).not.toHaveBeenCalled(); - expect(specB.server.wsHub.broadcast).not.toHaveBeenCalled(); + expect(specB.server.updateSeeds).not.toHaveBeenCalled(); }); }); }); diff --git a/packages/server/src/hot-reload.ts b/packages/server/src/hot-reload.ts index 556702a7..e150a20d 100644 --- a/packages/server/src/hot-reload.ts +++ b/packages/server/src/hot-reload.ts @@ -17,7 +17,7 @@ import type { ViteDevServer } from 'vite'; import { printError, printReloadNotification } from './banner.js'; import { loadHandlers } from './handlers.js'; import type { SpecInstance } from './orchestrator.js'; -import { loadSeeds } from './seeds.js'; +import { buildSeedMapFromStore, loadSeeds } from './seeds.js'; import type { ResolvedOptions } from './types.js'; // Segment-boundary patterns: match "node_modules" or "dist" as a directory @@ -460,8 +460,9 @@ export async function reloadSpecHandlers( /** * Reload seeds for a specific spec instance * - * Loads fresh seeds from disk, clears the spec's store, and re-executes - * seeds. Broadcasts a WebSocket event and logs the result. + * Loads fresh seeds from disk, clears the spec's store, re-executes + * seeds, and syncs the route builder's seed map via `updateSeeds()`. + * Broadcasts a WebSocket event and logs the result. * * Note: This operation is not fully atomic — there's a brief window between * clearing the store and repopulating it where requests may see empty data. @@ -480,41 +481,35 @@ export async function reloadSpecSeeds( ): Promise { try { // Load seeds first (before clearing) to minimize the window where store is empty. - // NOTE: We bypass instance.server.updateSeeds() because it expects static data - // (Map), while loadSeeds() returns seed functions (Map) - // that must be materialized via executeSeeds(). The explicit broadcast below is necessary - // because we're not going through updateSeeds()'s built-in broadcast. - // TODO: Epic 3 (Task 3.1) will wire the broadcast wrapper to add specId automatically. - // TODO: Epic 3 (Task 3.2) should also update registry hasSeed flags after seed reload, - // which are currently skipped because updateSeeds() is bypassed. const logger = options.logger ?? console; const seedsResult = await loadSeeds(instance.config.seedsDir, vite, cwd, logger); - let broadcastCount = seedsResult.seeds.size; instance.server.store.clearAll(); if (seedsResult.seeds.size > 0) { try { await executeSeeds(seedsResult.seeds, instance.server.store, instance.server.document); } catch (execError) { - // Store was already cleared — warn that it's now empty due to seed execution failure - broadcastCount = 0; + // Store was already cleared — warn that it's now empty due to seed execution failure. + // Sync an empty seed map so the route builder doesn't serve stale data. + instance.server.updateSeeds(new Map()); printError( `Seeds loaded but executeSeeds failed for spec "${instance.id}"; store is now empty`, execError, options, ); + return; } } - // Single broadcast for both success and failure paths - instance.server.wsHub.broadcast({ - type: 'seeds:updated', - data: { count: broadcastCount }, - }); + // Sync the route builder's seed map from the now-populated store. + // updateSeeds() handles: in-place map mutation, registry hasSeed flags, + // WebSocket broadcast, and logging. + const seedMap = buildSeedMapFromStore(instance.server.store); + instance.server.updateSeeds(seedMap); - if (broadcastCount > 0) { - printReloadNotification('seeds', broadcastCount, options); + if (seedMap.size > 0) { + printReloadNotification('seeds', seedMap.size, options); } } catch (error) { printError(`Failed to reload seeds for spec "${instance.id}"`, error, options); diff --git a/packages/server/src/orchestrator.ts b/packages/server/src/orchestrator.ts index 3207d205..aa61f107 100644 --- a/packages/server/src/orchestrator.ts +++ b/packages/server/src/orchestrator.ts @@ -28,7 +28,7 @@ import { loadHandlers } from './handlers.js'; import { mountMultiSpecInternalApi } from './multi-internal-api.js'; import { createMultiSpecWebSocketHub } from './multi-ws.js'; import { deriveProxyPath, validateUniqueProxyPaths } from './proxy-path.js'; -import { loadSeeds } from './seeds.js'; +import { buildSeedMapFromStore, loadSeeds } from './seeds.js'; import { deriveSpecId, slugify, validateUniqueIds } from './spec-id.js'; import type { ResolvedOptions, ResolvedSpecConfig } from './types.js'; @@ -191,9 +191,16 @@ async function processSpec( server.updateHandlers(handlersResult.handlers, { silent: true }); } - // Execute seed functions to populate the store + // Execute seed functions to populate the store, then sync route builder if (seedsResult.seeds.size > 0) { await executeSeeds(seedsResult.seeds, server.store, server.document); + + // Sync the route builder's seed map from the now-populated store. + // executeSeeds() only writes to the store — the route builder's seed map + // (used for the seed response priority path) is a separate reference that + // must be updated explicitly via updateSeeds(). + const seedMap = buildSeedMapFromStore(server.store); + server.updateSeeds(seedMap); } // Derive proxy path (from explicit config or servers[0].url) diff --git a/packages/server/src/seeds.ts b/packages/server/src/seeds.ts index 0758d985..dd074307 100644 --- a/packages/server/src/seeds.ts +++ b/packages/server/src/seeds.ts @@ -1,7 +1,7 @@ /** - * Seed Loading + * Seed Loading and Utilities * - * What: Loads seed files from a directory using glob patterns + * What: Loads seed files from a directory and provides seed data utilities * How: Uses Vite's ssrLoadModule to transform and load TypeScript files * Why: Enables users to define seed data for schemas in TypeScript * @@ -9,7 +9,12 @@ */ import path from 'node:path'; -import type { AnySeedFn, Logger, SeedDefinition } from '@websublime/vite-plugin-open-api-core'; +import type { + AnySeedFn, + Logger, + SeedDefinition, + Store, +} from '@websublime/vite-plugin-open-api-core'; import fg from 'fast-glob'; import type { ViteDevServer } from 'vite'; import { directoryExists } from './utils.js'; @@ -191,3 +196,26 @@ export async function getSeedFiles( return files; } + +/** + * Build a seed data Map from the store's current contents. + * + * After `executeSeeds()` populates the store, this function reads back + * the materialized data so it can be passed to `server.updateSeeds()`. + * The route builder's seed map needs static `Map` + * data (not seed functions), which is exactly what the store contains + * after execution. + * + * @param store - Store populated by executeSeeds() + * @returns Map of schema name to array of items + */ +export function buildSeedMapFromStore(store: Store): Map { + const seedMap = new Map(); + for (const schemaName of store.getSchemas()) { + const items = store.list(schemaName); + if (items.length > 0) { + seedMap.set(schemaName, items); + } + } + return seedMap; +}