From ff2cfda15392258b4cd7e9f6006165f368e0376f Mon Sep 17 00:00:00 2001 From: daesunp <138815173+daesunp@users.noreply.github.com> Date: Thu, 23 Oct 2025 15:42:15 +0000 Subject: [PATCH 1/5] Support "delete" keyword for object node --- .../node-kinds/object/objectNode.ts | 38 +++++++++++++++++-- .../node-kinds/object/objectNode.spec.ts | 9 +---- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/packages/dds/tree/src/simple-tree/node-kinds/object/objectNode.ts b/packages/dds/tree/src/simple-tree/node-kinds/object/objectNode.ts index 37e97a06b681..72836e7c0230 100644 --- a/packages/dds/tree/src/simple-tree/node-kinds/object/objectNode.ts +++ b/packages/dds/tree/src/simple-tree/node-kinds/object/objectNode.ts @@ -292,10 +292,40 @@ function createProxyHandler( 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; + } + + const proxy = targetToProxy.get(target) ?? fail("missing proxy"); + const innerNode = getInnerNode(proxy); + + const innerSchema = innerNode.context.schema.nodeSchema.get(brand(schema.identifier)); + assert(innerSchema instanceof ObjectNodeStoredSchema, "Expected ObjectNodeStoredSchema"); + + const field = innerNode.tryGetField(fieldInfo?.storedKey); + // If the field is already undefined, return true. + if (field === undefined) { + return true; + } + + // Delete should only be possible for optional fields. If called on a required field, throw a usage error. + switch (field.schema) { + case FieldKinds.optional.identifier: { + setField( + field, + fieldInfo?.schema, + undefined, + innerSchema?.getFieldSchema(fieldInfo?.storedKey), + ); + return true; + } + case FieldKinds.required.identifier: { + throw new UsageError("Required fields cannot be deleted."); + } + default: + fail("invalid FieldKind"); + } }, has: (target, propertyKey) => { return ( diff --git a/packages/dds/tree/src/test/simple-tree/node-kinds/object/objectNode.spec.ts b/packages/dds/tree/src/test/simple-tree/node-kinds/object/objectNode.spec.ts index dd143d788fef..ae67f07e7808 100644 --- a/packages/dds/tree/src/test/simple-tree/node-kinds/object/objectNode.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/node-kinds/object/objectNode.spec.ts @@ -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", () => { From 5b40bac8439d7b497042aaf03cc85296f03150e5 Mon Sep 17 00:00:00 2001 From: daesunp <138815173+daesunp@users.noreply.github.com> Date: Tue, 28 Oct 2025 17:56:31 +0000 Subject: [PATCH 2/5] PR review --- .../node-kinds/object/objectNode.ts | 92 ++++++++++--------- 1 file changed, 48 insertions(+), 44 deletions(-) diff --git a/packages/dds/tree/src/simple-tree/node-kinds/object/objectNode.ts b/packages/dds/tree/src/simple-tree/node-kinds/object/objectNode.ts index 72836e7c0230..c9c966c8e22b 100644 --- a/packages/dds/tree/src/simple-tree/node-kinds/object/objectNode.ts +++ b/packages/dds/tree/src/simple-tree/node-kinds/object/objectNode.ts @@ -9,6 +9,7 @@ import { isFluidHandle } from "@fluidframework/runtime-utils/internal"; import { ObjectNodeStoredSchema, + schemaDataIsEmpty, type FieldKey, type TreeFieldStoredSchema, } from "../../../core/index.js"; @@ -57,7 +58,7 @@ import { getUnhydratedContext, } from "../../createContext.js"; import { tryGetTreeNodeForField } from "../../getTreeNodeForField.js"; -import type { +import { ObjectNodeSchema, ObjectNodeSchemaInternalData, ObjectNodeSchemaPrivate, @@ -118,6 +119,23 @@ export type ObjectFromSchemaRecord { return ( @@ -780,3 +758,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"); + + if (value === undefined && inner.tryGetField(fieldInfo.storedKey) === undefined) { + return; + } + + setField( + inner.getBoxed(fieldInfo.storedKey), + fieldInfo.schema, + value, + storedSchema.getFieldSchema(fieldInfo.storedKey), + ); +} From cee0aa482685de927fc0afaf27650fa33c241134 Mon Sep 17 00:00:00 2001 From: daesunp <138815173+daesunp@users.noreply.github.com> Date: Tue, 28 Oct 2025 17:57:28 +0000 Subject: [PATCH 3/5] Import fix --- .../src/simple-tree/node-kinds/object/objectNode.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/dds/tree/src/simple-tree/node-kinds/object/objectNode.ts b/packages/dds/tree/src/simple-tree/node-kinds/object/objectNode.ts index c9c966c8e22b..90d67847fff3 100644 --- a/packages/dds/tree/src/simple-tree/node-kinds/object/objectNode.ts +++ b/packages/dds/tree/src/simple-tree/node-kinds/object/objectNode.ts @@ -9,7 +9,6 @@ import { isFluidHandle } from "@fluidframework/runtime-utils/internal"; import { ObjectNodeStoredSchema, - schemaDataIsEmpty, type FieldKey, type TreeFieldStoredSchema, } from "../../../core/index.js"; @@ -118,24 +117,24 @@ export type ObjectFromSchemaRecord Date: Tue, 28 Oct 2025 17:58:33 +0000 Subject: [PATCH 4/5] revert incorrect import change --- .../src/simple-tree/node-kinds/object/objectNode.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/dds/tree/src/simple-tree/node-kinds/object/objectNode.ts b/packages/dds/tree/src/simple-tree/node-kinds/object/objectNode.ts index 90d67847fff3..c6a9b453661b 100644 --- a/packages/dds/tree/src/simple-tree/node-kinds/object/objectNode.ts +++ b/packages/dds/tree/src/simple-tree/node-kinds/object/objectNode.ts @@ -57,7 +57,7 @@ import { getUnhydratedContext, } from "../../createContext.js"; import { tryGetTreeNodeForField } from "../../getTreeNodeForField.js"; -import { +import type { ObjectNodeSchema, ObjectNodeSchemaInternalData, ObjectNodeSchemaPrivate, @@ -117,24 +117,24 @@ export type ObjectFromSchemaRecord Date: Tue, 28 Oct 2025 23:34:02 +0000 Subject: [PATCH 5/5] Add changeset --- .changeset/fifty-crabs-add.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/fifty-crabs-add.md diff --git a/.changeset/fifty-crabs-add.md b/.changeset/fifty-crabs-add.md new file mode 100644 index 000000000000..6373411b3e98 --- /dev/null +++ b/.changeset/fifty-crabs-add.md @@ -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 +```