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
10 changes: 10 additions & 0 deletions .changeset/27707-promotion-no-draft-before-name-collision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@objectstack/metadata-protocol': patch
---

fix(metadata-protocol): promoting a permission set with no draft answers `NO_DRAFT` before another owner's name collision

Clause-②: no
Metadata-protocol: none

The draft promotion (`POST /api/v1/meta/permission/:name/publish`, and `publishPackageDrafts`, which promotes through the same step) asked the permission-set name judge (`NAMESPACE_CONFLICT` / 422) even when no draft existed at the address, so a publish with nothing to publish, of a name another package holds, answered the collision instead of `NO_DRAFT` / 404. The name judge now runs only when a draft was read, like the promotion's other draft judges, so the caller is told about its own state first. Refusal for refusal: with no draft the promotion was refused before and is refused now, and with a draft the same refusals run in the same order.
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,20 @@ describe('the draft promotion refuses a permission-set draft whose name another
]);
});

it('with no draft at the address, the caller is told about its own state first: the repository\'s NO_DRAFT (404), not the collision another owner\'s name would raise', async () => {
const h = makeProtocol();
// B holds the active name; A has no draft of it at all.
await h.protocol.saveMetaItem({ type: 'permission', name: SET, item: perm(SET), packageId: OWNER_B });
h.saveDoor.mockClear();

const { error } = await attempt(() => h.protocol.publishMetaItem({ type: 'permission', name: SET, packageId: OWNER_A }));

expect(error).toBeInstanceOf(Error);
expect(error.code).toBe('NO_DRAFT');
expect(error.status).toBe(404);
expect(storedSets(h)).toEqual([{ row: SET, state: 'active', binding: OWNER_B }]);
});

it('CONTROL: the owning package publishes its own first set, and later re-publishes its own draft of it', async () => {
const h = makeProtocol();
await h.protocol.saveMetaItem({ type: 'permission', name: SET, item: perm(SET), packageId: OWNER_B, mode: 'draft' });
Expand Down
5 changes: 4 additions & 1 deletion packages/metadata-protocol/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23043,7 +23043,10 @@ export class ObjectStackProtocolImplementation implements
// binding the active row is written under), before `repo.promoteDraft`
// writes — so a batch publish aborts on it as it aborts on the owner
// refusal above. See {@link permissionSetNameCollisionRefusal}.
{
// Asked only when a draft was read, like its siblings: with none at
// the address the caller is told `repo.promoteDraft`'s `NO_DRAFT`
// first, the fact about its own state, rather than another owner's.
if (draftForGate) {
const nameCollision = await this.permissionSetNameCollisionRefusal(
singularType, request.name, promoteInto, 'publish',
);
Expand Down
5 changes: 5 additions & 0 deletions packages/objectql/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3499,6 +3499,11 @@ export class SchemaRegistry {
* {@link securityCatalogPackageHolders}'s answer, the one predicate. Built-in
* names are skipped, as the cold boot skips them (see the method above).
* Sorted by type in that list's order, then name.
*
* Also read BY NAME from `@objectstack/metadata-protocol`
* (`protocol-save-door-refusals.ts`, `registryPackageHoldersOf`): renaming
* this member means updating that caller and
* `protocol-permission-set-name-registry-reading.test.ts`.
*/
private packageHeldSecurityCatalogNames(): Array<{ catalogType: SecurityCatalogType; name: string; packageIds: string[] }> {
const held: Array<{ catalogType: SecurityCatalogType; name: string; packageIds: string[] }> = [];
Expand Down
Loading