Skip to content

Commit

Permalink
feat(tree): Add the ability to associate metadata with Node Schema (#…
Browse files Browse the repository at this point in the history
…23321)

Users of TreeView can now specify metadata when creating Node Schema,
via `SchemaFactoryAlpha`.
This metadata may include system-understood properties like
`description`.

Example:

```typescript
const schemaFactory = new SchemaFactoryAlpha(...);
class Point extends schemaFactory.object("Point", {
	x: schemaFactory.required(schemaFactory.number),
	y: schemaFactory.required(schemaFactory.number),
},
{
	metadata: {
		description: "A point in 2D space",
	},
}) {}
```
  • Loading branch information
Josmithr authored Dec 23, 2024
1 parent 9bdb9b5 commit 58619c3
Show file tree
Hide file tree
Showing 31 changed files with 952 additions and 228 deletions.
79 changes: 79 additions & 0 deletions .changeset/grey-triangles-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
"@fluidframework/tree": minor
---
---
section: tree
---

Metadata can be associated with Node Schema

Users of TreeView can now specify metadata when creating Node Schema, via `SchemaFactoryAlpha`.
This metadata may include system-understood properties like `description`.

Example:

```typescript
const schemaFactory = new SchemaFactoryAlpha(...);
class Point extends schemaFactory.object("Point", {
x: schemaFactory.required(schemaFactory.number),
y: schemaFactory.required(schemaFactory.number),
},
{
metadata: {
description: "A point in 2D space",
},
}) {}

```

Functionality like the experimental conversion of Tree Schema to [JSON Schema](https://json-schema.org/) ([getJsonSchema](https://github.com/microsoft/FluidFramework/releases/tag/client_v2.4.0#user-content-metadata-can-now-be-associated-with-field-schema-22564)) leverages such system-understood metadata to generate useful information.
In the case of the `description` property, it is mapped directly to the `description` property supported by JSON Schema.

Custom, user-defined properties can also be specified.
These properties will not be used by the system by default, but can be used to associate common application-specific properties with Node Schema.

#### `SchemaFactoryAlpha` Updates

- `object` and `objectRecursive`, `arrayRecursive`, and `mapRecursive` now support `metadata` in their `options` parameter.
- (new) `arrayAlpha` - Variant of `array` that accepts an options parameter which supports `metadata`
- (new) `mapAlpha` - Variant of `map` that accepts an options parameter which supports `metadata`

#### Example

An application is implementing search functionality.
By default, the app author wishes for all app content to be potentially indexable by search, unless otherwise specified.
They can leverage schema metadata to decorate types of nodes that should be ignored by search, and leverage that information when walking the tree during a search.

```typescript

interface AppMetadata {
/**
* Whether or not nodes of this type should be ignored by search.
* @defaultValue `false`
*/
searchIgnore?: boolean;
}

const schemaFactory = new SchemaFactoryAlpha(...);
class Point extends schemaFactory.object("Point", {
x: schemaFactory.required(schemaFactory.number),
y: schemaFactory.required(schemaFactory.number),
},
{
metadata: {
description: "A point in 2D space",
custom: {
searchIgnore: true,
},
}
}) {}

```

Search can then be implemented to look for the appropriate metadata, and leverage it to omit the unwanted position data from search.

#### Potential for breaking existing code

These changes add the new property "metadata" to the base type from which all node schema derive.
If you have existing node schema subclasses that include a property of this name, there is a chance for potential conflict here that could be breaking.
If you encounter issues here, consider renaming your property or leveraging the new metadata support.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"handletable",
"incrementality",
"injective",
"insertable",
"losslessly",
"mitigations",
"mocharc",
Expand Down
170 changes: 95 additions & 75 deletions examples/apps/ai-collab/src/types/sharedTreeAppSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,99 +3,119 @@
* Licensed under the MIT License.
*/

import {
SchemaFactory,
Tree,
TreeViewConfiguration,
type TreeNode,
} from "@fluidframework/tree";
import { Tree, TreeViewConfiguration, type TreeNode } from "@fluidframework/tree";
import { SchemaFactoryAlpha } from "@fluidframework/tree/alpha";
import { SharedTree } from "fluid-framework";

// The string passed to the SchemaFactory should be unique
const sf = new SchemaFactory("ai-collab-sample-application");
const sf = new SchemaFactoryAlpha("ai-collab-sample-application");

// NOTE that there is currently a bug with the ai-collab library that requires us to rearrange the keys of each type to not have the same first key.

export class SharedTreeTask extends sf.object("Task", {
title: sf.required(sf.string, {
export class SharedTreeTask extends sf.object(
"Task",
{
title: sf.required(sf.string, {
metadata: {
description: `The title of the task.`,
},
}),
id: sf.identifier,
description: sf.required(sf.string, {
metadata: {
description: `The description of the task.`,
},
}),
priority: sf.required(sf.string, {
metadata: {
description: `The priority of the task which can ONLY be one of three levels: "Low", "Medium", "High" (case-sensitive).`,
},
}),
complexity: sf.required(sf.number, {
metadata: {
description: `The complexity of the task as a fibonacci number.`,
},
}),
status: sf.required(sf.string, {
metadata: {
description: `The status of the task which can ONLY be one of the following values: "To Do", "In Progress", "Done" (case-sensitive).`,
},
}),
assignee: sf.required(sf.string, {
metadata: {
description: `The name of the tasks assignee e.g. "Bob" or "Alice".`,
},
}),
},
{
metadata: {
description: `The title of the task.`,
description: `A task that can be assigned to an engineer.`,
},
}),
id: sf.identifier,
description: sf.required(sf.string, {
metadata: {
description: `The description of the task.`,
},
}),
priority: sf.required(sf.string, {
metadata: {
description: `The priority of the task which can ONLY be one of three levels: "Low", "Medium", "High" (case-sensitive).`,
},
}),
complexity: sf.required(sf.number, {
metadata: {
description: `The complexity of the task as a fibonacci number.`,
},
}),
status: sf.required(sf.string, {
metadata: {
description: `The status of the task which can ONLY be one of the following values: "To Do", "In Progress", "Done" (case-sensitive).`,
},
}),
assignee: sf.required(sf.string, {
metadata: {
description: `The name of the tasks assignee e.g. "Bob" or "Alice".`,
},
}),
}) {}
},
) {}

export class SharedTreeTaskList extends sf.array("TaskList", SharedTreeTask) {}

export class SharedTreeEngineer extends sf.object("Engineer", {
name: sf.required(sf.string, {
metadata: {
description: `The name of an engineer whom can be assigned to a task.`,
},
}),
id: sf.identifier,
skills: sf.required(sf.string, {
metadata: {
description: `A description of the engineers skills which influence what types of tasks they should be assigned to.`,
},
}),
maxCapacity: sf.required(sf.number, {
export class SharedTreeEngineer extends sf.object(
"Engineer",
{
name: sf.required(sf.string, {
metadata: {
description: `The name of the engineer.`,
},
}),
id: sf.identifier,
skills: sf.required(sf.string, {
metadata: {
description: `A description of the engineer's skills, which influence what types of tasks they should be assigned to.`,
},
}),
maxCapacity: sf.required(sf.number, {
metadata: {
description: `The maximum capacity of tasks this engineer can handle, measured in task complexity points.`,
},
}),
},
{
metadata: {
description: `The maximum capacity of tasks this engineer can handle measured in in task complexity points.`,
description: `An engineer to whom tasks may be assigned.`,
},
}),
}) {}
},
) {}

export class SharedTreeEngineerList extends sf.array("EngineerList", SharedTreeEngineer) {}

export class SharedTreeTaskGroup extends sf.object("TaskGroup", {
description: sf.required(sf.string, {
metadata: {
description: `The description of the task group, which is a collection of tasks and engineers that can be assigned to said tasks.`,
},
}),
id: sf.identifier,
title: sf.required(sf.string, {
export class SharedTreeTaskGroup extends sf.object(
"TaskGroup",
{
description: sf.required(sf.string, {
metadata: {
description: `The description of the task group.`,
},
}),
id: sf.identifier,
title: sf.required(sf.string, {
metadata: {
description: `The title of the task group.`,
},
}),
tasks: sf.required(SharedTreeTaskList, {
metadata: {
description: `The lists of tasks within this task group.`,
},
}),
engineers: sf.required(SharedTreeEngineerList, {
metadata: {
description: `The lists of engineers within this task group to whom tasks may be assigned.`,
},
}),
},
{
metadata: {
description: `The title of the task group.`,
description: "A collection of tasks and engineers to whom tasks may be assigned.",
},
}),
tasks: sf.required(SharedTreeTaskList, {
metadata: {
description: `The lists of tasks within this task group.`,
},
}),
engineers: sf.required(SharedTreeEngineerList, {
metadata: {
description: `The lists of engineers within this task group which can be assigned to tasks.`,
},
}),
}) {}
},
) {}

export class SharedTreeTaskGroupList extends sf.array("TaskGroupList", SharedTreeTaskGroup) {}

Expand Down
Loading

0 comments on commit 58619c3

Please sign in to comment.