diff --git a/.changeset/brown-dingos-switch.md b/.changeset/brown-dingos-switch.md new file mode 100644 index 000000000000..a63784d878e0 --- /dev/null +++ b/.changeset/brown-dingos-switch.md @@ -0,0 +1,8 @@ +--- +"@fluidframework/tree": minor +"fluid-framework": minor +"__section": tree +--- +Add APIs for declaring "persisted" schema metadata + +Add alpha APIs for declaring node and field schema metadata which future versions of the Fluid Framework will provide a way to opt into persisting in the document. diff --git a/examples/apps/ai-collab/src/app/page.tsx b/examples/apps/ai-collab/src/app/page.tsx index 7fe2e53e98fb..163968143cda 100644 --- a/examples/apps/ai-collab/src/app/page.tsx +++ b/examples/apps/ai-collab/src/app/page.tsx @@ -34,8 +34,8 @@ import { useFluidContainerNextJs } from "@/useFluidContainerNextjs"; import { useSharedTreeRerender } from "@/useSharedTreeRerender"; // Uncomment the import line that corresponds to the server you want to use -// import { createContainer, loadContainer, postAttach, containerIdFromUrl } from "./spe"; // eslint-disable-line import/order -import { createContainer, loadContainer, postAttach, containerIdFromUrl } from "./tinylicious"; // eslint-disable-line import/order +import { createContainer, loadContainer, postAttach, containerIdFromUrl } from "./spe"; // eslint-disable-line import/order +// import { createContainer, loadContainer, postAttach, containerIdFromUrl } from "./tinylicious"; // eslint-disable-line import/order export async function createAndInitializeContainer(): Promise< IFluidContainer diff --git a/examples/apps/ai-collab/src/types/sharedTreeAppSchema.ts b/examples/apps/ai-collab/src/types/sharedTreeAppSchema.ts index 3b03f162dd6d..eec8ec01dbea 100644 --- a/examples/apps/ai-collab/src/types/sharedTreeAppSchema.ts +++ b/examples/apps/ai-collab/src/types/sharedTreeAppSchema.ts @@ -16,33 +16,33 @@ const sf = new SchemaFactoryAlpha("ai-collab-sample-application"); export class SharedTreeTask extends sf.objectAlpha( "Task", { - title: sf.required(sf.string, { + title: sf.requiredAlpha(sf.string, { metadata: { description: `The title of the task.`, }, }), id: sf.identifier, - description: sf.required(sf.string, { + description: sf.requiredAlpha(sf.string, { metadata: { description: `The description of the task.`, }, }), - priority: sf.required(sf.string, { + priority: sf.requiredAlpha(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, { + complexity: sf.requiredAlpha(sf.number, { metadata: { description: `The complexity of the task as a fibonacci number.`, }, }), - status: sf.required(sf.string, { + status: sf.requiredAlpha(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, { + assignee: sf.requiredAlpha(sf.string, { metadata: { description: `The name of the tasks assignee e.g. "Bob" or "Alice".`, }, @@ -52,6 +52,9 @@ export class SharedTreeTask extends sf.objectAlpha( metadata: { description: `A task that can be assigned to an engineer.`, }, + persistedMetadata: { + "eDiscovery-exclude": "comment", + }, }, ) {} @@ -60,21 +63,28 @@ export class SharedTreeTaskList extends sf.array("TaskList", SharedTreeTask) {} export class SharedTreeEngineer extends sf.objectAlpha( "Engineer", { - name: sf.required(sf.string, { + name: sf.requiredAlpha(sf.string, { metadata: { description: `The name of the engineer.`, }, }), id: sf.identifier, - skills: sf.required(sf.string, { + skills: sf.requiredAlpha(sf.string, { metadata: { description: `A description of the engineer's skills, which influence what types of tasks they should be assigned to.`, }, + persistedMetadata: { + "eDiscovery-exclude": "comment", + }, }), - maxCapacity: sf.required(sf.number, { + maxCapacity: sf.requiredAlpha(sf.number, { metadata: { description: `The maximum capacity of tasks this engineer can handle, measured in task complexity points.`, }, + persistedMetadata: { + "eDiscovery-exclude": "exclude", + "search-exclude": "true", + }, }), }, { @@ -89,23 +99,23 @@ export class SharedTreeEngineerList extends sf.array("EngineerList", SharedTreeE export class SharedTreeTaskGroup extends sf.objectAlpha( "TaskGroup", { - description: sf.required(sf.string, { + description: sf.requiredAlpha(sf.string, { metadata: { description: `The description of the task group.`, }, }), id: sf.identifier, - title: sf.required(sf.string, { + title: sf.requiredAlpha(sf.string, { metadata: { description: `The title of the task group.`, }, }), - tasks: sf.required(SharedTreeTaskList, { + tasks: sf.requiredAlpha(SharedTreeTaskList, { metadata: { description: `The lists of tasks within this task group.`, }, }), - engineers: sf.required(SharedTreeEngineerList, { + engineers: sf.requiredAlpha(SharedTreeEngineerList, { metadata: { description: `The lists of engineers within this task group to whom tasks may be assigned.`, }, @@ -121,7 +131,7 @@ export class SharedTreeTaskGroup extends sf.objectAlpha( export class SharedTreeTaskGroupList extends sf.array("TaskGroupList", SharedTreeTaskGroup) {} export class SharedTreeAppState extends sf.object("AppState", { - taskGroups: sf.required(SharedTreeTaskGroupList, { + taskGroups: sf.requiredAlpha(SharedTreeTaskGroupList, { metadata: { description: `The list of task groups that are being managed by this task management application.`, }, diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index 4150645cbc46..580d98af51ad 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -181,6 +181,11 @@ export interface FieldProps { readonly metadata?: FieldSchemaMetadata; } +// @alpha +export interface FieldPropsAlpha extends FieldProps { + readonly persistedMetadata?: JsonCompatibleReadOnlyObject | undefined; +} + // @public @sealed export class FieldSchema { protected constructor( @@ -198,13 +203,14 @@ export class FieldSchema extends FieldSchema implements SimpleFieldSchema { - protected constructor(kind: Kind, types: Types, annotatedAllowedTypes: ImplicitAnnotatedAllowedTypes, props?: FieldProps); + protected constructor(kind: Kind, types: Types, annotatedAllowedTypes: ImplicitAnnotatedAllowedTypes, props?: FieldPropsAlpha); // (undocumented) get allowedTypesIdentifiers(): ReadonlySet; readonly allowedTypesMetadata: AllowedTypesMetadata; // (undocumented) readonly annotatedAllowedTypes: ImplicitAnnotatedAllowedTypes; get annotatedAllowedTypeSet(): ReadonlyMap; + get persistedMetadata(): JsonCompatibleReadOnlyObject | undefined; } // @alpha @sealed @system @@ -443,6 +449,14 @@ export type JsonCompatibleObject = { [P in string]?: JsonCompatible; }; +// @alpha +export type JsonCompatibleReadOnly = string | number | boolean | null | readonly JsonCompatibleReadOnly[] | JsonCompatibleReadOnlyObject; + +// @alpha +export type JsonCompatibleReadOnlyObject = { + readonly [P in string]?: JsonCompatibleReadOnly; +}; + // @alpha @sealed export type JsonFieldSchema = { readonly description?: string | undefined; @@ -591,6 +605,11 @@ export interface NodeSchemaOptions { readonly metadata?: NodeSchemaMetadata | undefined; } +// @alpha @sealed +export interface NodeSchemaOptionsAlpha extends NodeSchemaOptions { + readonly persistedMetadata?: JsonCompatibleReadOnlyObject | undefined; +} + // @alpha export const noopValidator: JsonValidator; @@ -756,30 +775,33 @@ export class SchemaFactory extends SchemaFactory { - arrayAlpha(name: Name, allowedTypes: T, options?: NodeSchemaOptions): ArrayNodeCustomizableSchema, T, true, TCustomMetadata>; + arrayAlpha(name: Name, allowedTypes: T, options?: NodeSchemaOptionsAlpha): ArrayNodeCustomizableSchema, T, true, TCustomMetadata>; arrayRecursive(name: Name, allowedTypes: T, options?: NodeSchemaOptions): ArrayNodeCustomizableSchemaUnsafe, T, TCustomMetadata>; - static readonly identifier: (props?: Omit, "defaultProvider"> | undefined) => FieldSchemaAlpha_2 & SimpleLeafNodeSchema_2, TCustomMetadata>; + static readonly identifier: (props?: Omit, "defaultProvider"> | undefined) => FieldSchemaAlpha & SimpleLeafNodeSchema_2, TCustomMetadata>; static readonly leaves: readonly [LeafSchema_2<"string", string> & SimpleLeafNodeSchema_2, LeafSchema_2<"number", number> & SimpleLeafNodeSchema_2, LeafSchema_2<"boolean", boolean> & SimpleLeafNodeSchema_2, LeafSchema_2<"null", null> & SimpleLeafNodeSchema_2, LeafSchema_2<"handle", IFluidHandle> & SimpleLeafNodeSchema_2]; - mapAlpha(name: Name, allowedTypes: T, options?: NodeSchemaOptions): MapNodeCustomizableSchema, T, true, TCustomMetadata>; + mapAlpha(name: Name, allowedTypes: T, options?: NodeSchemaOptionsAlpha): MapNodeCustomizableSchema, T, true, TCustomMetadata>; mapRecursive(name: Name, allowedTypes: T, options?: NodeSchemaOptions): MapNodeCustomizableSchemaUnsafe, T, TCustomMetadata>; objectAlpha, const TCustomMetadata = unknown>(name: Name, fields: T, options?: SchemaFactoryObjectOptions): ObjectNodeSchema, T, true, TCustomMetadata> & { readonly createFromInsertable: unknown; }; objectRecursive, const TCustomMetadata = unknown>(name: Name, t: T, options?: SchemaFactoryObjectOptions): TreeNodeSchemaClass, NodeKind.Object, System_Unsafe.TreeObjectNodeUnsafe>, object & System_Unsafe.InsertableObjectFromSchemaRecordUnsafe, false, T, never, TCustomMetadata> & SimpleObjectNodeSchema & Pick; static readonly optional: { - (t: T, props?: Omit, "defaultProvider"> | undefined): FieldSchemaAlpha_2; - (t: T_1, props?: Omit, "defaultProvider"> | undefined): FieldSchemaAlpha_2, TCustomMetadata_1>; + (t: T, props?: Omit, "defaultProvider"> | undefined): FieldSchemaAlpha; + (t: T_1, props?: Omit, "defaultProvider"> | undefined): FieldSchemaAlpha, TCustomMetadata_1>; }; - static readonly optionalRecursive: (t: T, props?: Omit, "defaultProvider"> | undefined) => FieldSchemaAlphaUnsafe_2; + optionalAlpha(t: T, props?: Omit, "defaultProvider">): FieldSchemaAlpha, TCustomMetadata>; + static readonly optionalRecursive: (t: T, props?: Omit, "defaultProvider"> | undefined) => FieldSchemaAlphaUnsafe; + optionalRecursiveAlpha(t: T, props?: Omit, "defaultProvider">): FieldSchemaAlphaUnsafe; static readonly required: { - (t: T, props?: Omit, "defaultProvider"> | undefined): FieldSchemaAlpha_2; - (t: T_1, props?: Omit, "defaultProvider"> | undefined): FieldSchemaAlpha_2, TCustomMetadata_1>; + (t: T, props?: Omit, "defaultProvider"> | undefined): FieldSchemaAlpha; + (t: T_1, props?: Omit, "defaultProvider"> | undefined): FieldSchemaAlpha, TCustomMetadata_1>; }; + requiredAlpha(t: T, props?: Omit, "defaultProvider">): FieldSchemaAlpha, TCustomMetadata>; scopedFactory(name: T): SchemaFactoryAlpha, TNameInner>; } // @alpha -export interface SchemaFactoryObjectOptions extends NodeSchemaOptions { +export interface SchemaFactoryObjectOptions extends NodeSchemaOptionsAlpha { allowUnknownOptionalFields?: boolean; } @@ -822,6 +844,7 @@ export const SharedTreeFormatVersion: { readonly v1: 1; readonly v2: 2; readonly v3: 3; + readonly v4: 4; }; // @alpha @@ -831,7 +854,7 @@ export type SharedTreeFormatVersion = typeof SharedTreeFormatVersion; export type SharedTreeOptions = Partial & Partial & ForestOptions; // @alpha @sealed -export interface SimpleArrayNodeSchema extends SimpleNodeSchemaBase { +export interface SimpleArrayNodeSchema extends SimpleNodeSchemaBaseAlpha { readonly allowedTypesIdentifiers: ReadonlySet; } @@ -843,12 +866,12 @@ export interface SimpleFieldSchema { } // @alpha @sealed -export interface SimpleLeafNodeSchema extends SimpleNodeSchemaBase { +export interface SimpleLeafNodeSchema extends SimpleNodeSchemaBaseAlpha { readonly leafKind: ValueSchema; } // @alpha @sealed -export interface SimpleMapNodeSchema extends SimpleNodeSchemaBase { +export interface SimpleMapNodeSchema extends SimpleNodeSchemaBaseAlpha { readonly allowedTypesIdentifiers: ReadonlySet; } @@ -861,13 +884,18 @@ export interface SimpleNodeSchemaBase; } +// @alpha @sealed @system +export interface SimpleNodeSchemaBaseAlpha extends SimpleNodeSchemaBase { + readonly persistedMetadata: JsonCompatibleReadOnlyObject | undefined; +} + // @alpha @sealed export interface SimpleObjectFieldSchema extends SimpleFieldSchema { readonly storedKey: string; } // @alpha @sealed -export interface SimpleObjectNodeSchema extends SimpleNodeSchemaBase { +export interface SimpleObjectNodeSchema extends SimpleNodeSchemaBaseAlpha { readonly fields: ReadonlyMap; } @@ -900,7 +928,7 @@ export namespace System_TableSchema { props: InsertableTreeFieldFromImplicitField>; }), true, { readonly props: TPropsSchema; - readonly id: FieldSchema_2, unknown>; + readonly id: FieldSchema_2, unknown>; }>; // @system export type CreateRowOptionsBase = OptionsWithSchemaFactory & OptionsWithCellSchema; @@ -914,8 +942,8 @@ export namespace System_TableSchema { props: InsertableTreeFieldFromImplicitField>; }), true, { readonly props: TPropsSchema; - readonly id: FieldSchema_2, unknown>; - readonly cells: FieldSchema_2, "Row.cells">, NodeKind.Map, TreeMapNode_2 & WithType, "Row.cells">, NodeKind.Map, unknown>, MapNodeInsertableData_2, true, TCellSchema, undefined>, unknown>; + readonly id: FieldSchema_2, unknown>; + readonly cells: FieldSchema_2, "Row.cells">, NodeKind.Map, TreeMapNode_2 & WithType, "Row.cells">, NodeKind.Map, unknown>, MapNodeInsertableData_2, true, TCellSchema, undefined>, unknown>; }>; // @system export function createTableSchema, const TRowSchema extends RowSchemaBase>(inputSchemaFactory: SchemaFactoryAlpha, _cellSchema: TCellSchema, columnSchema: TColumnSchema, rowSchema: TRowSchema): TreeNodeSchemaCore_2, "Table">, NodeKind.Object, true, { diff --git a/packages/dds/tree/src/core/index.ts b/packages/dds/tree/src/core/index.ts index 2135129ec435..015901277e3a 100644 --- a/packages/dds/tree/src/core/index.ts +++ b/packages/dds/tree/src/core/index.ts @@ -138,11 +138,13 @@ export { storedEmptyFieldSchema, type StoredSchemaCollection, schemaFormatV1, + schemaFormatV2, LeafNodeStoredSchema, ObjectNodeStoredSchema, MapNodeStoredSchema, decodeFieldSchema, - encodeFieldSchema, + encodeFieldSchemaV1, + encodeFieldSchemaV2, storedSchemaDecodeDispatcher, type SchemaAndPolicy, Multiplicity, diff --git a/packages/dds/tree/src/core/schema-stored/formatV2.ts b/packages/dds/tree/src/core/schema-stored/formatV2.ts new file mode 100644 index 000000000000..fbbf5ec635d2 --- /dev/null +++ b/packages/dds/tree/src/core/schema-stored/formatV2.ts @@ -0,0 +1,78 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { type ObjectOptions, type Static, Type } from "@sinclair/typebox"; + +import { JsonCompatibleReadOnlySchema } from "../../util/index.js"; +import { + FieldKindIdentifierSchema, + PersistedValueSchema, + TreeNodeSchemaIdentifierSchema, +} from "./formatV1.js"; +import { unionOptions } from "../../codec/index.js"; + +export const PersistedMetadataFormat = Type.Optional(JsonCompatibleReadOnlySchema); + +const FieldSchemaFormatBase = Type.Object({ + kind: FieldKindIdentifierSchema, + types: Type.Array(TreeNodeSchemaIdentifierSchema), + metadata: PersistedMetadataFormat, +}); + +const noAdditionalProps: ObjectOptions = { additionalProperties: false }; + +export const FieldSchemaFormat = Type.Composite([FieldSchemaFormatBase], noAdditionalProps); + +/** + * Format for the content of a {@link TreeNodeStoredSchema}. + * + * See {@link DiscriminatedUnionDispatcher} for more information on this pattern. + */ +export const TreeNodeSchemaUnionFormat = Type.Object( + { + /** + * Object node union member. + */ + object: Type.Optional(Type.Record(Type.String(), FieldSchemaFormat)), + /** + * Map node union member. + */ + map: Type.Optional(FieldSchemaFormat), + /** + * Leaf node union member. + */ + leaf: Type.Optional(Type.Enum(PersistedValueSchema)), + }, + unionOptions, +); + +export type TreeNodeSchemaUnionFormat = Static; + +/** + * Format for {@link TreeNodeStoredSchema}. + * + * See {@link DiscriminatedUnionDispatcher} for more information on this pattern. + */ +export const TreeNodeSchemaDataFormat = Type.Object( + { + /** + * Node kind specific data. + */ + kind: TreeNodeSchemaUnionFormat, + + // Data in common for all TreeNode schemas: + /** + * Leaf node union member. + */ + metadata: PersistedMetadataFormat, + }, + noAdditionalProps, +); + +export type TreeNodeSchemaDataFormat = Static; + +export type FieldSchemaFormat = Static; + +export type PersistedMetadataFormat = Static; diff --git a/packages/dds/tree/src/core/schema-stored/index.ts b/packages/dds/tree/src/core/schema-stored/index.ts index a972028f76d9..fa2c5575ad44 100644 --- a/packages/dds/tree/src/core/schema-stored/index.ts +++ b/packages/dds/tree/src/core/schema-stored/index.ts @@ -18,7 +18,8 @@ export { ObjectNodeStoredSchema, MapNodeStoredSchema, decodeFieldSchema, - encodeFieldSchema, + encodeFieldSchemaV1, + encodeFieldSchemaV2, storedSchemaDecodeDispatcher, type SchemaAndPolicy, type SchemaPolicy, @@ -37,3 +38,5 @@ export type { TreeNodeSchemaIdentifier, FieldKey, FieldKindIdentifier } from "./ import * as schemaFormatV1 from "./formatV1.js"; export { schemaFormatV1 }; +import * as schemaFormatV2 from "./formatV2.js"; +export { schemaFormatV2 }; diff --git a/packages/dds/tree/src/core/schema-stored/schema.ts b/packages/dds/tree/src/core/schema-stored/schema.ts index fff6cb1e66e4..c2d1c76a2263 100644 --- a/packages/dds/tree/src/core/schema-stored/schema.ts +++ b/packages/dds/tree/src/core/schema-stored/schema.ts @@ -11,11 +11,17 @@ import { type MakeNominal, brand, invertMap } from "../../util/index.js"; import { type FieldKey, type FieldKindIdentifier, - type FieldSchemaFormat, + type FieldSchemaFormat as FieldSchemaFormatV1, PersistedValueSchema, - type TreeNodeSchemaDataFormat, + type TreeNodeSchemaDataFormat as TreeNodeSchemaDataFormatV1, type TreeNodeSchemaIdentifier, } from "./formatV1.js"; +import type { + FieldSchemaFormat as FieldSchemaFormatV2, + PersistedMetadataFormat, + TreeNodeSchemaUnionFormat, + TreeNodeSchemaDataFormat as TreeNodeSchemaDataFormatV2, +} from "./formatV2.js"; import type { Multiplicity } from "./multiplicity.js"; /** @@ -23,8 +29,14 @@ import type { Multiplicity } from "./multiplicity.js"; */ export enum SchemaVersion { v1 = 1, + /** + * Adds persisted metadata to the node schema and field schema. + */ + v2 = 2, } +type FieldSchemaFormat = FieldSchemaFormatV1 | FieldSchemaFormatV2; + /** * Schema for what {@link TreeLeafValue} is allowed on a Leaf node. * @privateRemarks @@ -128,6 +140,13 @@ export interface TreeFieldStoredSchema { * If not specified, types are unconstrained. */ readonly types: TreeTypeSet; + + /** + * Portion of the metadata which can be persisted. + * @remarks + * Discarded when encoding to {@link SchemaFormatVersion.V1}. + */ + readonly metadata: PersistedMetadataFormat | undefined; } /** @@ -151,6 +170,7 @@ export const storedEmptyFieldSchema: TreeFieldStoredSchema = { kind: brand(forbiddenFieldKindIdentifier), // This type set also forces the field to be empty not not allowing any types as all. types: new Set(), + metadata: undefined, }; /** @@ -164,12 +184,21 @@ export abstract class TreeNodeStoredSchema { protected _typeCheck!: MakeNominal; /** - * @privateRemarks - * Returns TreeNodeSchemaDataFormat. - * This is uses an opaque type to avoid leaking these types out of the package, - * and is runtime validated by the codec. + * Constructor for a TreeNodeStoredSchema. + * @param metadata - Persisted metadata for this node schema. + */ + public constructor(public readonly metadata: PersistedMetadataFormat | undefined) {} + + /** + * Encode in the v1 schema format. */ - public abstract encode(): TreeNodeSchemaDataFormat; + public abstract encodeV1(): TreeNodeSchemaDataFormatV1; + + /** + * Encode in the v2 schema format. + * @remarks Post-condition: if metadata was specified on the input schema, it will be present in the output. + */ + public abstract encodeV2(): TreeNodeSchemaDataFormatV2; /** * Returns the schema for the provided field. @@ -190,29 +219,68 @@ export class ObjectNodeStoredSchema extends TreeNodeStoredSchema { */ public constructor( public readonly objectNodeFields: ReadonlyMap, + metadata?: PersistedMetadataFormat | undefined, ) { - super(); + console.log( + "Constructing ObjectNodeStoredSchema; metadata:", + metadata !== undefined ? JSON.stringify(metadata) : "[undefined]", "; objectNodeFields:", + JSON.stringify([...objectNodeFields.entries()]), + ); + super(metadata); } - public override encode(): TreeNodeSchemaDataFormat { + public override encodeV1(): TreeNodeSchemaDataFormatV1 { + console.log("In encodeV1"); const fieldsObject: Record = Object.create(null); // Sort fields to ensure output is identical for for equivalent schema (since field order is not considered significant). // This makes comparing schema easier, and ensures chunk reuse for schema summaries isn't needlessly broken. for (const key of [...this.objectNodeFields.keys()].sort()) { + const value = encodeFieldSchemaV1( + this.objectNodeFields.get(key) ?? fail(0xae7 /* missing field */), + ); + Object.defineProperty(fieldsObject, key, { enumerable: true, configurable: true, writable: true, - value: encodeFieldSchema( - this.objectNodeFields.get(key) ?? fail(0xae7 /* missing field */), - ), + value, }); } + console.log("Returning from encodeV1", JSON.stringify(fieldsObject)); return { object: fieldsObject, }; } + public override encodeV2(): TreeNodeSchemaDataFormatV2 { + console.log("In encodeV2"); + const fieldsObject: Record = Object.create(null); + // Sort fields to ensure output is identical for for equivalent schema (since field order is not considered significant). + // This makes comparing schema easier, and ensures chunk reuse for schema summaries isn't needlessly broken. + for (const key of [...this.objectNodeFields.keys()].sort()) { + const value = encodeFieldSchemaV2( + this.objectNodeFields.get(key) ?? fail(0xae7 /* missing field */), + ); + + Object.defineProperty(fieldsObject, key, { + enumerable: true, + configurable: true, + writable: true, + value, + }); + } + + const kind = { object: fieldsObject }; + + console.log("Returning from encodeV2", JSON.stringify(fieldsObject)); + console.log( + "Returning from encodeV2, metadata:", + this.metadata !== undefined ? JSON.stringify(this.metadata) : "[undefined]", + ); + // Omit metadata from the output if it is undefined + return this.metadata !== undefined ? { kind, metadata: this.metadata } : { kind }; + } + public override getFieldSchema(field: FieldKey): TreeFieldStoredSchema { return this.objectNodeFields.get(field) ?? storedEmptyFieldSchema; } @@ -229,14 +297,20 @@ export class MapNodeStoredSchema extends TreeNodeStoredSchema { * since no nodes can ever be in schema if you use `FieldKind.Value` here * (that would require infinite children). */ - public constructor(public readonly mapFields: TreeFieldStoredSchema) { - super(); + public constructor( + public readonly mapFields: TreeFieldStoredSchema, + metadata?: PersistedMetadataFormat | undefined, + ) { + super(metadata); } - public override encode(): TreeNodeSchemaDataFormat { - return { - map: encodeFieldSchema(this.mapFields), - }; + public override encodeV1(): TreeNodeSchemaDataFormatV1 { + return { map: encodeFieldSchemaV1(this.mapFields) }; + } + + public override encodeV2(): TreeNodeSchemaDataFormatV2 { + const kind = { map: encodeFieldSchemaV2(this.mapFields) }; + return this.metadata === undefined ? { kind, metadata: this.metadata } : { kind }; } public override getFieldSchema(field: FieldKey): TreeFieldStoredSchema { @@ -260,36 +334,58 @@ export class LeafNodeStoredSchema extends TreeNodeStoredSchema { * This is simply one approach that can work for modeling them in the internal schema representation. */ public constructor(public readonly leafValue: ValueSchema) { - super(); + // No metadata for leaf nodes. + super(undefined); } - public override encode(): TreeNodeSchemaDataFormat { + public override encodeV1(): TreeNodeSchemaDataFormatV1 { return { leaf: encodeValueSchema(this.leafValue), }; } + public override encodeV2(): TreeNodeSchemaDataFormatV2 { + return { + // No metadata for leaf nodes, so don't emit a metadata field. + kind: { + leaf: encodeValueSchema(this.leafValue), + }, + }; + } + public override getFieldSchema(field: FieldKey): TreeFieldStoredSchema { return storedEmptyFieldSchema; } } +/** + * Decoder wrapper function for {@link TreeNodeStoredSchema} implementations. + * Curries the constructor so that the caller can inject metadata. + */ +type StoredSchemaDecoder = ( + metadata: PersistedMetadataFormat | undefined, +) => TreeNodeStoredSchema; + export const storedSchemaDecodeDispatcher: DiscriminatedUnionDispatcher< - TreeNodeSchemaDataFormat, + TreeNodeSchemaUnionFormat, [], - TreeNodeStoredSchema + StoredSchemaDecoder > = new DiscriminatedUnionDispatcher({ - leaf: (data: PersistedValueSchema) => new LeafNodeStoredSchema(decodeValueSchema(data)), - object: ( - data: Record, - ): TreeNodeStoredSchema => { + leaf: (data: PersistedValueSchema) => (metadata) => + new LeafNodeStoredSchema(decodeValueSchema(data)), + object: (data: Record) => (metadata) => { + console.log( + "Processing object node inside storedSchemaDecodeDispatcher; field schema data:", + JSON.stringify(data), + ); const map = new Map(); for (const [key, value] of Object.entries(data)) { map.set(key, decodeFieldSchema(value)); } - return new ObjectNodeStoredSchema(map); + return new ObjectNodeStoredSchema(map, metadata); }, - map: (data: FieldSchemaFormat) => new MapNodeStoredSchema(decodeFieldSchema(data)), + map: (data: FieldSchemaFormat) => (metadata) => + new MapNodeStoredSchema(decodeFieldSchema(data), metadata), }); const valueSchemaEncode = new Map([ @@ -310,7 +406,7 @@ function decodeValueSchema(inMemory: PersistedValueSchema): ValueSchema { return valueSchemaDecode.get(inMemory) ?? fail(0xae9 /* missing ValueSchema */); } -export function encodeFieldSchema(schema: TreeFieldStoredSchema): FieldSchemaFormat { +export function encodeFieldSchemaV1(schema: TreeFieldStoredSchema): FieldSchemaFormatV1 { return { kind: schema.kind, // Types are sorted by identifier to improve stability of persisted data to increase chance of schema blob reuse. @@ -318,11 +414,22 @@ export function encodeFieldSchema(schema: TreeFieldStoredSchema): FieldSchemaFor }; } -export function decodeFieldSchema(schema: FieldSchemaFormat): TreeFieldStoredSchema { +export function encodeFieldSchemaV2(schema: TreeFieldStoredSchema): FieldSchemaFormatV2 { + const fieldSchema: FieldSchemaFormatV1 = encodeFieldSchemaV1(schema); + + console.log("Inside encodeFieldSchemaV2; schema:", JSON.stringify(schema)); + // Omit metadata from the output if it is undefined + return schema.metadata !== undefined + ? { ...fieldSchema, metadata: schema.metadata } + : { ...fieldSchema }; +} + +export function decodeFieldSchema(schema: FieldSchemaFormatV2): TreeFieldStoredSchema { const out: TreeFieldStoredSchema = { // TODO: maybe provide actual FieldKind objects here, error on unrecognized kinds. kind: schema.kind, types: new Set(schema.types), + metadata: schema.metadata, }; return out; } diff --git a/packages/dds/tree/src/feature-libraries/modular-schema/fieldKindWithEditor.ts b/packages/dds/tree/src/feature-libraries/modular-schema/fieldKindWithEditor.ts index 6a18db241170..ace34e2bd552 100644 --- a/packages/dds/tree/src/feature-libraries/modular-schema/fieldKindWithEditor.ts +++ b/packages/dds/tree/src/feature-libraries/modular-schema/fieldKindWithEditor.ts @@ -79,6 +79,8 @@ export class FieldKindWithEditor< isNeverField(policy, originalData, { kind: this.identifier, types: originalTypes, + // Metadata is not used for this check. + metadata: undefined, }) ) { return true; diff --git a/packages/dds/tree/src/feature-libraries/schema-edits/schemaChangeCodecs.ts b/packages/dds/tree/src/feature-libraries/schema-edits/schemaChangeCodecs.ts index a4afe65d722d..34ce95493c34 100644 --- a/packages/dds/tree/src/feature-libraries/schema-edits/schemaChangeCodecs.ts +++ b/packages/dds/tree/src/feature-libraries/schema-edits/schemaChangeCodecs.ts @@ -13,7 +13,7 @@ import { makeVersionDispatchingCodec, withSchemaValidation, } from "../../codec/index.js"; -import { makeSchemaCodec, type Format as FormatV1 } from "../schema-index/index.js"; +import { makeSchemaCodec } from "../schema-index/index.js"; import { EncodedSchemaChange } from "./schemaChangeFormat.js"; import type { SchemaChange } from "./schemaChangeTypes.js"; @@ -25,7 +25,10 @@ import { SchemaVersion } from "../../core/index.js"; * @returns The composed codec family. */ export function makeSchemaChangeCodecs(options: ICodecOptions): ICodecFamily { - return makeCodecFamily([[SchemaVersion.v1, makeSchemaChangeCodecV1(options)]]); + return makeCodecFamily([ + [SchemaVersion.v1, makeSchemaChangeCodecV1(options, SchemaVersion.v1)], + [SchemaVersion.v2, makeSchemaChangeCodecV1(options, SchemaVersion.v2)], + ]); } /** @@ -43,14 +46,16 @@ export function makeSchemaChangeCodec( } /** - * Compose the v1 schema change codec. + * Compose the change codec using mostly v1 logic. * @param options - The codec options. + * @param schemaWriteVersion - The schema write version. * @returns The composed schema change codec. */ function makeSchemaChangeCodecV1( options: ICodecOptions, + schemaWriteVersion: SchemaVersion, ): IJsonCodec { - const schemaCodec = makeSchemaCodec(options, SchemaVersion.v1); + const schemaCodec = makeSchemaCodec(options, schemaWriteVersion); const schemaChangeCodec: IJsonCodec = { encode: (schemaChange) => { assert( @@ -58,8 +63,8 @@ function makeSchemaChangeCodecV1( 0x933 /* Inverse schema changes should never be transmitted */, ); return { - new: schemaCodec.encode(schemaChange.schema.new) as FormatV1, - old: schemaCodec.encode(schemaChange.schema.old) as FormatV1, + new: schemaCodec.encode(schemaChange.schema.new), + old: schemaCodec.encode(schemaChange.schema.old), }; }, decode: (encoded) => { diff --git a/packages/dds/tree/src/feature-libraries/schema-edits/schemaChangeFormat.ts b/packages/dds/tree/src/feature-libraries/schema-edits/schemaChangeFormat.ts index 046aadcfe221..2b6e6c299c41 100644 --- a/packages/dds/tree/src/feature-libraries/schema-edits/schemaChangeFormat.ts +++ b/packages/dds/tree/src/feature-libraries/schema-edits/schemaChangeFormat.ts @@ -4,11 +4,11 @@ */ import { type Static, Type } from "@sinclair/typebox"; -import { Format as FormatV1 } from "../schema-index/index.js"; +import { JsonCompatibleReadOnlySchema } from "../../util/index.js"; export const EncodedSchemaChange = Type.Object({ - new: FormatV1, - old: FormatV1, + new: JsonCompatibleReadOnlySchema, + old: JsonCompatibleReadOnlySchema, }); export type EncodedSchemaChange = Static; diff --git a/packages/dds/tree/src/feature-libraries/schema-index/codec.ts b/packages/dds/tree/src/feature-libraries/schema-index/codec.ts index 1c231ad1f513..0c4eb5dbabd7 100644 --- a/packages/dds/tree/src/feature-libraries/schema-index/codec.ts +++ b/packages/dds/tree/src/feature-libraries/schema-index/codec.ts @@ -19,13 +19,16 @@ import { type TreeNodeStoredSchema, type TreeStoredSchema, decodeFieldSchema, - encodeFieldSchema, + encodeFieldSchemaV1, + encodeFieldSchemaV2, type schemaFormatV1, + type schemaFormatV2, storedSchemaDecodeDispatcher, } from "../../core/index.js"; import { brand, type JsonCompatible } from "../../util/index.js"; import { Format as FormatV1 } from "./formatV1.js"; +import { Format as FormatV2 } from "./formatV2.js"; /** * Convert a FluidClientVersion to a SchemaVersion. @@ -61,7 +64,10 @@ export function makeSchemaCodec( * @returns The composed codec family. */ export function makeSchemaCodecs(options: ICodecOptions): ICodecFamily { - return makeCodecFamily([[SchemaVersion.v1, makeSchemaCodecV1(options)]]); + return makeCodecFamily([ + [SchemaVersion.v1, makeSchemaCodecV1(options)], + [SchemaVersion.v2, makeSchemaCodecV2(options)], + ]); } /** @@ -73,7 +79,9 @@ export function makeSchemaCodecs(options: ICodecOptions): ICodecFamily = Object.create(null); - const rootFieldSchema = encodeFieldSchema(repo.rootFieldSchema); + const rootFieldSchema = encodeFieldSchemaV1(repo.rootFieldSchema); for (const name of [...repo.nodeSchema.keys()].sort()) { const schema = repo.nodeSchema.get(name) ?? fail(0xb28 /* missing schema */); Object.defineProperty(nodeSchema, name, { enumerable: true, configurable: true, writable: true, - value: schema.encode(), + value: schema.encodeV1(), }); } return { @@ -99,10 +107,47 @@ function encodeRepoV1(repo: TreeStoredSchema): FormatV1 { }; } -function decode(f: FormatV1): TreeStoredSchema { +function encodeRepoV2(repo: TreeStoredSchema): FormatV2 { + const nodeSchema: Record = + Object.create(null); + const rootFieldSchema = encodeFieldSchemaV2(repo.rootFieldSchema); + for (const name of [...repo.nodeSchema.keys()].sort()) { + const schema = repo.nodeSchema.get(name) ?? fail(0xb28 /* missing schema */); + Object.defineProperty(nodeSchema, name, { + enumerable: true, + configurable: true, + writable: true, + value: schema.encodeV2(), + }); + } + return { + version: SchemaVersion.v2, + nodes: nodeSchema, + root: rootFieldSchema, + }; +} + +function decodeV1(f: FormatV1): TreeStoredSchema { + const nodeSchema: Map = new Map(); + for (const [key, schema] of Object.entries(f.nodes)) { + const storedSchemaDecoder = storedSchemaDecodeDispatcher.dispatch(schema); + + // No metadata in v1, so pass undefined + nodeSchema.set(brand(key), storedSchemaDecoder(undefined)); + } + return { + rootFieldSchema: decodeFieldSchema(f.root), + nodeSchema, + }; +} + +function decodeV2(f: FormatV2): TreeStoredSchema { const nodeSchema: Map = new Map(); for (const [key, schema] of Object.entries(f.nodes)) { - nodeSchema.set(brand(key), storedSchemaDecodeDispatcher.dispatch(schema)); + const storedSchemaDecoder = storedSchemaDecodeDispatcher.dispatch(schema.kind); + + // Pass in the node metadata + nodeSchema.set(brand(key), storedSchemaDecoder(schema.metadata)); } return { rootFieldSchema: decodeFieldSchema(f.root), @@ -118,6 +163,18 @@ function decode(f: FormatV1): TreeStoredSchema { function makeSchemaCodecV1(options: ICodecOptions): IJsonCodec { return makeVersionedValidatedCodec(options, new Set([SchemaVersion.v1]), FormatV1, { encode: (data: TreeStoredSchema) => encodeRepoV1(data), - decode: (data: FormatV1) => decode(data), + decode: (data: FormatV1) => decodeV1(data), + }); +} + +/** + * Creates a codec which performs synchronous monolithic encoding of schema content. + * @param options - Specifies common codec options, including which `validator` to use. + * @returns The codec. + */ +function makeSchemaCodecV2(options: ICodecOptions): IJsonCodec { + return makeVersionedValidatedCodec(options, new Set([SchemaVersion.v2]), FormatV2, { + encode: (data: TreeStoredSchema) => encodeRepoV2(data), + decode: (data: FormatV2) => decodeV2(data), }); } diff --git a/packages/dds/tree/src/feature-libraries/schema-index/formatV2.ts b/packages/dds/tree/src/feature-libraries/schema-index/formatV2.ts new file mode 100644 index 000000000000..f37c804ee063 --- /dev/null +++ b/packages/dds/tree/src/feature-libraries/schema-index/formatV2.ts @@ -0,0 +1,30 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { type ObjectOptions, type Static, Type } from "@sinclair/typebox"; + +import { SchemaVersion, schemaFormatV2 } from "../../core/index.js"; + +const noAdditionalProps: ObjectOptions = { additionalProperties: false }; + +/** + * Format for encoding as json. + * + * For consistency all lists are sorted and undefined values are omitted. + * + * This chooses to use lists of named objects instead of maps: + * this choice is somewhat arbitrary, but avoids user data being used as object keys, + * which can sometimes be an issue (for example handling that for "__proto__" can require care). + * It also makes it simpler to determinately sort by keys. + */ +export const Format = Type.Object( + { + version: Type.Literal(SchemaVersion.v2), + nodes: Type.Record(Type.String(), schemaFormatV2.TreeNodeSchemaDataFormat), + root: schemaFormatV2.FieldSchemaFormat, + }, + noAdditionalProps, +); +export type Format = Static; diff --git a/packages/dds/tree/src/feature-libraries/schema-index/index.ts b/packages/dds/tree/src/feature-libraries/schema-index/index.ts index d79072edfb34..aae9846c76fe 100644 --- a/packages/dds/tree/src/feature-libraries/schema-index/index.ts +++ b/packages/dds/tree/src/feature-libraries/schema-index/index.ts @@ -9,4 +9,5 @@ export { makeSchemaCodecs, clientVersionToSchemaVersion, } from "./codec.js"; -export { Format } from "./formatV1.js"; +export { Format as FormatV1 } from "./formatV1.js"; +export { Format as FormatV2 } from "./formatV2.js"; diff --git a/packages/dds/tree/src/index.ts b/packages/dds/tree/src/index.ts index d1b99943f8a2..c5e6179b857c 100644 --- a/packages/dds/tree/src/index.ts +++ b/packages/dds/tree/src/index.ts @@ -113,6 +113,7 @@ export { type TreeNodeSchemaClass, type SchemaCompatibilityStatus, type FieldProps, + type FieldPropsAlpha, type InternalTreeNode, type WithType, type NodeChangedData, @@ -202,6 +203,7 @@ export { type LazyItem, type Unenforced, type SimpleNodeSchemaBase, + type SimpleNodeSchemaBaseAlpha, type SimpleTreeSchema, type SimpleNodeSchema, type SimpleFieldSchema, @@ -219,6 +221,7 @@ export { type TreeBranchEvents, asTreeViewAlpha, type NodeSchemaOptions, + type NodeSchemaOptionsAlpha, type NodeSchemaMetadata, type SchemaStatics, type ITreeAlpha, @@ -285,6 +288,8 @@ export type { PopUnion, JsonCompatible, JsonCompatibleObject, + JsonCompatibleReadOnly, + JsonCompatibleReadOnlyObject, } from "./util/index.js"; export { cloneWithReplacements } from "./util/index.js"; diff --git a/packages/dds/tree/src/shared-tree-core/sharedTreeCore.ts b/packages/dds/tree/src/shared-tree-core/sharedTreeCore.ts index eb9857eeb755..6cef462a0d95 100644 --- a/packages/dds/tree/src/shared-tree-core/sharedTreeCore.ts +++ b/packages/dds/tree/src/shared-tree-core/sharedTreeCore.ts @@ -213,10 +213,14 @@ export class SharedTreeCore telemetryContext?: ITelemetryContext, incrementalSummaryContext?: IExperimentalIncrementalSummaryContext, ): ISummaryTreeWithStats { + console.log("In summarizeCore"); const builder = new SummaryTreeBuilder(); const summarizableBuilder = new SummaryTreeBuilder(); // Merge the summaries of all summarizables together under a single ISummaryTree for (const s of this.summarizables) { + if (s.key === "Schema") { + console.log("Adding summarizable for summary key:", s.key); + } summarizableBuilder.addWithStats( s.key, s.getAttachSummary( @@ -229,8 +233,11 @@ export class SharedTreeCore ); } + console.log("Adding summarizablesTreeKey to summary", summarizablesTreeKey); builder.addWithStats(summarizablesTreeKey, summarizableBuilder.getSummaryTree()); - return builder.getSummaryTree(); + const result = builder.getSummaryTree(); + console.log("Returning summary", JSON.stringify(result)); + return result; } public async loadCore(services: IChannelStorageService): Promise { diff --git a/packages/dds/tree/src/shared-tree/schematizeTree.ts b/packages/dds/tree/src/shared-tree/schematizeTree.ts index 6e8f3d736947..26c291968921 100644 --- a/packages/dds/tree/src/shared-tree/schematizeTree.ts +++ b/packages/dds/tree/src/shared-tree/schematizeTree.ts @@ -67,6 +67,7 @@ export function initializeContent( rootFieldSchema: { kind: FieldKinds.optional.identifier, types: rootSchema.types, + metadata: rootSchema.metadata, }, }; } diff --git a/packages/dds/tree/src/shared-tree/sharedTree.ts b/packages/dds/tree/src/shared-tree/sharedTree.ts index 66b51004ec87..56c9959c19bd 100644 --- a/packages/dds/tree/src/shared-tree/sharedTree.ts +++ b/packages/dds/tree/src/shared-tree/sharedTree.ts @@ -100,7 +100,7 @@ import { throwIfBroken, } from "../util/index.js"; // eslint-disable-next-line import/no-internal-modules -import type { Format } from "../feature-libraries/schema-index/index.js"; +import type { FormatV1 } from "../feature-libraries/schema-index/index.js"; /** * Copy of data from an {@link ITreePrivate} at some point in time. @@ -186,7 +186,7 @@ const formatVersionToTopLevelCodecVersions = new Map= 2.0.0. */ v3: 3, + + /** + * Requires \@fluidframework/tree \>= 2.0.0. + */ + v4: 4, } as const; /** @@ -697,7 +703,7 @@ export const defaultSharedTreeOptions: Required = { jsonValidator: noopValidator, forest: ForestTypeReference, treeEncodeType: TreeCompressionStrategy.Compressed, - formatVersion: SharedTreeFormatVersion.v3, + formatVersion: SharedTreeFormatVersion.v4, disposeForksAfterTransaction: true, }; @@ -737,20 +743,25 @@ function exportSimpleFieldSchemaStored(schema: TreeFieldStoredSchema): SimpleFie default: fail(0xaca /* invalid field kind */); } - return { kind, allowedTypesIdentifiers: schema.types, metadata: {} }; + return { kind, allowedTypesIdentifiers: schema.types, metadata: schema.metadata }; } function exportSimpleNodeSchemaStored(schema: TreeNodeStoredSchema): SimpleNodeSchema { const arrayTypes = tryStoredSchemaAsArray(schema); if (arrayTypes !== undefined) { - return { kind: NodeKind.Array, allowedTypesIdentifiers: arrayTypes, metadata: {} }; + return { + kind: NodeKind.Array, + allowedTypesIdentifiers: arrayTypes, + metadata: {}, + persistedMetadata: schema.metadata, + }; } if (schema instanceof ObjectNodeStoredSchema) { const fields = new Map(); for (const [storedKey, field] of schema.objectNodeFields) { fields.set(storedKey, { ...exportSimpleFieldSchemaStored(field), storedKey }); } - return { kind: NodeKind.Object, fields, metadata: {} }; + return { kind: NodeKind.Object, fields, metadata: {}, persistedMetadata: schema.metadata }; } if (schema instanceof MapNodeStoredSchema) { assert( @@ -761,10 +772,16 @@ function exportSimpleNodeSchemaStored(schema: TreeNodeStoredSchema): SimpleNodeS kind: NodeKind.Map, allowedTypesIdentifiers: schema.mapFields.types, metadata: {}, + persistedMetadata: schema.metadata, }; } if (schema instanceof LeafNodeStoredSchema) { - return { kind: NodeKind.Leaf, leafKind: schema.leafValue, metadata: {} }; + return { + kind: NodeKind.Leaf, + leafKind: schema.leafValue, + metadata: {}, + persistedMetadata: schema.metadata, + }; } fail(0xacb /* invalid schema kind */); } diff --git a/packages/dds/tree/src/shared-tree/treeAlpha.ts b/packages/dds/tree/src/shared-tree/treeAlpha.ts index 342819b137ec..ab0db2e8bb0c 100644 --- a/packages/dds/tree/src/shared-tree/treeAlpha.ts +++ b/packages/dds/tree/src/shared-tree/treeAlpha.ts @@ -43,7 +43,7 @@ import { mapTreeFromNodeData, } from "../simple-tree/index.js"; import { extractFromOpaque, type JsonCompatible } from "../util/index.js"; -import { noopValidator, type FluidClientVersion, type ICodecOptions } from "../codec/index.js"; +import { FluidClientVersion, noopValidator, type ICodecOptions } from "../codec/index.js"; import type { ITreeCursorSynchronous } from "../core/index.js"; import { cursorForMapTreeField, @@ -59,7 +59,6 @@ import { } from "../feature-libraries/index.js"; import { independentInitializedView, type ViewContent } from "./independentView.js"; import { SchematizingSimpleTreeView, ViewSlot } from "./schematizingTreeView.js"; -import { currentVersion } from "../codec/index.js"; import { createFromMapTree } from "../simple-tree/index.js"; const identifier: TreeIdentifierUtils = (node: TreeNode): string | undefined => { @@ -461,7 +460,8 @@ export const TreeAlpha: TreeAlpha = { ): Unhydrated> { const config = new TreeViewConfigurationAlpha({ schema }); const content: ViewContent = { - schema: extractPersistedSchema(config, currentVersion), + // Always use a v1 schema codec for consistency. + schema: extractPersistedSchema(config, FluidClientVersion.v2_0), tree: compressedData, idCompressor: options.idCompressor ?? createIdCompressor(), }; diff --git a/packages/dds/tree/src/simple-tree/api/schemaCompatibilityTester.ts b/packages/dds/tree/src/simple-tree/api/schemaCompatibilityTester.ts index 5db11af0aaae..7e5f63041019 100644 --- a/packages/dds/tree/src/simple-tree/api/schemaCompatibilityTester.ts +++ b/packages/dds/tree/src/simple-tree/api/schemaCompatibilityTester.ts @@ -273,7 +273,7 @@ export class SchemaCompatibilityTester { } } - return { kind: original.kind, types }; + return { kind: original.kind, types, metadata: undefined }; } return original; } diff --git a/packages/dds/tree/src/simple-tree/api/schemaFactory.ts b/packages/dds/tree/src/simple-tree/api/schemaFactory.ts index 2b912a8ce36e..5a3e6583284a 100644 --- a/packages/dds/tree/src/simple-tree/api/schemaFactory.ts +++ b/packages/dds/tree/src/simple-tree/api/schemaFactory.ts @@ -36,12 +36,12 @@ import { createFieldSchema, type DefaultProvider, getDefaultProvider, - type NodeSchemaOptions, markSchemaMostDerived, type FieldSchemaAlpha, type ImplicitAnnotatedAllowedTypes, type UnannotateImplicitAllowedTypes, type UnannotateSchemaRecord, + type NodeSchemaOptionsAlpha, } from "../schemaTypes.js"; import type { NodeKind, @@ -91,7 +91,7 @@ export function schemaFromValue(value: TreeValue): TreeNodeSchema { * @alpha */ export interface SchemaFactoryObjectOptions - extends NodeSchemaOptions { + extends NodeSchemaOptionsAlpha { /** * 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 @@ -137,8 +137,11 @@ export interface SchemaFactoryObjectOptions allowUnknownOptionalFields?: boolean; } +/** + * Omit parameters that are not relevant for common use cases. + */ export const defaultSchemaFactoryObjectOptions: Required< - Omit + Omit > = { allowUnknownOptionalFields: false, }; @@ -290,7 +293,7 @@ export interface SchemaStatics { ) => System_Unsafe.FieldSchemaUnsafe; } -const defaultOptionalProvider: DefaultProvider = getDefaultProvider(() => { +export const defaultOptionalProvider: DefaultProvider = getDefaultProvider(() => { return undefined; }); diff --git a/packages/dds/tree/src/simple-tree/api/schemaFactoryAlpha.ts b/packages/dds/tree/src/simple-tree/api/schemaFactoryAlpha.ts index 0eb6057fbd11..9f7b16f78afb 100644 --- a/packages/dds/tree/src/simple-tree/api/schemaFactoryAlpha.ts +++ b/packages/dds/tree/src/simple-tree/api/schemaFactoryAlpha.ts @@ -4,24 +4,32 @@ */ import { + defaultOptionalProvider, defaultSchemaFactoryObjectOptions, SchemaFactory, schemaStatics, type SchemaFactoryObjectOptions, type ScopedSchemaName, } from "./schemaFactory.js"; -import type { - ImplicitAllowedTypes, - ImplicitAnnotatedAllowedTypes, - ImplicitAnnotatedFieldSchema, - ImplicitFieldSchema, - NodeSchemaOptions, +import { + createFieldSchema, + FieldKind, + type FieldPropsAlpha, + type FieldSchemaAlpha, + type ImplicitAllowedTypes, + type ImplicitAnnotatedAllowedTypes, + type ImplicitAnnotatedFieldSchema, + type ImplicitFieldSchema, + type NodeSchemaOptions, + type NodeSchemaOptionsAlpha, + type UnannotateImplicitAllowedTypes, } from "../schemaTypes.js"; import { objectSchema } from "../objectNode.js"; import type { RestrictiveStringRecord } from "../../util/index.js"; import type { NodeKind, TreeNodeSchemaClass } from "../core/index.js"; import type { ArrayNodeCustomizableSchemaUnsafe, + FieldSchemaAlphaUnsafe, MapNodeCustomizableSchemaUnsafe, System_Unsafe, } from "./typesUnsafe.js"; @@ -31,6 +39,7 @@ import type { ObjectNodeSchema } from "../objectNodeTypes.js"; import type { SimpleObjectNodeSchema } from "../simpleSchema.js"; import type { ArrayNodeCustomizableSchema } from "../arrayNodeTypes.js"; import type { MapNodeCustomizableSchema } from "../mapNodeTypes.js"; +import { createFieldSchemaUnsafe } from "./schemaFactoryRecursive.js"; /** * {@link SchemaFactory} with additional alpha APIs. @@ -86,6 +95,7 @@ export class SchemaFactoryAlpha< options?.allowUnknownOptionalFields ?? defaultSchemaFactoryObjectOptions.allowUnknownOptionalFields, options?.metadata, + options?.persistedMetadata, ); } @@ -155,16 +165,61 @@ export class SchemaFactoryAlpha< */ public static override readonly optional = schemaStatics.optional; + /** + * {@inheritDoc SchemaStatics.optional} + */ + public optionalAlpha< + const T extends ImplicitAnnotatedAllowedTypes, + const TCustomMetadata = unknown, + >( + t: T, + props?: Omit, "defaultProvider">, + ): FieldSchemaAlpha, TCustomMetadata> { + return createFieldSchema(FieldKind.Optional, t, { + defaultProvider: defaultOptionalProvider, + ...props, + }); + } + /** * {@inheritDoc SchemaStatics.required} */ public static override readonly required = schemaStatics.required; + /** + * {@inheritDoc SchemaStatics.required} + */ + public requiredAlpha< + const T extends ImplicitAnnotatedAllowedTypes, + const TCustomMetadata = unknown, + >( + t: T, + props?: Omit, "defaultProvider">, + ): FieldSchemaAlpha, TCustomMetadata> { + return createFieldSchema(FieldKind.Required, t, props); + } + /** * {@inheritDoc SchemaStatics.optionalRecursive} */ public static override readonly optionalRecursive = schemaStatics.optionalRecursive; + /** + * {@inheritDoc SchemaStatics.optionalRecursive} + */ + public optionalRecursiveAlpha< + const T extends System_Unsafe.ImplicitAllowedTypesUnsafe, + const TCustomMetadata = unknown, + >( + t: T, + props?: Omit, "defaultProvider">, + ): FieldSchemaAlphaUnsafe { + return createFieldSchemaUnsafe(FieldKind.Optional, t, { + defaultProvider: defaultOptionalProvider, + ...props, + }); + } + /** * Like {@link SchemaFactory.identifier} but static and a factory function that can be provided {@link FieldProps}. */ @@ -191,9 +246,16 @@ export class SchemaFactoryAlpha< >( name: Name, allowedTypes: T, - options?: NodeSchemaOptions, + options?: NodeSchemaOptionsAlpha, ): MapNodeCustomizableSchema, T, true, TCustomMetadata> { - return mapSchema(this.scoped2(name), allowedTypes, true, true, options?.metadata); + return mapSchema( + this.scoped2(name), + allowedTypes, + true, + true, + options?.metadata, + options?.persistedMetadata, + ); } /** @@ -235,9 +297,16 @@ export class SchemaFactoryAlpha< >( name: Name, allowedTypes: T, - options?: NodeSchemaOptions, + options?: NodeSchemaOptionsAlpha, ): ArrayNodeCustomizableSchema, T, true, TCustomMetadata> { - return arraySchema(this.scoped2(name), allowedTypes, true, true, options?.metadata); + return arraySchema( + this.scoped2(name), + allowedTypes, + true, + true, + options?.metadata, + options?.persistedMetadata, + ); } /** diff --git a/packages/dds/tree/src/simple-tree/api/storedSchema.ts b/packages/dds/tree/src/simple-tree/api/storedSchema.ts index 53adc5df45c6..6d2fe576734f 100644 --- a/packages/dds/tree/src/simple-tree/api/storedSchema.ts +++ b/packages/dds/tree/src/simple-tree/api/storedSchema.ts @@ -12,7 +12,7 @@ import { } from "../../feature-libraries/index.js"; import { clientVersionToSchemaVersion, - type Format, + type FormatV1, // eslint-disable-next-line import/no-internal-modules } from "../../feature-libraries/schema-index/index.js"; import type { JsonCompatible } from "../../util/index.js"; @@ -57,8 +57,8 @@ export function extractPersistedSchema( oldestCompatibleClient: FluidClientVersion, ): JsonCompatible { const stored = simpleToStoredSchema(schema); - const writeVersion = clientVersionToSchemaVersion(oldestCompatibleClient); - return encodeTreeSchema(stored, writeVersion); + const schemaWriteVersion = clientVersionToSchemaVersion(oldestCompatibleClient); + return encodeTreeSchema(stored, schemaWriteVersion); } /** @@ -99,7 +99,7 @@ export function comparePersistedSchema( // Any version can be passed down to makeSchemaCodec here. // We only use the decode part, which always dispatches to the correct codec based on the version in the data, not the version passed to `makeSchemaCodec`. const schemaCodec = makeSchemaCodec(options, SchemaVersion.v1); - const stored = schemaCodec.decode(persisted as Format); + const stored = schemaCodec.decode(persisted as FormatV1); const viewSchema = new SchemaCompatibilityTester( defaultSchemaPolicy, {}, diff --git a/packages/dds/tree/src/simple-tree/api/viewSchemaToSimpleSchema.ts b/packages/dds/tree/src/simple-tree/api/viewSchemaToSimpleSchema.ts index 74f53ffcff25..40b3eb4f9de1 100644 --- a/packages/dds/tree/src/simple-tree/api/viewSchemaToSimpleSchema.ts +++ b/packages/dds/tree/src/simple-tree/api/viewSchemaToSimpleSchema.ts @@ -96,6 +96,7 @@ function copySimpleLeafSchema(schema: SimpleLeafNodeSchema): SimpleLeafNodeSchem kind: NodeKind.Leaf, leafKind: schema.leafKind, metadata: schema.metadata, + persistedMetadata: schema.persistedMetadata, }; } @@ -106,6 +107,7 @@ function copySimpleMapOrArraySchema( kind: schema.kind, allowedTypesIdentifiers: schema.allowedTypesIdentifiers, metadata: schema.metadata, + persistedMetadata: schema.persistedMetadata, }; } @@ -125,5 +127,6 @@ function copySimpleObjectSchema(schema: SimpleObjectNodeSchema): SimpleObjectNod kind: NodeKind.Object, fields, metadata: schema.metadata, + persistedMetadata: schema.persistedMetadata, }; } diff --git a/packages/dds/tree/src/simple-tree/arrayNode.ts b/packages/dds/tree/src/simple-tree/arrayNode.ts index a689fb5fb912..465256abd1ef 100644 --- a/packages/dds/tree/src/simple-tree/arrayNode.ts +++ b/packages/dds/tree/src/simple-tree/arrayNode.ts @@ -52,6 +52,7 @@ import type { ArrayNodeCustomizableSchema, ArrayNodePojoEmulationSchema, } from "./arrayNodeTypes.js"; +import type { JsonCompatibleReadOnlyObject } from "../util/index.js"; /** * A covariant base type for {@link (TreeArrayNode:interface)}. @@ -1093,6 +1094,7 @@ export function arraySchema< implicitlyConstructable: ImplicitlyConstructable, customizable: boolean, metadata?: NodeSchemaMetadata, + persistedMetadata?: JsonCompatibleReadOnlyObject | undefined, ) { type Output = ArrayNodeCustomizableSchema< TName, @@ -1191,6 +1193,8 @@ export function arraySchema< return lazyChildTypes.value; } public static readonly metadata: NodeSchemaMetadata = metadata ?? {}; + public static readonly persistedMetadata: JsonCompatibleReadOnlyObject | undefined = + persistedMetadata; // eslint-disable-next-line import/no-deprecated public get [typeNameSymbol](): TName { diff --git a/packages/dds/tree/src/simple-tree/index.ts b/packages/dds/tree/src/simple-tree/index.ts index efde745fc981..d55a1eb2c0e5 100644 --- a/packages/dds/tree/src/simple-tree/index.ts +++ b/packages/dds/tree/src/simple-tree/index.ts @@ -136,6 +136,7 @@ export type { SimpleArrayNodeSchema, SimpleObjectNodeSchema, SimpleNodeSchemaBase, + SimpleNodeSchemaBaseAlpha, SimpleObjectFieldSchema, } from "./simpleSchema.js"; export { @@ -169,6 +170,7 @@ export { type NodeBuilderData, type DefaultProvider, type FieldProps, + type FieldPropsAlpha, normalizeFieldSchema, areFieldSchemaEqual, areImplicitFieldSchemaEqual, @@ -184,6 +186,7 @@ export { type ReadableField, type ReadSchema, type NodeSchemaOptions, + type NodeSchemaOptionsAlpha, type NodeSchemaMetadata, evaluateLazySchema, } from "./schemaTypes.js"; diff --git a/packages/dds/tree/src/simple-tree/leafNodeSchema.ts b/packages/dds/tree/src/simple-tree/leafNodeSchema.ts index 865ab6504bd1..68ce3c2d7ed2 100644 --- a/packages/dds/tree/src/simple-tree/leafNodeSchema.ts +++ b/packages/dds/tree/src/simple-tree/leafNodeSchema.ts @@ -14,6 +14,7 @@ import { import { NodeKind, type TreeNodeSchema, type TreeNodeSchemaNonClass } from "./core/index.js"; import type { NodeSchemaMetadata, TreeLeafValue } from "./schemaTypes.js"; import type { SimpleLeafNodeSchema } from "./simpleSchema.js"; +import type { JsonCompatibleReadOnlyObject } from "../util/index.js"; /** * Instances of this class are schema for leaf nodes. @@ -49,6 +50,7 @@ export class LeafNodeSchema public readonly leafKind: ValueSchema; public readonly metadata: NodeSchemaMetadata = {}; + public readonly persistedMetadata: JsonCompatibleReadOnlyObject | undefined; public constructor(name: Name, t: T) { this.identifier = name; diff --git a/packages/dds/tree/src/simple-tree/mapNode.ts b/packages/dds/tree/src/simple-tree/mapNode.ts index 2a475bcdec5b..981df9cb0869 100644 --- a/packages/dds/tree/src/simple-tree/mapNode.ts +++ b/packages/dds/tree/src/simple-tree/mapNode.ts @@ -42,7 +42,12 @@ import { type InsertableContent, } from "./toMapTree.js"; import { prepareForInsertion } from "./prepareForInsertion.js"; -import { brand, count, type RestrictiveStringRecord } from "../util/index.js"; +import { + brand, + count, + type JsonCompatibleReadOnlyObject, + type RestrictiveStringRecord, +} from "../util/index.js"; import { TreeNodeValid, type MostDerivedData } from "./treeNodeValid.js"; import type { ExclusiveMapTree } from "../core/index.js"; import { getUnhydratedContext } from "./createContext.js"; @@ -241,6 +246,7 @@ export function mapSchema< implicitlyConstructable: ImplicitlyConstructable, useMapPrototype: boolean, metadata?: NodeSchemaMetadata, + persistedMetadata?: JsonCompatibleReadOnlyObject | undefined, ) { const lazyChildTypes = new Lazy(() => normalizeAllowedTypes(unannotateImplicitAllowedTypes(info)), @@ -297,6 +303,8 @@ export function mapSchema< return lazyChildTypes.value; } public static readonly metadata: NodeSchemaMetadata = metadata ?? {}; + public static readonly persistedMetadata: JsonCompatibleReadOnlyObject | undefined = + persistedMetadata; // eslint-disable-next-line import/no-deprecated public get [typeNameSymbol](): TName { diff --git a/packages/dds/tree/src/simple-tree/objectNode.ts b/packages/dds/tree/src/simple-tree/objectNode.ts index 938f5ba2897e..d015e1b43b33 100644 --- a/packages/dds/tree/src/simple-tree/objectNode.ts +++ b/packages/dds/tree/src/simple-tree/objectNode.ts @@ -47,7 +47,11 @@ import { } from "./core/index.js"; import { mapTreeFromNodeData, type InsertableContent } from "./toMapTree.js"; import { prepareForInsertion } from "./prepareForInsertion.js"; -import type { RestrictiveStringRecord, FlattenKeys } from "../util/index.js"; +import type { + RestrictiveStringRecord, + FlattenKeys, + JsonCompatibleReadOnlyObject, +} from "../util/index.js"; import { isObjectNodeSchema, type ObjectNodeSchema, @@ -371,6 +375,7 @@ export function objectSchema< implicitlyConstructable: ImplicitlyConstructable, allowUnknownOptionalFields: boolean, metadata?: NodeSchemaMetadata, + persistedMetadata?: JsonCompatibleReadOnlyObject | undefined, ): ObjectNodeSchema & ObjectNodeSchemaInternalData { // Field set can't be modified after this since derived data is stored in maps. @@ -517,6 +522,8 @@ export function objectSchema< return lazyChildTypes.value; } public static readonly metadata: NodeSchemaMetadata = metadata ?? {}; + public static readonly persistedMetadata: JsonCompatibleReadOnlyObject | undefined = + persistedMetadata; // eslint-disable-next-line import/no-deprecated public get [typeNameSymbol](): TName { diff --git a/packages/dds/tree/src/simple-tree/prepareForInsertion.ts b/packages/dds/tree/src/simple-tree/prepareForInsertion.ts index 408f22aea455..3dcd2d745973 100644 --- a/packages/dds/tree/src/simple-tree/prepareForInsertion.ts +++ b/packages/dds/tree/src/simple-tree/prepareForInsertion.ts @@ -90,7 +90,11 @@ export function prepareArrayContentForInsertion( validateAndPrepare( getSchemaAndPolicy(destinationContext), destinationContext.isHydrated() ? destinationContext : undefined, - { kind: FieldKinds.sequence.identifier, types: fieldSchema.types }, + { + kind: FieldKinds.sequence.identifier, + types: fieldSchema.types, + metadata: undefined, + }, mapTrees, ); diff --git a/packages/dds/tree/src/simple-tree/schemaTypes.ts b/packages/dds/tree/src/simple-tree/schemaTypes.ts index 6cbe171144bf..20225aa098a3 100644 --- a/packages/dds/tree/src/simple-tree/schemaTypes.ts +++ b/packages/dds/tree/src/simple-tree/schemaTypes.ts @@ -19,6 +19,7 @@ import { getOrCreate, type RestrictiveStringRecord, type IsUnion, + type JsonCompatibleReadOnlyObject, } from "../util/index.js"; import type { Unhydrated, @@ -294,6 +295,22 @@ export interface FieldProps { readonly metadata?: FieldSchemaMetadata; } +/** + * Additional information to provide to a {@link FieldSchema}. Includes fields for alpha features. + * + * @typeParam TCustomMetadata - Custom metadata properties to associate with the field. + * See {@link FieldSchemaMetadata.custom}. + * + * @alpha + */ +export interface FieldPropsAlpha + extends FieldProps { + /** + * The persisted metadata for this schema element. + */ + readonly persistedMetadata?: JsonCompatibleReadOnlyObject | undefined; +} + /** * A {@link FieldProvider} which requires additional context in order to produce its content */ @@ -359,6 +376,22 @@ export interface FieldSchemaMetadata { readonly description?: string | undefined; } +/** + * Metadata associated with a {@link FieldSchema}. Includes fields used by alpha features. + * + * @remarks Specified via {@link FieldProps.metadata}. + * + * @sealed + * @alpha + */ +export interface FieldSchemaMetadataAlpha + extends FieldSchemaMetadata { + /** + * The persisted metadata for this schema element. + */ + readonly persistedMetadata?: JsonCompatibleReadOnlyObject | undefined; +} + /** * Package internal construction API. */ @@ -507,12 +540,20 @@ export class FieldSchemaAlpha< { private readonly lazyIdentifiers: Lazy>; private readonly lazyAnnotatedTypes: Lazy>; + private readonly propsAlpha: FieldPropsAlpha | undefined; /** * Metadata on the types of tree nodes allowed on this field. */ public readonly allowedTypesMetadata: AllowedTypesMetadata; + /** + * Persisted metadata for this field schema. + */ + public get persistedMetadata(): JsonCompatibleReadOnlyObject | undefined { + return this.propsAlpha?.persistedMetadata ?? {}; + } + static { createFieldSchemaPrivate = < Kind2 extends FieldKind, @@ -521,7 +562,7 @@ export class FieldSchemaAlpha< >( kind: Kind2, annotatedAllowedTypes: Types2, - props?: FieldProps, + props?: FieldPropsAlpha, ) => new FieldSchemaAlpha( kind, @@ -535,7 +576,7 @@ export class FieldSchemaAlpha< kind: Kind, types: Types, public readonly annotatedAllowedTypes: ImplicitAnnotatedAllowedTypes, - props?: FieldProps, + props?: FieldPropsAlpha, ) { super(kind, types, props); @@ -548,6 +589,7 @@ export class FieldSchemaAlpha< this.lazyIdentifiers = new Lazy( () => new Set([...this.allowedTypeSet].map((t) => t.identifier)), ); + this.propsAlpha = props; } public get allowedTypesIdentifiers(): ReadonlySet { @@ -773,17 +815,23 @@ function areFieldPropsEqual(a: FieldProps | undefined, b: FieldProps | undefined * @remarks FieldSchemaMetadata are considered equivalent if their custom data and descriptions are (respectively) reference equal. */ function areMetadataEqual( - a: FieldSchemaMetadata | undefined, - b: FieldSchemaMetadata | undefined, + a: FieldSchemaMetadataAlpha | undefined, + b: FieldSchemaMetadataAlpha | undefined, ): boolean { // If any new fields are added to FieldSchemaMetadata, this check will stop compiling as a reminder that this function needs to be updated. - type _keys = requireTrue>; + type _keys = requireTrue< + areOnlyKeys + >; if (a === b) { return true; } - return a?.custom === b?.custom && a?.description === b?.description; + return ( + a?.custom === b?.custom && + a?.description === b?.description && + a?.persistedMetadata === b?.persistedMetadata + ); } const cachedLazyItem = new WeakMap<() => unknown, unknown>(); @@ -1311,6 +1359,23 @@ export interface NodeSchemaOptions { readonly metadata?: NodeSchemaMetadata | undefined; } +/** + * Additional information to provide to Node Schema creation. Includes fields for alpha features. + * + * @typeParam TCustomMetadata - Custom metadata properties to associate with the Node Schema. + * See {@link NodeSchemaMetadata.custom}. + * + * @sealed + * @alpha + */ +export interface NodeSchemaOptionsAlpha + extends NodeSchemaOptions { + /** + * The persisted metadata for this schema element. + */ + readonly persistedMetadata?: JsonCompatibleReadOnlyObject | undefined; +} + /** * Metadata associated with a Node Schema. * diff --git a/packages/dds/tree/src/simple-tree/simpleSchema.ts b/packages/dds/tree/src/simple-tree/simpleSchema.ts index be253ce8939f..4eb44fe1dda9 100644 --- a/packages/dds/tree/src/simple-tree/simpleSchema.ts +++ b/packages/dds/tree/src/simple-tree/simpleSchema.ts @@ -4,6 +4,7 @@ */ import type { ValueSchema } from "../core/index.js"; +import type { JsonCompatibleReadOnlyObject } from "../util/index.js"; import type { NodeKind } from "./core/index.js"; import type { FieldKind, FieldSchemaMetadata, NodeSchemaMetadata } from "./schemaTypes.js"; @@ -36,6 +37,23 @@ export interface SimpleNodeSchemaBase< readonly metadata: NodeSchemaMetadata; } +/** + * A {@link SimpleNodeSchema} containing fields for alpha features. + * + * @system + * @alpha + * @sealed + */ +export interface SimpleNodeSchemaBaseAlpha< + out TNodeKind extends NodeKind, + out TCustomMetadata = unknown, +> extends SimpleNodeSchemaBase { + /** + * Persisted metadata for this node schema. + */ + readonly persistedMetadata: JsonCompatibleReadOnlyObject | undefined; +} + /** * A {@link SimpleNodeSchema} for an object node. * @@ -43,7 +61,7 @@ export interface SimpleNodeSchemaBase< * @sealed */ export interface SimpleObjectNodeSchema - extends SimpleNodeSchemaBase { + extends SimpleNodeSchemaBaseAlpha { /** * Schemas for each of the object's fields, keyed off of schema's keys. * @remarks @@ -81,7 +99,7 @@ export interface SimpleObjectFieldSchema extends SimpleFieldSchema { * @sealed */ export interface SimpleArrayNodeSchema - extends SimpleNodeSchemaBase { + extends SimpleNodeSchemaBaseAlpha { /** * The types allowed in the array. * @@ -98,7 +116,7 @@ export interface SimpleArrayNodeSchema * @sealed */ export interface SimpleMapNodeSchema - extends SimpleNodeSchemaBase { + extends SimpleNodeSchemaBaseAlpha { /** * The types allowed as values in the map. * @@ -114,7 +132,7 @@ export interface SimpleMapNodeSchema * @alpha * @sealed */ -export interface SimpleLeafNodeSchema extends SimpleNodeSchemaBase { +export interface SimpleLeafNodeSchema extends SimpleNodeSchemaBaseAlpha { /** * The kind of leaf node. */ @@ -150,6 +168,10 @@ export type SimpleNodeSchema = * * @alpha * @sealed + * + * @privateRemarks + * Seems like we need an Alpha version of this one that has persistedMetadata so we can avoid the hack + * in toStoredSchema.ts line 93 */ export interface SimpleFieldSchema { /** diff --git a/packages/dds/tree/src/simple-tree/toStoredSchema.ts b/packages/dds/tree/src/simple-tree/toStoredSchema.ts index ca972cc1c0a2..dece7c687746 100644 --- a/packages/dds/tree/src/simple-tree/toStoredSchema.ts +++ b/packages/dds/tree/src/simple-tree/toStoredSchema.ts @@ -89,7 +89,8 @@ export function convertField(schema: SimpleFieldSchema): TreeFieldStoredSchema { const kind: FieldKindIdentifier = convertFieldKind.get(schema.kind)?.identifier ?? fail(0xae3 /* Invalid field kind */); const types: TreeTypeSet = schema.allowedTypesIdentifiers as TreeTypeSet; - return { kind, types }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Temporary hack + return { kind, types, metadata: (schema as unknown as any)?.persistedMetadata ?? undefined }; } const convertFieldKind = new Map([ @@ -110,23 +111,32 @@ export function getStoredSchema(schema: SimpleNodeSchema): TreeNodeStoredSchema } case NodeKind.Map: { const types = schema.allowedTypesIdentifiers as TreeTypeSet; - return new MapNodeStoredSchema({ kind: FieldKinds.optional.identifier, types }); + return new MapNodeStoredSchema( + { + kind: FieldKinds.optional.identifier, + types, + metadata: schema.persistedMetadata, + }, + // TODO: Find a way to avoid injecting persistedMetadata twice in these constructor calls. + schema.persistedMetadata, + ); } case NodeKind.Array: { const types = schema.allowedTypesIdentifiers as TreeTypeSet; const field = { kind: FieldKinds.sequence.identifier, types, + metadata: schema.persistedMetadata, }; const fields = new Map([[EmptyKey, field]]); - return new ObjectNodeStoredSchema(fields); + return new ObjectNodeStoredSchema(fields, schema.persistedMetadata); } case NodeKind.Object: { const fields: Map = new Map(); for (const fieldSchema of schema.fields.values()) { fields.set(brand(fieldSchema.storedKey), convertField(fieldSchema)); } - return new ObjectNodeStoredSchema(fields); + return new ObjectNodeStoredSchema(fields, schema.persistedMetadata); } default: unreachableCase(kind); diff --git a/packages/dds/tree/src/test/feature-libraries/chunked-forest/codec/schemaBasedEncode.spec.ts b/packages/dds/tree/src/test/feature-libraries/chunked-forest/codec/schemaBasedEncode.spec.ts index f13ee5c4d143..d9841c6841ab 100644 --- a/packages/dds/tree/src/test/feature-libraries/chunked-forest/codec/schemaBasedEncode.spec.ts +++ b/packages/dds/tree/src/test/feature-libraries/chunked-forest/codec/schemaBasedEncode.spec.ts @@ -152,7 +152,11 @@ describe("schemaBasedEncoding", () => { return onlyTypeShape; }, }, - { kind: FieldKinds.sequence.identifier, types: new Set([brand(Minimal.identifier)]) }, + { + kind: FieldKinds.sequence.identifier, + types: new Set([brand(Minimal.identifier)]), + metadata: undefined, + }, cache, { nodeSchema: new Map() }, ); @@ -182,6 +186,7 @@ describe("schemaBasedEncoding", () => { const storedSchema: TreeFieldStoredSchema = { kind: FieldKinds.identifier.identifier, types: new Set([brand(stringSchema.identifier)]), + metadata: undefined, }; const shape = fieldShaper( diff --git a/packages/dds/tree/src/test/feature-libraries/default-schema/schemaChecker.spec.ts b/packages/dds/tree/src/test/feature-libraries/default-schema/schemaChecker.spec.ts index b161bb8fccf8..9a48f572b83b 100644 --- a/packages/dds/tree/src/test/feature-libraries/default-schema/schemaChecker.spec.ts +++ b/packages/dds/tree/src/test/feature-libraries/default-schema/schemaChecker.spec.ts @@ -71,6 +71,7 @@ function getFieldSchema( return { kind: kind.identifier, types: new Set(allowedTypes), + metadata: undefined, }; } diff --git a/packages/dds/tree/src/test/feature-libraries/flex-tree/lazyField.spec.ts b/packages/dds/tree/src/test/feature-libraries/flex-tree/lazyField.spec.ts index 92b7cf1b5eb5..fa168f59715e 100644 --- a/packages/dds/tree/src/test/feature-libraries/flex-tree/lazyField.spec.ts +++ b/packages/dds/tree/src/test/feature-libraries/flex-tree/lazyField.spec.ts @@ -444,6 +444,7 @@ describe("LazyField", () => { const rootSchema: TreeFieldStoredSchema = { kind: FieldKinds.sequence.identifier, types: new Set([brand(numberSchema.identifier)]), + metadata: undefined, }; const schema: TreeStoredSchema = { rootFieldSchema: rootSchema, diff --git a/packages/dds/tree/src/test/feature-libraries/modular-schema/comparison.spec.ts b/packages/dds/tree/src/test/feature-libraries/modular-schema/comparison.spec.ts index fc81bd37e139..2aae6904eb2b 100644 --- a/packages/dds/tree/src/test/feature-libraries/modular-schema/comparison.spec.ts +++ b/packages/dds/tree/src/test/feature-libraries/modular-schema/comparison.spec.ts @@ -46,6 +46,7 @@ export function fieldSchema( return { kind: kind.identifier, types: new Set(types), + metadata: undefined, }; } diff --git a/packages/dds/tree/src/test/feature-libraries/modular-schema/isNeverTree.spec.ts b/packages/dds/tree/src/test/feature-libraries/modular-schema/isNeverTree.spec.ts index 86d606bf41ed..c015027a8827 100644 --- a/packages/dds/tree/src/test/feature-libraries/modular-schema/isNeverTree.spec.ts +++ b/packages/dds/tree/src/test/feature-libraries/modular-schema/isNeverTree.spec.ts @@ -42,6 +42,7 @@ function fieldSchema( return { kind: kind.identifier, types: new Set(types), + metadata: undefined, }; } diff --git a/packages/dds/tree/src/test/feature-libraries/schema-index/codec.spec.ts b/packages/dds/tree/src/test/feature-libraries/schema-index/codec.spec.ts index 5f6b9e2e314e..176e95d041dc 100644 --- a/packages/dds/tree/src/test/feature-libraries/schema-index/codec.spec.ts +++ b/packages/dds/tree/src/test/feature-libraries/schema-index/codec.spec.ts @@ -20,6 +20,8 @@ import { } from "../../../feature-libraries/index.js"; /* eslint-disable-next-line import/no-internal-modules */ import { Format as FormatV1 } from "../../../feature-libraries/schema-index/formatV1.js"; +// eslint-disable-next-line import/no-internal-modules +import { Format as FormatV2 } from "../../../feature-libraries/schema-index/formatV2.js"; import { takeJsonSnapshot, useSnapshotDirectory } from "../../snapshots/index.js"; import { type EncodingTestData, makeEncodingTestSuite } from "../../utils.js"; // eslint-disable-next-line import/no-internal-modules @@ -31,6 +33,7 @@ import { makeSchemaCodecs } from "../../../feature-libraries/schema-index/index. const schemaCodecs = makeSchemaCodecs({ jsonValidator: typeboxValidator }); const codecV1 = makeSchemaCodec({ jsonValidator: typeboxValidator }, SchemaVersion.v1); +const codecV2 = makeSchemaCodec({ jsonValidator: typeboxValidator }, SchemaVersion.v2); const schema2 = toStoredSchema(SchemaFactory.optional(JsonAsTree.Primitive)); @@ -49,6 +52,11 @@ describe("SchemaIndex", () => { takeJsonSnapshot(FormatV1); }); + it("SchemaIndexFormat - schema v2", () => { + // Capture the json schema for the format as a snapshot, so any change to what schema is allowed shows up in this tests. + takeJsonSnapshot(FormatV2); + }); + it("accepts valid data - schema v1", () => { // TODO: should test way more cases, and check results are correct. const cases = [ @@ -63,26 +71,54 @@ describe("SchemaIndex", () => { } }); - it("rejects malformed data - schema v1", () => { - // TODO: should test way more cases - // TODO: maybe well formed but semantically invalid data should be rejected (ex: with duplicates keys)? - const badCases = [ - undefined, - null, - {}, - { version: "1.0.0" }, - { version: "1" }, - { version: "2.0.0" }, - { version: 1 }, - { version: 2 }, - { version: 1, nodeSchema: [], globalFieldSchema: [] }, - { version: 1, nodeSchema: [], extraField: 0 }, + it("accepts valid data - schema v2", () => { + // TODO: should test way more cases, and check results are correct. + const cases = [ + { + version: 2 as const, + nodes: {}, + root: { + kind: "x" as FieldKindIdentifier, + types: [], + metadata: { "ff-system": { "eDiscovery-exclude": "true" } }, + }, + } satisfies FormatV2, ]; - for (const data of badCases) { + for (const data of cases) { + codecV2.decode(data); + } + }); + + // TODO: should test way more cases + // TODO: maybe well formed but semantically invalid data should be rejected (ex: with duplicates keys)? + /** + * A set of cases that are expected to be rejected by both the v1 and v2 codecs. + */ + const badCasesV1AndV2 = [ + undefined, + null, + {}, + { version: "1.0.0" }, + { version: "1" }, + { version: "2.0.0" }, + { version: 1 }, + { version: 2 }, + { version: 1, nodeSchema: [], globalFieldSchema: [] }, + { version: 1, nodeSchema: [], extraField: 0 }, + ]; + + it(`rejects malformed data - schema v1`, () => { + for (const data of badCasesV1AndV2) { assert.throws(() => codecV1.decode(data as unknown as FormatV1)); } }); + it(`rejects malformed data - schema v2`, () => { + for (const data of badCasesV1AndV2) { + assert.throws(() => codecV2.decode(data as unknown as FormatV2)); + } + }); + describe("codec", () => { makeEncodingTestSuite(schemaCodecs, testCases, (a, b) => { assert(allowsRepoSuperset(defaultSchemaPolicy, a, b)); diff --git a/packages/dds/tree/src/test/sequenceRootUtils.ts b/packages/dds/tree/src/test/sequenceRootUtils.ts index e7bfef8c8877..1e29a3f3449a 100644 --- a/packages/dds/tree/src/test/sequenceRootUtils.ts +++ b/packages/dds/tree/src/test/sequenceRootUtils.ts @@ -31,6 +31,7 @@ export const jsonSequenceRootSchema: TreeStoredSchema = { brand(s.identifier), ), ), + metadata: undefined, }, }; diff --git a/packages/dds/tree/src/test/shared-tree/sharedTreeChangeFamily.spec.ts b/packages/dds/tree/src/test/shared-tree/sharedTreeChangeFamily.spec.ts index 434c767c64bf..d376ac70c6e6 100644 --- a/packages/dds/tree/src/test/shared-tree/sharedTreeChangeFamily.spec.ts +++ b/packages/dds/tree/src/test/shared-tree/sharedTreeChangeFamily.spec.ts @@ -75,6 +75,7 @@ const emptySchema: TreeStoredSchema = { rootFieldSchema: { kind: forbidden.identifier, types: new Set(), + metadata: undefined, }, }; const stSchemaChange: SharedTreeChange = { diff --git a/packages/dds/tree/src/test/simple-tree/api/getSimpleSchema.spec.ts b/packages/dds/tree/src/test/simple-tree/api/getSimpleSchema.spec.ts index 56e41da67f64..db3ffb35cb96 100644 --- a/packages/dds/tree/src/test/simple-tree/api/getSimpleSchema.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/api/getSimpleSchema.spec.ts @@ -24,12 +24,14 @@ const simpleString: SimpleLeafNodeSchema = { leafKind: ValueSchema.String, kind: NodeKind.Leaf, metadata: {}, + persistedMetadata: undefined, }; const simpleNumber: SimpleLeafNodeSchema = { leafKind: ValueSchema.Number, kind: NodeKind.Leaf, metadata: {}, + persistedMetadata: undefined, }; describe("getSimpleSchema", () => { @@ -125,6 +127,7 @@ describe("getSimpleSchema", () => { kind: NodeKind.Array, allowedTypesIdentifiers: new Set(["com.fluidframework.leaf.string"]), metadata: {}, + persistedMetadata: undefined, }, ], ["com.fluidframework.leaf.string", simpleString], @@ -150,6 +153,7 @@ describe("getSimpleSchema", () => { { kind: NodeKind.Map, metadata: {}, + persistedMetadata: undefined, allowedTypesIdentifiers: new Set(["com.fluidframework.leaf.string"]), }, ], @@ -180,6 +184,7 @@ describe("getSimpleSchema", () => { { kind: NodeKind.Object, metadata: {}, + persistedMetadata: undefined, fields: new Map([ [ "foo", @@ -229,6 +234,7 @@ describe("getSimpleSchema", () => { { kind: NodeKind.Object, metadata: {}, + persistedMetadata: undefined, fields: new Map([ [ "id", @@ -269,6 +275,7 @@ describe("getSimpleSchema", () => { { kind: NodeKind.Object, metadata: {}, + persistedMetadata: undefined, fields: new Map([ [ "foo", @@ -312,6 +319,7 @@ describe("getSimpleSchema", () => { { kind: NodeKind.Object, metadata: {}, + persistedMetadata: undefined, fields: new Map([ [ "foo", 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 8a9f4ad39ca6..8f2be6beeaff 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 @@ -414,6 +414,56 @@ describe("schemaFactory", () => { assert.deepEqual(schema.fields.get("bar")!.metadata, barMetadata); }); + it("Node schema persisted metadata", () => { + const factory = new SchemaFactoryAlpha(""); + + const fooMetadata = { + persistedMetadata: { "a": 2 }, + }; + + class Foo extends factory.objectAlpha( + "Foo", + { bar: factory.number }, + { persistedMetadata: fooMetadata }, + ) {} + + assert.deepEqual(Foo.persistedMetadata, fooMetadata); + }); + + it("Field schema persisted metadata", () => { + const schemaFactory = new SchemaFactoryAlpha("com.example"); + const fooMetadata = { + persistedMetadata: { "a": 2 }, + }; + + class Foo extends schemaFactory.objectAlpha("Foo", { + bar: schemaFactory.requiredAlpha(schemaFactory.number, { + persistedMetadata: fooMetadata, + }), + baz: schemaFactory.optionalAlpha(schemaFactory.string, { + persistedMetadata: fooMetadata, + }), + qux: schemaFactory.optionalRecursiveAlpha( + schemaFactory.objectAlpha("Qux", { quux: schemaFactory.string }), + { persistedMetadata: fooMetadata }, + ), + }) {} + + const foo = hydrate(Foo, { + bar: 37, + baz: "test", + qux: { quux: "test" }, + }); + + const schema = Tree.schema(foo) as ObjectNodeSchema; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + assert.deepEqual(schema.fields.get("bar")!.persistedMetadata, fooMetadata); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + assert.deepEqual(schema.fields.get("baz")!.persistedMetadata, fooMetadata); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + assert.deepEqual(schema.fields.get("qux")!.persistedMetadata, fooMetadata); + }); + describe("deep equality", () => { const schema = new SchemaFactory("com.example"); diff --git a/packages/dds/tree/src/test/simple-tree/api/simpleSchemaToJsonSchema.spec.ts b/packages/dds/tree/src/test/simple-tree/api/simpleSchemaToJsonSchema.spec.ts index 85451538d4cc..8af9cdfbdeba 100644 --- a/packages/dds/tree/src/test/simple-tree/api/simpleSchemaToJsonSchema.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/api/simpleSchemaToJsonSchema.spec.ts @@ -88,7 +88,12 @@ describe("simpleSchemaToJsonSchema", () => { definitions: new Map([ [ "test.handle", - { leafKind: ValueSchema.FluidHandle, metadata: {}, kind: NodeKind.Leaf }, + { + leafKind: ValueSchema.FluidHandle, + metadata: {}, + persistedMetadata: undefined, + kind: NodeKind.Leaf, + }, ], ]), }; @@ -109,6 +114,7 @@ describe("simpleSchemaToJsonSchema", () => { { kind: NodeKind.Array, metadata: {}, + persistedMetadata: undefined, allowedTypesIdentifiers: new Set([stringSchema.identifier]), }, ], @@ -161,6 +167,7 @@ describe("simpleSchemaToJsonSchema", () => { { kind: NodeKind.Map, metadata: {}, + persistedMetadata: undefined, allowedTypesIdentifiers: new Set([stringSchema.identifier]), }, ], @@ -271,6 +278,7 @@ describe("simpleSchemaToJsonSchema", () => { { kind: NodeKind.Object, metadata: {}, + persistedMetadata: undefined, fields: new Map([ [ "foo", @@ -278,6 +286,7 @@ describe("simpleSchemaToJsonSchema", () => { kind: FieldKind.Optional, allowedTypesIdentifiers: new Set([numberSchema.identifier]), metadata: { description: "A number representing the concept of Foo." }, + persistedMetadata: undefined, storedKey: "foo", }, ], @@ -287,6 +296,7 @@ describe("simpleSchemaToJsonSchema", () => { kind: FieldKind.Required, allowedTypesIdentifiers: new Set([stringSchema.identifier]), metadata: { description: "A string representing the concept of Bar." }, + persistedMetadata: undefined, storedKey: "bar", }, ], @@ -298,6 +308,7 @@ describe("simpleSchemaToJsonSchema", () => { metadata: { description: "Unique identifier for the test object.", }, + persistedMetadata: undefined, storedKey: "id", }, ], @@ -404,6 +415,7 @@ describe("simpleSchemaToJsonSchema", () => { { kind: NodeKind.Object, metadata: {}, + persistedMetadata: undefined, fields: new Map([ [ "id", @@ -459,12 +471,14 @@ describe("simpleSchemaToJsonSchema", () => { { kind: NodeKind.Object, metadata: {}, + persistedMetadata: undefined, fields: new Map([ [ "foo", { kind: FieldKind.Required, metadata: {}, + persistedMetadata: undefined, allowedTypesIdentifiers: new Set([ numberSchema.identifier, stringSchema.identifier, @@ -525,12 +539,14 @@ describe("simpleSchemaToJsonSchema", () => { { kind: NodeKind.Object, metadata: {}, + persistedMetadata: undefined, fields: new Map([ [ "foo", { kind: FieldKind.Optional, metadata: {}, + persistedMetadata: undefined, allowedTypesIdentifiers: new Set([ stringSchema.identifier, "test.recursive-object", diff --git a/packages/dds/tree/src/test/simple-tree/prepareForInsertion.spec.ts b/packages/dds/tree/src/test/simple-tree/prepareForInsertion.spec.ts index 441b777514ec..f486a8f5db54 100644 --- a/packages/dds/tree/src/test/simple-tree/prepareForInsertion.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/prepareForInsertion.spec.ts @@ -135,6 +135,7 @@ describe("prepareForInsertion", () => { return { kind: kind.identifier, types: new Set(allowedTypes), + metadata: undefined, }; } diff --git a/packages/dds/tree/src/test/simple-tree/toMapTree.spec.ts b/packages/dds/tree/src/test/simple-tree/toMapTree.spec.ts index 2c1882bcf195..f2bed820eff1 100644 --- a/packages/dds/tree/src/test/simple-tree/toMapTree.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/toMapTree.spec.ts @@ -14,7 +14,10 @@ import { EmptyKey, type ExclusiveMapTree, type FieldKey, + type FieldKindIdentifier, type MapTree, + type TreeFieldStoredSchema, + type TreeNodeSchemaIdentifier, } from "../../core/index.js"; import { booleanSchema, @@ -50,6 +53,20 @@ import { } from "../../feature-libraries/index.js"; import { validateUsageError } from "../utils.js"; +/** + * Helper for building {@link TreeFieldStoredSchema}. + */ +function getFieldSchema( + kind: { identifier: FieldKindIdentifier }, + allowedTypes?: Iterable, +): TreeFieldStoredSchema { + return { + kind: kind.identifier, + types: new Set(allowedTypes), + metadata: undefined, + }; +} + describe("toMapTree", () => { let nodeKeyManager: MockNodeIdentifierManager; beforeEach(() => { diff --git a/packages/dds/tree/src/test/snapshots/encodeTreeSchema/empty - schema v2.json b/packages/dds/tree/src/test/snapshots/encodeTreeSchema/empty - schema v2.json new file mode 100644 index 000000000000..359cc020d13b --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/encodeTreeSchema/empty - schema v2.json @@ -0,0 +1,8 @@ +{ + "version": 2, + "nodes": {}, + "root": { + "kind": "Forbidden", + "types": [] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/encodeTreeSchema/simple encoded schema - schema v2.json b/packages/dds/tree/src/test/snapshots/encodeTreeSchema/simple encoded schema - schema v2.json new file mode 100644 index 000000000000..f783f848ff29 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/encodeTreeSchema/simple encoded schema - schema v2.json @@ -0,0 +1,68 @@ +{ + "version": 2, + "nodes": { + "com.fluidframework.json.array": { + "kind": { + "object": { + "": { + "kind": "Sequence", + "types": [ + "com.fluidframework.json.array", + "com.fluidframework.json.object", + "com.fluidframework.leaf.boolean", + "com.fluidframework.leaf.null", + "com.fluidframework.leaf.number", + "com.fluidframework.leaf.string" + ] + } + } + } + }, + "com.fluidframework.json.object": { + "kind": { + "map": { + "kind": "Optional", + "types": [ + "com.fluidframework.json.array", + "com.fluidframework.json.object", + "com.fluidframework.leaf.boolean", + "com.fluidframework.leaf.null", + "com.fluidframework.leaf.number", + "com.fluidframework.leaf.string" + ] + } + } + }, + "com.fluidframework.leaf.boolean": { + "kind": { + "leaf": 2 + } + }, + "com.fluidframework.leaf.null": { + "kind": { + "leaf": 4 + } + }, + "com.fluidframework.leaf.number": { + "kind": { + "leaf": 0 + } + }, + "com.fluidframework.leaf.string": { + "kind": { + "leaf": 1 + } + } + }, + "root": { + "kind": "Value", + "types": [ + "com.fluidframework.json.array", + "com.fluidframework.json.object", + "com.fluidframework.leaf.boolean", + "com.fluidframework.leaf.null", + "com.fluidframework.leaf.number", + "com.fluidframework.leaf.string" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/files/SchemaIndexFormat - schema v2.json b/packages/dds/tree/src/test/snapshots/files/SchemaIndexFormat - schema v2.json new file mode 100644 index 000000000000..740b8c921385 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/files/SchemaIndexFormat - schema v2.json @@ -0,0 +1,127 @@ +{ + "additionalProperties": false, + "type": "object", + "properties": { + "version": { + "const": 2, + "type": "number" + }, + "nodes": { + "type": "object", + "patternProperties": { + "^(.*)$": { + "additionalProperties": false, + "type": "object", + "properties": { + "kind": { + "additionalProperties": false, + "minProperties": 1, + "maxProperties": 1, + "type": "object", + "properties": { + "object": { + "type": "object", + "patternProperties": { + "^(.*)$": { + "additionalProperties": false, + "type": "object", + "properties": { + "kind": { + "type": "string" + }, + "types": { + "type": "array", + "items": { + "type": "string" + } + }, + "metadata": {} + }, + "required": [ + "kind", + "types" + ] + } + } + }, + "map": { + "additionalProperties": false, + "type": "object", + "properties": { + "kind": { + "type": "string" + }, + "types": { + "type": "array", + "items": { + "type": "string" + } + }, + "metadata": {} + }, + "required": [ + "kind", + "types" + ] + }, + "leaf": { + "anyOf": [ + { + "const": 0, + "type": "number" + }, + { + "const": 1, + "type": "number" + }, + { + "const": 2, + "type": "number" + }, + { + "const": 3, + "type": "number" + }, + { + "const": 4, + "type": "number" + } + ] + } + } + }, + "metadata": {} + }, + "required": [ + "kind" + ] + } + } + }, + "root": { + "additionalProperties": false, + "type": "object", + "properties": { + "kind": { + "type": "string" + }, + "types": { + "type": "array", + "items": { + "type": "string" + } + }, + "metadata": {} + }, + "required": [ + "kind", + "types" + ] + } + }, + "required": [ + "version", + "nodes", + "root" + ] +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/opFormat.spec.ts b/packages/dds/tree/src/test/snapshots/opFormat.spec.ts index 096d4cce8514..89571f8763b3 100644 --- a/packages/dds/tree/src/test/snapshots/opFormat.spec.ts +++ b/packages/dds/tree/src/test/snapshots/opFormat.spec.ts @@ -44,6 +44,10 @@ describe("SharedTree op format snapshots", () => { let tree: ITree & IChannel; for (const versionKey of Object.keys(SharedTreeFormatVersion)) { + // Skipping tests for v4 since we don't have the snapshots for it yet + if (versionKey === "v4") { + continue; + } describe(`using SharedTreeFormatVersion.${versionKey}`, () => { useSnapshotDirectory(`op-format/${versionKey}`); beforeEach(() => { diff --git a/packages/dds/tree/src/test/snapshots/schema-files/allTheFields-full - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/allTheFields-full - schema v2.json new file mode 100644 index 000000000000..496671aadb98 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/allTheFields-full - schema v2.json @@ -0,0 +1,45 @@ +{ + "version": 2, + "nodes": { + "com.fluidframework.leaf.number": { + "kind": { + "leaf": 0 + } + }, + "test.allTheFields": { + "kind": { + "object": { + "optional": { + "kind": "Optional", + "types": [ + "com.fluidframework.leaf.number" + ] + }, + "sequence": { + "kind": "Sequence", + "types": [ + "com.fluidframework.leaf.number" + ] + }, + "valueField": { + "kind": "Value", + "types": [ + "com.fluidframework.leaf.number" + ] + } + } + } + }, + "test.minimal": { + "kind": { + "object": {} + } + } + }, + "root": { + "kind": "Value", + "types": [ + "test.allTheFields" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/allTheFields-minimal - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/allTheFields-minimal - schema v2.json new file mode 100644 index 000000000000..496671aadb98 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/allTheFields-minimal - schema v2.json @@ -0,0 +1,45 @@ +{ + "version": 2, + "nodes": { + "com.fluidframework.leaf.number": { + "kind": { + "leaf": 0 + } + }, + "test.allTheFields": { + "kind": { + "object": { + "optional": { + "kind": "Optional", + "types": [ + "com.fluidframework.leaf.number" + ] + }, + "sequence": { + "kind": "Sequence", + "types": [ + "com.fluidframework.leaf.number" + ] + }, + "valueField": { + "kind": "Value", + "types": [ + "com.fluidframework.leaf.number" + ] + } + } + } + }, + "test.minimal": { + "kind": { + "object": {} + } + } + }, + "root": { + "kind": "Value", + "types": [ + "test.allTheFields" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/empty - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/empty - schema v2.json new file mode 100644 index 000000000000..6ae447caba61 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/empty - schema v2.json @@ -0,0 +1,8 @@ +{ + "version": 2, + "nodes": {}, + "root": { + "kind": "Optional", + "types": [] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/false boolean - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/false boolean - schema v2.json new file mode 100644 index 000000000000..4f478d762a80 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/false boolean - schema v2.json @@ -0,0 +1,16 @@ +{ + "version": 2, + "nodes": { + "com.fluidframework.leaf.boolean": { + "kind": { + "leaf": 2 + } + } + }, + "root": { + "kind": "Value", + "types": [ + "com.fluidframework.leaf.boolean" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/handle - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/handle - schema v2.json new file mode 100644 index 000000000000..670a2e2eb00d --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/handle - schema v2.json @@ -0,0 +1,16 @@ +{ + "version": 2, + "nodes": { + "com.fluidframework.leaf.handle": { + "kind": { + "leaf": 3 + } + } + }, + "root": { + "kind": "Value", + "types": [ + "com.fluidframework.leaf.handle" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/hasAllMetadata - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/hasAllMetadata - schema v2.json new file mode 100644 index 000000000000..9a7cc02e9e5a --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/hasAllMetadata - schema v2.json @@ -0,0 +1,28 @@ +{ + "version": 2, + "nodes": { + "test.hasDescriptions": { + "kind": { + "object": { + "stored-name": { + "kind": "Value", + "types": [ + "test.minimal" + ] + } + } + } + }, + "test.minimal": { + "kind": { + "object": {} + } + } + }, + "root": { + "kind": "Value", + "types": [ + "test.hasDescriptions" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/hasAllMetadataRootField - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/hasAllMetadataRootField - schema v2.json new file mode 100644 index 000000000000..2f26b1f20162 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/hasAllMetadataRootField - schema v2.json @@ -0,0 +1,28 @@ +{ + "version": 2, + "nodes": { + "test.hasDescriptions": { + "kind": { + "object": { + "stored-name": { + "kind": "Value", + "types": [ + "test.minimal" + ] + } + } + } + }, + "test.minimal": { + "kind": { + "object": {} + } + } + }, + "root": { + "kind": "Optional", + "types": [ + "test.hasDescriptions" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/hasAmbiguousField - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/hasAmbiguousField - schema v2.json new file mode 100644 index 000000000000..38a1bd182866 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/hasAmbiguousField - schema v2.json @@ -0,0 +1,34 @@ +{ + "version": 2, + "nodes": { + "test.hasAmbiguousField": { + "kind": { + "object": { + "field": { + "kind": "Value", + "types": [ + "test.minimal", + "test.minimal2" + ] + } + } + } + }, + "test.minimal": { + "kind": { + "object": {} + } + }, + "test.minimal2": { + "kind": { + "object": {} + } + } + }, + "root": { + "kind": "Value", + "types": [ + "test.hasAmbiguousField" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/hasDescriptions - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/hasDescriptions - schema v2.json new file mode 100644 index 000000000000..1a673ef5226e --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/hasDescriptions - schema v2.json @@ -0,0 +1,28 @@ +{ + "version": 2, + "nodes": { + "test.hasDescriptions": { + "kind": { + "object": { + "field": { + "kind": "Value", + "types": [ + "test.minimal" + ] + } + } + } + }, + "test.minimal": { + "kind": { + "object": {} + } + } + }, + "root": { + "kind": "Value", + "types": [ + "test.hasDescriptions" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/hasMinimalValueField - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/hasMinimalValueField - schema v2.json new file mode 100644 index 000000000000..dbef03ea4d01 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/hasMinimalValueField - schema v2.json @@ -0,0 +1,28 @@ +{ + "version": 2, + "nodes": { + "test.hasMinimalValueField": { + "kind": { + "object": { + "field": { + "kind": "Value", + "types": [ + "test.minimal" + ] + } + } + } + }, + "test.minimal": { + "kind": { + "object": {} + } + } + }, + "root": { + "kind": "Value", + "types": [ + "test.hasMinimalValueField" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/hasNumericValueField - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/hasNumericValueField - schema v2.json new file mode 100644 index 000000000000..bab5225cba21 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/hasNumericValueField - schema v2.json @@ -0,0 +1,28 @@ +{ + "version": 2, + "nodes": { + "com.fluidframework.leaf.number": { + "kind": { + "leaf": 0 + } + }, + "test.hasNumericValueField": { + "kind": { + "object": { + "field": { + "kind": "Value", + "types": [ + "com.fluidframework.leaf.number" + ] + } + } + } + } + }, + "root": { + "kind": "Value", + "types": [ + "test.hasNumericValueField" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/hasOptionalField-empty - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/hasOptionalField-empty - schema v2.json new file mode 100644 index 000000000000..ab5793c516ed --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/hasOptionalField-empty - schema v2.json @@ -0,0 +1,28 @@ +{ + "version": 2, + "nodes": { + "com.fluidframework.leaf.number": { + "kind": { + "leaf": 0 + } + }, + "test.hasOptionalField": { + "kind": { + "object": { + "field": { + "kind": "Optional", + "types": [ + "com.fluidframework.leaf.number" + ] + } + } + } + } + }, + "root": { + "kind": "Value", + "types": [ + "test.hasOptionalField" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/hasPolymorphicValueField - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/hasPolymorphicValueField - schema v2.json new file mode 100644 index 000000000000..4d3de67bd772 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/hasPolymorphicValueField - schema v2.json @@ -0,0 +1,34 @@ +{ + "version": 2, + "nodes": { + "com.fluidframework.leaf.number": { + "kind": { + "leaf": 0 + } + }, + "test.hasPolymorphicValueField": { + "kind": { + "object": { + "field": { + "kind": "Value", + "types": [ + "com.fluidframework.leaf.number", + "test.minimal" + ] + } + } + } + }, + "test.minimal": { + "kind": { + "object": {} + } + } + }, + "root": { + "kind": "Value", + "types": [ + "test.hasPolymorphicValueField" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/hasRenamedField - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/hasRenamedField - schema v2.json new file mode 100644 index 000000000000..13165b281105 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/hasRenamedField - schema v2.json @@ -0,0 +1,28 @@ +{ + "version": 2, + "nodes": { + "test.hasRenamedField": { + "kind": { + "object": { + "stored-name": { + "kind": "Value", + "types": [ + "test.minimal" + ] + } + } + } + }, + "test.minimal": { + "kind": { + "object": {} + } + } + }, + "root": { + "kind": "Value", + "types": [ + "test.hasRenamedField" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/identifier-field - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/identifier-field - schema v2.json new file mode 100644 index 000000000000..54464581a27a --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/identifier-field - schema v2.json @@ -0,0 +1,16 @@ +{ + "version": 2, + "nodes": { + "com.fluidframework.leaf.string": { + "kind": { + "leaf": 1 + } + } + }, + "root": { + "kind": "Identifier", + "types": [ + "com.fluidframework.leaf.string" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/minimal - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/minimal - schema v2.json new file mode 100644 index 000000000000..1faedea5f391 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/minimal - schema v2.json @@ -0,0 +1,16 @@ +{ + "version": 2, + "nodes": { + "test.minimal": { + "kind": { + "object": {} + } + } + }, + "root": { + "kind": "Value", + "types": [ + "test.minimal" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/node-with-identifier-field - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/node-with-identifier-field - schema v2.json new file mode 100644 index 000000000000..b2e88a500afd --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/node-with-identifier-field - schema v2.json @@ -0,0 +1,28 @@ +{ + "version": 2, + "nodes": { + "com.fluidframework.leaf.string": { + "kind": { + "leaf": 1 + } + }, + "test.hasIdentifierField": { + "kind": { + "object": { + "field": { + "kind": "Identifier", + "types": [ + "com.fluidframework.leaf.string" + ] + } + } + } + } + }, + "root": { + "kind": "Value", + "types": [ + "test.hasIdentifierField" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/null - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/null - schema v2.json new file mode 100644 index 000000000000..6e78978c22a9 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/null - schema v2.json @@ -0,0 +1,16 @@ +{ + "version": 2, + "nodes": { + "com.fluidframework.leaf.null": { + "kind": { + "leaf": 4 + } + } + }, + "root": { + "kind": "Value", + "types": [ + "com.fluidframework.leaf.null" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/numeric - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/numeric - schema v2.json new file mode 100644 index 000000000000..1a169e308c83 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/numeric - schema v2.json @@ -0,0 +1,16 @@ +{ + "version": 2, + "nodes": { + "com.fluidframework.leaf.number": { + "kind": { + "leaf": 0 + } + } + }, + "root": { + "kind": "Value", + "types": [ + "com.fluidframework.leaf.number" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/numericMap-empty - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/numericMap-empty - schema v2.json new file mode 100644 index 000000000000..d635814e4494 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/numericMap-empty - schema v2.json @@ -0,0 +1,26 @@ +{ + "version": 2, + "nodes": { + "com.fluidframework.leaf.number": { + "kind": { + "leaf": 0 + } + }, + "test.numericMap": { + "kind": { + "map": { + "kind": "Optional", + "types": [ + "com.fluidframework.leaf.number" + ] + } + } + } + }, + "root": { + "kind": "Value", + "types": [ + "test.numericMap" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/numericMap-full - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/numericMap-full - schema v2.json new file mode 100644 index 000000000000..d635814e4494 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/numericMap-full - schema v2.json @@ -0,0 +1,26 @@ +{ + "version": 2, + "nodes": { + "com.fluidframework.leaf.number": { + "kind": { + "leaf": 0 + } + }, + "test.numericMap": { + "kind": { + "map": { + "kind": "Optional", + "types": [ + "com.fluidframework.leaf.number" + ] + } + } + } + }, + "root": { + "kind": "Value", + "types": [ + "test.numericMap" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/numericSequence - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/numericSequence - schema v2.json new file mode 100644 index 000000000000..e6c2fd36deec --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/numericSequence - schema v2.json @@ -0,0 +1,16 @@ +{ + "version": 2, + "nodes": { + "com.fluidframework.leaf.number": { + "kind": { + "leaf": 0 + } + } + }, + "root": { + "kind": "Sequence", + "types": [ + "com.fluidframework.leaf.number" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/recursiveType-deeper - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/recursiveType-deeper - schema v2.json new file mode 100644 index 000000000000..a2cd9bd24370 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/recursiveType-deeper - schema v2.json @@ -0,0 +1,23 @@ +{ + "version": 2, + "nodes": { + "test.recursiveType": { + "kind": { + "object": { + "field": { + "kind": "Optional", + "types": [ + "test.recursiveType" + ] + } + } + } + } + }, + "root": { + "kind": "Value", + "types": [ + "test.recursiveType" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/recursiveType-empty - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/recursiveType-empty - schema v2.json new file mode 100644 index 000000000000..a2cd9bd24370 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/recursiveType-empty - schema v2.json @@ -0,0 +1,23 @@ +{ + "version": 2, + "nodes": { + "test.recursiveType": { + "kind": { + "object": { + "field": { + "kind": "Optional", + "types": [ + "test.recursiveType" + ] + } + } + } + } + }, + "root": { + "kind": "Value", + "types": [ + "test.recursiveType" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/recursiveType-recursive - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/recursiveType-recursive - schema v2.json new file mode 100644 index 000000000000..a2cd9bd24370 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/recursiveType-recursive - schema v2.json @@ -0,0 +1,23 @@ +{ + "version": 2, + "nodes": { + "test.recursiveType": { + "kind": { + "object": { + "field": { + "kind": "Optional", + "types": [ + "test.recursiveType" + ] + } + } + } + } + }, + "root": { + "kind": "Value", + "types": [ + "test.recursiveType" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/schema-files/true boolean - schema v2.json b/packages/dds/tree/src/test/snapshots/schema-files/true boolean - schema v2.json new file mode 100644 index 000000000000..4f478d762a80 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/schema-files/true boolean - schema v2.json @@ -0,0 +1,16 @@ +{ + "version": 2, + "nodes": { + "com.fluidframework.leaf.boolean": { + "kind": { + "leaf": 2 + } + } + }, + "root": { + "kind": "Value", + "types": [ + "com.fluidframework.leaf.boolean" + ] + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/summary.spec.ts b/packages/dds/tree/src/test/snapshots/summary.spec.ts index 9befc24496a9..db6729ac3c96 100644 --- a/packages/dds/tree/src/test/snapshots/summary.spec.ts +++ b/packages/dds/tree/src/test/snapshots/summary.spec.ts @@ -18,6 +18,10 @@ describe("snapshot tests", () => { // Friendly description of tree encoding type const treeEncodeKey = TreeCompressionStrategy[treeEncodeType]; for (const formatVersionKey of Object.keys(SharedTreeFormatVersion)) { + // Skipping tests for v4 since we don't have the snapshots for it yet + if (formatVersionKey === "v4") { + continue; + } describe(`Using TreeCompressionStrategy.${treeEncodeKey} and SharedTreeFormatVersion.${formatVersionKey}`, () => { useSnapshotDirectory(`summary/${treeEncodeKey}/${formatVersionKey}`); const options: SharedTreeOptions = { diff --git a/packages/dds/tree/src/test/testTrees.ts b/packages/dds/tree/src/test/testTrees.ts index 11d137488192..4ba392c1b3c4 100644 --- a/packages/dds/tree/src/test/testTrees.ts +++ b/packages/dds/tree/src/test/testTrees.ts @@ -180,6 +180,7 @@ export const allTheFields = new ObjectNodeStoredSchema( { kind: FieldKinds.optional.identifier, types: numberSet, + metadata: undefined, }, ], [ @@ -187,6 +188,7 @@ export const allTheFields = new ObjectNodeStoredSchema( { kind: FieldKinds.required.identifier, types: numberSet, + metadata: undefined, }, ], [ @@ -194,6 +196,7 @@ export const allTheFields = new ObjectNodeStoredSchema( { kind: FieldKinds.sequence.identifier, types: numberSet, + metadata: undefined, }, ], ]), @@ -270,7 +273,11 @@ export const testTrees: readonly TestTree[] = [ "numericSequence", { ...toStoredSchema(factory.number), - rootFieldSchema: { kind: FieldKinds.sequence.identifier, types: numberSet }, + rootFieldSchema: { + kind: FieldKinds.sequence.identifier, + types: numberSet, + metadata: undefined, + }, }, jsonableTreesFromFieldCursor(fieldJsonCursor([1, 2, 3])), ), @@ -302,6 +309,7 @@ export const testTrees: readonly TestTree[] = [ rootFieldSchema: { kind: FieldKinds.required.identifier, types: new Set([allTheFieldsName]), + metadata: undefined, }, }, [ @@ -318,6 +326,7 @@ export const testTrees: readonly TestTree[] = [ rootFieldSchema: { kind: FieldKinds.required.identifier, types: new Set([allTheFieldsName]), + metadata: undefined, }, }, [ diff --git a/packages/dds/tree/src/test/utils.ts b/packages/dds/tree/src/test/utils.ts index 7835753c61e2..4dfa567052bf 100644 --- a/packages/dds/tree/src/test/utils.ts +++ b/packages/dds/tree/src/test/utils.ts @@ -630,15 +630,11 @@ export function validateTree(tree: ITreeCheckout, expected: JsonableTree[]): voi assert.deepEqual(actual, expected); } -const schemaCodec = makeSchemaCodec({ jsonValidator: typeboxValidator }, SchemaVersion.v1); - -// If you are adding a new schema format, consider changing the encoding format used in the above codec, given +// If you are adding a new schema format, consider changing the encoding format used for this codec, given // that equality of two schemas in tests is achieved by deep-comparing their persisted representations. -// Note we have to divide the length of the return value from `Object.keys` to get the number of enum entries. -assert( - Object.keys(SchemaVersion).length / 2 === 1, - "This code only handles a single schema codec version.", -); +// If the newer format is a superset of the previous format, it can be safely used for comparisons. This is the +// case with schema format v2. +const schemaCodec = makeSchemaCodec({ jsonValidator: typeboxValidator }, SchemaVersion.v2); export function checkRemovedRootsAreSynchronized(trees: readonly ITreeCheckout[]): void { if (trees.length > 1) { diff --git a/packages/dds/tree/src/util/utils.ts b/packages/dds/tree/src/util/utils.ts index 97b5c89c92b2..7cbb68bbbddf 100644 --- a/packages/dds/tree/src/util/utils.ts +++ b/packages/dds/tree/src/util/utils.ts @@ -310,6 +310,7 @@ export type JsonCompatibleObject = { [P in string]?: JsonCompati * @remarks * This does not robustly forbid non json comparable data via type checking, * but instead mostly restricts access to it. + * @alpha */ export type JsonCompatibleReadOnly = | string @@ -325,6 +326,7 @@ export type JsonCompatibleReadOnly = * @remarks * This does not robustly forbid non json comparable data via type checking, * but instead mostly restricts access to it. + * @alpha */ export type JsonCompatibleReadOnlyObject = { readonly [P in string]?: JsonCompatibleReadOnly }; 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 a4d703ea634a..568217867f54 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 @@ -225,6 +225,11 @@ export interface FieldProps { readonly metadata?: FieldSchemaMetadata; } +// @alpha +export interface FieldPropsAlpha extends FieldProps { + readonly persistedMetadata?: JsonCompatibleReadOnlyObject | undefined; +} + // @public @sealed export class FieldSchema { protected constructor( @@ -242,13 +247,14 @@ export class FieldSchema extends FieldSchema implements SimpleFieldSchema { - protected constructor(kind: Kind, types: Types, annotatedAllowedTypes: ImplicitAnnotatedAllowedTypes, props?: FieldProps); + protected constructor(kind: Kind, types: Types, annotatedAllowedTypes: ImplicitAnnotatedAllowedTypes, props?: FieldPropsAlpha); // (undocumented) get allowedTypesIdentifiers(): ReadonlySet; readonly allowedTypesMetadata: AllowedTypesMetadata; // (undocumented) readonly annotatedAllowedTypes: ImplicitAnnotatedAllowedTypes; get annotatedAllowedTypeSet(): ReadonlyMap; + get persistedMetadata(): JsonCompatibleReadOnlyObject | undefined; } // @alpha @sealed @system @@ -782,6 +788,14 @@ export type JsonCompatibleObject = { [P in string]?: JsonCompatible; }; +// @alpha +export type JsonCompatibleReadOnly = string | number | boolean | null | readonly JsonCompatibleReadOnly[] | JsonCompatibleReadOnlyObject; + +// @alpha +export type JsonCompatibleReadOnlyObject = { + readonly [P in string]?: JsonCompatibleReadOnly; +}; + // @alpha @sealed export type JsonFieldSchema = { readonly description?: string | undefined; @@ -943,6 +957,11 @@ export interface NodeSchemaOptions { readonly metadata?: NodeSchemaMetadata | undefined; } +// @alpha @sealed +export interface NodeSchemaOptionsAlpha extends NodeSchemaOptions { + readonly persistedMetadata?: JsonCompatibleReadOnlyObject | undefined; +} + // @alpha export const noopValidator: JsonValidator; @@ -1116,30 +1135,33 @@ export class SchemaFactory extends SchemaFactory { - arrayAlpha(name: Name, allowedTypes: T, options?: NodeSchemaOptions): ArrayNodeCustomizableSchema, T, true, TCustomMetadata>; + arrayAlpha(name: Name, allowedTypes: T, options?: NodeSchemaOptionsAlpha): ArrayNodeCustomizableSchema, T, true, TCustomMetadata>; arrayRecursive(name: Name, allowedTypes: T, options?: NodeSchemaOptions): ArrayNodeCustomizableSchemaUnsafe, T, TCustomMetadata>; - static readonly identifier: (props?: Omit, "defaultProvider"> | undefined) => FieldSchemaAlpha_2 & SimpleLeafNodeSchema_2, TCustomMetadata>; + static readonly identifier: (props?: Omit, "defaultProvider"> | undefined) => FieldSchemaAlpha & SimpleLeafNodeSchema_2, TCustomMetadata>; static readonly leaves: readonly [LeafSchema_2<"string", string> & SimpleLeafNodeSchema_2, LeafSchema_2<"number", number> & SimpleLeafNodeSchema_2, LeafSchema_2<"boolean", boolean> & SimpleLeafNodeSchema_2, LeafSchema_2<"null", null> & SimpleLeafNodeSchema_2, LeafSchema_2<"handle", IFluidHandle_2> & SimpleLeafNodeSchema_2]; - mapAlpha(name: Name, allowedTypes: T, options?: NodeSchemaOptions): MapNodeCustomizableSchema, T, true, TCustomMetadata>; + mapAlpha(name: Name, allowedTypes: T, options?: NodeSchemaOptionsAlpha): MapNodeCustomizableSchema, T, true, TCustomMetadata>; mapRecursive(name: Name, allowedTypes: T, options?: NodeSchemaOptions): MapNodeCustomizableSchemaUnsafe, T, TCustomMetadata>; objectAlpha, const TCustomMetadata = unknown>(name: Name, fields: T, options?: SchemaFactoryObjectOptions): ObjectNodeSchema, T, true, TCustomMetadata> & { readonly createFromInsertable: unknown; }; objectRecursive, const TCustomMetadata = unknown>(name: Name, t: T, options?: SchemaFactoryObjectOptions): TreeNodeSchemaClass, NodeKind.Object, System_Unsafe.TreeObjectNodeUnsafe>, object & System_Unsafe.InsertableObjectFromSchemaRecordUnsafe, false, T, never, TCustomMetadata> & SimpleObjectNodeSchema & Pick; static readonly optional: { - (t: T, props?: Omit, "defaultProvider"> | undefined): FieldSchemaAlpha_2; - (t: T_1, props?: Omit, "defaultProvider"> | undefined): FieldSchemaAlpha_2, TCustomMetadata_1>; + (t: T, props?: Omit, "defaultProvider"> | undefined): FieldSchemaAlpha; + (t: T_1, props?: Omit, "defaultProvider"> | undefined): FieldSchemaAlpha, TCustomMetadata_1>; }; - static readonly optionalRecursive: (t: T, props?: Omit, "defaultProvider"> | undefined) => FieldSchemaAlphaUnsafe_2; + optionalAlpha(t: T, props?: Omit, "defaultProvider">): FieldSchemaAlpha, TCustomMetadata>; + static readonly optionalRecursive: (t: T, props?: Omit, "defaultProvider"> | undefined) => FieldSchemaAlphaUnsafe; + optionalRecursiveAlpha(t: T, props?: Omit, "defaultProvider">): FieldSchemaAlphaUnsafe; static readonly required: { - (t: T, props?: Omit, "defaultProvider"> | undefined): FieldSchemaAlpha_2; - (t: T_1, props?: Omit, "defaultProvider"> | undefined): FieldSchemaAlpha_2, TCustomMetadata_1>; + (t: T, props?: Omit, "defaultProvider"> | undefined): FieldSchemaAlpha; + (t: T_1, props?: Omit, "defaultProvider"> | undefined): FieldSchemaAlpha, TCustomMetadata_1>; }; + requiredAlpha(t: T, props?: Omit, "defaultProvider">): FieldSchemaAlpha, TCustomMetadata>; scopedFactory(name: T): SchemaFactoryAlpha, TNameInner>; } // @alpha -export interface SchemaFactoryObjectOptions extends NodeSchemaOptions { +export interface SchemaFactoryObjectOptions extends NodeSchemaOptionsAlpha { allowUnknownOptionalFields?: boolean; } @@ -1190,6 +1212,7 @@ export const SharedTreeFormatVersion: { readonly v1: 1; readonly v2: 2; readonly v3: 3; + readonly v4: 4; }; // @alpha @@ -1199,7 +1222,7 @@ export type SharedTreeFormatVersion = typeof SharedTreeFormatVersion; export type SharedTreeOptions = Partial & Partial & ForestOptions; // @alpha @sealed -export interface SimpleArrayNodeSchema extends SimpleNodeSchemaBase { +export interface SimpleArrayNodeSchema extends SimpleNodeSchemaBaseAlpha { readonly allowedTypesIdentifiers: ReadonlySet; } @@ -1211,12 +1234,12 @@ export interface SimpleFieldSchema { } // @alpha @sealed -export interface SimpleLeafNodeSchema extends SimpleNodeSchemaBase { +export interface SimpleLeafNodeSchema extends SimpleNodeSchemaBaseAlpha { readonly leafKind: ValueSchema; } // @alpha @sealed -export interface SimpleMapNodeSchema extends SimpleNodeSchemaBase { +export interface SimpleMapNodeSchema extends SimpleNodeSchemaBaseAlpha { readonly allowedTypesIdentifiers: ReadonlySet; } @@ -1229,13 +1252,18 @@ export interface SimpleNodeSchemaBase; } +// @alpha @sealed @system +export interface SimpleNodeSchemaBaseAlpha extends SimpleNodeSchemaBase { + readonly persistedMetadata: JsonCompatibleReadOnlyObject | undefined; +} + // @alpha @sealed export interface SimpleObjectFieldSchema extends SimpleFieldSchema { readonly storedKey: string; } // @alpha @sealed -export interface SimpleObjectNodeSchema extends SimpleNodeSchemaBase { +export interface SimpleObjectNodeSchema extends SimpleNodeSchemaBaseAlpha { readonly fields: ReadonlyMap; } @@ -1268,7 +1296,7 @@ export namespace System_TableSchema { props: InsertableTreeFieldFromImplicitField>; }), true, { readonly props: TPropsSchema; - readonly id: FieldSchema_2, unknown>; + readonly id: FieldSchema_2, unknown>; }>; // @system export type CreateRowOptionsBase = OptionsWithSchemaFactory & OptionsWithCellSchema; @@ -1282,8 +1310,8 @@ export namespace System_TableSchema { props: InsertableTreeFieldFromImplicitField>; }), true, { readonly props: TPropsSchema; - readonly id: FieldSchema_2, unknown>; - readonly cells: FieldSchema_2, "Row.cells">, NodeKind.Map, TreeMapNode_2 & WithType, "Row.cells">, NodeKind.Map, unknown>, MapNodeInsertableData_2, true, TCellSchema, undefined>, unknown>; + readonly id: FieldSchema_2, unknown>; + readonly cells: FieldSchema_2, "Row.cells">, NodeKind.Map, TreeMapNode_2 & WithType, "Row.cells">, NodeKind.Map, unknown>, MapNodeInsertableData_2, true, TCellSchema, undefined>, unknown>; }>; // @system export function createTableSchema, const TRowSchema extends RowSchemaBase>(inputSchemaFactory: SchemaFactoryAlpha, _cellSchema: TCellSchema, columnSchema: TColumnSchema, rowSchema: TRowSchema): TreeNodeSchemaCore_2, "Table">, NodeKind.Object, true, {