diff --git a/packages/common/core-utils/src/index.ts b/packages/common/core-utils/src/index.ts index 4dc3f7527a43..0cc4e184964d 100644 --- a/packages/common/core-utils/src/index.ts +++ b/packages/common/core-utils/src/index.ts @@ -33,3 +33,4 @@ export { PromiseTimer, setLongTimeout, Timer } from "./timer.js"; export { unreachableCase } from "./unreachable.js"; export { isObject, isPromiseLike } from "./typesGuards.js"; export { oob } from "./oob.js"; +export { transformMapValues } from "./map.js"; diff --git a/packages/common/core-utils/src/map.ts b/packages/common/core-utils/src/map.ts new file mode 100644 index 000000000000..67f882c8ed46 --- /dev/null +++ b/packages/common/core-utils/src/map.ts @@ -0,0 +1,19 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * Transform the values of a Map using the provided transform function. + * @param map - The map to transform. + * @param transformValue - A method for transforming values in the map. + * @returns A new map with the transformed values. + * + * @internal + */ +export function transformMapValues( + map: ReadonlyMap, + transformValue: (value: InputValue, key: Key) => OutputValue, +): Map { + return new Map([...map.entries()].map(([key, value]) => [key, transformValue(value, key)])); +} diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index c5326a088e65..c309bc302c0c 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -198,6 +198,9 @@ export function createSimpleTreeIndex(view: TreeView, indexer: Map, getValue: (nodes: TreeIndexNodes>) => TValue, isKeyValid: (key: TreeIndexKey) => key is TKey, indexableSchema: readonly TSchema[]): SimpleTreeIndex; +// @alpha +export function decodeSimpleSchema(encodedSchema: JsonCompatibleReadOnly, validator?: FormatValidator): SimpleTreeSchema; + // @public @sealed @system interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } @@ -213,6 +216,9 @@ export interface DirtyTreeMap { // @alpha export type DirtyTreeStatus = "new" | "changed" | "moved"; +// @alpha +export function encodeSimpleSchema(simpleSchema: SimpleTreeSchema): JsonCompatibleReadOnly; + // @beta export function enumFromStrings(factory: SchemaFactory, members: Members): ((value: TValue) => TValue extends unknown ? TreeNode & { readonly value: TValue; @@ -1071,6 +1077,7 @@ export interface SimpleObjectFieldSchema extends SimpleFieldSchema { // @alpha @sealed export interface SimpleObjectNodeSchema extends SimpleNodeSchemaBaseAlpha { + readonly allowUnknownOptionalFields: boolean | undefined; readonly fields: ReadonlyMap; } diff --git a/packages/dds/tree/src/index.ts b/packages/dds/tree/src/index.ts index b1b2077e872a..fb204e1fb2c5 100644 --- a/packages/dds/tree/src/index.ts +++ b/packages/dds/tree/src/index.ts @@ -283,6 +283,8 @@ export { type SchemaFactory_base, type NumberKeys, type SimpleAllowedTypeAttributes, + encodeSimpleSchema, + decodeSimpleSchema, } from "./simple-tree/index.js"; export { SharedTree, diff --git a/packages/dds/tree/src/shared-tree/sharedTree.ts b/packages/dds/tree/src/shared-tree/sharedTree.ts index 81905a810108..4f4d2975d7a4 100644 --- a/packages/dds/tree/src/shared-tree/sharedTree.ts +++ b/packages/dds/tree/src/shared-tree/sharedTree.ts @@ -935,6 +935,8 @@ function exportSimpleFieldSchemaStored(schema: TreeFieldStoredSchema): SimpleFie * Export a {@link SimpleNodeSchema} from a {@link TreeNodeStoredSchema}. * @privateRemarks * TODO: Persist node metadata once schema FormatV2 is supported. + * Note on SimpleNodeSchema construction: In the persisted format `persistedMetadata` is just called `metadata` whereas the `metadata` + * field on SimpleNodeSchema is not persisted. */ function exportSimpleNodeSchemaStored(schema: TreeNodeStoredSchema): SimpleNodeSchema { const arrayTypes = tryStoredSchemaAsArray(schema); @@ -951,7 +953,13 @@ function exportSimpleNodeSchemaStored(schema: TreeNodeStoredSchema): SimpleNodeS for (const [storedKey, field] of schema.objectNodeFields) { fields.set(storedKey, { ...exportSimpleFieldSchemaStored(field), storedKey }); } - return { kind: NodeKind.Object, fields, metadata: {}, persistedMetadata: schema.metadata }; + return { + kind: NodeKind.Object, + fields, + allowUnknownOptionalFields: undefined, + metadata: {}, + persistedMetadata: schema.metadata, + }; } if (schema instanceof MapNodeStoredSchema) { assert( diff --git a/packages/dds/tree/src/simple-tree/api/index.ts b/packages/dds/tree/src/simple-tree/api/index.ts index 2b7830687431..a7a05f753d82 100644 --- a/packages/dds/tree/src/simple-tree/api/index.ts +++ b/packages/dds/tree/src/simple-tree/api/index.ts @@ -167,3 +167,7 @@ export { getShouldIncrementallySummarizeAllowedTypes, incrementalSummaryHint, } from "./incrementalAllowedTypes.js"; +export { + encodeSimpleSchema, + decodeSimpleSchema, +} from "./simpleSchemaCodec.js"; diff --git a/packages/dds/tree/src/simple-tree/api/schemaFromSimple.ts b/packages/dds/tree/src/simple-tree/api/schemaFromSimple.ts index 0fe717923caa..6980e7cd7d98 100644 --- a/packages/dds/tree/src/simple-tree/api/schemaFromSimple.ts +++ b/packages/dds/tree/src/simple-tree/api/schemaFromSimple.ts @@ -106,9 +106,12 @@ function generateNode( for (const [key, field] of schema.fields) { fields[key] = generateFieldSchema(field, context, field.storedKey); } - // Here allowUnknownOptionalFields is implicitly defaulting. This is a subjective policy choice: - // users of this code are expected to handle what ever choice this code makes for cases like this. - return factory.objectAlpha(id, fields, { metadata: schema.metadata }); + // Here allowUnknownOptionalFields is implicitly defaulting in the case where the input schema does not explicitly specify the value. + // This is a subjective policy choice: users of this code are expected to handle what ever choice this code makes for cases like this. + return factory.objectAlpha(id, fields, { + metadata: schema.metadata, + allowUnknownOptionalFields: schema.allowUnknownOptionalFields ?? false, + }); } case NodeKind.Array: return factory.arrayAlpha(id, generateAllowedTypes(schema.simpleAllowedTypes, context), { diff --git a/packages/dds/tree/src/simple-tree/api/simpleSchemaCodec.ts b/packages/dds/tree/src/simple-tree/api/simpleSchemaCodec.ts new file mode 100644 index 000000000000..dce051d447a2 --- /dev/null +++ b/packages/dds/tree/src/simple-tree/api/simpleSchemaCodec.ts @@ -0,0 +1,360 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { objectToMap, type JsonCompatibleReadOnly } from "../../util/index.js"; +import { unreachableCase, transformMapValues } from "@fluidframework/core-utils/internal"; +import type { + SimpleAllowedTypeAttributes, + SimpleArrayNodeSchema, + SimpleFieldSchema, + SimpleLeafNodeSchema, + SimpleMapNodeSchema, + SimpleNodeSchema, + SimpleObjectFieldSchema, + SimpleObjectNodeSchema, + SimpleRecordNodeSchema, + SimpleTreeSchema, +} from "../simpleSchema.js"; +import { NodeKind } from "../core/index.js"; +import type { FieldKind } from "../fieldSchema.js"; +import type { ValueSchema } from "../../core/index.js"; +import { UsageError } from "@fluidframework/telemetry-utils/internal"; +import * as Format from "../simpleSchemaFormatV1.js"; +import { + DiscriminatedUnionDispatcher, + extractJsonValidator, + FormatValidatorNoOp, + type FormatValidator, +} from "../../codec/index.js"; + +/** + * Encodes a simple schema (view or stored) into a serializable format. + * @remarks The JSON-compatible schema returned from this method is only intended for use in snapshots/comparisons of schemas. + * It is not possible to reconstruct a full schema (including metadata and persistedMetadata) from the encoded format. + * @param treeSchema - The tree schema to convert. + * @returns A serializable representation of the schema. + * + * @alpha + */ +export function encodeSimpleSchema(simpleSchema: SimpleTreeSchema): JsonCompatibleReadOnly { + // Convert types to serializable forms + const encodedDefinitions: Format.SimpleSchemaDefinitionsFormat = {}; + + for (const [identifier, schema] of simpleSchema.definitions) { + const encodedDefinition = encodeNodeSchema(schema); + encodedDefinitions[identifier] = encodedDefinition; + } + + const encodedSchema: Format.SimpleTreeSchemaFormat = { + version: Format.SimpleSchemaFormatVersion.v1, + root: encodeField(simpleSchema.root), + definitions: encodedDefinitions, + }; + + return encodedSchema; +} + +/** + * Decodes a JSON-compatible schema into a simple schema. + * @param encodedSchema - The encoded schema to decode. + * @param validator - The format validator to use to validate the encoded schema. + * @returns A decoded simple schema. + * @throws Will throw a usage error if the encoded schema is not in the expected format. + * + * @alpha + */ +export function decodeSimpleSchema( + encodedSchema: JsonCompatibleReadOnly, + validator?: FormatValidator, +): SimpleTreeSchema { + const effectiveValidator = validator ?? FormatValidatorNoOp; + const compiledValidator = extractJsonValidator(effectiveValidator).compile( + Format.SimpleTreeSchemaFormat, + ); + if (!compiledValidator.check(encodedSchema)) { + throw new UsageError( + "The provided simple schema is not valid according to the schema format.", + ); + } + + return { + root: decodeSimpleFieldSchema(encodedSchema.root), + definitions: new Map( + transformMapValues(objectToMap(encodedSchema.definitions), (value, key) => { + return decodeNodeSchema(value); + }), + ), + }; +} + +/** + * Encodes a node schema to a serializable object. + * @param schema - The node schema to convert. + * @returns A serializable representation of the node schema. + */ +function encodeNodeSchema(schema: SimpleNodeSchema): Format.SimpleNodeSchemaUnionFormat { + const kind = schema.kind; + switch (kind) { + case NodeKind.Leaf: + return { leaf: encodeLeafNode(schema) }; + case NodeKind.Array: + return { array: encodeContainerNode(schema) }; + case NodeKind.Map: + return { map: encodeContainerNode(schema) }; + case NodeKind.Record: + return { record: encodeContainerNode(schema) }; + case NodeKind.Object: + return { object: encodeObjectNode(schema) }; + default: { + unreachableCase(kind); + } + } +} + +/** + * Encodes a leaf node schema to a serializable object. + * @param schema - The leaf node schema to convert. + * @returns A serializable representation of the leaf node schema. + */ +function encodeLeafNode(schema: SimpleLeafNodeSchema): Format.SimpleLeafNodeSchemaFormat { + return { + kind: schema.kind, + leafKind: schema.leafKind, + }; +} + +/** + * Encodes a container node schema (a simple schema that is a Map, Array, or Record) to a serializable object. + * @param schema - The container node schema to convert. + * @returns A serializable representation of the container node schema. Includes the `kind` for disambiguation between different + * container kinds. + */ +function encodeContainerNode( + schema: SimpleArrayNodeSchema | SimpleMapNodeSchema | SimpleRecordNodeSchema, +): + | Format.SimpleArrayNodeSchemaFormat + | Format.SimpleMapNodeSchemaFormat + | Format.SimpleRecordNodeSchemaFormat { + return { + kind: schema.kind, + simpleAllowedTypes: encodeSimpleAllowedTypes(schema.simpleAllowedTypes), + }; +} + +/** + * Encodes a simple allowed types map to a serializable object. Needed because JSON serialization does not support Maps. + * @param simpleAllowedTypes - The simple allowed types map to convert. + * @returns A serializable representation of the simple allowed types. + */ +function encodeSimpleAllowedTypes( + simpleAllowedTypes: ReadonlyMap, +): Format.SimpleAllowedTypesFormat { + const encodedAllowedTypes: Format.SimpleAllowedTypesFormat = {}; + for (const [identifier, attributes] of simpleAllowedTypes) { + encodedAllowedTypes[identifier] = { + isStaged: attributes.isStaged, + }; + } + return encodedAllowedTypes; +} + +/** + * Encodes an object node schema to a serializable object. + * @param schema - The object node schema to convert. + * @returns A serializable representation of the object node schema. + */ +function encodeObjectNode( + schema: SimpleObjectNodeSchema, +): Format.SimpleObjectNodeSchemaFormat { + const encodedFields: Format.SimpleObjectFieldSchemasFormat = {}; + for (const [fieldKey, fieldSchema] of schema.fields) { + encodedFields[fieldKey] = encodeObjectField(fieldSchema); + } + + return { + kind: schema.kind, + fields: encodedFields, + allowUnknownOptionalFields: schema.allowUnknownOptionalFields, + }; +} + +/** + * Encodes an object field schema to a serializable object. + * @param fieldSchema - The object field schema to convert. + * @returns A serializable representation of the object field schema. + */ +function encodeObjectField( + fieldSchema: SimpleObjectFieldSchema, +): Format.SimpleObjectFieldSchemaFormat { + const encodedField = encodeField(fieldSchema); + return { ...encodedField, storedKey: fieldSchema.storedKey }; +} + +/** + * Encodes a field schema to a serializable object. + * @param fieldSchema - The field schema to convert. + * @returns A serializable representation of the field schema. + */ +function encodeField(fieldSchema: SimpleFieldSchema): Format.SimpleFieldSchemaFormat { + return { + kind: fieldSchema.kind, + simpleAllowedTypes: encodeSimpleAllowedTypes(fieldSchema.simpleAllowedTypes), + }; +} + +const decodeNodeSchemaDispatcher: DiscriminatedUnionDispatcher< + Format.SimpleNodeSchemaUnionFormat, + [], + | SimpleLeafNodeSchema + | SimpleArrayNodeSchema + | SimpleMapNodeSchema + | SimpleRecordNodeSchema + | SimpleObjectNodeSchema +> = new DiscriminatedUnionDispatcher({ + leaf: decodeLeafNode, + array: decodeContainerNode, + map: decodeContainerNode, + record: decodeContainerNode, + object: decodeObjectNode, +}); + +/** + * Decodes a node schema from a JSON-compatible object. + * @param encodedNodeSchema - The encoded node schema to decode. + * @returns The decoded node schema. + */ +function decodeNodeSchema( + encodedNodeSchema: Format.SimpleNodeSchemaUnionFormat, +): + | SimpleLeafNodeSchema + | SimpleArrayNodeSchema + | SimpleMapNodeSchema + | SimpleRecordNodeSchema + | SimpleObjectNodeSchema { + return decodeNodeSchemaDispatcher.dispatch(encodedNodeSchema); +} + +/** + * Decodes a container node schema (array, map, record) from a JSON-compatible object. + * @param encodedContainerSchema - The encoded schema to decode. + * @returns The decoded container node schema. + */ +function decodeContainerNode( + encodedContainerSchema: + | Format.SimpleArrayNodeSchemaFormat + | Format.SimpleMapNodeSchemaFormat + | Format.SimpleRecordNodeSchemaFormat, +): SimpleArrayNodeSchema | SimpleMapNodeSchema | SimpleRecordNodeSchema { + return { + kind: encodedContainerSchema.kind as NodeKind.Array | NodeKind.Map | NodeKind.Record, + simpleAllowedTypes: decodeSimpleAllowedTypes(encodedContainerSchema.simpleAllowedTypes), + // We cannot encode persistedMetadata or metadata, so we explicitly set them to empty values. + persistedMetadata: undefined, + metadata: {}, + }; +} + +/** + * Decodes a leaf node schema from a JSON-compatible object. + * @param encodedLeafSchema - The encoded leaf node schema. + * @returns The decoded leaf node schema. + */ +function decodeLeafNode( + encodedLeafSchema: Format.SimpleLeafNodeSchemaFormat, +): SimpleLeafNodeSchema { + return { + kind: NodeKind.Leaf, + leafKind: encodedLeafSchema.leafKind as ValueSchema, + // We cannot encode persistedMetadata or metadata, so we explicitly set them to empty values. + persistedMetadata: undefined, + metadata: {}, + }; +} + +/** + * Decodes a object node schema from a JSON-compatible object. + * @param encodedObjectSchema - The encoded object node schema. + * @returns The decoded object node schema. + */ +function decodeObjectNode( + encodedObjectSchema: Format.SimpleObjectNodeSchemaFormat, +): SimpleObjectNodeSchema { + return { + kind: NodeKind.Object, + fields: decodeObjectFields(encodedObjectSchema.fields), + // It is possible for allowUnknownOptionalFields to be undefined. This happens when serializing a Simple Schema derived + // from a stored schema. + allowUnknownOptionalFields: encodedObjectSchema.allowUnknownOptionalFields, + // We cannot encode persistedMetadata or metadata, so we explicitly set them to empty values when decoding. + persistedMetadata: undefined, + metadata: {}, + }; +} + +/** + * Decodes a map of object fields from a JSON-compatible object. + * @param encodedFields - The encoded fields. + * @returns A map of the decoded object fields. + */ +function decodeObjectFields( + encodedFields: Format.SimpleObjectFieldSchemasFormat, +): ReadonlyMap { + const fields = new Map(); + for (const [fieldKey, fieldSchema] of Object.entries(encodedFields)) { + fields.set(fieldKey, decodeObjectField(fieldSchema)); + } + return fields; +} + +/** + * Decodes a {@link SimpleObjectFieldSchema} from a JSON-compatible object. + * @param encodedField - The encoded field schema. + * @returns The decoded simple object field schema. + */ +function decodeObjectField( + encodedField: Format.SimpleObjectFieldSchemaFormat, +): SimpleObjectFieldSchema { + const baseField = decodeSimpleFieldSchema(encodedField); + return { + ...baseField, + storedKey: encodedField.storedKey, + }; +} + +/** + * Decodes a {@link SimpleFieldSchema} from a JSON-compatible object. + * @param encodedField - The encoded field schema. + * @returns The decoded simple field schema. + */ +function decodeSimpleFieldSchema( + encodedField: Format.SimpleFieldSchemaFormat, +): SimpleFieldSchema { + return { + kind: encodedField.kind as FieldKind, + simpleAllowedTypes: decodeSimpleAllowedTypes(encodedField.simpleAllowedTypes), + // We cannot encode persistedMetadata or metadata, so we explicitly set them to empty values when decoding. + persistedMetadata: undefined, + metadata: {}, + }; +} + +/** + * Decodes a simple allowed types map from a JSON-compatible object. + * @param encodedAllowedTypes - The encoded simple allowed types. + * @returns A map of the decoded simple allowed types. + */ +function decodeSimpleAllowedTypes( + encodedAllowedTypes: Format.SimpleAllowedTypesFormat, +): ReadonlyMap { + const untypedMap = objectToMap(encodedAllowedTypes); + + const simpleAllowedTypes = transformMapValues(untypedMap, (value) => { + return { + isStaged: value.isStaged, + } satisfies SimpleAllowedTypeAttributes; + }); + + return simpleAllowedTypes; +} diff --git a/packages/dds/tree/src/simple-tree/api/viewSchemaToSimpleSchema.ts b/packages/dds/tree/src/simple-tree/api/viewSchemaToSimpleSchema.ts index 735fedc9ec37..1d5e2e774557 100644 --- a/packages/dds/tree/src/simple-tree/api/viewSchemaToSimpleSchema.ts +++ b/packages/dds/tree/src/simple-tree/api/viewSchemaToSimpleSchema.ts @@ -6,7 +6,6 @@ import { assert, unreachableCase } from "@fluidframework/core-utils/internal"; import { normalizeFieldSchema, type ImplicitFieldSchema } from "../fieldSchema.js"; import type { - SimpleAllowedTypeAttributes, SimpleArrayNodeSchema, SimpleFieldSchema, SimpleLeafNodeSchema, @@ -32,7 +31,6 @@ import { LeafNodeSchema } from "../leafNodeSchema.js"; * * @param schema - The schema to convert * @param copySchemaObjects - If true, TreeNodeSchema and FieldSchema are copied into plain JavaScript objects. Either way, custom metadata is referenced and not copied. - * @param isViewSchema - If true (default), properties used by view schema but not part of stored schema (for example, `isStaged` on allowed types) are preserved in the output. * * @remarks * Given that the Schema types used in {@link ImplicitFieldSchema} already implement the {@link SimpleNodeSchema} interfaces, there are limited use-cases for this function. @@ -47,7 +45,6 @@ import { LeafNodeSchema } from "../leafNodeSchema.js"; export function toSimpleTreeSchema( schema: ImplicitFieldSchema, copySchemaObjects: boolean, - isViewSchema: boolean = true, ): SimpleTreeSchema { const normalizedSchema = normalizeFieldSchema(schema); const definitions = new Map(); @@ -62,9 +59,7 @@ export function toSimpleTreeSchema( nodeSchema instanceof RecordNodeSchema, 0xb60 /* Invalid schema */, ); - const outSchema = copySchemaObjects - ? copySimpleNodeSchema(nodeSchema, isViewSchema) - : nodeSchema; + const outSchema = copySchemaObjects ? copySimpleNodeSchema(nodeSchema) : nodeSchema; definitions.set(nodeSchema.identifier, outSchema); }, }); @@ -72,10 +67,7 @@ export function toSimpleTreeSchema( return { root: copySchemaObjects ? ({ - simpleAllowedTypes: normalizeSimpleAllowedTypes( - normalizedSchema.simpleAllowedTypes, - isViewSchema, - ), + simpleAllowedTypes: normalizedSchema.simpleAllowedTypes, kind: normalizedSchema.kind, metadata: normalizedSchema.metadata, persistedMetadata: normalizedSchema.persistedMetadata, @@ -85,36 +77,12 @@ export function toSimpleTreeSchema( }; } -/** - * Normalizes the {@link SimpleAllowedTypeAttributes} by either preserving or omitting view-specific schema properties. - * @param simpleAllowedTypes - The simple allowed types to normalize. - * @param isViewSchema - If true, properties used by view schema but not part of stored schema (for example, `isStaged` on allowed types) are preserved in the output. - * @returns The normalized simple allowed types. - */ -function normalizeSimpleAllowedTypes( - simpleAllowedTypes: ReadonlyMap, - isViewSchema: boolean, -): ReadonlyMap { - if (isViewSchema) { - return simpleAllowedTypes; - } else { - const normalized = new Map(); - for (const [identifier, attributes] of simpleAllowedTypes.entries()) { - normalized.set(identifier, { ...attributes, isStaged: undefined }); - } - return normalized; - } -} - /** * Copies a {@link SimpleNodeSchema} into a new plain JavaScript object. * * @remarks Caches the result on the input schema for future calls. */ -function copySimpleNodeSchema( - schema: SimpleNodeSchema, - isViewSchema: boolean, -): SimpleNodeSchema { +export function copySimpleNodeSchema(schema: SimpleNodeSchema): SimpleNodeSchema { const kind = schema.kind; switch (kind) { case NodeKind.Leaf: @@ -122,9 +90,9 @@ function copySimpleNodeSchema( case NodeKind.Array: case NodeKind.Map: case NodeKind.Record: - return copySimpleSchemaWithAllowedTypes(schema, isViewSchema); + return copySimpleSchemaWithAllowedTypes(schema); case NodeKind.Object: - return copySimpleObjectSchema(schema, isViewSchema); + return copySimpleObjectSchema(schema); default: unreachableCase(kind); } @@ -141,36 +109,101 @@ function copySimpleLeafSchema(schema: SimpleLeafNodeSchema): SimpleLeafNodeSchem function copySimpleSchemaWithAllowedTypes( schema: SimpleMapNodeSchema | SimpleArrayNodeSchema | SimpleRecordNodeSchema, - isViewSchema: boolean, ): SimpleMapNodeSchema | SimpleArrayNodeSchema | SimpleRecordNodeSchema { return { kind: schema.kind, - simpleAllowedTypes: normalizeSimpleAllowedTypes(schema.simpleAllowedTypes, isViewSchema), + simpleAllowedTypes: schema.simpleAllowedTypes, metadata: schema.metadata, persistedMetadata: schema.persistedMetadata, }; } -function copySimpleObjectSchema( - schema: SimpleObjectNodeSchema, - isViewSchema: boolean, -): SimpleObjectNodeSchema { +function copySimpleObjectSchema(schema: SimpleObjectNodeSchema): SimpleObjectNodeSchema { const fields: Map = new Map(); for (const [propertyKey, field] of schema.fields) { // field already is a SimpleObjectFieldSchema, but copy the subset of the properties needed by this interface to get a clean simple object. - fields.set(propertyKey, { + const simpleField = { kind: field.kind, - simpleAllowedTypes: normalizeSimpleAllowedTypes(field.simpleAllowedTypes, isViewSchema), + simpleAllowedTypes: field.simpleAllowedTypes, metadata: field.metadata, persistedMetadata: field.persistedMetadata, storedKey: field.storedKey, - }); + }; + + fields.set(propertyKey, simpleField); } - return { + const simpleObject = { kind: NodeKind.Object, fields, metadata: schema.metadata, persistedMetadata: schema.persistedMetadata, + allowUnknownOptionalFields: schema.allowUnknownOptionalFields, + } satisfies SimpleObjectNodeSchema; + + return simpleObject; +} + +/** + * Creates a copy of a SimpleTreeSchema without metadata fields. This is useful for comparing deserialized view schemas with in-memory schemas. + * metadata and persistedMetadata are not relevant for schema compatibility checks and are not serialized by the Simple Schema serializer. + * @see {@link simpleSchemaSerializer.ts} for the serialization logic. + * + * @param schema - The SimpleTreeSchema to remove fields from. + * @param fieldsToRemove - An object specifying which fields to remove. + * @returns A new SimpleTreeSchema without the specified fields. + */ +export function copySimpleTreeSchemaWithoutMetadata( + schema: SimpleTreeSchema, +): SimpleTreeSchema { + const definitions = new Map(); + + for (const [identifier, nodeSchema] of schema.definitions.entries()) { + const kind = nodeSchema.kind; + switch (kind) { + case NodeKind.Array: + case NodeKind.Map: + case NodeKind.Record: + case NodeKind.Leaf: { + const outputNodeSchema = { + ...nodeSchema, + metadata: {}, + persistedMetadata: undefined, + }; + definitions.set(identifier, outputNodeSchema); + break; + } + case NodeKind.Object: { + const outputFields = new Map(); + for (const [propertyKey, fieldSchema] of nodeSchema.fields.entries()) { + const outputField: SimpleObjectFieldSchema = { + ...fieldSchema, + metadata: {}, + persistedMetadata: undefined, + }; + outputFields.set(propertyKey, outputField); + } + const outputNodeSchema = { + ...nodeSchema, + metadata: {}, + persistedMetadata: undefined, + fields: outputFields, + }; + definitions.set(identifier, outputNodeSchema); + break; + } + default: + unreachableCase(kind); + } + } + + return { + root: { + kind: schema.root.kind, + simpleAllowedTypes: schema.root.simpleAllowedTypes, + metadata: {}, + persistedMetadata: undefined, + }, + definitions, }; } diff --git a/packages/dds/tree/src/simple-tree/index.ts b/packages/dds/tree/src/simple-tree/index.ts index 6bbd4418ac70..32d99f8f8b14 100644 --- a/packages/dds/tree/src/simple-tree/index.ts +++ b/packages/dds/tree/src/simple-tree/index.ts @@ -280,3 +280,7 @@ export { nullSchema, } from "./leafNodeSchema.js"; export type { LeafSchema } from "./leafNodeSchema.js"; +export { + encodeSimpleSchema, + decodeSimpleSchema, +} from "./api/index.js"; diff --git a/packages/dds/tree/src/simple-tree/simpleSchema.ts b/packages/dds/tree/src/simple-tree/simpleSchema.ts index 689307c28a45..09f1e0fa5f60 100644 --- a/packages/dds/tree/src/simple-tree/simpleSchema.ts +++ b/packages/dds/tree/src/simple-tree/simpleSchema.ts @@ -58,6 +58,15 @@ export interface SimpleObjectNodeSchema * especially if/when TreeNodeSchema for objects provide more maps. */ readonly fields: ReadonlyMap; + + /** + * Whether the object node allows unknown optional fields. + * + * @see {@link ObjectSchemaOptions.allowUnknownOptionalFields} for the API where this field is set as part of authoring a schema. + * + * @remarks Only populated for view schemas, undefined otherwise. Relevant for compatibility checking scenarios. + */ + readonly allowUnknownOptionalFields: boolean | undefined; } /** @@ -163,7 +172,7 @@ export type SimpleNodeSchema = | SimpleRecordNodeSchema; /** - * Information about allowed types. + * Information about allowed types under a field. * * @alpha * @sealed diff --git a/packages/dds/tree/src/simple-tree/simpleSchemaFormatV1.ts b/packages/dds/tree/src/simple-tree/simpleSchemaFormatV1.ts new file mode 100644 index 000000000000..e64418afa5ab --- /dev/null +++ b/packages/dds/tree/src/simple-tree/simpleSchemaFormatV1.ts @@ -0,0 +1,179 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { Type, type ObjectOptions, type Static } from "@sinclair/typebox"; + +const noAdditionalProps: ObjectOptions = { additionalProperties: false }; + +/** + * The format version for the schema. + */ +export const SimpleSchemaFormatVersion = { + v1: 1, +} as const; + +/** + * The allowed types and their attributes in the simple schema format. + * @see {@link SimpleAllowedTypes}. + */ +export const SimpleAllowedTypeAttributesFormat = Type.Object( + { + isStaged: Type.Optional(Type.Boolean()), + }, + noAdditionalProps, +); + +export type SimpleAllowedTypeAttributesFormat = Static< + typeof SimpleAllowedTypeAttributesFormat +>; + +/** + * A set of allowed types in the simple schema format. + * The keys are the type identifiers, and the values are their attributes. + */ +export const SimpleAllowedTypesFormat = Type.Record( + Type.String(), + SimpleAllowedTypeAttributesFormat, +); +export type SimpleAllowedTypesFormat = Static; + +/** + * Persisted format for a field schema in the simple schema format. + * @see {@link SimpleFieldSchema}. + */ +export const SimpleFieldSchemaFormat = Type.Object( + { + kind: Type.Integer(), + simpleAllowedTypes: SimpleAllowedTypesFormat, + }, + noAdditionalProps, +); +export type SimpleFieldSchemaFormat = Static; + +/** + * Persisted format for an object field schema in the simple schema format. + * @see {@link SimpleObjectFieldSchema}. + */ +export const SimpleObjectFieldSchemaFormat = Type.Object( + { + kind: Type.Integer(), + simpleAllowedTypes: SimpleAllowedTypesFormat, + storedKey: Type.String(), + }, + noAdditionalProps, +); +export type SimpleObjectFieldSchemaFormat = Static; + +/** + * Persisted format for an array node schema in the simple schema format. + * @see {@link SimpleArrayNodeSchema}. + */ +export const SimpleArrayNodeSchemaFormat = Type.Object( + { + kind: Type.Integer(), + simpleAllowedTypes: SimpleAllowedTypesFormat, + }, + noAdditionalProps, +); +export type SimpleArrayNodeSchemaFormat = Static; + +/** + * Persisted format for a map node schema in the simple schema format. + * @see {@link SimpleMapNodeSchema}. + */ +export const SimpleMapNodeSchemaFormat = Type.Object( + { + kind: Type.Integer(), + simpleAllowedTypes: SimpleAllowedTypesFormat, + }, + noAdditionalProps, +); +export type SimpleMapNodeSchemaFormat = Static; + +/** + * Persisted format for a record node schema in the simple schema format. + * @see {@link SimpleRecordNodeSchema}. + */ +export const SimpleRecordNodeSchemaFormat = Type.Object( + { + kind: Type.Integer(), + simpleAllowedTypes: SimpleAllowedTypesFormat, + }, + noAdditionalProps, +); +export type SimpleRecordNodeSchemaFormat = Static; + +/** + * Persisted format for a leaf node schema in the simple schema format. + * @see {@link SimpleLeafNodeSchema}. + */ +export const SimpleLeafNodeSchemaFormat = Type.Object( + { + kind: Type.Integer(), + leafKind: Type.Integer(), + }, + noAdditionalProps, +); +export type SimpleLeafNodeSchemaFormat = Static; + +/** + * Persisted format for the field schemas of an object node in the simple schema format. + */ +export const SimpleObjectFieldSchemasFormat = Type.Record( + Type.String(), + SimpleObjectFieldSchemaFormat, +); +export type SimpleObjectFieldSchemasFormat = Static; + +/** + * Persisted format for an object node schema in the simple schema format. + * @see {@link SimpleObjectNodeSchema}. + */ +export const SimpleObjectNodeSchemaFormat = Type.Object( + { + kind: Type.Integer(), + fields: SimpleObjectFieldSchemasFormat, + allowUnknownOptionalFields: Type.Optional(Type.Boolean()), + }, + noAdditionalProps, +); +export type SimpleObjectNodeSchemaFormat = Static; + +/** + * Discriminated union of all possible node schemas. + * + * See {@link DiscriminatedUnionDispatcher} for more information on this pattern. + */ +export const SimpleNodeSchemaUnionFormat = Type.Object({ + array: Type.Optional(SimpleArrayNodeSchemaFormat), + map: Type.Optional(SimpleMapNodeSchemaFormat), + record: Type.Optional(SimpleRecordNodeSchemaFormat), + leaf: Type.Optional(SimpleLeafNodeSchemaFormat), + object: Type.Optional(SimpleObjectNodeSchemaFormat), +}); +export type SimpleNodeSchemaUnionFormat = Static; + +/** + * Helper type for the schema definitions map in the persisted format. + */ +export const SimpleSchemaDefinitionsFormat = Type.Record( + Type.String(), + SimpleNodeSchemaUnionFormat, +); +export type SimpleSchemaDefinitionsFormat = Static; + +/** + * Persisted format for the entire tree schema in the simple schema format. + * @see {@link SimpleTreeSchema}. + */ +export const SimpleTreeSchemaFormat = Type.Object( + { + version: Type.Literal(SimpleSchemaFormatVersion.v1), + root: SimpleFieldSchemaFormat, + definitions: SimpleSchemaDefinitionsFormat, + }, + noAdditionalProps, +); +export type SimpleTreeSchemaFormat = Static; diff --git a/packages/dds/tree/src/test/shared-tree/sharedTree.spec.ts b/packages/dds/tree/src/test/shared-tree/sharedTree.spec.ts index 5d65a2daceca..5f3c4c328fd4 100644 --- a/packages/dds/tree/src/test/shared-tree/sharedTree.spec.ts +++ b/packages/dds/tree/src/test/shared-tree/sharedTree.spec.ts @@ -29,6 +29,7 @@ import { type ChangeFamily, type ChangeFamilyEditor, EmptyKey, + ValueSchema, } from "../../core/index.js"; import { FormatValidatorBasic } from "../../external-utilities/index.js"; import { @@ -68,6 +69,10 @@ import { SchemaFactoryAlpha, type ITree, toInitialSchema, + NodeKind, + type SimpleTreeSchema, + FieldKind, + type SimpleLeafNodeSchema, } from "../../simple-tree/index.js"; import { brand } from "../../util/index.js"; import { @@ -2499,14 +2504,29 @@ describe("SharedTree", () => { view.initialize(10); assert.deepEqual(tree.exportVerbose(), 10); - assert.deepEqual( - tree.exportSimpleSchema(), - toSimpleTreeSchema( - numberSchema, - true, - false /* Don't process this schema as a view schema (exclude isStaged from simpleAllowedTypes). */, - ), - ); + + const expected: SimpleTreeSchema = { + root: { + kind: FieldKind.Required, + simpleAllowedTypes: new Map([ + ["com.fluidframework.leaf.number", { isStaged: undefined }], + ]), + metadata: {}, + persistedMetadata: undefined, + }, + definitions: new Map([ + [ + "com.fluidframework.leaf.number", + { + kind: NodeKind.Leaf, + leafKind: ValueSchema.Number, + metadata: {}, + persistedMetadata: undefined, + } satisfies SimpleLeafNodeSchema, + ], + ]), + }; + assert.deepEqual(tree.exportSimpleSchema(), expected); }); it("supports multiple shared branches", () => { 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 5c3408ba9876..4eeae9dc0bdc 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 @@ -5,10 +5,12 @@ import { strict as assert } from "node:assert"; import { + decodeSimpleSchema, FieldKind, NodeKind, SchemaFactory, SchemaFactoryAlpha, + encodeSimpleSchema, stringSchema, type SimpleLeafNodeSchema, type SimpleNodeSchema, @@ -17,8 +19,16 @@ import { type SimpleTreeSchema, } from "../../../simple-tree/index.js"; import { ValueSchema } from "../../../core/index.js"; -// eslint-disable-next-line import-x/no-internal-modules -import { toSimpleTreeSchema } from "../../../simple-tree/api/viewSchemaToSimpleSchema.js"; + +import { + copySimpleTreeSchemaWithoutMetadata, + toSimpleTreeSchema, + // eslint-disable-next-line import-x/no-internal-modules +} from "../../../simple-tree/api/viewSchemaToSimpleSchema.js"; +import { takeJsonSnapshot, useSnapshotDirectory } from "../../snapshots/index.js"; +import { HasUnknownOptionalFields } from "../../testTrees.js"; +import { ajvValidator } from "../../codec/index.js"; +import type { FormatValidator } from "../../../codec/index.js"; const simpleString: SimpleLeafNodeSchema = { leafKind: ValueSchema.String, @@ -34,365 +44,611 @@ const simpleNumber: SimpleLeafNodeSchema = { persistedMetadata: undefined, }; +// The format validator used in these tests +const formatValidator: FormatValidator = ajvValidator; + describe("getSimpleSchema", () => { + useSnapshotDirectory("get-simple-schema"); + it("non-copying", () => { const Schema = stringSchema; const root = SchemaFactoryAlpha.optional(Schema); - const actual = toSimpleTreeSchema(root, false); - const expected: SimpleTreeSchema = { root, definitions: new Map([[Schema.identifier, Schema]]), }; + + const actual = toSimpleTreeSchema(root, false); + assert.deepEqual(actual, expected); assert.equal(actual.root, root); assert.equal(actual.definitions.get(Schema.identifier), Schema); }); - it("Field Schema", () => { + describe("Field Schema", () => { const schemaFactory = new SchemaFactory("test"); const Schema = schemaFactory.optional(schemaFactory.string, { metadata: { description: "An optional string." }, }); - const actual = toSimpleTreeSchema(Schema, true); + it("toSimpleTreeSchema", () => { + const actual = toSimpleTreeSchema(Schema, true); + + const expected: SimpleTreeSchema = { + root: { + kind: FieldKind.Optional, + metadata: { description: "An optional string." }, + simpleAllowedTypes: new Map([ + ["com.fluidframework.leaf.string", { isStaged: false }], + ]), + persistedMetadata: undefined, + }, + definitions: new Map([["com.fluidframework.leaf.string", simpleString]]), + }; + + assert.deepEqual(actual, expected); + }); - const expected: SimpleTreeSchema = { - root: { - kind: FieldKind.Optional, - metadata: { description: "An optional string." }, - simpleAllowedTypes: new Map([["com.fluidframework.leaf.string", { isStaged: false }]]), - persistedMetadata: undefined, - }, - definitions: new Map([["com.fluidframework.leaf.string", simpleString]]), - }; - assert.deepEqual(actual, expected); + it("serialized - Field Schema", () => { + const actual = encodeSimpleSchema(toSimpleTreeSchema(Schema, true)); + takeJsonSnapshot(actual); + }); + + it("Roundtrip serialization - Field Schema", () => { + const simpleTree = toSimpleTreeSchema(Schema, true); + const expected = copySimpleTreeSchemaWithoutMetadata(simpleTree); + const actual = decodeSimpleSchema(encodeSimpleSchema(simpleTree), formatValidator); + assert.deepEqual(actual, expected); + }); }); - it("Leaf node", () => { + describe("Leaf node", () => { const Schema = SchemaFactory.string; - const actual = toSimpleTreeSchema(Schema, true); + it("toSimpleTreeSchema", () => { + const actual = toSimpleTreeSchema(Schema, true); + + const expected: SimpleTreeSchema = { + root: { + kind: FieldKind.Required, + metadata: {}, + simpleAllowedTypes: new Map([ + ["com.fluidframework.leaf.string", { isStaged: false }], + ]), + persistedMetadata: undefined, + }, + definitions: new Map([["com.fluidframework.leaf.string", simpleString]]), + }; + + assert.deepEqual(actual, expected); + }); - const expected: SimpleTreeSchema = { - root: { - kind: FieldKind.Required, - metadata: {}, - simpleAllowedTypes: new Map([["com.fluidframework.leaf.string", { isStaged: false }]]), - persistedMetadata: undefined, - }, - definitions: new Map([["com.fluidframework.leaf.string", simpleString]]), - }; - assert.deepEqual(actual, expected); + it("serialized - Leaf node", () => { + const actual = encodeSimpleSchema(toSimpleTreeSchema(Schema, true)); + takeJsonSnapshot(actual); + }); + + it("Roundtrip serialization - Leaf node", () => { + const simpleTree = toSimpleTreeSchema(Schema, true); + const expected = copySimpleTreeSchemaWithoutMetadata(simpleTree); + const actual = decodeSimpleSchema(encodeSimpleSchema(simpleTree), formatValidator); + assert.deepEqual(actual, expected); + }); }); - it("Union root", () => { + describe("Union root", () => { const Schema = [SchemaFactory.number, SchemaFactory.string]; - const actual = toSimpleTreeSchema(Schema, true); - - const expected: SimpleTreeSchema = { - root: { - kind: FieldKind.Required, - metadata: {}, - persistedMetadata: undefined, - simpleAllowedTypes: new Map([ - ["com.fluidframework.leaf.number", { isStaged: false }], - ["com.fluidframework.leaf.string", { isStaged: false }], + it("toSimpleTreeSchema", () => { + const actual = toSimpleTreeSchema(Schema, true); + + const expected: SimpleTreeSchema = { + root: { + kind: FieldKind.Required, + metadata: {}, + persistedMetadata: undefined, + simpleAllowedTypes: new Map([ + ["com.fluidframework.leaf.number", { isStaged: false }], + ["com.fluidframework.leaf.string", { isStaged: false }], + ]), + }, + definitions: new Map([ + ["com.fluidframework.leaf.number", simpleNumber], + ["com.fluidframework.leaf.string", simpleString], ]), - }, - definitions: new Map([ - ["com.fluidframework.leaf.number", simpleNumber], - ["com.fluidframework.leaf.string", simpleString], - ]), - }; - assert.deepEqual(actual, expected); + }; + + assert.deepEqual(actual, expected); + }); + + it("serialized - Union root", () => { + const actual = encodeSimpleSchema(toSimpleTreeSchema(Schema, true)); + takeJsonSnapshot(actual); + }); + + it("Roundtrip serialization - Field Schema", () => { + const simpleTree = toSimpleTreeSchema(Schema, true); + const expected = copySimpleTreeSchemaWithoutMetadata(simpleTree); + const actual = decodeSimpleSchema(encodeSimpleSchema(simpleTree), formatValidator); + assert.deepEqual(actual, expected); + }); }); - it("Array schema", () => { + describe("Array schema", () => { const schemaFactory = new SchemaFactory("test"); class Schema extends schemaFactory.array("array", schemaFactory.string) {} - const actual = toSimpleTreeSchema(Schema, true); + it("toSimpleTreeSchema", () => { + const actual = toSimpleTreeSchema(Schema, true); + + const expected: SimpleTreeSchema = { + root: { + kind: FieldKind.Required, + metadata: {}, + persistedMetadata: undefined, + simpleAllowedTypes: new Map([["test.array", { isStaged: false }]]), + }, + definitions: new Map([ + [ + "test.array", + { + kind: NodeKind.Array, + simpleAllowedTypes: new Map([ + ["com.fluidframework.leaf.string", { isStaged: false }], + ]), + metadata: {}, + persistedMetadata: undefined, + }, + ], + ["com.fluidframework.leaf.string", simpleString], + ]), + }; - const expected: SimpleTreeSchema = { - root: { - kind: FieldKind.Required, - metadata: {}, - persistedMetadata: undefined, - simpleAllowedTypes: new Map([["test.array", { isStaged: false }]]), - }, - definitions: new Map([ - [ - "test.array", - { - kind: NodeKind.Array, - simpleAllowedTypes: new Map([ - ["com.fluidframework.leaf.string", { isStaged: false }], - ]), - metadata: {}, - persistedMetadata: undefined, - }, - ], - ["com.fluidframework.leaf.string", simpleString], - ]), - }; - assert.deepEqual(actual, expected); + assert.deepEqual(actual, expected); + }); + + it("serialized - Array schema", () => { + const actual = encodeSimpleSchema(toSimpleTreeSchema(Schema, true)); + takeJsonSnapshot(actual); + }); + + it("Roundtrip serialization - Array Schema", () => { + const simpleTree = toSimpleTreeSchema(Schema, true); + const expected = copySimpleTreeSchemaWithoutMetadata(simpleTree); + const actual = decodeSimpleSchema(encodeSimpleSchema(simpleTree), formatValidator); + assert.deepEqual(actual, expected); + }); }); - it("Map schema", () => { + describe("Map schema", () => { const schemaFactory = new SchemaFactory("test"); class Schema extends schemaFactory.map("map", schemaFactory.string) {} - const actual = toSimpleTreeSchema(Schema, true); - const expected: SimpleTreeSchema = { - root: { - kind: FieldKind.Required, - metadata: {}, - persistedMetadata: undefined, - simpleAllowedTypes: new Map([["test.map", { isStaged: false }]]), - }, - definitions: new Map([ - [ - "test.map", - { - kind: NodeKind.Map, - metadata: {}, - persistedMetadata: undefined, - simpleAllowedTypes: new Map([ - ["com.fluidframework.leaf.string", { isStaged: false }], - ]), - }, - ], - ["com.fluidframework.leaf.string", simpleString], - ]), - }; - assert.deepEqual(actual, expected); + it("toSimpleTreeSchema", () => { + const actual = toSimpleTreeSchema(Schema, true); + + const expected: SimpleTreeSchema = { + root: { + kind: FieldKind.Required, + metadata: {}, + persistedMetadata: undefined, + simpleAllowedTypes: new Map([["test.map", { isStaged: false }]]), + }, + definitions: new Map([ + [ + "test.map", + { + kind: NodeKind.Map, + metadata: {}, + persistedMetadata: undefined, + simpleAllowedTypes: new Map([ + ["com.fluidframework.leaf.string", { isStaged: false }], + ]), + }, + ], + ["com.fluidframework.leaf.string", simpleString], + ]), + }; + + assert.deepEqual(actual, expected); + }); + + it("serialized - Map schema", () => { + const actual = encodeSimpleSchema(toSimpleTreeSchema(Schema, true)); + takeJsonSnapshot(actual); + }); + + it("Roundtrip serialization - Map schema", () => { + const simpleTree = toSimpleTreeSchema(Schema, true); + const expected = copySimpleTreeSchemaWithoutMetadata(simpleTree); + const actual = decodeSimpleSchema(encodeSimpleSchema(simpleTree), formatValidator); + assert.deepEqual(actual, expected); + }); }); - it("Record schema", () => { + describe("Record schema", () => { const schemaFactory = new SchemaFactoryAlpha("test"); class Schema extends schemaFactory.record("record", schemaFactory.string) {} - const actual = toSimpleTreeSchema(Schema, true); - const expected: SimpleTreeSchema = { - root: { - kind: FieldKind.Required, - metadata: {}, - persistedMetadata: undefined, - simpleAllowedTypes: new Map([["test.record", { isStaged: false }]]), - }, - definitions: new Map([ - [ - "test.record", - { - kind: NodeKind.Record, - metadata: {}, - persistedMetadata: undefined, - simpleAllowedTypes: new Map([ - ["com.fluidframework.leaf.string", { isStaged: false }], - ]), - }, - ], - ["com.fluidframework.leaf.string", simpleString], - ]), - }; - assert.deepEqual(actual, expected); + it("toSimpleTreeSchema", () => { + const actual = toSimpleTreeSchema(Schema, true); + + const expected: SimpleTreeSchema = { + root: { + kind: FieldKind.Required, + metadata: {}, + persistedMetadata: undefined, + simpleAllowedTypes: new Map([["test.record", { isStaged: false }]]), + }, + definitions: new Map([ + [ + "test.record", + { + kind: NodeKind.Record, + metadata: {}, + persistedMetadata: undefined, + simpleAllowedTypes: new Map([ + ["com.fluidframework.leaf.string", { isStaged: false }], + ]), + }, + ], + ["com.fluidframework.leaf.string", simpleString], + ]), + }; + + assert.deepEqual(actual, expected); + }); + + it("serialized - Record schema", () => { + const actual = encodeSimpleSchema(toSimpleTreeSchema(Schema, true)); + takeJsonSnapshot(actual); + }); + + it("Roundtrip serialization - Record schema", () => { + const simpleTree = toSimpleTreeSchema(Schema, true); + const expected = copySimpleTreeSchemaWithoutMetadata(simpleTree); + const actual = decodeSimpleSchema(encodeSimpleSchema(simpleTree), formatValidator); + assert.deepEqual(actual, expected); + }); }); - it("Object schema", () => { + describe("Object schema", () => { const schemaFactory = new SchemaFactory("test"); class Schema extends schemaFactory.object("object", { foo: schemaFactory.optional(schemaFactory.number), bar: schemaFactory.required(schemaFactory.string), }) {} - const actual = toSimpleTreeSchema(Schema, true); + it("toSimpleTreeSchema", () => { + const actual = toSimpleTreeSchema(Schema, true); + + const expected: SimpleTreeSchema = { + root: { + kind: FieldKind.Required, + metadata: {}, + persistedMetadata: undefined, + simpleAllowedTypes: new Map([["test.object", { isStaged: false }]]), + }, + definitions: new Map([ + [ + "test.object", + { + kind: NodeKind.Object, + metadata: {}, + persistedMetadata: undefined, + allowUnknownOptionalFields: false, + fields: new Map([ + [ + "foo", + { + kind: FieldKind.Optional, + metadata: {}, + persistedMetadata: undefined, + simpleAllowedTypes: new Map([ + ["com.fluidframework.leaf.number", { isStaged: false }], + ]), + storedKey: "foo", + }, + ], + [ + "bar", + { + kind: FieldKind.Required, + metadata: {}, + persistedMetadata: undefined, + simpleAllowedTypes: new Map([ + ["com.fluidframework.leaf.string", { isStaged: false }], + ]), + storedKey: "bar", + }, + ], + ]), + } satisfies SimpleObjectNodeSchema, + ], + ["com.fluidframework.leaf.number", simpleNumber], + ["com.fluidframework.leaf.string", simpleString], + ]), + }; - const expected: SimpleTreeSchema = { - root: { - kind: FieldKind.Required, - metadata: {}, - persistedMetadata: undefined, - simpleAllowedTypes: new Map([["test.object", { isStaged: false }]]), - }, - definitions: new Map([ - [ - "test.object", - { - kind: NodeKind.Object, - metadata: {}, - persistedMetadata: undefined, - fields: new Map([ - [ - "foo", - { - kind: FieldKind.Optional, - metadata: {}, - persistedMetadata: undefined, - simpleAllowedTypes: new Map([ - ["com.fluidframework.leaf.number", { isStaged: false }], - ]), - storedKey: "foo", - }, - ], - [ - "bar", - { - kind: FieldKind.Required, - metadata: {}, - persistedMetadata: undefined, - simpleAllowedTypes: new Map([ - ["com.fluidframework.leaf.string", { isStaged: false }], - ]), - storedKey: "bar", - }, - ], - ]), - } satisfies SimpleObjectNodeSchema, - ], - ["com.fluidframework.leaf.number", simpleNumber], - ["com.fluidframework.leaf.string", simpleString], - ]), - }; - assert.deepEqual(actual, expected); + assert.deepEqual(actual, expected); + }); + + it("serialized - Object schema", () => { + const actual = encodeSimpleSchema(toSimpleTreeSchema(Schema, true)); + takeJsonSnapshot(actual); + }); + + it("Roundtrip serialization - Object schema", () => { + const simpleTree = toSimpleTreeSchema(Schema, true); + const expected = copySimpleTreeSchemaWithoutMetadata(simpleTree); + const actual = decodeSimpleSchema(encodeSimpleSchema(simpleTree), formatValidator); + assert.deepEqual(actual, expected); + }); }); - it("Object schema including an identifier field", () => { + describe("Object schema including an identifier field", () => { const schemaFactory = new SchemaFactory("test"); class Schema extends schemaFactory.object("object", { id: schemaFactory.identifier, }) {} - const actual = toSimpleTreeSchema(Schema, true); + it("toSimpleTreeSchema", () => { + const actual = toSimpleTreeSchema(Schema, true); + + const expected: SimpleTreeSchema = { + root: { + kind: FieldKind.Required, + metadata: {}, + persistedMetadata: undefined, + simpleAllowedTypes: new Map([["test.object", { isStaged: false }]]), + }, + definitions: new Map([ + [ + "test.object", + { + kind: NodeKind.Object, + metadata: {}, + persistedMetadata: undefined, + allowUnknownOptionalFields: false, + fields: new Map([ + [ + "id", + { + kind: FieldKind.Identifier, + metadata: {}, + persistedMetadata: undefined, + simpleAllowedTypes: new Map([ + ["com.fluidframework.leaf.string", { isStaged: false }], + ]), + storedKey: "id", + }, + ], + ]), + }, + ], + ["com.fluidframework.leaf.string", simpleString], + ]), + }; - const expected: SimpleTreeSchema = { - root: { - kind: FieldKind.Required, - metadata: {}, - persistedMetadata: undefined, - simpleAllowedTypes: new Map([["test.object", { isStaged: false }]]), - }, - definitions: new Map([ - [ - "test.object", - { - kind: NodeKind.Object, - metadata: {}, - persistedMetadata: undefined, - fields: new Map([ - [ - "id", - { - kind: FieldKind.Identifier, - metadata: {}, - persistedMetadata: undefined, - simpleAllowedTypes: new Map([ - ["com.fluidframework.leaf.string", { isStaged: false }], - ]), - storedKey: "id", - }, - ], - ]), - }, - ], - ["com.fluidframework.leaf.string", simpleString], - ]), - }; - assert.deepEqual(actual, expected); + assert.deepEqual(actual, expected); + }); + + it("serialized - Object schema including an identifier field", () => { + const actual = encodeSimpleSchema(toSimpleTreeSchema(Schema, true)); + takeJsonSnapshot(actual); + }); + + it("Roundtrip serialization - Object schema including an identifier field", () => { + const simpleTree = toSimpleTreeSchema(Schema, true); + const expected = copySimpleTreeSchemaWithoutMetadata(simpleTree); + const actual = decodeSimpleSchema(encodeSimpleSchema(simpleTree), formatValidator); + assert.deepEqual(actual, expected); + }); }); - it("Object schema including a union field", () => { + describe("Object schema including a union field", () => { const schemaFactory = new SchemaFactory("test"); class Schema extends schemaFactory.object("object", { foo: schemaFactory.required([schemaFactory.number, schemaFactory.string]), }) {} - // Must enable copy so deep equality passes. - const actual = toSimpleTreeSchema(Schema, true); + it("toSimpleTreeSchema", () => { + // Must enable copy so deep equality passes. + const actual = toSimpleTreeSchema(Schema, true); + + const expected: SimpleTreeSchema = { + root: { + kind: FieldKind.Required, + metadata: {}, + persistedMetadata: undefined, + simpleAllowedTypes: new Map([["test.object", { isStaged: false }]]), + }, + definitions: new Map([ + [ + "test.object", + { + kind: NodeKind.Object, + metadata: {}, + persistedMetadata: undefined, + allowUnknownOptionalFields: false, + fields: new Map([ + [ + "foo", + { + kind: FieldKind.Required, + metadata: {}, + persistedMetadata: undefined, + simpleAllowedTypes: new Map([ + ["com.fluidframework.leaf.number", { isStaged: false }], + ["com.fluidframework.leaf.string", { isStaged: false }], + ]), + storedKey: "foo", + }, + ], + ]), + }, + ], + ["com.fluidframework.leaf.number", simpleNumber], + ["com.fluidframework.leaf.string", simpleString], + ]), + }; - const expected: SimpleTreeSchema = { - root: { - kind: FieldKind.Required, - metadata: {}, - persistedMetadata: undefined, - simpleAllowedTypes: new Map([["test.object", { isStaged: false }]]), - }, - definitions: new Map([ - [ - "test.object", - { - kind: NodeKind.Object, - metadata: {}, - persistedMetadata: undefined, - fields: new Map([ - [ - "foo", - { - kind: FieldKind.Required, - metadata: {}, - persistedMetadata: undefined, - simpleAllowedTypes: new Map([ - ["com.fluidframework.leaf.number", { isStaged: false }], - ["com.fluidframework.leaf.string", { isStaged: false }], - ]), - storedKey: "foo", - }, - ], - ]), - }, - ], - ["com.fluidframework.leaf.number", simpleNumber], - ["com.fluidframework.leaf.string", simpleString], - ]), - }; - assert.deepEqual(actual, expected); + assert.deepEqual(actual, expected); + }); + + it("serialized - Object schema including a union field", () => { + const actual = encodeSimpleSchema(toSimpleTreeSchema(Schema, true)); + takeJsonSnapshot(actual); + }); + + it("Roundtrip serialization - Object schema including a union field", () => { + const simpleTree = toSimpleTreeSchema(Schema, true); + const expected = copySimpleTreeSchemaWithoutMetadata(simpleTree); + const actual = decodeSimpleSchema(encodeSimpleSchema(simpleTree), formatValidator); + assert.deepEqual(actual, expected); + }); }); - it("Recursive object schema", () => { + describe("Recursive object schema", () => { const schemaFactory = new SchemaFactory("test"); class Schema extends schemaFactory.objectRecursive("recursive-object", { foo: schemaFactory.optionalRecursive([schemaFactory.string, () => Schema]), }) {} - const actual = toSimpleTreeSchema(Schema, true); + it("toSimpleTreeSchema", () => { + const actual = toSimpleTreeSchema(Schema, true); + + const expected: SimpleTreeSchema = { + root: { + kind: FieldKind.Required, + metadata: {}, + persistedMetadata: undefined, + simpleAllowedTypes: new Map([["test.recursive-object", { isStaged: false }]]), + }, + definitions: new Map([ + [ + "test.recursive-object", + { + kind: NodeKind.Object, + metadata: {}, + persistedMetadata: undefined, + allowUnknownOptionalFields: false, + fields: new Map([ + [ + "foo", + { + kind: FieldKind.Optional, + metadata: {}, + persistedMetadata: undefined, + simpleAllowedTypes: new Map([ + ["com.fluidframework.leaf.string", { isStaged: false }], + ["test.recursive-object", { isStaged: false }], + ]), + storedKey: "foo", + }, + ], + ]), + }, + ], + ["com.fluidframework.leaf.string", simpleString], + ]), + }; - const expected: SimpleTreeSchema = { - root: { - kind: FieldKind.Required, - metadata: {}, - persistedMetadata: undefined, - simpleAllowedTypes: new Map([["test.recursive-object", { isStaged: false }]]), - }, - definitions: new Map([ - [ - "test.recursive-object", - { - kind: NodeKind.Object, - metadata: {}, - persistedMetadata: undefined, - fields: new Map([ - [ - "foo", - { - kind: FieldKind.Optional, - metadata: {}, - persistedMetadata: undefined, - simpleAllowedTypes: new Map([ - ["com.fluidframework.leaf.string", { isStaged: false }], - ["test.recursive-object", { isStaged: false }], - ]), - storedKey: "foo", - }, - ], - ]), - }, - ], - ["com.fluidframework.leaf.string", simpleString], - ]), - }; - assert.deepEqual(actual, expected); + assert.deepEqual(actual, expected); + }); + + it("serialized - Recursive object schema", () => { + const actual = encodeSimpleSchema(toSimpleTreeSchema(Schema, true)); + takeJsonSnapshot(actual); + }); + + it("Roundtrip serialization - Recursive object schema", () => { + const simpleTree = toSimpleTreeSchema(Schema, true); + const expected = copySimpleTreeSchemaWithoutMetadata(simpleTree); + const actual = decodeSimpleSchema(encodeSimpleSchema(simpleTree), formatValidator); + assert.deepEqual(actual, expected); + }); + }); + + describe("With staged schema upgrades", () => { + const leafSchema = stringSchema; + const schemaFactory = new SchemaFactoryAlpha("test"); + const schema = schemaFactory.optional( + // Staged allowed types are read-only for the sake of schema migrations + schemaFactory.types([schemaFactory.staged(leafSchema)]), + ); + + it("Should preserve isReadOnly when converting to SimpleTreeSchema", () => { + const expected: SimpleTreeSchema = { + root: { + kind: FieldKind.Optional, + simpleAllowedTypes: new Map([[leafSchema.identifier, { isStaged: true }]]), + metadata: {}, + persistedMetadata: undefined, + }, + definitions: new Map([[leafSchema.identifier, leafSchema]]), + }; + + const actual = toSimpleTreeSchema(schema, true); + assert.deepEqual(actual.root.simpleAllowedTypes, expected.root.simpleAllowedTypes); + }); + + it("serialized - simpleAllowedTypes", () => { + const actual = encodeSimpleSchema(toSimpleTreeSchema(schema, true)); + takeJsonSnapshot(actual); + }); + + it("Roundtrip serialization - simpleAllowedTypes", () => { + const simpleTree = toSimpleTreeSchema(schema, true); + const expected = copySimpleTreeSchemaWithoutMetadata(simpleTree); + const actual = decodeSimpleSchema(encodeSimpleSchema(simpleTree), formatValidator); + assert.deepEqual(actual, expected); + }); + }); + + describe("With allowUnknownOptionalFields in object schema", () => { + const schema = HasUnknownOptionalFields; + + it("Should preserve allowUnknownOptionalFields when converting to SimpleTreeSchema", () => { + const expected: SimpleTreeSchema = { + root: { + kind: FieldKind.Required, + simpleAllowedTypes: new Map([ + ["test.hasUnknownOptionalFields", { isStaged: false }], + ]), + metadata: {}, + persistedMetadata: undefined, + }, + definitions: new Map([ + [ + "test.hasUnknownOptionalFields", + { + kind: NodeKind.Object, + metadata: {}, + persistedMetadata: undefined, + allowUnknownOptionalFields: true, + fields: new Map([]), + }, + ], + ]), + }; + + const actual = toSimpleTreeSchema(schema, true); + assert.deepEqual(actual, expected); + }); + + it("serialized - allowUnknownOptionalFields", () => { + const actual = encodeSimpleSchema(toSimpleTreeSchema(schema, true)); + takeJsonSnapshot(actual); + }); + + it("Roundtrip serialization - allowUnknownOptionalFields", () => { + const simpleTree = toSimpleTreeSchema(schema, true); + const expected = copySimpleTreeSchemaWithoutMetadata(simpleTree); + const actual = decodeSimpleSchema(encodeSimpleSchema(simpleTree), formatValidator); + assert.deepEqual(actual, expected); + }); }); }); 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 2bfdc6d05b82..d633032589f1 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 @@ -365,6 +365,7 @@ describe("simpleSchemaToJsonSchema", () => { kind: NodeKind.Object, metadata: {}, persistedMetadata: undefined, + allowUnknownOptionalFields: false, fields: new Map([ [ "foo", @@ -510,6 +511,7 @@ describe("simpleSchemaToJsonSchema", () => { kind: NodeKind.Object, metadata: {}, persistedMetadata: undefined, + allowUnknownOptionalFields: false, fields: new Map([ [ "id", @@ -570,6 +572,7 @@ describe("simpleSchemaToJsonSchema", () => { kind: NodeKind.Object, metadata: {}, persistedMetadata: undefined, + allowUnknownOptionalFields: false, fields: new Map([ [ "foo", @@ -640,6 +643,7 @@ describe("simpleSchemaToJsonSchema", () => { kind: NodeKind.Object, metadata: {}, persistedMetadata: undefined, + allowUnknownOptionalFields: false, fields: new Map([ [ "foo", diff --git a/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Array schema.json b/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Array schema.json new file mode 100644 index 000000000000..48b906ed8a48 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Array schema.json @@ -0,0 +1,29 @@ +{ + "version": 1, + "root": { + "kind": 1, + "simpleAllowedTypes": { + "test.array": { + "isStaged": false + } + } + }, + "definitions": { + "com.fluidframework.leaf.string": { + "leaf": { + "kind": 3, + "leafKind": 1 + } + }, + "test.array": { + "array": { + "kind": 1, + "simpleAllowedTypes": { + "com.fluidframework.leaf.string": { + "isStaged": false + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Field Schema.json b/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Field Schema.json new file mode 100644 index 000000000000..52f3f57b9d0f --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Field Schema.json @@ -0,0 +1,19 @@ +{ + "version": 1, + "root": { + "kind": 0, + "simpleAllowedTypes": { + "com.fluidframework.leaf.string": { + "isStaged": false + } + } + }, + "definitions": { + "com.fluidframework.leaf.string": { + "leaf": { + "kind": 3, + "leafKind": 1 + } + } + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Leaf node.json b/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Leaf node.json new file mode 100644 index 000000000000..12ebf0985cc4 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Leaf node.json @@ -0,0 +1,19 @@ +{ + "version": 1, + "root": { + "kind": 1, + "simpleAllowedTypes": { + "com.fluidframework.leaf.string": { + "isStaged": false + } + } + }, + "definitions": { + "com.fluidframework.leaf.string": { + "leaf": { + "kind": 3, + "leafKind": 1 + } + } + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Map schema.json b/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Map schema.json new file mode 100644 index 000000000000..7fb3b0d28e5e --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Map schema.json @@ -0,0 +1,29 @@ +{ + "version": 1, + "root": { + "kind": 1, + "simpleAllowedTypes": { + "test.map": { + "isStaged": false + } + } + }, + "definitions": { + "com.fluidframework.leaf.string": { + "leaf": { + "kind": 3, + "leafKind": 1 + } + }, + "test.map": { + "map": { + "kind": 0, + "simpleAllowedTypes": { + "com.fluidframework.leaf.string": { + "isStaged": false + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Object schema including a union field.json b/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Object schema including a union field.json new file mode 100644 index 000000000000..2cd6d348d65b --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Object schema including a union field.json @@ -0,0 +1,45 @@ +{ + "version": 1, + "root": { + "kind": 1, + "simpleAllowedTypes": { + "test.object": { + "isStaged": false + } + } + }, + "definitions": { + "com.fluidframework.leaf.number": { + "leaf": { + "kind": 3, + "leafKind": 0 + } + }, + "com.fluidframework.leaf.string": { + "leaf": { + "kind": 3, + "leafKind": 1 + } + }, + "test.object": { + "object": { + "kind": 2, + "fields": { + "foo": { + "kind": 1, + "simpleAllowedTypes": { + "com.fluidframework.leaf.number": { + "isStaged": false + }, + "com.fluidframework.leaf.string": { + "isStaged": false + } + }, + "storedKey": "foo" + } + }, + "allowUnknownOptionalFields": false + } + } + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Object schema including an identifier field.json b/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Object schema including an identifier field.json new file mode 100644 index 000000000000..0991abe24079 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Object schema including an identifier field.json @@ -0,0 +1,36 @@ +{ + "version": 1, + "root": { + "kind": 1, + "simpleAllowedTypes": { + "test.object": { + "isStaged": false + } + } + }, + "definitions": { + "com.fluidframework.leaf.string": { + "leaf": { + "kind": 3, + "leafKind": 1 + } + }, + "test.object": { + "object": { + "kind": 2, + "fields": { + "id": { + "kind": 2, + "simpleAllowedTypes": { + "com.fluidframework.leaf.string": { + "isStaged": false + } + }, + "storedKey": "id" + } + }, + "allowUnknownOptionalFields": false + } + } + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Object schema.json b/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Object schema.json new file mode 100644 index 000000000000..e122eee7a44b --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Object schema.json @@ -0,0 +1,51 @@ +{ + "version": 1, + "root": { + "kind": 1, + "simpleAllowedTypes": { + "test.object": { + "isStaged": false + } + } + }, + "definitions": { + "com.fluidframework.leaf.number": { + "leaf": { + "kind": 3, + "leafKind": 0 + } + }, + "com.fluidframework.leaf.string": { + "leaf": { + "kind": 3, + "leafKind": 1 + } + }, + "test.object": { + "object": { + "kind": 2, + "fields": { + "foo": { + "kind": 0, + "simpleAllowedTypes": { + "com.fluidframework.leaf.number": { + "isStaged": false + } + }, + "storedKey": "foo" + }, + "bar": { + "kind": 1, + "simpleAllowedTypes": { + "com.fluidframework.leaf.string": { + "isStaged": false + } + }, + "storedKey": "bar" + } + }, + "allowUnknownOptionalFields": false + } + } + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Record schema.json b/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Record schema.json new file mode 100644 index 000000000000..ff2e1a8fb399 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Record schema.json @@ -0,0 +1,29 @@ +{ + "version": 1, + "root": { + "kind": 1, + "simpleAllowedTypes": { + "test.record": { + "isStaged": false + } + } + }, + "definitions": { + "com.fluidframework.leaf.string": { + "leaf": { + "kind": 3, + "leafKind": 1 + } + }, + "test.record": { + "record": { + "kind": 4, + "simpleAllowedTypes": { + "com.fluidframework.leaf.string": { + "isStaged": false + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Recursive object schema.json b/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Recursive object schema.json new file mode 100644 index 000000000000..58921f908aa7 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Recursive object schema.json @@ -0,0 +1,39 @@ +{ + "version": 1, + "root": { + "kind": 1, + "simpleAllowedTypes": { + "test.recursive-object": { + "isStaged": false + } + } + }, + "definitions": { + "com.fluidframework.leaf.string": { + "leaf": { + "kind": 3, + "leafKind": 1 + } + }, + "test.recursive-object": { + "object": { + "kind": 2, + "fields": { + "foo": { + "kind": 0, + "simpleAllowedTypes": { + "com.fluidframework.leaf.string": { + "isStaged": false + }, + "test.recursive-object": { + "isStaged": false + } + }, + "storedKey": "foo" + } + }, + "allowUnknownOptionalFields": false + } + } + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Union root.json b/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Union root.json new file mode 100644 index 000000000000..51ce59d82047 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - Union root.json @@ -0,0 +1,28 @@ +{ + "version": 1, + "root": { + "kind": 1, + "simpleAllowedTypes": { + "com.fluidframework.leaf.number": { + "isStaged": false + }, + "com.fluidframework.leaf.string": { + "isStaged": false + } + } + }, + "definitions": { + "com.fluidframework.leaf.number": { + "leaf": { + "kind": 3, + "leafKind": 0 + } + }, + "com.fluidframework.leaf.string": { + "leaf": { + "kind": 3, + "leafKind": 1 + } + } + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - allowUnknownOptionalFields.json b/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - allowUnknownOptionalFields.json new file mode 100644 index 000000000000..682268166a04 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - allowUnknownOptionalFields.json @@ -0,0 +1,20 @@ +{ + "version": 1, + "root": { + "kind": 1, + "simpleAllowedTypes": { + "test.hasUnknownOptionalFields": { + "isStaged": false + } + } + }, + "definitions": { + "test.hasUnknownOptionalFields": { + "object": { + "kind": 2, + "fields": {}, + "allowUnknownOptionalFields": true + } + } + } +} \ No newline at end of file diff --git a/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - simpleAllowedTypes.json b/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - simpleAllowedTypes.json new file mode 100644 index 000000000000..5a38a8d13bd9 --- /dev/null +++ b/packages/dds/tree/src/test/snapshots/output/get-simple-schema/serialized - simpleAllowedTypes.json @@ -0,0 +1,19 @@ +{ + "version": 1, + "root": { + "kind": 0, + "simpleAllowedTypes": { + "com.fluidframework.leaf.string": { + "isStaged": true + } + } + }, + "definitions": { + "com.fluidframework.leaf.string": { + "leaf": { + "kind": 3, + "leafKind": 1 + } + } + } +} \ No newline at end of file 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 cf9eba063040..0ecad0e6fd7b 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 @@ -240,6 +240,9 @@ export function createSimpleTreeIndex(view: TreeView, indexer: Map, getValue: (nodes: TreeIndexNodes>) => TValue, isKeyValid: (key: TreeIndexKey) => key is TKey, indexableSchema: readonly TSchema[]): SimpleTreeIndex; +// @alpha +export function decodeSimpleSchema(encodedSchema: JsonCompatibleReadOnly, validator?: FormatValidator): SimpleTreeSchema; + // @public @sealed @system interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } @@ -255,6 +258,9 @@ export interface DirtyTreeMap { // @alpha export type DirtyTreeStatus = "new" | "changed" | "moved"; +// @alpha +export function encodeSimpleSchema(simpleSchema: SimpleTreeSchema): JsonCompatibleReadOnly; + // @beta export function enumFromStrings(factory: SchemaFactory, members: Members): ((value: TValue) => TValue extends unknown ? TreeNode & { readonly value: TValue; @@ -1450,6 +1456,7 @@ export interface SimpleObjectFieldSchema extends SimpleFieldSchema { // @alpha @sealed export interface SimpleObjectNodeSchema extends SimpleNodeSchemaBaseAlpha { + readonly allowUnknownOptionalFields: boolean | undefined; readonly fields: ReadonlyMap; }