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
30 changes: 30 additions & 0 deletions packages/objectql/src/sys-metadata-repository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,36 @@ describe('SysMetadataRepository', () => {
expect(ops).toContain('publish');
});

it('promoteDraft carries the draft package binding onto the promoted active row', async () => {
// A whole-app build stages every artifact bound to the app's workspace
// package and the app starts `hidden`. Promotion MUST preserve that
// binding — otherwise the active app row lands unbound (package_id NULL)
// and the ADR-0045 publish visibility flip (which looks up hidden apps
// by package: getMetaItems({ type:'app', packageId })) never matches it,
// leaving the freshly-built app hidden from the app switcher forever.
const ref = { org: 'org_alpha', type: 'app' as const, name: 'ticket_service_app' };
await repo.put(
ref,
{ name: 'ticket_service_app', label: 'Tickets', hidden: true },
{ parentVersion: null, actor: 'studio', state: 'draft', packageId: 'app.tickets' },
);
await repo.promoteDraft(ref, { actor: 'admin' });
const activeRow = Array.from(engine.rows.values()).find(
(r) => (r as any).type === 'app'
&& (r as any).name === 'ticket_service_app'
&& (r as any).state === 'active',
) as any;
expect(activeRow).toBeDefined();
expect(activeRow.package_id).toBe('app.tickets');
// The draft row is consumed by the promotion.
const draftRow = Array.from(engine.rows.values()).find(
(r) => (r as any).type === 'app'
&& (r as any).name === 'ticket_service_app'
&& (r as any).state === 'draft',
);
expect(draftRow).toBeUndefined();
});

it('promoteDraft throws no_draft when nothing is pending', async () => {
const ref = { org: 'org_alpha', type: 'view' as const, name: 'case_grid' };
await repo.put(ref, sampleView, { parentVersion: null, actor: 'studio' });
Expand Down
23 changes: 20 additions & 3 deletions packages/objectql/src/sys-metadata-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -579,16 +579,32 @@ export class SysMetadataRepository implements MetadataRepository {
opts: { actor: string; source?: string; message?: string; intent?: MetadataWriteIntent },
): Promise<{ version: string; seq: number; item: MetadataItem }> {
this.assertOpen();
const draft = await this.get(ref, { state: 'draft' });
if (!draft) {
// Read the RAW draft row (not just the body) so the promotion can carry
// the draft's package binding onto the active row. ADR-0048 keys overlay
// rows by `(org, type, name, package_id)`; promoteDraft historically
// called put() WITHOUT a packageId, so the freshly-created active row
// landed unbound (`package_id = NULL`). That silently broke every
// package-scoped reader — most visibly the ADR-0045 publish visibility
// flip (`getMetaItems({ type:'app', packageId })` → unhide), which then
// never matched the just-published app and left AI-built apps `hidden`
// (invisible in the app switcher / home) forever.
const draftRow = await this.engine.findOne('sys_metadata', {
where: this.whereFor(ref, 'draft'),
});
if (!draftRow) {
const err: any = new Error(
`[no_draft] No pending draft exists for ${ref.type}/${ref.name} — nothing to publish.`,
);
err.code = 'no_draft';
err.status = 404;
throw err;
}
const currentActive = await this.get(ref, { state: 'active' });
const draftPackageId = (draftRow as { package_id?: string | null }).package_id ?? null;
const draft = this.rowToItem(ref, draftRow);
// Read the active row through the SAME package scope we will write, so the
// optimistic-lock `parentVersion` matches the exact row `put` upserts.
// (Package-less drafts → packageId null → identical to the prior behaviour.)
const currentActive = await this.get(ref, { state: 'active', packageId: draftPackageId });
const result = await this.put(ref, draft.body, {
parentVersion: currentActive?.hash ?? null,
actor: opts.actor,
Expand All @@ -597,6 +613,7 @@ export class SysMetadataRepository implements MetadataRepository {
intent: opts.intent ?? 'override-artifact',
state: 'active',
opType: 'publish',
packageId: draftPackageId,
});
// Drop the draft row — it has been promoted. Tolerate races where
// a second publisher already drained it.
Expand Down
Loading