Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion packages/rest/src/rest-server.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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);

Expand DownExpand Up@@ -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,
Expand DownExpand Up@@ -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:
Expand Down
61 changes: 61 additions & 0 deletions packages/rest/src/rest.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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();
Expand Down