diff --git a/packages/rest/src/rest-server.ts b/packages/rest/src/rest-server.ts index f3bc7dabf4..fe1502b539 100644 --- a/packages/rest/src/rest-server.ts +++ b/packages/rest/src/rest-server.ts @@ -1749,9 +1749,18 @@ export class RestServer { const packageId = req.query?.package || undefined; const environmentId = isScoped ? req.params?.environmentId : undefined; const p = await this.resolveProtocol(environmentId, req); + // ADR-0033/0037 draft-overlay preview: `?preview=draft` + // overlays pending drafts on the active list, exactly as + // the runtime dispatcher's /metadata/:type route does — + // the console's draft preview (Live Canvas) reads THIS + // route, so dropping the flag here silently renders the + // published-only world. + const previewDrafts = typeof req.query?.preview === 'string' + && req.query.preview.toLowerCase() === 'draft'; const items = await p.getMetaItems({ type: req.params.type, packageId, + ...(previewDrafts ? { previewDrafts: true } : {}), ...(environmentId ? { environmentId } : {}), } as any); @@ -1897,7 +1906,14 @@ export class RestServer { const isAppType = req.params.type === 'app'; const isDraftRead = typeof req.query?.state === 'string' && req.query.state.toLowerCase() === 'draft'; - if (metadata.enableCache && p.getMetaItemCached && !isAppType && !isDraftRead) { + // ADR-0033/0037 — `?preview=draft` overlays a pending + // draft on the active item (draft wins, falls back to + // active). Must also bypass the cache: ETags are keyed + // on the published checksum, so a cached 304 would pin + // the preview to the stale published world. + const previewDrafts = typeof req.query?.preview === 'string' + && req.query.preview.toLowerCase() === 'draft'; + if (metadata.enableCache && p.getMetaItemCached && !isAppType && !isDraftRead && !previewDrafts) { const cacheRequest = { ifNoneMatch: req.headers['if-none-match'] as string, ifModifiedSince: req.headers['if-modified-since'] as string, @@ -1955,6 +1971,7 @@ export class RestServer { name: req.params.name, packageId, ...(stateParam === 'draft' ? { state: 'draft' } : {}), + ...(previewDrafts ? { previewDrafts: true } : {}), } as any); // Same per-user RBAC filtering as the list endpoint: diff --git a/packages/rest/src/rest.test.ts b/packages/rest/src/rest.test.ts index c84668c933..b02fb6f950 100644 --- a/packages/rest/src/rest.test.ts +++ b/packages/rest/src/rest.test.ts @@ -499,6 +499,67 @@ describe('RestServer', () => { }); }); + describe('meta routes preview=draft forwarding (ADR-0033/0037)', () => { + function getMetaRoute(rest: any, method: string, path: string) { + return rest + .getRoutes() + .find((r: any) => r.method === method && r.path === path); + } + const mockRes = () => ({ + json: vi.fn(), + status: vi.fn().mockReturnThis(), + header: vi.fn(), + send: vi.fn(), + }); + + it('GET /meta/:type forwards previewDrafts to protocol.getMetaItems', async () => { + const rest = new RestServer(server as any, protocol as any); + rest.registerRoutes(); + const route = getMetaRoute(rest, 'GET', '/api/v1/meta/:type'); + expect(route).toBeDefined(); + + await route!.handler( + { params: { type: 'app' }, query: { preview: 'draft' }, headers: {} }, + mockRes(), + ); + expect(protocol.getMetaItems).toHaveBeenCalledWith( + expect.objectContaining({ type: 'app', previewDrafts: true }), + ); + }); + + it('GET /meta/:type omits previewDrafts without the flag', async () => { + const rest = new RestServer(server as any, protocol as any); + rest.registerRoutes(); + const route = getMetaRoute(rest, 'GET', '/api/v1/meta/:type'); + + await route!.handler( + { params: { type: 'app' }, query: {}, headers: {} }, + mockRes(), + ); + const arg = protocol.getMetaItems.mock.calls.at(-1)![0]; + expect(arg).not.toHaveProperty('previewDrafts'); + }); + + it('GET /meta/:type/:name forwards previewDrafts and bypasses the cached path', async () => { + // A cached protocol would normally win; preview must skip it (ETags are + // keyed on the published checksum). + protocol.getMetaItemCached = vi.fn(); + const rest = new RestServer(server as any, protocol as any); + rest.registerRoutes(); + const route = getMetaRoute(rest, 'GET', '/api/v1/meta/:type/:name'); + expect(route).toBeDefined(); + + await route!.handler( + { params: { type: 'object', name: 'lead' }, query: { preview: 'draft' }, headers: {} }, + mockRes(), + ); + expect(protocol.getMetaItemCached).not.toHaveBeenCalled(); + expect(protocol.getMetaItem).toHaveBeenCalledWith( + expect.objectContaining({ type: 'object', name: 'lead', previewDrafts: true }), + ); + }); + }); + describe('findData handler expand/populate forwarding', () => { function getListRoute(rest: any) { const routes = rest.getRoutes();