diff --git a/.changeset/grey-triangles-shout.md b/.changeset/grey-triangles-shout.md new file mode 100644 index 000000000000..1e4c66331aa6 --- /dev/null +++ b/.changeset/grey-triangles-shout.md @@ -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. diff --git a/.vscode/settings.json b/.vscode/settings.json index 62a869fe150e..29ef09ef141d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -43,6 +43,7 @@ "handletable", "incrementality", "injective", + "insertable", "losslessly", "mitigations", "mocharc", diff --git a/examples/apps/ai-collab/src/types/sharedTreeAppSchema.ts b/examples/apps/ai-collab/src/types/sharedTreeAppSchema.ts index e9ab918e99f3..2959d66f3a96 100644 --- a/examples/apps/ai-collab/src/types/sharedTreeAppSchema.ts +++ b/examples/apps/ai-collab/src/types/sharedTreeAppSchema.ts @@ -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) {} diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index 9863210ffb02..efb7e266a0be 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -394,6 +394,7 @@ export type JsonNodeSchema = JsonLeafNodeSchema | JsonMapNodeSchema | JsonArrayN // @alpha @sealed export interface JsonNodeSchemaBase { + readonly description?: string | undefined; readonly _treeNodeSchemaKind: TNodeKind; readonly type: TJsonSchemaType; } @@ -476,6 +477,17 @@ export enum NodeKind { Object = 2 } +// @public @sealed +export interface NodeSchemaMetadata { + readonly custom?: TCustomMetadata | undefined; + readonly description?: string | undefined; +} + +// @public @sealed +export interface NodeSchemaOptions { + readonly metadata?: NodeSchemaMetadata | undefined; +} + // @alpha export const noopValidator: JsonValidator; @@ -607,8 +619,8 @@ export class SchemaFactory>(name: Name, allowedTypes: T): TreeNodeSchemaClass, NodeKind.Array, TreeArrayNodeUnsafe & WithType, NodeKind.Array, unknown>, { [Symbol.iterator](): Iterator>; }, false, T, undefined>; - readonly boolean: TreeNodeSchemaNonClass<"com.fluidframework.leaf.boolean", NodeKind.Leaf, boolean, boolean, true, unknown, never>; - readonly handle: TreeNodeSchemaNonClass<"com.fluidframework.leaf.handle", NodeKind.Leaf, IFluidHandle, IFluidHandle, true, unknown, never>; + readonly boolean: TreeNodeSchemaNonClass<"com.fluidframework.leaf.boolean", NodeKind.Leaf, boolean, boolean, true, unknown, never, unknown>; + readonly handle: TreeNodeSchemaNonClass<"com.fluidframework.leaf.handle", NodeKind.Leaf, IFluidHandle, IFluidHandle, true, unknown, never, unknown>; get identifier(): FieldSchema; map(allowedTypes: T): TreeNodeSchemaNonClass`>, NodeKind.Map, TreeMapNode & WithType`>, NodeKind.Map>, MapNodeInsertableData, true, T, undefined>; map(name: Name, allowedTypes: T): TreeNodeSchemaClass, NodeKind.Map, TreeMapNode & WithType, NodeKind.Map>, MapNodeInsertableData, true, T, undefined>; @@ -620,8 +632,8 @@ export class SchemaFactory; }, false, T, undefined>; - readonly null: TreeNodeSchemaNonClass<"com.fluidframework.leaf.null", NodeKind.Leaf, null, null, true, unknown, never>; - readonly number: TreeNodeSchemaNonClass<"com.fluidframework.leaf.number", NodeKind.Leaf, number, number, true, unknown, never>; + readonly null: TreeNodeSchemaNonClass<"com.fluidframework.leaf.null", NodeKind.Leaf, null, null, true, unknown, never, unknown>; + readonly number: TreeNodeSchemaNonClass<"com.fluidframework.leaf.number", NodeKind.Leaf, number, number, true, unknown, never, unknown>; object>(name: Name, fields: T): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNode>, object & InsertableObjectFromSchemaRecord, true, T>; objectRecursive>>(name: Name, t: T): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNodeUnsafe>, object & InsertableObjectFromSchemaRecordUnsafe, false, T>; optional(t: T, props?: Omit, "defaultProvider">): FieldSchema; @@ -629,17 +641,30 @@ export class SchemaFactory(t: T, props?: Omit, "defaultProvider">): FieldSchema; requiredRecursive>(t: T, props?: Omit): FieldSchemaUnsafe; readonly scope: TScope; - readonly string: TreeNodeSchemaNonClass<"com.fluidframework.leaf.string", NodeKind.Leaf, string, string, true, unknown, never>; + readonly string: TreeNodeSchemaNonClass<"com.fluidframework.leaf.string", NodeKind.Leaf, string, string, true, unknown, never, unknown>; } // @alpha export class SchemaFactoryAlpha extends SchemaFactory { - object>(name: Name, fields: T, options?: SchemaFactoryObjectOptions): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNode>, object & InsertableObjectFromSchemaRecord, true, T>; - objectRecursive>>(name: Name, t: T, options?: SchemaFactoryObjectOptions): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNodeUnsafe>, object & InsertableObjectFromSchemaRecordUnsafe, false, T>; + arrayAlpha(name: Name, allowedTypes: T, options?: NodeSchemaOptions): TreeNodeSchemaClass, NodeKind.Array, TreeArrayNode & WithType, NodeKind.Array>, Iterable>, true, T, undefined, TCustomMetadata>; + arrayRecursive, const TCustomMetadata = unknown>(name: Name, allowedTypes: T, options?: NodeSchemaOptions): TreeNodeSchemaClass, NodeKind.Array, TreeArrayNodeUnsafe & WithType, NodeKind.Array, unknown>, { + [Symbol.iterator](): Iterator>; + }, false, T, undefined, TCustomMetadata>; + mapAlpha(name: Name, allowedTypes: T, options?: NodeSchemaOptions): TreeNodeSchemaClass, NodeKind.Map, TreeMapNode & WithType, NodeKind.Map>, MapNodeInsertableData, true, T, undefined, TCustomMetadata>; + mapRecursive, const TCustomMetadata = unknown>(name: Name, allowedTypes: T, options?: NodeSchemaOptions): TreeNodeSchemaClass, NodeKind.Map, TreeMapNodeUnsafe & WithType, NodeKind.Map, unknown>, { + [Symbol.iterator](): Iterator<[ + string, + InsertableTreeNodeFromImplicitAllowedTypesUnsafe + ]>; + } | { + readonly [x: string]: InsertableTreeNodeFromImplicitAllowedTypesUnsafe; + }, false, T, undefined, TCustomMetadata>; + object, const TCustomMetadata = unknown>(name: Name, fields: T, options?: SchemaFactoryObjectOptions): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNode>, object & InsertableObjectFromSchemaRecord, true, T, never, TCustomMetadata>; + objectRecursive>, const TCustomMetadata = unknown>(name: Name, t: T, options?: SchemaFactoryObjectOptions): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNodeUnsafe>, object & InsertableObjectFromSchemaRecordUnsafe, false, T, never, TCustomMetadata>; } // @alpha -export interface SchemaFactoryObjectOptions { +export interface SchemaFactoryObjectOptions extends NodeSchemaOptions { allowUnknownOptionalFields?: boolean; } @@ -877,10 +902,10 @@ export type TreeNodeFromImplicitAllowedTypes> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; // @public @sealed -export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; +export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; // @public @sealed -export type TreeNodeSchemaClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { +export type TreeNodeSchemaClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { new (data?: TInsertable | InternalTreeNode | TConstructorExtra): Unhydrated; } : { new (data: TInsertable | InternalTreeNode | TConstructorExtra): Unhydrated; @@ -893,7 +918,7 @@ export interface TreeNodeSchemaClassUnsafe { +export interface TreeNodeSchemaCore { readonly childTypes: ReadonlySet; // @sealed createFromInsertable(data: TInsertable): Unhydrated; @@ -902,10 +927,11 @@ export interface TreeNodeSchemaCore | undefined; } // @public @sealed -export type TreeNodeSchemaNonClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { +export type TreeNodeSchemaNonClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { create(data?: TInsertable | TConstructorExtra): TNode; } : { create(data: TInsertable | TConstructorExtra): TNode; diff --git a/packages/dds/tree/api-report/tree.beta.api.md b/packages/dds/tree/api-report/tree.beta.api.md index fee4b9796129..4f1655de0a8e 100644 --- a/packages/dds/tree/api-report/tree.beta.api.md +++ b/packages/dds/tree/api-report/tree.beta.api.md @@ -266,6 +266,17 @@ export enum NodeKind { Object = 2 } +// @public @sealed +export interface NodeSchemaMetadata { + readonly custom?: TCustomMetadata | undefined; + readonly description?: string | undefined; +} + +// @public @sealed +export interface NodeSchemaOptions { + readonly metadata?: NodeSchemaMetadata | undefined; +} + // @public type ObjectFromSchemaRecord> = { -readonly [Property in keyof T]: Property extends string ? TreeFieldFromImplicitField : unknown; @@ -364,8 +375,8 @@ export class SchemaFactory>(name: Name, allowedTypes: T): TreeNodeSchemaClass, NodeKind.Array, TreeArrayNodeUnsafe & WithType, NodeKind.Array, unknown>, { [Symbol.iterator](): Iterator>; }, false, T, undefined>; - readonly boolean: TreeNodeSchemaNonClass<"com.fluidframework.leaf.boolean", NodeKind.Leaf, boolean, boolean, true, unknown, never>; - readonly handle: TreeNodeSchemaNonClass<"com.fluidframework.leaf.handle", NodeKind.Leaf, IFluidHandle, IFluidHandle, true, unknown, never>; + readonly boolean: TreeNodeSchemaNonClass<"com.fluidframework.leaf.boolean", NodeKind.Leaf, boolean, boolean, true, unknown, never, unknown>; + readonly handle: TreeNodeSchemaNonClass<"com.fluidframework.leaf.handle", NodeKind.Leaf, IFluidHandle, IFluidHandle, true, unknown, never, unknown>; get identifier(): FieldSchema; map(allowedTypes: T): TreeNodeSchemaNonClass`>, NodeKind.Map, TreeMapNode & WithType`>, NodeKind.Map>, MapNodeInsertableData, true, T, undefined>; map(name: Name, allowedTypes: T): TreeNodeSchemaClass, NodeKind.Map, TreeMapNode & WithType, NodeKind.Map>, MapNodeInsertableData, true, T, undefined>; @@ -377,8 +388,8 @@ export class SchemaFactory; }, false, T, undefined>; - readonly null: TreeNodeSchemaNonClass<"com.fluidframework.leaf.null", NodeKind.Leaf, null, null, true, unknown, never>; - readonly number: TreeNodeSchemaNonClass<"com.fluidframework.leaf.number", NodeKind.Leaf, number, number, true, unknown, never>; + readonly null: TreeNodeSchemaNonClass<"com.fluidframework.leaf.null", NodeKind.Leaf, null, null, true, unknown, never, unknown>; + readonly number: TreeNodeSchemaNonClass<"com.fluidframework.leaf.number", NodeKind.Leaf, number, number, true, unknown, never, unknown>; object>(name: Name, fields: T): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNode>, object & InsertableObjectFromSchemaRecord, true, T>; objectRecursive>>(name: Name, t: T): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNodeUnsafe>, object & InsertableObjectFromSchemaRecordUnsafe, false, T>; optional(t: T, props?: Omit, "defaultProvider">): FieldSchema; @@ -386,7 +397,7 @@ export class SchemaFactory(t: T, props?: Omit, "defaultProvider">): FieldSchema; requiredRecursive>(t: T, props?: Omit): FieldSchemaUnsafe; readonly scope: TScope; - readonly string: TreeNodeSchemaNonClass<"com.fluidframework.leaf.string", NodeKind.Leaf, string, string, true, unknown, never>; + readonly string: TreeNodeSchemaNonClass<"com.fluidframework.leaf.string", NodeKind.Leaf, string, string, true, unknown, never, unknown>; } // @public @@ -505,10 +516,10 @@ export type TreeNodeFromImplicitAllowedTypes> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; // @public @sealed -export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; +export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; // @public @sealed -export type TreeNodeSchemaClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { +export type TreeNodeSchemaClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { new (data?: TInsertable | InternalTreeNode | TConstructorExtra): Unhydrated; } : { new (data: TInsertable | InternalTreeNode | TConstructorExtra): Unhydrated; @@ -521,7 +532,7 @@ export interface TreeNodeSchemaClassUnsafe { +export interface TreeNodeSchemaCore { readonly childTypes: ReadonlySet; // @sealed createFromInsertable(data: TInsertable): Unhydrated; @@ -530,10 +541,11 @@ export interface TreeNodeSchemaCore | undefined; } // @public @sealed -export type TreeNodeSchemaNonClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { +export type TreeNodeSchemaNonClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { create(data?: TInsertable | TConstructorExtra): TNode; } : { create(data: TInsertable | TConstructorExtra): TNode; diff --git a/packages/dds/tree/api-report/tree.legacy.alpha.api.md b/packages/dds/tree/api-report/tree.legacy.alpha.api.md index 612d9718d608..1ee33886234f 100644 --- a/packages/dds/tree/api-report/tree.legacy.alpha.api.md +++ b/packages/dds/tree/api-report/tree.legacy.alpha.api.md @@ -261,6 +261,17 @@ export enum NodeKind { Object = 2 } +// @public @sealed +export interface NodeSchemaMetadata { + readonly custom?: TCustomMetadata | undefined; + readonly description?: string | undefined; +} + +// @public @sealed +export interface NodeSchemaOptions { + readonly metadata?: NodeSchemaMetadata | undefined; +} + // @public type ObjectFromSchemaRecord> = { -readonly [Property in keyof T]: Property extends string ? TreeFieldFromImplicitField : unknown; @@ -359,8 +370,8 @@ export class SchemaFactory>(name: Name, allowedTypes: T): TreeNodeSchemaClass, NodeKind.Array, TreeArrayNodeUnsafe & WithType, NodeKind.Array, unknown>, { [Symbol.iterator](): Iterator>; }, false, T, undefined>; - readonly boolean: TreeNodeSchemaNonClass<"com.fluidframework.leaf.boolean", NodeKind.Leaf, boolean, boolean, true, unknown, never>; - readonly handle: TreeNodeSchemaNonClass<"com.fluidframework.leaf.handle", NodeKind.Leaf, IFluidHandle, IFluidHandle, true, unknown, never>; + readonly boolean: TreeNodeSchemaNonClass<"com.fluidframework.leaf.boolean", NodeKind.Leaf, boolean, boolean, true, unknown, never, unknown>; + readonly handle: TreeNodeSchemaNonClass<"com.fluidframework.leaf.handle", NodeKind.Leaf, IFluidHandle, IFluidHandle, true, unknown, never, unknown>; get identifier(): FieldSchema; map(allowedTypes: T): TreeNodeSchemaNonClass`>, NodeKind.Map, TreeMapNode & WithType`>, NodeKind.Map>, MapNodeInsertableData, true, T, undefined>; map(name: Name, allowedTypes: T): TreeNodeSchemaClass, NodeKind.Map, TreeMapNode & WithType, NodeKind.Map>, MapNodeInsertableData, true, T, undefined>; @@ -372,8 +383,8 @@ export class SchemaFactory; }, false, T, undefined>; - readonly null: TreeNodeSchemaNonClass<"com.fluidframework.leaf.null", NodeKind.Leaf, null, null, true, unknown, never>; - readonly number: TreeNodeSchemaNonClass<"com.fluidframework.leaf.number", NodeKind.Leaf, number, number, true, unknown, never>; + readonly null: TreeNodeSchemaNonClass<"com.fluidframework.leaf.null", NodeKind.Leaf, null, null, true, unknown, never, unknown>; + readonly number: TreeNodeSchemaNonClass<"com.fluidframework.leaf.number", NodeKind.Leaf, number, number, true, unknown, never, unknown>; object>(name: Name, fields: T): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNode>, object & InsertableObjectFromSchemaRecord, true, T>; objectRecursive>>(name: Name, t: T): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNodeUnsafe>, object & InsertableObjectFromSchemaRecordUnsafe, false, T>; optional(t: T, props?: Omit, "defaultProvider">): FieldSchema; @@ -381,7 +392,7 @@ export class SchemaFactory(t: T, props?: Omit, "defaultProvider">): FieldSchema; requiredRecursive>(t: T, props?: Omit): FieldSchemaUnsafe; readonly scope: TScope; - readonly string: TreeNodeSchemaNonClass<"com.fluidframework.leaf.string", NodeKind.Leaf, string, string, true, unknown, never>; + readonly string: TreeNodeSchemaNonClass<"com.fluidframework.leaf.string", NodeKind.Leaf, string, string, true, unknown, never, unknown>; } // @public @@ -492,10 +503,10 @@ export type TreeNodeFromImplicitAllowedTypes> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; // @public @sealed -export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; +export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; // @public @sealed -export type TreeNodeSchemaClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { +export type TreeNodeSchemaClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { new (data?: TInsertable | InternalTreeNode | TConstructorExtra): Unhydrated; } : { new (data: TInsertable | InternalTreeNode | TConstructorExtra): Unhydrated; @@ -508,7 +519,7 @@ export interface TreeNodeSchemaClassUnsafe { +export interface TreeNodeSchemaCore { readonly childTypes: ReadonlySet; // @sealed createFromInsertable(data: TInsertable): Unhydrated; @@ -517,10 +528,11 @@ export interface TreeNodeSchemaCore | undefined; } // @public @sealed -export type TreeNodeSchemaNonClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { +export type TreeNodeSchemaNonClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { create(data?: TInsertable | TConstructorExtra): TNode; } : { create(data: TInsertable | TConstructorExtra): TNode; diff --git a/packages/dds/tree/api-report/tree.legacy.public.api.md b/packages/dds/tree/api-report/tree.legacy.public.api.md index 601b5497063c..8caed641af7f 100644 --- a/packages/dds/tree/api-report/tree.legacy.public.api.md +++ b/packages/dds/tree/api-report/tree.legacy.public.api.md @@ -261,6 +261,17 @@ export enum NodeKind { Object = 2 } +// @public @sealed +export interface NodeSchemaMetadata { + readonly custom?: TCustomMetadata | undefined; + readonly description?: string | undefined; +} + +// @public @sealed +export interface NodeSchemaOptions { + readonly metadata?: NodeSchemaMetadata | undefined; +} + // @public type ObjectFromSchemaRecord> = { -readonly [Property in keyof T]: Property extends string ? TreeFieldFromImplicitField : unknown; @@ -359,8 +370,8 @@ export class SchemaFactory>(name: Name, allowedTypes: T): TreeNodeSchemaClass, NodeKind.Array, TreeArrayNodeUnsafe & WithType, NodeKind.Array, unknown>, { [Symbol.iterator](): Iterator>; }, false, T, undefined>; - readonly boolean: TreeNodeSchemaNonClass<"com.fluidframework.leaf.boolean", NodeKind.Leaf, boolean, boolean, true, unknown, never>; - readonly handle: TreeNodeSchemaNonClass<"com.fluidframework.leaf.handle", NodeKind.Leaf, IFluidHandle, IFluidHandle, true, unknown, never>; + readonly boolean: TreeNodeSchemaNonClass<"com.fluidframework.leaf.boolean", NodeKind.Leaf, boolean, boolean, true, unknown, never, unknown>; + readonly handle: TreeNodeSchemaNonClass<"com.fluidframework.leaf.handle", NodeKind.Leaf, IFluidHandle, IFluidHandle, true, unknown, never, unknown>; get identifier(): FieldSchema; map(allowedTypes: T): TreeNodeSchemaNonClass`>, NodeKind.Map, TreeMapNode & WithType`>, NodeKind.Map>, MapNodeInsertableData, true, T, undefined>; map(name: Name, allowedTypes: T): TreeNodeSchemaClass, NodeKind.Map, TreeMapNode & WithType, NodeKind.Map>, MapNodeInsertableData, true, T, undefined>; @@ -372,8 +383,8 @@ export class SchemaFactory; }, false, T, undefined>; - readonly null: TreeNodeSchemaNonClass<"com.fluidframework.leaf.null", NodeKind.Leaf, null, null, true, unknown, never>; - readonly number: TreeNodeSchemaNonClass<"com.fluidframework.leaf.number", NodeKind.Leaf, number, number, true, unknown, never>; + readonly null: TreeNodeSchemaNonClass<"com.fluidframework.leaf.null", NodeKind.Leaf, null, null, true, unknown, never, unknown>; + readonly number: TreeNodeSchemaNonClass<"com.fluidframework.leaf.number", NodeKind.Leaf, number, number, true, unknown, never, unknown>; object>(name: Name, fields: T): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNode>, object & InsertableObjectFromSchemaRecord, true, T>; objectRecursive>>(name: Name, t: T): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNodeUnsafe>, object & InsertableObjectFromSchemaRecordUnsafe, false, T>; optional(t: T, props?: Omit, "defaultProvider">): FieldSchema; @@ -381,7 +392,7 @@ export class SchemaFactory(t: T, props?: Omit, "defaultProvider">): FieldSchema; requiredRecursive>(t: T, props?: Omit): FieldSchemaUnsafe; readonly scope: TScope; - readonly string: TreeNodeSchemaNonClass<"com.fluidframework.leaf.string", NodeKind.Leaf, string, string, true, unknown, never>; + readonly string: TreeNodeSchemaNonClass<"com.fluidframework.leaf.string", NodeKind.Leaf, string, string, true, unknown, never, unknown>; } // @public @@ -489,10 +500,10 @@ export type TreeNodeFromImplicitAllowedTypes> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; // @public @sealed -export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; +export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; // @public @sealed -export type TreeNodeSchemaClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { +export type TreeNodeSchemaClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { new (data?: TInsertable | InternalTreeNode | TConstructorExtra): Unhydrated; } : { new (data: TInsertable | InternalTreeNode | TConstructorExtra): Unhydrated; @@ -505,7 +516,7 @@ export interface TreeNodeSchemaClassUnsafe { +export interface TreeNodeSchemaCore { readonly childTypes: ReadonlySet; // @sealed createFromInsertable(data: TInsertable): Unhydrated; @@ -514,10 +525,11 @@ export interface TreeNodeSchemaCore | undefined; } // @public @sealed -export type TreeNodeSchemaNonClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { +export type TreeNodeSchemaNonClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { create(data?: TInsertable | TConstructorExtra): TNode; } : { create(data: TInsertable | TConstructorExtra): TNode; diff --git a/packages/dds/tree/api-report/tree.public.api.md b/packages/dds/tree/api-report/tree.public.api.md index 601b5497063c..8caed641af7f 100644 --- a/packages/dds/tree/api-report/tree.public.api.md +++ b/packages/dds/tree/api-report/tree.public.api.md @@ -261,6 +261,17 @@ export enum NodeKind { Object = 2 } +// @public @sealed +export interface NodeSchemaMetadata { + readonly custom?: TCustomMetadata | undefined; + readonly description?: string | undefined; +} + +// @public @sealed +export interface NodeSchemaOptions { + readonly metadata?: NodeSchemaMetadata | undefined; +} + // @public type ObjectFromSchemaRecord> = { -readonly [Property in keyof T]: Property extends string ? TreeFieldFromImplicitField : unknown; @@ -359,8 +370,8 @@ export class SchemaFactory>(name: Name, allowedTypes: T): TreeNodeSchemaClass, NodeKind.Array, TreeArrayNodeUnsafe & WithType, NodeKind.Array, unknown>, { [Symbol.iterator](): Iterator>; }, false, T, undefined>; - readonly boolean: TreeNodeSchemaNonClass<"com.fluidframework.leaf.boolean", NodeKind.Leaf, boolean, boolean, true, unknown, never>; - readonly handle: TreeNodeSchemaNonClass<"com.fluidframework.leaf.handle", NodeKind.Leaf, IFluidHandle, IFluidHandle, true, unknown, never>; + readonly boolean: TreeNodeSchemaNonClass<"com.fluidframework.leaf.boolean", NodeKind.Leaf, boolean, boolean, true, unknown, never, unknown>; + readonly handle: TreeNodeSchemaNonClass<"com.fluidframework.leaf.handle", NodeKind.Leaf, IFluidHandle, IFluidHandle, true, unknown, never, unknown>; get identifier(): FieldSchema; map(allowedTypes: T): TreeNodeSchemaNonClass`>, NodeKind.Map, TreeMapNode & WithType`>, NodeKind.Map>, MapNodeInsertableData, true, T, undefined>; map(name: Name, allowedTypes: T): TreeNodeSchemaClass, NodeKind.Map, TreeMapNode & WithType, NodeKind.Map>, MapNodeInsertableData, true, T, undefined>; @@ -372,8 +383,8 @@ export class SchemaFactory; }, false, T, undefined>; - readonly null: TreeNodeSchemaNonClass<"com.fluidframework.leaf.null", NodeKind.Leaf, null, null, true, unknown, never>; - readonly number: TreeNodeSchemaNonClass<"com.fluidframework.leaf.number", NodeKind.Leaf, number, number, true, unknown, never>; + readonly null: TreeNodeSchemaNonClass<"com.fluidframework.leaf.null", NodeKind.Leaf, null, null, true, unknown, never, unknown>; + readonly number: TreeNodeSchemaNonClass<"com.fluidframework.leaf.number", NodeKind.Leaf, number, number, true, unknown, never, unknown>; object>(name: Name, fields: T): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNode>, object & InsertableObjectFromSchemaRecord, true, T>; objectRecursive>>(name: Name, t: T): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNodeUnsafe>, object & InsertableObjectFromSchemaRecordUnsafe, false, T>; optional(t: T, props?: Omit, "defaultProvider">): FieldSchema; @@ -381,7 +392,7 @@ export class SchemaFactory(t: T, props?: Omit, "defaultProvider">): FieldSchema; requiredRecursive>(t: T, props?: Omit): FieldSchemaUnsafe; readonly scope: TScope; - readonly string: TreeNodeSchemaNonClass<"com.fluidframework.leaf.string", NodeKind.Leaf, string, string, true, unknown, never>; + readonly string: TreeNodeSchemaNonClass<"com.fluidframework.leaf.string", NodeKind.Leaf, string, string, true, unknown, never, unknown>; } // @public @@ -489,10 +500,10 @@ export type TreeNodeFromImplicitAllowedTypes> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; // @public @sealed -export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; +export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; // @public @sealed -export type TreeNodeSchemaClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { +export type TreeNodeSchemaClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { new (data?: TInsertable | InternalTreeNode | TConstructorExtra): Unhydrated; } : { new (data: TInsertable | InternalTreeNode | TConstructorExtra): Unhydrated; @@ -505,7 +516,7 @@ export interface TreeNodeSchemaClassUnsafe { +export interface TreeNodeSchemaCore { readonly childTypes: ReadonlySet; // @sealed createFromInsertable(data: TInsertable): Unhydrated; @@ -514,10 +525,11 @@ export interface TreeNodeSchemaCore | undefined; } // @public @sealed -export type TreeNodeSchemaNonClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { +export type TreeNodeSchemaNonClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { create(data?: TInsertable | TConstructorExtra): TNode; } : { create(data: TInsertable | TConstructorExtra): TNode; diff --git a/packages/dds/tree/src/index.ts b/packages/dds/tree/src/index.ts index 0d1cec3076f9..a182906ce6b9 100644 --- a/packages/dds/tree/src/index.ts +++ b/packages/dds/tree/src/index.ts @@ -184,6 +184,8 @@ export { type TreeBranch, type TreeBranchEvents, asTreeViewAlpha, + type NodeSchemaOptions, + type NodeSchemaMetadata, } from "./simple-tree/index.js"; export { SharedTree, diff --git a/packages/dds/tree/src/simple-tree/api/jsonSchema.ts b/packages/dds/tree/src/simple-tree/api/jsonSchema.ts index 78815e4cbd8f..4b3c6e438448 100644 --- a/packages/dds/tree/src/simple-tree/api/jsonSchema.ts +++ b/packages/dds/tree/src/simple-tree/api/jsonSchema.ts @@ -58,6 +58,13 @@ export interface JsonNodeSchemaBase< * {@inheritDoc JsonSchemaType} */ readonly type: TJsonSchemaType; + + /** + * Description of the node schema. + * @remarks Derived from {@link NodeSchemaMetadata.description}. + * @see {@link https://json-schema.org/draft/2020-12/json-schema-validation#name-title-and-description} + */ + readonly description?: string | undefined; } /** diff --git a/packages/dds/tree/src/simple-tree/api/schemaFactory.ts b/packages/dds/tree/src/simple-tree/api/schemaFactory.ts index b88b17920a00..af9d390a4a3a 100644 --- a/packages/dds/tree/src/simple-tree/api/schemaFactory.ts +++ b/packages/dds/tree/src/simple-tree/api/schemaFactory.ts @@ -40,6 +40,7 @@ import { createFieldSchema, type DefaultProvider, getDefaultProvider, + type NodeSchemaOptions, } from "../schemaTypes.js"; import { inPrototypeChain } from "../core/index.js"; import type { @@ -106,7 +107,8 @@ export function schemaFromValue(value: TreeValue): TreeNodeSchema { * * @alpha */ -export interface SchemaFactoryObjectOptions { +export interface SchemaFactoryObjectOptions + extends NodeSchemaOptions { /** * Allow nodes typed with this object node schema to contain optional fields that are not present in the schema declaration. * Such nodes can come into existence either via import APIs (see remarks) or by way of collaboration with another client @@ -152,7 +154,9 @@ export interface SchemaFactoryObjectOptions { allowUnknownOptionalFields?: boolean; } -export const defaultSchemaFactoryObjectOptions: Required = { +export const defaultSchemaFactoryObjectOptions: Required< + Omit +> = { allowUnknownOptionalFields: false, }; @@ -403,6 +407,8 @@ export class SchemaFactory< /** * Define a structurally typed {@link TreeNodeSchema} for a {@link TreeMapNode}. * + * @param allowedTypes - The types that may appear as values in the map. + * * @remarks * The unique identifier for this Map is defined as a function of the provided types. * It is still scoped to this SchemaBuilder, but multiple calls with the same arguments will return the same schema object, providing somewhat structural typing. @@ -439,6 +445,7 @@ export class SchemaFactory< * Define a {@link TreeNodeSchema} for a {@link TreeMapNode}. * * @param name - Unique identifier for this schema within this factory's scope. + * @param allowedTypes - The types that may appear as values in the map. * * @example * ```typescript @@ -459,6 +466,8 @@ export class SchemaFactory< >; /** + * {@link SchemaFactory.map} implementation. + * * @privateRemarks * This should return `TreeNodeSchemaBoth`, however TypeScript gives an error if one of the overloads implicitly up-casts the return type of the implementation. * This seems like a TypeScript bug getting variance backwards for overload return types since it's erroring when the relation between the overload @@ -533,12 +542,15 @@ export class SchemaFactory< implicitlyConstructable, // The current policy is customizable nodes don't get fake prototypes. !customizable, + undefined, ); } /** * Define a structurally typed {@link TreeNodeSchema} for a {@link (TreeArrayNode:interface)}. * + * @param allowedTypes - The types that may appear in the array. + * * @remarks * The identifier for this Array is defined as a function of the provided types. * It is still scoped to this SchemaFactory, but multiple calls with the same arguments will return the same schema object, providing somewhat structural typing. @@ -585,6 +597,7 @@ export class SchemaFactory< * Define (and add to this library) a {@link TreeNodeSchemaClass} for a {@link (TreeArrayNode:interface)}. * * @param name - Unique identifier for this schema within this factory's scope. + * @param allowedTypes - The types that may appear in the array. * * @example * ```typescript @@ -607,6 +620,8 @@ export class SchemaFactory< >; /** + * {@link SchemaFactory.array} implementation. + * * @privateRemarks * This should return TreeNodeSchemaBoth: see note on "map" implementation for details. */ diff --git a/packages/dds/tree/src/simple-tree/api/schemaFactoryAlpha.ts b/packages/dds/tree/src/simple-tree/api/schemaFactoryAlpha.ts index a67e2c97f09a..0693e53b8e02 100644 --- a/packages/dds/tree/src/simple-tree/api/schemaFactoryAlpha.ts +++ b/packages/dds/tree/src/simple-tree/api/schemaFactoryAlpha.ts @@ -9,13 +9,28 @@ import type { TreeObjectNodeUnsafe, InsertableObjectFromSchemaRecordUnsafe, } from "../../internalTypes.js"; -import { SchemaFactory, type SchemaFactoryObjectOptions } from "./schemaFactory.js"; -import type { ImplicitFieldSchema } from "../schemaTypes.js"; -import { defaultSchemaFactoryObjectOptions } from "./schemaFactory.js"; +import { + defaultSchemaFactoryObjectOptions, + SchemaFactory, + type SchemaFactoryObjectOptions, +} from "./schemaFactory.js"; +import type { + ImplicitAllowedTypes, + ImplicitFieldSchema, + InsertableTreeNodeFromImplicitAllowedTypes, + NodeSchemaOptions, +} from "../schemaTypes.js"; import { type TreeObjectNode, objectSchema } from "../objectNode.js"; import type { RestrictiveStringRecord } from "../../util/index.js"; -import type { NodeKind, TreeNodeSchemaClass } from "../core/index.js"; -import type { Unenforced } from "./typesUnsafe.js"; +import type { NodeKind, TreeNodeSchemaClass, WithType } from "../core/index.js"; +import type { + InsertableTreeNodeFromImplicitAllowedTypesUnsafe, + TreeArrayNodeUnsafe, + TreeMapNodeUnsafe, + Unenforced, +} from "./typesUnsafe.js"; +import { mapSchema, type MapNodeInsertableData, type TreeMapNode } from "../mapNode.js"; +import { arraySchema, type TreeArrayNode } from "../arrayNode.js"; /** * {@link SchemaFactory} with additional alpha APIs. @@ -41,21 +56,25 @@ export class SchemaFactoryAlpha< * * @param name - Unique identifier for this schema within this factory's scope. * @param fields - Schema for fields of the object node's schema. Defines what children can be placed under each key. + * @param options - Additional options for the schema. */ public override object< const Name extends TName, const T extends RestrictiveStringRecord, + const TCustomMetadata = unknown, >( name: Name, fields: T, - options?: SchemaFactoryObjectOptions, + options?: SchemaFactoryObjectOptions, ): TreeNodeSchemaClass< ScopedSchemaName, NodeKind.Object, TreeObjectNode>, object & InsertableObjectFromSchemaRecord, true, - T + T, + never, + TCustomMetadata > { return objectSchema( this.scoped2(name), @@ -63,6 +82,7 @@ export class SchemaFactoryAlpha< true, options?.allowUnknownOptionalFields ?? defaultSchemaFactoryObjectOptions.allowUnknownOptionalFields, + options?.metadata, ); } @@ -72,17 +92,20 @@ export class SchemaFactoryAlpha< public override objectRecursive< const Name extends TName, const T extends Unenforced>, + const TCustomMetadata = unknown, >( name: Name, t: T, - options?: SchemaFactoryObjectOptions, + options?: SchemaFactoryObjectOptions, ): TreeNodeSchemaClass< ScopedSchemaName, NodeKind.Object, TreeObjectNodeUnsafe>, object & InsertableObjectFromSchemaRecordUnsafe, false, - T + T, + never, + TCustomMetadata > { type TScopedName = ScopedSchemaName; return this.object( @@ -95,7 +118,136 @@ export class SchemaFactoryAlpha< TreeObjectNodeUnsafe, object & InsertableObjectFromSchemaRecordUnsafe, false, - T + T, + never, + TCustomMetadata + >; + } + + /** + * Define a {@link TreeNodeSchema} for a {@link TreeMapNode}. + * + * @param name - Unique identifier for this schema within this factory's scope. + * @param allowedTypes - The types that may appear as values in the map. + * @param options - Additional options for the schema. + * + * @example + * ```typescript + * class NamedMap extends factory.map("name", factory.number, { + * metadata: { description: "A map of numbers" } + * }) {} + * ``` + */ + public mapAlpha< + Name extends TName, + const T extends ImplicitAllowedTypes, + const TCustomMetadata = unknown, + >( + name: Name, + allowedTypes: T, + options?: NodeSchemaOptions, + ): TreeNodeSchemaClass< + ScopedSchemaName, + NodeKind.Map, + TreeMapNode & WithType, NodeKind.Map>, + MapNodeInsertableData, + true, + T, + undefined, + TCustomMetadata + > { + return mapSchema(this.scoped2(name), allowedTypes, true, true, options?.metadata); + } + + /** + * {@inheritDoc SchemaFactory.objectRecursive} + */ + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + public override mapRecursive< + Name extends TName, + const T extends Unenforced, + const TCustomMetadata = unknown, + >(name: Name, allowedTypes: T, options?: NodeSchemaOptions) { + return this.mapAlpha( + name, + allowedTypes as T & ImplicitAllowedTypes, + options, + ) as unknown as TreeNodeSchemaClass< + ScopedSchemaName, + NodeKind.Map, + TreeMapNodeUnsafe & WithType, NodeKind.Map>, + | { + [Symbol.iterator](): Iterator< + [string, InsertableTreeNodeFromImplicitAllowedTypesUnsafe] + >; + } + | { + readonly [P in string]: InsertableTreeNodeFromImplicitAllowedTypesUnsafe; + }, + false, + T, + undefined, + TCustomMetadata + >; + } + + /** + * Define (and add to this library) a {@link TreeNodeSchemaClass} for a {@link (TreeArrayNode:interface)}. + * + * @param name - Unique identifier for this schema within this factory's scope. + * @param allowedTypes - The types that may appear in the array. + * @param options - Additional options for the schema. + * + * @example + * ```typescript + * class NamedArray extends factory.array("name", factory.number) {} + * ``` + */ + public arrayAlpha< + const Name extends TName, + const T extends ImplicitAllowedTypes, + const TCustomMetadata = unknown, + >( + name: Name, + allowedTypes: T, + options?: NodeSchemaOptions, + ): TreeNodeSchemaClass< + ScopedSchemaName, + NodeKind.Array, + TreeArrayNode & WithType, NodeKind.Array>, + Iterable>, + true, + T, + undefined, + TCustomMetadata + > { + return arraySchema(this.scoped2(name), allowedTypes, true, true, options?.metadata); + } + + /** + * {@inheritDoc SchemaFactory.objectRecursive} + */ + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + public override arrayRecursive< + const Name extends TName, + const T extends Unenforced, + const TCustomMetadata = unknown, + >(name: Name, allowedTypes: T, options?: NodeSchemaOptions) { + return this.arrayAlpha( + name, + allowedTypes as T & ImplicitAllowedTypes, + options, + ) as unknown as TreeNodeSchemaClass< + ScopedSchemaName, + NodeKind.Array, + TreeArrayNodeUnsafe & WithType, NodeKind.Array>, + { + [Symbol.iterator](): Iterator>; + }, + false, + T, + undefined, + TCustomMetadata >; } } diff --git a/packages/dds/tree/src/simple-tree/api/simpleSchema.ts b/packages/dds/tree/src/simple-tree/api/simpleSchema.ts index e23639e6add2..225a035e6abf 100644 --- a/packages/dds/tree/src/simple-tree/api/simpleSchema.ts +++ b/packages/dds/tree/src/simple-tree/api/simpleSchema.ts @@ -5,7 +5,7 @@ import type { ValueSchema } from "../../core/index.js"; import type { NodeKind } from "../core/index.js"; -import type { FieldKind, FieldSchemaMetadata } from "../schemaTypes.js"; +import type { FieldKind, FieldSchemaMetadata, NodeSchemaMetadata } from "../schemaTypes.js"; /** * Base interface for all {@link SimpleNodeSchema} implementations. @@ -20,6 +20,11 @@ export interface SimpleNodeSchemaBase { * @remarks can be used to type-switch between implementations. */ readonly kind: TNodeKind; + + /** + * {@inheritDoc NodeSchemaMetadata} + */ + readonly metadata?: NodeSchemaMetadata | undefined; } /** diff --git a/packages/dds/tree/src/simple-tree/api/simpleSchemaToJsonSchema.ts b/packages/dds/tree/src/simple-tree/api/simpleSchemaToJsonSchema.ts index 6e1334c13a1b..1787b87bd38a 100644 --- a/packages/dds/tree/src/simple-tree/api/simpleSchemaToJsonSchema.ts +++ b/packages/dds/tree/src/simple-tree/api/simpleSchemaToJsonSchema.ts @@ -100,11 +100,15 @@ function convertArrayNodeSchema(schema: SimpleArrayNodeSchema): JsonArrayNodeSch ? allowedTypes[0] : { anyOf: allowedTypes }; - return { + const output: Mutable = { type: "array", _treeNodeSchemaKind: NodeKind.Array, items, }; + + copyProperty(schema.metadata, "description", output); + + return output; } function convertLeafNodeSchema(schema: SimpleLeafNodeSchema): JsonLeafNodeSchema { @@ -137,9 +141,9 @@ function convertLeafNodeSchema(schema: SimpleLeafNodeSchema): JsonLeafNodeSchema function convertObjectNodeSchema(schema: SimpleObjectNodeSchema): JsonObjectNodeSchema { const properties: Record = {}; const required: string[] = []; - for (const [key, value] of Object.entries(schema.fields)) { + for (const [key, fieldSchema] of Object.entries(schema.fields)) { const allowedTypes: JsonSchemaRef[] = []; - for (const allowedType of value.allowedTypes) { + for (const allowedType of fieldSchema.allowedTypes) { allowedTypes.push(createSchemaRef(allowedType)); } @@ -149,20 +153,25 @@ function convertObjectNodeSchema(schema: SimpleObjectNodeSchema): JsonObjectNode anyOf: allowedTypes, }; - copyProperty(value.metadata, "description", output); + copyProperty(fieldSchema.metadata, "description", output); properties[key] = output; - if (value.kind === FieldKind.Required) { + if (fieldSchema.kind === FieldKind.Required) { required.push(key); } } - return { + + const transformedNode: Mutable = { type: "object", _treeNodeSchemaKind: NodeKind.Object, properties, required, additionalProperties: false, }; + + copyProperty(schema.metadata, "description", transformedNode); + + return transformedNode; } function convertMapNodeSchema(schema: SimpleMapNodeSchema): JsonMapNodeSchema { @@ -170,7 +179,8 @@ function convertMapNodeSchema(schema: SimpleMapNodeSchema): JsonMapNodeSchema { schema.allowedTypes.forEach((type) => { allowedTypes.push(createSchemaRef(type)); }); - return { + + const output: Mutable = { type: "object", _treeNodeSchemaKind: NodeKind.Map, patternProperties: { @@ -181,6 +191,10 @@ function convertMapNodeSchema(schema: SimpleMapNodeSchema): JsonMapNodeSchema { }, }, }; + + copyProperty(schema.metadata, "description", output); + + return output; } function createSchemaRef(schemaId: string): JsonSchemaRef { diff --git a/packages/dds/tree/src/simple-tree/api/viewSchemaToSimpleSchema.ts b/packages/dds/tree/src/simple-tree/api/viewSchemaToSimpleSchema.ts index b46bcc8a70b9..12e2ab8cd263 100644 --- a/packages/dds/tree/src/simple-tree/api/viewSchemaToSimpleSchema.ts +++ b/packages/dds/tree/src/simple-tree/api/viewSchemaToSimpleSchema.ts @@ -91,20 +91,28 @@ function leafSchemaToSimpleSchema(schema: TreeNodeSchema): SimpleLeafNodeSchema function arraySchemaToSimpleSchema(schema: TreeNodeSchema): SimpleArrayNodeSchema { const fieldSchema = normalizeFieldSchema(schema.info as ImplicitAllowedTypes); const allowedTypes = allowedTypesFromFieldSchema(fieldSchema); - return { + const output: Mutable = { kind: NodeKind.Array, allowedTypes, }; + + copyProperty(schema, "metadata", output); + + return output; } // TODO: Use a stronger type for map schemas once one is available (see object schema handler for an example). function mapSchemaToSimpleSchema(schema: TreeNodeSchema): SimpleMapNodeSchema { const fieldSchema = normalizeFieldSchema(schema.info as ImplicitAllowedTypes); const allowedTypes = allowedTypesFromFieldSchema(fieldSchema); - return { + const output: Mutable = { kind: NodeKind.Map, allowedTypes, }; + + copyProperty(schema, "metadata", output); + + return output; } function objectSchemaToSimpleSchema(schema: ObjectNodeSchema): SimpleObjectNodeSchema { @@ -112,10 +120,15 @@ function objectSchemaToSimpleSchema(schema: ObjectNodeSchema): SimpleObjectNodeS for (const [key, field] of schema.fields) { fields[key] = fieldSchemaToSimpleSchema(field); } - return { + + const output: Mutable = { kind: NodeKind.Object, fields, }; + + copyProperty(schema, "metadata", output); + + return output; } /** diff --git a/packages/dds/tree/src/simple-tree/arrayNode.ts b/packages/dds/tree/src/simple-tree/arrayNode.ts index 01b9e1108732..34d90aca5f22 100644 --- a/packages/dds/tree/src/simple-tree/arrayNode.ts +++ b/packages/dds/tree/src/simple-tree/arrayNode.ts @@ -18,6 +18,7 @@ import { normalizeAllowedTypes, type ImplicitAllowedTypes, type InsertableTreeNodeFromImplicitAllowedTypes, + type NodeSchemaMetadata, type TreeLeafValue, type TreeNodeFromImplicitAllowedTypes, } from "./schemaTypes.js"; @@ -1062,11 +1063,13 @@ export function arraySchema< TName extends string, const T extends ImplicitAllowedTypes, const ImplicitlyConstructable extends boolean, + const TCustomMetadata = unknown, >( identifier: TName, info: T, implicitlyConstructable: ImplicitlyConstructable, customizable: boolean, + metadata?: NodeSchemaMetadata, ) { type Output = TreeNodeSchemaBoth< TName, @@ -1075,7 +1078,8 @@ export function arraySchema< Iterable>, ImplicitlyConstructable, T, - undefined + undefined, + TCustomMetadata >; const lazyChildTypes = new Lazy(() => normalizeAllowedTypes(info)); @@ -1157,6 +1161,8 @@ export function arraySchema< public static get childTypes(): ReadonlySet { return lazyChildTypes.value; } + public static readonly metadata: NodeSchemaMetadata | undefined = + metadata; // eslint-disable-next-line import/no-deprecated public get [typeNameSymbol](): TName { diff --git a/packages/dds/tree/src/simple-tree/core/treeNodeSchema.ts b/packages/dds/tree/src/simple-tree/core/treeNodeSchema.ts index 6d84f146c63b..79ee247a3ab3 100644 --- a/packages/dds/tree/src/simple-tree/core/treeNodeSchema.ts +++ b/packages/dds/tree/src/simple-tree/core/treeNodeSchema.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import type { TreeLeafValue } from "../schemaTypes.js"; +import type { NodeSchemaMetadata, TreeLeafValue } from "../schemaTypes.js"; import type { InternalTreeNode, TreeNode, Unhydrated } from "./types.js"; /** @@ -27,11 +27,30 @@ export type TreeNodeSchema< TBuild = never, ImplicitlyConstructable extends boolean = boolean, Info = unknown, + TCustomMetadata = unknown, > = | (TNode extends TreeNode - ? TreeNodeSchemaClass + ? TreeNodeSchemaClass< + Name, + Kind, + TNode, + TBuild, + ImplicitlyConstructable, + Info, + never, + TCustomMetadata + > : never) - | TreeNodeSchemaNonClass; + | TreeNodeSchemaNonClass< + Name, + Kind, + TNode, + TBuild, + ImplicitlyConstructable, + Info, + never, + TCustomMetadata + >; /** * Schema which is not a class. @@ -49,7 +68,15 @@ export type TreeNodeSchemaNonClass< ImplicitlyConstructable extends boolean = boolean, Info = unknown, TConstructorExtra = never, -> = TreeNodeSchemaCore & + TCustomMetadata = unknown, +> = TreeNodeSchemaCore< + Name, + Kind, + ImplicitlyConstructable, + Info, + TInsertable, + TCustomMetadata +> & (undefined extends TConstructorExtra ? { /** @@ -119,7 +146,15 @@ export type TreeNodeSchemaClass< ImplicitlyConstructable extends boolean = boolean, Info = unknown, TConstructorExtra = never, -> = TreeNodeSchemaCore & + TCustomMetadata = unknown, +> = TreeNodeSchemaCore< + Name, + Kind, + ImplicitlyConstructable, + Info, + TInsertable, + TCustomMetadata +> & (undefined extends TConstructorExtra ? { /** @@ -157,6 +192,7 @@ export type TreeNodeSchemaBoth< ImplicitlyConstructable extends boolean = boolean, Info = unknown, TConstructorExtra = never, + TCustomMetadata = unknown, > = TreeNodeSchemaClass< Name, Kind, @@ -164,7 +200,8 @@ export type TreeNodeSchemaBoth< TInsertable, ImplicitlyConstructable, Info, - TConstructorExtra + TConstructorExtra, + TCustomMetadata > & TreeNodeSchemaNonClass< Name, @@ -173,7 +210,8 @@ export type TreeNodeSchemaBoth< TInsertable, ImplicitlyConstructable, Info, - TConstructorExtra + TConstructorExtra, + TCustomMetadata >; /** @@ -188,6 +226,7 @@ export interface TreeNodeSchemaCore< out ImplicitlyConstructable extends boolean, out Info = unknown, out TInsertable = never, + out TCustomMetadata = unknown, > { /** * Unique (within a document's schema) identifier used to associate nodes with their schema. @@ -244,6 +283,11 @@ export interface TreeNodeSchemaCore< */ readonly childTypes: ReadonlySet; + /** + * User-provided {@link NodeSchemaMetadata} for this schema. + */ + readonly metadata?: NodeSchemaMetadata | undefined; + /** * Constructs an instance of this node type. * @remarks diff --git a/packages/dds/tree/src/simple-tree/index.ts b/packages/dds/tree/src/simple-tree/index.ts index f52ebd6062b2..55e433b32a86 100644 --- a/packages/dds/tree/src/simple-tree/index.ts +++ b/packages/dds/tree/src/simple-tree/index.ts @@ -156,6 +156,8 @@ export { type Input, type ReadableField, type ReadSchema, + type NodeSchemaOptions, + type NodeSchemaMetadata, } from "./schemaTypes.js"; export { getTreeNodeForField, diff --git a/packages/dds/tree/src/simple-tree/mapNode.ts b/packages/dds/tree/src/simple-tree/mapNode.ts index f43a0c15a9a9..1ccfe6187bf1 100644 --- a/packages/dds/tree/src/simple-tree/mapNode.ts +++ b/packages/dds/tree/src/simple-tree/mapNode.ts @@ -17,6 +17,7 @@ import { normalizeAllowedTypes, type ImplicitAllowedTypes, type InsertableTreeNodeFromImplicitAllowedTypes, + type NodeSchemaMetadata, type TreeNodeFromImplicitAllowedTypes, } from "./schemaTypes.js"; import { @@ -235,11 +236,13 @@ export function mapSchema< TName extends string, const T extends ImplicitAllowedTypes, const ImplicitlyConstructable extends boolean, + const TCustomMetadata = unknown, >( identifier: TName, info: T, implicitlyConstructable: ImplicitlyConstructable, useMapPrototype: boolean, + metadata?: NodeSchemaMetadata, ) { const lazyChildTypes = new Lazy(() => normalizeAllowedTypes(info)); @@ -283,6 +286,8 @@ export function mapSchema< public static get childTypes(): ReadonlySet { return lazyChildTypes.value; } + public static readonly metadata: NodeSchemaMetadata | undefined = + metadata; // eslint-disable-next-line import/no-deprecated public get [typeNameSymbol](): TName { @@ -299,7 +304,8 @@ export function mapSchema< MapNodeInsertableData, ImplicitlyConstructable, T, - undefined + undefined, + TCustomMetadata > = Schema; return schemaErased; } diff --git a/packages/dds/tree/src/simple-tree/objectNode.ts b/packages/dds/tree/src/simple-tree/objectNode.ts index db62bb46eecb..0f25b85d3210 100644 --- a/packages/dds/tree/src/simple-tree/objectNode.ts +++ b/packages/dds/tree/src/simple-tree/objectNode.ts @@ -26,6 +26,7 @@ import { normalizeFieldSchema, type ImplicitAllowedTypes, FieldKind, + type NodeSchemaMetadata, } from "./schemaTypes.js"; import { type TreeNodeSchema, @@ -330,12 +331,15 @@ export function objectSchema< TName extends string, const T extends RestrictiveStringRecord, const ImplicitlyConstructable extends boolean, + const TCustomMetadata = unknown, >( identifier: TName, info: T, implicitlyConstructable: ImplicitlyConstructable, allowUnknownOptionalFields: boolean, -): ObjectNodeSchema & ObjectNodeSchemaInternalData { + metadata?: NodeSchemaMetadata, +): ObjectNodeSchema & + ObjectNodeSchemaInternalData { // Ensure no collisions between final set of property keys, and final set of stored keys (including those // implicitly derived from property keys) assertUniqueKeys(identifier, info); @@ -460,6 +464,8 @@ export function objectSchema< public static get childTypes(): ReadonlySet { return lazyChildTypes.value; } + public static readonly metadata: NodeSchemaMetadata | undefined = + metadata; // eslint-disable-next-line import/no-deprecated public get [typeNameSymbol](): TName { diff --git a/packages/dds/tree/src/simple-tree/objectNodeTypes.ts b/packages/dds/tree/src/simple-tree/objectNodeTypes.ts index acf1ab5bb510..cf166b19484e 100644 --- a/packages/dds/tree/src/simple-tree/objectNodeTypes.ts +++ b/packages/dds/tree/src/simple-tree/objectNodeTypes.ts @@ -23,13 +23,16 @@ export interface ObjectNodeSchema< T extends RestrictiveStringRecord = RestrictiveStringRecord, ImplicitlyConstructable extends boolean = boolean, + TCustomMetadata = unknown, > extends TreeNodeSchemaClass< TName, NodeKind.Object, TreeObjectNode, object & InsertableObjectFromSchemaRecord, ImplicitlyConstructable, - T + T, + never, + TCustomMetadata > { /** * From property keys to the associated schema. diff --git a/packages/dds/tree/src/simple-tree/schemaTypes.ts b/packages/dds/tree/src/simple-tree/schemaTypes.ts index fd64bab4ba35..5baea3ed98b3 100644 --- a/packages/dds/tree/src/simple-tree/schemaTypes.ts +++ b/packages/dds/tree/src/simple-tree/schemaTypes.ts @@ -190,7 +190,10 @@ export interface FieldProps { /** * Optional metadata to associate with the field. - * @remarks Note: this metadata is not persisted in the document. + * + * @remarks + * Note: this metadata is not persisted nor made part of the collaborative state; it is strictly client-local. + * Different clients in the same collaborative session may see different metadata for the same field. */ readonly metadata?: FieldSchemaMetadata; } @@ -824,3 +827,49 @@ export type NodeBuilderData { + /** + * Optional metadata to associate with the Node Schema. + * + * @remarks + * Note: this metadata is not persisted nor made part of the collaborative state; it is strictly client-local. + * Different clients in the same collaborative session may see different metadata for the same field. + */ + readonly metadata?: NodeSchemaMetadata | undefined; +} + +/** + * Metadata associated with a Node Schema. + * + * @remarks Specified via {@link NodeSchemaOptions.metadata}. + * + * @sealed + * @public + */ +export interface NodeSchemaMetadata { + /** + * User-defined metadata. + */ + readonly custom?: TCustomMetadata | undefined; + + /** + * The description of the Node Schema. + * + * @remarks + * + * If provided, will be used by the system in scenarios where a description of the kind of node is useful. + * E.g., when converting a Node Schema to {@link https://json-schema.org/ | JSON Schema}, this description will be + * used as the `description` property. + */ + readonly description?: string | undefined; +} diff --git a/packages/dds/tree/src/test/simple-tree/api/getJsonSchema.spec.ts b/packages/dds/tree/src/test/simple-tree/api/getJsonSchema.spec.ts index 6b13489ca571..c4ee936581c4 100644 --- a/packages/dds/tree/src/test/simple-tree/api/getJsonSchema.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/api/getJsonSchema.spec.ts @@ -8,6 +8,7 @@ import { getJsonSchema, NodeKind, SchemaFactory, + SchemaFactoryAlpha, type JsonTreeSchema, } from "../../../simple-tree/index.js"; @@ -125,16 +126,21 @@ describe("getJsonSchema", () => { }); it("Array schema", () => { - const schemaFactory = new SchemaFactory("test"); - const Schema = schemaFactory.array(schemaFactory.string); + const schemaFactory = new SchemaFactoryAlpha("test"); + const Schema = schemaFactory.arrayAlpha("array", schemaFactory.string, { + metadata: { + description: "An array of strings", + }, + }); const actual = getJsonSchema(Schema); const expected: JsonTreeSchema = { $defs: { - 'test.Array<["com.fluidframework.leaf.string"]>': { + "test.array": { type: "array", _treeNodeSchemaKind: NodeKind.Array, + description: "An array of strings", items: { $ref: "#/$defs/com.fluidframework.leaf.string", }, @@ -144,7 +150,7 @@ describe("getJsonSchema", () => { _treeNodeSchemaKind: NodeKind.Leaf, }, }, - $ref: '#/$defs/test.Array<["com.fluidframework.leaf.string"]>', + $ref: "#/$defs/test.array", }; assert.deepEqual(actual, expected); @@ -152,7 +158,8 @@ describe("getJsonSchema", () => { const validator = getJsonValidator(actual); // Verify expected data validation behavior. - validator(hydrate(Schema, ["Hello", "world"]), true); + // Array nodes do not satisfy AJV's array validation. This should be uncommented if/when we change this behavior. + // validator(hydrate(Schema, ["Hello", "world"]), true); validator([], true); validator(["Hello", "world"], true); validator("Hello world", false); @@ -162,15 +169,20 @@ describe("getJsonSchema", () => { }); it("Map schema", () => { - const schemaFactory = new SchemaFactory("test"); - const Schema = schemaFactory.map(schemaFactory.string); + const schemaFactory = new SchemaFactoryAlpha("test"); + const Schema = schemaFactory.mapAlpha("map", schemaFactory.string, { + metadata: { + description: "A map containing strings", + }, + }); const actual = getJsonSchema(Schema); const expected: JsonTreeSchema = { $defs: { - 'test.Map<["com.fluidframework.leaf.string"]>': { + "test.map": { type: "object", _treeNodeSchemaKind: NodeKind.Map, + description: "A map containing strings", patternProperties: { "^.*$": { $ref: "#/$defs/com.fluidframework.leaf.string" }, }, @@ -180,7 +192,7 @@ describe("getJsonSchema", () => { _treeNodeSchemaKind: NodeKind.Leaf, }, }, - $ref: '#/$defs/test.Map<["com.fluidframework.leaf.string"]>', + $ref: "#/$defs/test.map", }; assert.deepEqual(actual, expected); @@ -219,15 +231,19 @@ describe("getJsonSchema", () => { }); it("Object schema", () => { - const schemaFactory = new SchemaFactory("test"); - const Schema = schemaFactory.object("object", { - foo: schemaFactory.optional(schemaFactory.number, { - metadata: { description: "A number representing the concept of Foo." }, - }), - bar: schemaFactory.required(schemaFactory.string, { - metadata: { description: "A string representing the concept of Bar." }, - }), - }); + const schemaFactory = new SchemaFactoryAlpha("test"); + const Schema = schemaFactory.object( + "object", + { + foo: schemaFactory.optional(schemaFactory.number, { + metadata: { description: "A number representing the concept of Foo." }, + }), + bar: schemaFactory.required(schemaFactory.string, { + metadata: { description: "A string representing the concept of Bar." }, + }), + }, + { metadata: { description: "An object with Foo and Bar." } }, + ); const actual = getJsonSchema(Schema); @@ -236,6 +252,7 @@ describe("getJsonSchema", () => { "test.object": { type: "object", _treeNodeSchemaKind: NodeKind.Object, + description: "An object with Foo and Bar.", properties: { foo: { $ref: "#/$defs/com.fluidframework.leaf.number", diff --git a/packages/dds/tree/src/test/simple-tree/api/schemaFactory.spec.ts b/packages/dds/tree/src/test/simple-tree/api/schemaFactory.spec.ts index 2753e2415001..121a0dc50291 100644 --- a/packages/dds/tree/src/test/simple-tree/api/schemaFactory.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/api/schemaFactory.spec.ts @@ -5,7 +5,7 @@ import { strict as assert } from "node:assert"; -import { unreachableCase } from "@fluidframework/core-utils/internal"; +import { oob, unreachableCase } from "@fluidframework/core-utils/internal"; import { createIdCompressor } from "@fluidframework/id-compressor/internal"; import { MockFluidDataStoreRuntime, @@ -15,6 +15,7 @@ import { import { TreeStatus } from "../../../feature-libraries/index.js"; import { + SchemaFactoryAlpha, treeNodeApi as Tree, TreeViewConfiguration, type TreeArrayNode, @@ -363,7 +364,30 @@ describe("schemaFactory", () => { ); }); - it("Field Metadata", () => { + it("Node schema metadata", () => { + const factory = new SchemaFactoryAlpha(""); + + const fooMetadata = { + description: "An object called Foo", + custom: { + baz: true, + }, + }; + + class Foo extends factory.object( + "Foo", + { bar: factory.number }, + { metadata: fooMetadata }, + ) {} + + assert.deepEqual(Foo.metadata, fooMetadata); + + // Ensure `Foo.metadata` is typed as we expect, and we can access its fields without casting. + const description = Foo.metadata.description; + const baz = Foo.metadata.custom.baz; + }); + + it("Field schema metadata", () => { const schemaFactory = new SchemaFactory("com.example"); const barMetadata = { description: "Bar", @@ -460,7 +484,7 @@ describe("schemaFactory", () => { ); const stuff = view.root.stuff; assert(stuff instanceof NodeList); - const item = stuff[0]; + const item = stuff[0] ?? oob(); const s: string = item.text; assert.equal(s, "hi"); }); @@ -543,6 +567,25 @@ describe("schemaFactory", () => { class NamedList extends factory.array("name", factory.number) {} const namedInstance = new NamedList([5]); }); + + it("Node schema metadata", () => { + const factory = new SchemaFactoryAlpha(""); + + const fooMetadata = { + description: "An array of numbers", + custom: { + baz: true, + }, + }; + + class Foo extends factory.arrayAlpha("Foo", factory.number, { metadata: fooMetadata }) {} + + assert.deepEqual(Foo.metadata, fooMetadata); + + // Ensure `Foo.metadata` is typed as we expect, and we can access its fields without casting. + const description = Foo.metadata.description; + const baz = Foo.metadata.custom.baz; + }); }); describe("Map", () => { @@ -599,6 +642,25 @@ describe("schemaFactory", () => { class NamedMap extends factory.map("name", factory.number) {} const namedInstance = new NamedMap(new Map([["x", 5]])); }); + + it("Node schema metadata", () => { + const factory = new SchemaFactoryAlpha(""); + + const fooMetadata = { + description: "A map of numbers", + custom: { + baz: true, + }, + }; + + class Foo extends factory.mapAlpha("Foo", factory.number, { metadata: fooMetadata }) {} + + assert.deepEqual(Foo.metadata, fooMetadata); + + // Ensure `Foo.metadata` is typed as we expect, and we can access its fields without casting. + const description = Foo.metadata.description; + const baz = Foo.metadata.custom.baz; + }); }); describe("produces proxies that can be read after insertion for trees of", () => { diff --git a/packages/dds/tree/src/test/simple-tree/api/schemaFactoryRecursive.spec.ts b/packages/dds/tree/src/test/simple-tree/api/schemaFactoryRecursive.spec.ts index 2ff896ebc044..c960e1ac3e2f 100644 --- a/packages/dds/tree/src/test/simple-tree/api/schemaFactoryRecursive.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/api/schemaFactoryRecursive.spec.ts @@ -22,6 +22,7 @@ import { type FlexListToUnion, type ApplyKindInput, type NodeBuilderData, + SchemaFactoryAlpha, } from "../../../simple-tree/index.js"; import type { ValidateRecursiveSchema, @@ -456,6 +457,40 @@ describe("SchemaFactory Recursive methods", () => { assert.equal((tree.a as B).b!.a, 6); } }); + + it("Node schema metadata", () => { + const factory = new SchemaFactoryAlpha(""); + + class Foo extends factory.objectRecursive( + "Foo", + { bar: () => Bar }, + { + metadata: { + description: "A recursive object called Foo", + custom: { baz: true }, + }, + }, + ) {} + class Bar extends factory.objectRecursive( + "Bar", + { foo: () => Foo }, + { + metadata: { + description: "A recursive object called Bar", + custom: { baz: false }, + }, + }, + ) {} + + assert.deepEqual(Foo.metadata, { + description: "A recursive object called Foo", + custom: { baz: true }, + }); + assert.deepEqual(Bar.metadata, { + description: "A recursive object called Bar", + custom: { baz: false }, + }); + }); }); describe("ValidateRecursiveSchema", () => { it("Valid cases", () => { @@ -577,6 +612,25 @@ describe("SchemaFactory Recursive methods", () => { assert.deepEqual([...data], []); } }); + + it("Node schema metadata", () => { + const factory = new SchemaFactoryAlpha(""); + + class Foo extends factory.objectRecursive("Foo", { + fooList: sf.arrayRecursive("FooList", [() => Foo]), + }) {} + class FooList extends factory.arrayRecursive("FooList", [() => Foo], { + metadata: { + description: "A recursive list", + custom: { baz: true }, + }, + }) {} + + assert.deepEqual(FooList.metadata, { + description: "A recursive list", + custom: { baz: true }, + }); + }); }); describe("mapRecursive", () => { @@ -624,6 +678,25 @@ describe("SchemaFactory Recursive methods", () => { // @ts-expect-error Implicit construction disabled const fromNestedObject = new MapRecursive({ x: { x: [] } }); }); + + it("Node schema metadata", () => { + const factory = new SchemaFactoryAlpha(""); + + class Foo extends factory.objectRecursive("Foo", { + fooList: sf.arrayRecursive("FooList", [() => Foo]), + }) {} + class FooList extends factory.mapRecursive("FooList", [() => Foo], { + metadata: { + description: "A recursive map", + custom: { baz: true }, + }, + }) {} + + assert.deepEqual(FooList.metadata, { + description: "A recursive map", + custom: { baz: true }, + }); + }); }); it("recursive under non-recursive", () => { diff --git a/packages/dds/tree/src/util/utils.ts b/packages/dds/tree/src/util/utils.ts index ee2a857e585d..476f02b4b471 100644 --- a/packages/dds/tree/src/util/utils.ts +++ b/packages/dds/tree/src/util/utils.ts @@ -16,7 +16,7 @@ export interface MapGetSet { } /** - * Make all transitive properties in T readonly + * Make all transitive properties in `T` readonly */ export type RecursiveReadonly = { readonly [P in keyof T]: RecursiveReadonly; diff --git a/packages/framework/fluid-framework/api-report/fluid-framework.alpha.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.alpha.api.md index 72226c8df244..a7aca9a670ed 100644 --- a/packages/framework/fluid-framework/api-report/fluid-framework.alpha.api.md +++ b/packages/framework/fluid-framework/api-report/fluid-framework.alpha.api.md @@ -735,6 +735,7 @@ export type JsonNodeSchema = JsonLeafNodeSchema | JsonMapNodeSchema | JsonArrayN // @alpha @sealed export interface JsonNodeSchemaBase { + readonly description?: string | undefined; readonly _treeNodeSchemaKind: TNodeKind; readonly type: TJsonSchemaType; } @@ -832,6 +833,17 @@ export enum NodeKind { Object = 2 } +// @public @sealed +export interface NodeSchemaMetadata { + readonly custom?: TCustomMetadata | undefined; + readonly description?: string | undefined; +} + +// @public @sealed +export interface NodeSchemaOptions { + readonly metadata?: NodeSchemaMetadata | undefined; +} + // @alpha export const noopValidator: JsonValidator; @@ -969,8 +981,8 @@ export class SchemaFactory>(name: Name, allowedTypes: T): TreeNodeSchemaClass, NodeKind.Array, TreeArrayNodeUnsafe & WithType, NodeKind.Array, unknown>, { [Symbol.iterator](): Iterator>; }, false, T, undefined>; - readonly boolean: TreeNodeSchemaNonClass<"com.fluidframework.leaf.boolean", NodeKind.Leaf, boolean, boolean, true, unknown, never>; - readonly handle: TreeNodeSchemaNonClass<"com.fluidframework.leaf.handle", NodeKind.Leaf, IFluidHandle, IFluidHandle, true, unknown, never>; + readonly boolean: TreeNodeSchemaNonClass<"com.fluidframework.leaf.boolean", NodeKind.Leaf, boolean, boolean, true, unknown, never, unknown>; + readonly handle: TreeNodeSchemaNonClass<"com.fluidframework.leaf.handle", NodeKind.Leaf, IFluidHandle, IFluidHandle, true, unknown, never, unknown>; get identifier(): FieldSchema; map(allowedTypes: T): TreeNodeSchemaNonClass`>, NodeKind.Map, TreeMapNode & WithType`>, NodeKind.Map>, MapNodeInsertableData, true, T, undefined>; map(name: Name, allowedTypes: T): TreeNodeSchemaClass, NodeKind.Map, TreeMapNode & WithType, NodeKind.Map>, MapNodeInsertableData, true, T, undefined>; @@ -982,8 +994,8 @@ export class SchemaFactory; }, false, T, undefined>; - readonly null: TreeNodeSchemaNonClass<"com.fluidframework.leaf.null", NodeKind.Leaf, null, null, true, unknown, never>; - readonly number: TreeNodeSchemaNonClass<"com.fluidframework.leaf.number", NodeKind.Leaf, number, number, true, unknown, never>; + readonly null: TreeNodeSchemaNonClass<"com.fluidframework.leaf.null", NodeKind.Leaf, null, null, true, unknown, never, unknown>; + readonly number: TreeNodeSchemaNonClass<"com.fluidframework.leaf.number", NodeKind.Leaf, number, number, true, unknown, never, unknown>; object>(name: Name, fields: T): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNode>, object & InsertableObjectFromSchemaRecord, true, T>; objectRecursive>>(name: Name, t: T): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNodeUnsafe>, object & InsertableObjectFromSchemaRecordUnsafe, false, T>; optional(t: T, props?: Omit, "defaultProvider">): FieldSchema; @@ -991,17 +1003,30 @@ export class SchemaFactory(t: T, props?: Omit, "defaultProvider">): FieldSchema; requiredRecursive>(t: T, props?: Omit): FieldSchemaUnsafe; readonly scope: TScope; - readonly string: TreeNodeSchemaNonClass<"com.fluidframework.leaf.string", NodeKind.Leaf, string, string, true, unknown, never>; + readonly string: TreeNodeSchemaNonClass<"com.fluidframework.leaf.string", NodeKind.Leaf, string, string, true, unknown, never, unknown>; } // @alpha export class SchemaFactoryAlpha extends SchemaFactory { - object>(name: Name, fields: T, options?: SchemaFactoryObjectOptions): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNode>, object & InsertableObjectFromSchemaRecord, true, T>; - objectRecursive>>(name: Name, t: T, options?: SchemaFactoryObjectOptions): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNodeUnsafe>, object & InsertableObjectFromSchemaRecordUnsafe, false, T>; + arrayAlpha(name: Name, allowedTypes: T, options?: NodeSchemaOptions): TreeNodeSchemaClass, NodeKind.Array, TreeArrayNode & WithType, NodeKind.Array>, Iterable>, true, T, undefined, TCustomMetadata>; + arrayRecursive, const TCustomMetadata = unknown>(name: Name, allowedTypes: T, options?: NodeSchemaOptions): TreeNodeSchemaClass, NodeKind.Array, TreeArrayNodeUnsafe & WithType, NodeKind.Array, unknown>, { + [Symbol.iterator](): Iterator>; + }, false, T, undefined, TCustomMetadata>; + mapAlpha(name: Name, allowedTypes: T, options?: NodeSchemaOptions): TreeNodeSchemaClass, NodeKind.Map, TreeMapNode & WithType, NodeKind.Map>, MapNodeInsertableData, true, T, undefined, TCustomMetadata>; + mapRecursive, const TCustomMetadata = unknown>(name: Name, allowedTypes: T, options?: NodeSchemaOptions): TreeNodeSchemaClass, NodeKind.Map, TreeMapNodeUnsafe & WithType, NodeKind.Map, unknown>, { + [Symbol.iterator](): Iterator<[ + string, + InsertableTreeNodeFromImplicitAllowedTypesUnsafe + ]>; + } | { + readonly [x: string]: InsertableTreeNodeFromImplicitAllowedTypesUnsafe; + }, false, T, undefined, TCustomMetadata>; + object, const TCustomMetadata = unknown>(name: Name, fields: T, options?: SchemaFactoryObjectOptions): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNode>, object & InsertableObjectFromSchemaRecord, true, T, never, TCustomMetadata>; + objectRecursive>, const TCustomMetadata = unknown>(name: Name, t: T, options?: SchemaFactoryObjectOptions): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNodeUnsafe>, object & InsertableObjectFromSchemaRecordUnsafe, false, T, never, TCustomMetadata>; } // @alpha -export interface SchemaFactoryObjectOptions { +export interface SchemaFactoryObjectOptions extends NodeSchemaOptions { allowUnknownOptionalFields?: boolean; } @@ -1261,10 +1286,10 @@ export type TreeNodeFromImplicitAllowedTypes> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; // @public @sealed -export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; +export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; // @public @sealed -export type TreeNodeSchemaClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { +export type TreeNodeSchemaClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { new (data?: TInsertable | InternalTreeNode | TConstructorExtra): Unhydrated; } : { new (data: TInsertable | InternalTreeNode | TConstructorExtra): Unhydrated; @@ -1277,7 +1302,7 @@ export interface TreeNodeSchemaClassUnsafe { +export interface TreeNodeSchemaCore { readonly childTypes: ReadonlySet; // @sealed createFromInsertable(data: TInsertable): Unhydrated; @@ -1286,10 +1311,11 @@ export interface TreeNodeSchemaCore | undefined; } // @public @sealed -export type TreeNodeSchemaNonClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { +export type TreeNodeSchemaNonClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { create(data?: TInsertable | TConstructorExtra): TNode; } : { create(data: TInsertable | TConstructorExtra): TNode; diff --git a/packages/framework/fluid-framework/api-report/fluid-framework.beta.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.beta.api.md index 52682e6bcb32..60462048e315 100644 --- a/packages/framework/fluid-framework/api-report/fluid-framework.beta.api.md +++ b/packages/framework/fluid-framework/api-report/fluid-framework.beta.api.md @@ -619,6 +619,17 @@ export enum NodeKind { Object = 2 } +// @public @sealed +export interface NodeSchemaMetadata { + readonly custom?: TCustomMetadata | undefined; + readonly description?: string | undefined; +} + +// @public @sealed +export interface NodeSchemaOptions { + readonly metadata?: NodeSchemaMetadata | undefined; +} + // @public type ObjectFromSchemaRecord> = { -readonly [Property in keyof T]: Property extends string ? TreeFieldFromImplicitField : unknown; @@ -723,8 +734,8 @@ export class SchemaFactory>(name: Name, allowedTypes: T): TreeNodeSchemaClass, NodeKind.Array, TreeArrayNodeUnsafe & WithType, NodeKind.Array, unknown>, { [Symbol.iterator](): Iterator>; }, false, T, undefined>; - readonly boolean: TreeNodeSchemaNonClass<"com.fluidframework.leaf.boolean", NodeKind.Leaf, boolean, boolean, true, unknown, never>; - readonly handle: TreeNodeSchemaNonClass<"com.fluidframework.leaf.handle", NodeKind.Leaf, IFluidHandle, IFluidHandle, true, unknown, never>; + readonly boolean: TreeNodeSchemaNonClass<"com.fluidframework.leaf.boolean", NodeKind.Leaf, boolean, boolean, true, unknown, never, unknown>; + readonly handle: TreeNodeSchemaNonClass<"com.fluidframework.leaf.handle", NodeKind.Leaf, IFluidHandle, IFluidHandle, true, unknown, never, unknown>; get identifier(): FieldSchema; map(allowedTypes: T): TreeNodeSchemaNonClass`>, NodeKind.Map, TreeMapNode & WithType`>, NodeKind.Map>, MapNodeInsertableData, true, T, undefined>; map(name: Name, allowedTypes: T): TreeNodeSchemaClass, NodeKind.Map, TreeMapNode & WithType, NodeKind.Map>, MapNodeInsertableData, true, T, undefined>; @@ -736,8 +747,8 @@ export class SchemaFactory; }, false, T, undefined>; - readonly null: TreeNodeSchemaNonClass<"com.fluidframework.leaf.null", NodeKind.Leaf, null, null, true, unknown, never>; - readonly number: TreeNodeSchemaNonClass<"com.fluidframework.leaf.number", NodeKind.Leaf, number, number, true, unknown, never>; + readonly null: TreeNodeSchemaNonClass<"com.fluidframework.leaf.null", NodeKind.Leaf, null, null, true, unknown, never, unknown>; + readonly number: TreeNodeSchemaNonClass<"com.fluidframework.leaf.number", NodeKind.Leaf, number, number, true, unknown, never, unknown>; object>(name: Name, fields: T): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNode>, object & InsertableObjectFromSchemaRecord, true, T>; objectRecursive>>(name: Name, t: T): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNodeUnsafe>, object & InsertableObjectFromSchemaRecordUnsafe, false, T>; optional(t: T, props?: Omit, "defaultProvider">): FieldSchema; @@ -745,7 +756,7 @@ export class SchemaFactory(t: T, props?: Omit, "defaultProvider">): FieldSchema; requiredRecursive>(t: T, props?: Omit): FieldSchemaUnsafe; readonly scope: TScope; - readonly string: TreeNodeSchemaNonClass<"com.fluidframework.leaf.string", NodeKind.Leaf, string, string, true, unknown, never>; + readonly string: TreeNodeSchemaNonClass<"com.fluidframework.leaf.string", NodeKind.Leaf, string, string, true, unknown, never, unknown>; } // @public @@ -886,10 +897,10 @@ export type TreeNodeFromImplicitAllowedTypes> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; // @public @sealed -export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; +export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; // @public @sealed -export type TreeNodeSchemaClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { +export type TreeNodeSchemaClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { new (data?: TInsertable | InternalTreeNode | TConstructorExtra): Unhydrated; } : { new (data: TInsertable | InternalTreeNode | TConstructorExtra): Unhydrated; @@ -902,7 +913,7 @@ export interface TreeNodeSchemaClassUnsafe { +export interface TreeNodeSchemaCore { readonly childTypes: ReadonlySet; // @sealed createFromInsertable(data: TInsertable): Unhydrated; @@ -911,10 +922,11 @@ export interface TreeNodeSchemaCore | undefined; } // @public @sealed -export type TreeNodeSchemaNonClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { +export type TreeNodeSchemaNonClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { create(data?: TInsertable | TConstructorExtra): TNode; } : { create(data: TInsertable | TConstructorExtra): TNode; diff --git a/packages/framework/fluid-framework/api-report/fluid-framework.legacy.alpha.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.legacy.alpha.api.md index adf830c7f6e1..6f4225b3418a 100644 --- a/packages/framework/fluid-framework/api-report/fluid-framework.legacy.alpha.api.md +++ b/packages/framework/fluid-framework/api-report/fluid-framework.legacy.alpha.api.md @@ -917,6 +917,17 @@ export enum NodeKind { Object = 2 } +// @public @sealed +export interface NodeSchemaMetadata { + readonly custom?: TCustomMetadata | undefined; + readonly description?: string | undefined; +} + +// @public @sealed +export interface NodeSchemaOptions { + readonly metadata?: NodeSchemaMetadata | undefined; +} + // @public type ObjectFromSchemaRecord> = { -readonly [Property in keyof T]: Property extends string ? TreeFieldFromImplicitField : unknown; @@ -1021,8 +1032,8 @@ export class SchemaFactory>(name: Name, allowedTypes: T): TreeNodeSchemaClass, NodeKind.Array, TreeArrayNodeUnsafe & WithType, NodeKind.Array, unknown>, { [Symbol.iterator](): Iterator>; }, false, T, undefined>; - readonly boolean: TreeNodeSchemaNonClass<"com.fluidframework.leaf.boolean", NodeKind.Leaf, boolean, boolean, true, unknown, never>; - readonly handle: TreeNodeSchemaNonClass<"com.fluidframework.leaf.handle", NodeKind.Leaf, IFluidHandle, IFluidHandle, true, unknown, never>; + readonly boolean: TreeNodeSchemaNonClass<"com.fluidframework.leaf.boolean", NodeKind.Leaf, boolean, boolean, true, unknown, never, unknown>; + readonly handle: TreeNodeSchemaNonClass<"com.fluidframework.leaf.handle", NodeKind.Leaf, IFluidHandle, IFluidHandle, true, unknown, never, unknown>; get identifier(): FieldSchema; map(allowedTypes: T): TreeNodeSchemaNonClass`>, NodeKind.Map, TreeMapNode & WithType`>, NodeKind.Map>, MapNodeInsertableData, true, T, undefined>; map(name: Name, allowedTypes: T): TreeNodeSchemaClass, NodeKind.Map, TreeMapNode & WithType, NodeKind.Map>, MapNodeInsertableData, true, T, undefined>; @@ -1034,8 +1045,8 @@ export class SchemaFactory; }, false, T, undefined>; - readonly null: TreeNodeSchemaNonClass<"com.fluidframework.leaf.null", NodeKind.Leaf, null, null, true, unknown, never>; - readonly number: TreeNodeSchemaNonClass<"com.fluidframework.leaf.number", NodeKind.Leaf, number, number, true, unknown, never>; + readonly null: TreeNodeSchemaNonClass<"com.fluidframework.leaf.null", NodeKind.Leaf, null, null, true, unknown, never, unknown>; + readonly number: TreeNodeSchemaNonClass<"com.fluidframework.leaf.number", NodeKind.Leaf, number, number, true, unknown, never, unknown>; object>(name: Name, fields: T): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNode>, object & InsertableObjectFromSchemaRecord, true, T>; objectRecursive>>(name: Name, t: T): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNodeUnsafe>, object & InsertableObjectFromSchemaRecordUnsafe, false, T>; optional(t: T, props?: Omit, "defaultProvider">): FieldSchema; @@ -1043,7 +1054,7 @@ export class SchemaFactory(t: T, props?: Omit, "defaultProvider">): FieldSchema; requiredRecursive>(t: T, props?: Omit): FieldSchemaUnsafe; readonly scope: TScope; - readonly string: TreeNodeSchemaNonClass<"com.fluidframework.leaf.string", NodeKind.Leaf, string, string, true, unknown, never>; + readonly string: TreeNodeSchemaNonClass<"com.fluidframework.leaf.string", NodeKind.Leaf, string, string, true, unknown, never, unknown>; } // @public @@ -1251,10 +1262,10 @@ export type TreeNodeFromImplicitAllowedTypes> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; // @public @sealed -export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; +export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; // @public @sealed -export type TreeNodeSchemaClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { +export type TreeNodeSchemaClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { new (data?: TInsertable | InternalTreeNode | TConstructorExtra): Unhydrated; } : { new (data: TInsertable | InternalTreeNode | TConstructorExtra): Unhydrated; @@ -1267,7 +1278,7 @@ export interface TreeNodeSchemaClassUnsafe { +export interface TreeNodeSchemaCore { readonly childTypes: ReadonlySet; // @sealed createFromInsertable(data: TInsertable): Unhydrated; @@ -1276,10 +1287,11 @@ export interface TreeNodeSchemaCore | undefined; } // @public @sealed -export type TreeNodeSchemaNonClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { +export type TreeNodeSchemaNonClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { create(data?: TInsertable | TConstructorExtra): TNode; } : { create(data: TInsertable | TConstructorExtra): TNode; diff --git a/packages/framework/fluid-framework/api-report/fluid-framework.legacy.public.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.legacy.public.api.md index 6ec5110111e2..e4cc49c1ca71 100644 --- a/packages/framework/fluid-framework/api-report/fluid-framework.legacy.public.api.md +++ b/packages/framework/fluid-framework/api-report/fluid-framework.legacy.public.api.md @@ -650,6 +650,17 @@ export enum NodeKind { Object = 2 } +// @public @sealed +export interface NodeSchemaMetadata { + readonly custom?: TCustomMetadata | undefined; + readonly description?: string | undefined; +} + +// @public @sealed +export interface NodeSchemaOptions { + readonly metadata?: NodeSchemaMetadata | undefined; +} + // @public type ObjectFromSchemaRecord> = { -readonly [Property in keyof T]: Property extends string ? TreeFieldFromImplicitField : unknown; @@ -754,8 +765,8 @@ export class SchemaFactory>(name: Name, allowedTypes: T): TreeNodeSchemaClass, NodeKind.Array, TreeArrayNodeUnsafe & WithType, NodeKind.Array, unknown>, { [Symbol.iterator](): Iterator>; }, false, T, undefined>; - readonly boolean: TreeNodeSchemaNonClass<"com.fluidframework.leaf.boolean", NodeKind.Leaf, boolean, boolean, true, unknown, never>; - readonly handle: TreeNodeSchemaNonClass<"com.fluidframework.leaf.handle", NodeKind.Leaf, IFluidHandle, IFluidHandle, true, unknown, never>; + readonly boolean: TreeNodeSchemaNonClass<"com.fluidframework.leaf.boolean", NodeKind.Leaf, boolean, boolean, true, unknown, never, unknown>; + readonly handle: TreeNodeSchemaNonClass<"com.fluidframework.leaf.handle", NodeKind.Leaf, IFluidHandle, IFluidHandle, true, unknown, never, unknown>; get identifier(): FieldSchema; map(allowedTypes: T): TreeNodeSchemaNonClass`>, NodeKind.Map, TreeMapNode & WithType`>, NodeKind.Map>, MapNodeInsertableData, true, T, undefined>; map(name: Name, allowedTypes: T): TreeNodeSchemaClass, NodeKind.Map, TreeMapNode & WithType, NodeKind.Map>, MapNodeInsertableData, true, T, undefined>; @@ -767,8 +778,8 @@ export class SchemaFactory; }, false, T, undefined>; - readonly null: TreeNodeSchemaNonClass<"com.fluidframework.leaf.null", NodeKind.Leaf, null, null, true, unknown, never>; - readonly number: TreeNodeSchemaNonClass<"com.fluidframework.leaf.number", NodeKind.Leaf, number, number, true, unknown, never>; + readonly null: TreeNodeSchemaNonClass<"com.fluidframework.leaf.null", NodeKind.Leaf, null, null, true, unknown, never, unknown>; + readonly number: TreeNodeSchemaNonClass<"com.fluidframework.leaf.number", NodeKind.Leaf, number, number, true, unknown, never, unknown>; object>(name: Name, fields: T): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNode>, object & InsertableObjectFromSchemaRecord, true, T>; objectRecursive>>(name: Name, t: T): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNodeUnsafe>, object & InsertableObjectFromSchemaRecordUnsafe, false, T>; optional(t: T, props?: Omit, "defaultProvider">): FieldSchema; @@ -776,7 +787,7 @@ export class SchemaFactory(t: T, props?: Omit, "defaultProvider">): FieldSchema; requiredRecursive>(t: T, props?: Omit): FieldSchemaUnsafe; readonly scope: TScope; - readonly string: TreeNodeSchemaNonClass<"com.fluidframework.leaf.string", NodeKind.Leaf, string, string, true, unknown, never>; + readonly string: TreeNodeSchemaNonClass<"com.fluidframework.leaf.string", NodeKind.Leaf, string, string, true, unknown, never, unknown>; } // @public @@ -910,10 +921,10 @@ export type TreeNodeFromImplicitAllowedTypes> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; // @public @sealed -export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; +export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; // @public @sealed -export type TreeNodeSchemaClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { +export type TreeNodeSchemaClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { new (data?: TInsertable | InternalTreeNode | TConstructorExtra): Unhydrated; } : { new (data: TInsertable | InternalTreeNode | TConstructorExtra): Unhydrated; @@ -926,7 +937,7 @@ export interface TreeNodeSchemaClassUnsafe { +export interface TreeNodeSchemaCore { readonly childTypes: ReadonlySet; // @sealed createFromInsertable(data: TInsertable): Unhydrated; @@ -935,10 +946,11 @@ export interface TreeNodeSchemaCore | undefined; } // @public @sealed -export type TreeNodeSchemaNonClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { +export type TreeNodeSchemaNonClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { create(data?: TInsertable | TConstructorExtra): TNode; } : { create(data: TInsertable | TConstructorExtra): TNode; diff --git a/packages/framework/fluid-framework/api-report/fluid-framework.public.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.public.api.md index 832e115cf979..26762b5d926d 100644 --- a/packages/framework/fluid-framework/api-report/fluid-framework.public.api.md +++ b/packages/framework/fluid-framework/api-report/fluid-framework.public.api.md @@ -614,6 +614,17 @@ export enum NodeKind { Object = 2 } +// @public @sealed +export interface NodeSchemaMetadata { + readonly custom?: TCustomMetadata | undefined; + readonly description?: string | undefined; +} + +// @public @sealed +export interface NodeSchemaOptions { + readonly metadata?: NodeSchemaMetadata | undefined; +} + // @public type ObjectFromSchemaRecord> = { -readonly [Property in keyof T]: Property extends string ? TreeFieldFromImplicitField : unknown; @@ -718,8 +729,8 @@ export class SchemaFactory>(name: Name, allowedTypes: T): TreeNodeSchemaClass, NodeKind.Array, TreeArrayNodeUnsafe & WithType, NodeKind.Array, unknown>, { [Symbol.iterator](): Iterator>; }, false, T, undefined>; - readonly boolean: TreeNodeSchemaNonClass<"com.fluidframework.leaf.boolean", NodeKind.Leaf, boolean, boolean, true, unknown, never>; - readonly handle: TreeNodeSchemaNonClass<"com.fluidframework.leaf.handle", NodeKind.Leaf, IFluidHandle, IFluidHandle, true, unknown, never>; + readonly boolean: TreeNodeSchemaNonClass<"com.fluidframework.leaf.boolean", NodeKind.Leaf, boolean, boolean, true, unknown, never, unknown>; + readonly handle: TreeNodeSchemaNonClass<"com.fluidframework.leaf.handle", NodeKind.Leaf, IFluidHandle, IFluidHandle, true, unknown, never, unknown>; get identifier(): FieldSchema; map(allowedTypes: T): TreeNodeSchemaNonClass`>, NodeKind.Map, TreeMapNode & WithType`>, NodeKind.Map>, MapNodeInsertableData, true, T, undefined>; map(name: Name, allowedTypes: T): TreeNodeSchemaClass, NodeKind.Map, TreeMapNode & WithType, NodeKind.Map>, MapNodeInsertableData, true, T, undefined>; @@ -731,8 +742,8 @@ export class SchemaFactory; }, false, T, undefined>; - readonly null: TreeNodeSchemaNonClass<"com.fluidframework.leaf.null", NodeKind.Leaf, null, null, true, unknown, never>; - readonly number: TreeNodeSchemaNonClass<"com.fluidframework.leaf.number", NodeKind.Leaf, number, number, true, unknown, never>; + readonly null: TreeNodeSchemaNonClass<"com.fluidframework.leaf.null", NodeKind.Leaf, null, null, true, unknown, never, unknown>; + readonly number: TreeNodeSchemaNonClass<"com.fluidframework.leaf.number", NodeKind.Leaf, number, number, true, unknown, never, unknown>; object>(name: Name, fields: T): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNode>, object & InsertableObjectFromSchemaRecord, true, T>; objectRecursive>>(name: Name, t: T): TreeNodeSchemaClass, NodeKind.Object, TreeObjectNodeUnsafe>, object & InsertableObjectFromSchemaRecordUnsafe, false, T>; optional(t: T, props?: Omit, "defaultProvider">): FieldSchema; @@ -740,7 +751,7 @@ export class SchemaFactory(t: T, props?: Omit, "defaultProvider">): FieldSchema; requiredRecursive>(t: T, props?: Omit): FieldSchemaUnsafe; readonly scope: TScope; - readonly string: TreeNodeSchemaNonClass<"com.fluidframework.leaf.string", NodeKind.Leaf, string, string, true, unknown, never>; + readonly string: TreeNodeSchemaNonClass<"com.fluidframework.leaf.string", NodeKind.Leaf, string, string, true, unknown, never, unknown>; } // @public @@ -870,10 +881,10 @@ export type TreeNodeFromImplicitAllowedTypes> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe> : unknown; // @public @sealed -export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; +export type TreeNodeSchema = (TNode extends TreeNode ? TreeNodeSchemaClass : never) | TreeNodeSchemaNonClass; // @public @sealed -export type TreeNodeSchemaClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { +export type TreeNodeSchemaClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { new (data?: TInsertable | InternalTreeNode | TConstructorExtra): Unhydrated; } : { new (data: TInsertable | InternalTreeNode | TConstructorExtra): Unhydrated; @@ -886,7 +897,7 @@ export interface TreeNodeSchemaClassUnsafe { +export interface TreeNodeSchemaCore { readonly childTypes: ReadonlySet; // @sealed createFromInsertable(data: TInsertable): Unhydrated; @@ -895,10 +906,11 @@ export interface TreeNodeSchemaCore | undefined; } // @public @sealed -export type TreeNodeSchemaNonClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { +export type TreeNodeSchemaNonClass = TreeNodeSchemaCore & (undefined extends TConstructorExtra ? { create(data?: TInsertable | TConstructorExtra): TNode; } : { create(data: TInsertable | TConstructorExtra): TNode;