From 49bce11ece1d5a0378a0142afd67e17be30a1cfd Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sun, 23 Aug 2026 06:57:41 +0800 Subject: [PATCH] fix(rest): GET /meta/_drafts reads in the caller's org scope (#11087) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A draft saved by a session carrying an active org lands in that org's overlay scope (saveMetaItem's organizationId: ctx?.tenantId). The drafts route read with NO org — getOverlayRepo(null) sees only env-wide (organization_id IS NULL) rows — so every org-scoped draft was invisible to the pending-changes surfaces while single reads (which thread the ctx) and the publisher (which resolves each draft's own scope) saw it fine: the write-org/read-null split behind cloud#1593, measured live on a staging tenant (view draft saved 'org=org_mt42…' → _drafts answered only the older env-wide row). With the org threaded, SysMetadataRepository.listDrafts' own $or contract surfaces BOTH the caller's org overlay and env-wide drafts. Co-Authored-By: Claude Opus 5 --- .changeset/drafts-route-org-scope-11087.md | 5 +++++ ...est-server-meta-org-scope-url-spelling.test.ts | 13 +++++++++++++ packages/rest/src/rest-server.ts | 15 +++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 .changeset/drafts-route-org-scope-11087.md diff --git a/.changeset/drafts-route-org-scope-11087.md b/.changeset/drafts-route-org-scope-11087.md new file mode 100644 index 0000000000..c89a6df1e4 --- /dev/null +++ b/.changeset/drafts-route-org-scope-11087.md @@ -0,0 +1,5 @@ +--- +'@objectstack/rest': patch +--- + +`GET /meta/_drafts` threads the caller's org into `listDrafts` (#11087) — read scope symmetric with the save route, so org-scoped drafts (saved by sessions carrying an active organization) appear in the pending-changes list alongside env-wide ones instead of being invisible to every package/pending surface. diff --git a/packages/rest/src/rest-server-meta-org-scope-url-spelling.test.ts b/packages/rest/src/rest-server-meta-org-scope-url-spelling.test.ts index 17b44b1c98..7f0b94c3aa 100644 --- a/packages/rest/src/rest-server-meta-org-scope-url-spelling.test.ts +++ b/packages/rest/src/rest-server-meta-org-scope-url-spelling.test.ts @@ -273,6 +273,19 @@ describe('#10340 the /meta doors decide org scope on the FOLDED type, not the ra }); expect(requestFrom(b.listDrafts).type).toBe('translations'); }); + + it('threads the CALLER org into GET /meta/_drafts — read scope symmetric with the save route (#11087)', async () => { + // A draft saved by a session carrying an active org lands in that + // org's overlay scope (`saveMetaItem`'s `organizationId: + // ctx?.tenantId`). Reading with NO org sees only env-wide rows + // (`getOverlayRepo(null)` → `organization_id IS NULL`), so every + // org-scoped draft was invisible to the pending-changes surfaces — + // the write-org/read-null split behind cloud#1593. The repository's + // own `$or` contract surfaces BOTH scopes once the org is threaded. + const b = boot(AUTHORIZED); + await b.drive('GET', `${META}/_drafts`, {}); + expect(requestFrom(b.listDrafts).organizationId).toBe(ORG); + }); }); describe('what the fold deliberately does NOT touch', () => { diff --git a/packages/rest/src/rest-server.ts b/packages/rest/src/rest-server.ts index 645da09e85..1a02afaf33 100644 --- a/packages/rest/src/rest-server.ts +++ b/packages/rest/src/rest-server.ts @@ -3950,9 +3950,24 @@ export class RestServer { // [#6877] Both narrow the draft list to one package / // one type; an array reached `listDrafts` untouched. if (refuseRepeatedQueryParams(req, res, ['packageId', 'type'])) return; + // [#11087] Read in the CALLER'S org scope, symmetric with + // the save route (`saveMetaItem`'s `organizationId: + // ctx?.tenantId`, below): a draft saved by a session + // carrying an active org lands in that org's overlay + // scope, and this route used to read with NO org — + // `getOverlayRepo(null)` sees only env-wide + // (`organization_id IS NULL`) rows, so every org-scoped + // draft was invisible to the pending-changes surfaces + // while single reads (which thread the ctx) and the + // publisher (which resolves each draft's own scope) + // saw it fine — the write-org/read-null split behind + // cloud#1593. With the org threaded, the repository's + // own `$or` contract surfaces BOTH the caller's org + // overlay and env-wide drafts. const result = await (p as any).listDrafts({ packageId: (req.query?.packageId as string | undefined) || undefined, type: (req.query?.type as string | undefined) || undefined, + organizationId: ctx?.tenantId ?? undefined, }); res.json(result); } catch (error: any) {