Skip to content

finding(plugin-designer): MetadataObjectsPage keys its delete-detection lookup by blind assignment — an object named constructor can never be deleted #6522

Description

@os-support-ai

Found while implementing objectui#6489 (PR #6520), which closed the same construction defect in the sibling page MetadataFieldsPage. Filed rather than folded in: different function, different write path, and #6489's dispatch fenced its file face to the fields map construction.

The code

packages/plugin-designer/src/MetadataObjectsPage.tsx, handleObjectsChange:

constnextByName: Record<string,ObjectDefinition>={};for(constoofnext)nextByName[o.name]=o;consterrors: string[]=[];// Deletionsfor(constnameofObject.keys(prev)){if(!nextByName[name]){try{awaitclient.reset('object',name);}catch(err){}}}

Two prototype-reachable reads in one loop, and the consequential one is the read, not the write.

The hazard: !nextByName[name] answers out of Object.prototype

nextByName is a plain object literal, so a lookup for any name that exists on Object.prototype returns a truthy inherited value even when the designer's new list does not contain that object. Measured:

object named `constructor` reads as present: true => delete detected: false
object named `__proto__` reads as present: true => delete detected: false

So deleting such an object from the Object Manager issues noclient.reset('object', …) at all. The row disappears from the UI, the save reports success, and the object is still there after a reload — a silent no-op rather than a refusal.

Both names are legal object names. Measured against the installed @objectstack/spec 17.2.0:

ObjectSchema.safeParse({ name: 'constructor', label: 'C', fields: {} }) => success = true
ObjectSchema.safeParse({ name: '__proto__', label: 'P', fields: {} }) => success = true

constructor is the realistic one — it is an ordinary lowercase identifier a business object could plausibly be called, with none of __proto__'s "obviously exotic" look.

The same two secondary hazards #6489 measured

  • nextByName[o.name] = o for a nameless object keys as the literal string "undefined" (this map is a local lookup rather than a serialised body, so the impact here is misrouted delete detection, not a corrupt document).
  • nextByName['__proto__'] = o invokes the prototype setter rather than creating a key. That one is self-cancelling for this loop — the later read finds the value on the prototype — which is exactly why it hides.

Suggested shape

The fix objectui#6240 and objectui#6489 both landed: build through Object.fromEntries and read the lookup as an own property (Object.prototype.hasOwnProperty.call(…)), or use a Map, which has neither hazard and is the natural fit for a lookup that is never serialised. Whether the nameless/duplicate cases should also be refused here is a triage question — this page's writes are per-object rather than one map, so the #6489 refusals do not port across unchanged.

Refs: objectui#6489 (the same construction in the sibling page, fixed in PR #6520) · objectui#6240 (the app-shell writer) · objectui#5761 (the parity family)


Generated by Claude Code

Metadata

Metadata

Assignees

Labels

designerdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions