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
12 changes: 12 additions & 0 deletions .changeset/fifty-crabs-add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@fluidframework/tree": minor
"__section": feature
---
`delete` keyword support for ObjectNodes

Added support for using the `delete` keyword to remove content under optional fields for ObjectNodes.

```ts
// This is now equivalent to node.foo = undefined
delete node.foo
```
69 changes: 51 additions & 18 deletions packages/dds/tree/src/simple-tree/node-kinds/object/objectNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,23 @@ export type ObjectFromSchemaRecord<T extends RestrictiveStringRecord<ImplicitFie
* No other own `own` or `enumerable` properties are included on object nodes unless the user of the node manually adds custom session only state.
* This allows a majority of general purpose JavaScript object processing operations (like `for...in`, `Reflect.ownKeys()` and `Object.entries()`) to enumerate all the children.
*
* Field Assignment and Deletion
* Assigning to a field updates the tree value as long as it is still valid based on the schema.
* - For required fields, assigning `undefined` is invalid, and will throw.
* - For optional fields, assigning `undefined` removes the field's contents, and marks it as empty (non-enumerable and value set to undefined).
*
* Example:
* ```ts
* const Foo = schemaFactory.object("Foo", {bar: schemaFactory.optional(schemaFactory.number)});
* const node = new Foo({bar: 1})
*
* // This clears the field, is non-enumerable, and value is undefined.
* delete node.bar;
*
* // This is equivalent to the delete example.
* node.bar = undefined
* ```
*
* The API for fields is defined by {@link ObjectFromSchemaRecord}.
* @public
*/
Expand Down Expand Up @@ -275,27 +292,17 @@ function createProxyHandler(
: false;
}

const innerNode = getInnerNode(proxy);

const innerSchema = innerNode.context.schema.nodeSchema.get(brand(schema.identifier));
assert(
innerSchema instanceof ObjectNodeStoredSchema,
0xc18 /* Expected ObjectNodeStoredSchema */,
);

setField(
innerNode.getBoxed(fieldInfo.storedKey),
fieldInfo.schema,
value,
innerSchema.getFieldSchema(fieldInfo.storedKey),
);
applyFieldChange(schema, { kind: "proxy", node: proxy }, fieldInfo, value);
return true;
},
deleteProperty(target, propertyKey): boolean {
// TODO: supporting delete when it makes sense (custom local fields, and optional field) could be added as a feature in the future.
throw new UsageError(
`Object nodes do not support the delete operator. Optional fields can be assigned to undefined instead.`,
);
const fieldInfo = schema.flexKeyMap.get(propertyKey);
if (fieldInfo === undefined) {
return allowAdditionalProperties ? Reflect.deleteProperty(target, propertyKey) : false;
}

applyFieldChange(schema, { kind: "target", node: target }, fieldInfo, undefined);
return true;
},
has: (target, propertyKey) => {
return (
Expand Down Expand Up @@ -750,3 +757,29 @@ function getFieldProperty(
}
return undefined;
}

function applyFieldChange(
schema: ObjectNodeSchemaPrivate,
from: { kind: "proxy"; node: TreeNode } | { kind: "target"; node: object },
fieldInfo: { storedKey: FieldKey; schema: FieldSchema },
value: InsertableContent | undefined,
): void {
const proxy =
from.kind === "proxy"
? from.node
: (targetToProxy.get(from.node) ?? fail("missing proxy"));
const inner = getInnerNode(proxy);
const storedSchema = inner.context.schema.nodeSchema.get(brand(schema.identifier));
assert(storedSchema instanceof ObjectNodeStoredSchema, "Expected ObjectNodeStoredSchema");
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When writing asserts (from @fluidframework/core-utils), please use a string literal for the error message, not a hex assert code. This instruction should only apply to newly added asserts, not existing ones.

Copilot generated this review using guidance from repository custom instructions.

if (value === undefined && inner.tryGetField(fieldInfo.storedKey) === undefined) {
return;
}

setField(
inner.getBoxed(fieldInfo.storedKey),
fieldInfo.schema,
value,
storedSchema.getFieldSchema(fieldInfo.storedKey),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -355,13 +355,8 @@ describeHydration(
foo: schemaFactory.optional(schemaFactory.number),
}) {}
const n = init(Schema, { foo: 0 });
assert.throws(
() => {
// Since we do not have exactOptionalPropertyTypes enabled, this compiles, but should error at runtime:
delete n.foo;
},
validateUsageError(/delete operator/),
);
delete n.foo;
assert.equal(n.foo, undefined);
});

it("assigning identifier errors", () => {
Expand Down
Loading