-
Notifications
You must be signed in to change notification settings - Fork 558
Shared Tree: Snapshot compatibility testing APIs #25861
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
d720010
Updated getSimpleSchema to handle allowed types with staging info.
TommyBrosman 63d65d7
Update packages/dds/tree/src/simple-tree/api/schemaFromSimple.ts
TommyBrosman 364e828
Lint.
TommyBrosman c802787
PR feedback.
TommyBrosman f2447ff
Merge branch 'main' into gen-simple-schema
TommyBrosman ac47976
Initial version of snapshot compatibility checker APIs.
TommyBrosman 674606b
Better docs.
TommyBrosman 83037e8
- Docs.
TommyBrosman 3ede308
Apply suggestions from code review
TommyBrosman ef8f47c
Test fix.
TommyBrosman 08b97dc
Better docs.
TommyBrosman 976c767
Removed custom JSON type.
TommyBrosman 25bb351
Update .changeset/angry-wings-battle.md
TommyBrosman 7ba1a02
Throws.
TommyBrosman 1701812
Merge branch 'gen-simple-schema' of https://github.com/TommyBrosman/F…
TommyBrosman 3ab88d3
Minor: indentation.
TommyBrosman d82c0d5
- Tweaked signature around TreeViewConfiguration parameter and docume…
TommyBrosman 7933bed
- Updated API Extractor output.
TommyBrosman fccb005
Apply suggestions from code review
TommyBrosman 41498ef
Docs, tests.
TommyBrosman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| --- | ||
| "@fluidframework/tree": minor | ||
| "fluid-framework": minor | ||
| "__section": tree | ||
| --- | ||
| Schema snapshot compatibility checker | ||
|
|
||
| This change adds alpha APIs for creating snapshots of view schema and testing their compatibility for the purposes | ||
| of schema migrations. | ||
|
|
||
| New APIs: | ||
|
|
||
| - `checkCompatibility` - Checks the compatibility of the view schema which created the document against the view schema | ||
| being used to open it. | ||
| - `importCompatibilitySchemaSnapshot` - Parse a JSON representation of a tree schema into a concrete schema. | ||
| - `exportCompatibilitySchemaSnapshot` - Returns a JSON representation of the tree schema for snapshot compatibility checking. | ||
|
|
||
| #### Example: Current view schema vs. historical view schema | ||
|
|
||
| An application author is developing an app that has a schema for storing 2D Points. | ||
| They wish to maintain backwards compatibility in future versions and avoid changing their view schema in a way that breaks | ||
| this behavior. | ||
| When introducing a new initial schema, they persists a snapshot using `exportCompatibilitySchemaSnapshot`: | ||
|
|
||
| ```ts | ||
| const factory = new SchemaFactory("test"); | ||
|
|
||
| // The past view schema, for the purposes of illustration. This wouldn't normally appear as a concrete schema in the test | ||
| // checking compatibility, but rather would be loaded from a snapshot. | ||
| class Point2D extends factory.object("Point", { | ||
| x: factory.number, | ||
| y: factory.number, | ||
| }) {} | ||
| const viewSchema = new TreeViewConfiguration({ schema: Point2D }); | ||
| const encodedSchema = JSON.stringify(exportCompatibilitySchemaSnapshot(viewSchema)); | ||
| fs.writeFileSync("PointSchema.json", encodedSchema); | ||
| ``` | ||
|
|
||
| Next they create a regression test to ensure that the current view schema can read content written by the original view | ||
| schema (`SchemaCompatibilityStatus.canUpgrade`). Initially `currentViewSchema === Point2D`: | ||
|
|
||
| ```ts | ||
| const encodedSchema = JSON.parse(fs.readFileSync("PointSchema.json", "utf8")); | ||
| const oldViewSchema = importCompatibilitySchemaSnapshot(encodedSchema); | ||
|
|
||
| // Check to see if the document created by the historical view schema can be opened with the current view schema | ||
| const compatibilityStatus = checkCompatibility(oldViewSchema, currentViewSchema); | ||
|
|
||
| // Check to see if the document created by the historical view schema can be opened with the current view schema | ||
| const backwardsCompatibilityStatus = checkCompatibility(oldViewSchema, currentViewSchema); | ||
|
|
||
| // z is not present in Point2D, so the schema must be upgraded | ||
| assert.equal(backwardsCompatibilityStatus.canView, false); | ||
|
|
||
| // The schema can be upgraded to add the new optional field | ||
| assert.equal(backwardsCompatibilityStatus.canUpgrade, true); | ||
| ``` | ||
|
|
||
| Additionally, they a regression test to ensure that older view schemas can read content written by the current view | ||
| schema (`SchemaCompatibilityStatus.canView`): | ||
|
|
||
| ```ts | ||
| // Test what the old version of the application would do with a tree using the new schema: | ||
| const forwardsCompatibilityStatus = checkCompatibility(currentViewSchema, oldViewSchema); | ||
|
|
||
| // If the old schema set allowUnknownOptionalFields, this would be true, but since it did not, | ||
| // this assert will fail, detecting the forwards compatibility break: | ||
| // this means these two versions of the application cannot collaborate on content using these schema. | ||
| assert.equal(forwardsCompatibilityStatus.canView, true); | ||
| ``` | ||
|
|
||
| Later in the application development cycle, the application author decides they want to change their Point2D to | ||
| a Point3D, adding an extra field: | ||
|
|
||
| ```ts | ||
| // Build the current view schema | ||
| const schemaFactory = new SchemaFactory("test"); | ||
| class Point3D extends schemaFactory.object("Point", { | ||
| x: factory.number, | ||
| y: factory.number, | ||
|
|
||
| // The current schema has a new optional field that was not present on Point2D | ||
| z: factory.optional(factory.number), | ||
| }) {} | ||
| ``` | ||
|
|
||
| The test first compatibility test will pass as the Point2D schema is upgradeable to a Point3D schema. | ||
| However, the second compatibility test fill fail as an application using the Point2D view schema cannot collaborate on | ||
| content authored using the Point3D schema. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
135 changes: 135 additions & 0 deletions
135
packages/dds/tree/src/simple-tree/api/snapshotCompatibilityChecker.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| /*! | ||
| * Copyright (c) Microsoft Corporation and contributors. All rights reserved. | ||
| * Licensed under the MIT License. | ||
| */ | ||
|
|
||
| import type { JsonCompatibleReadOnly } from "../../util/index.js"; | ||
| import { toStoredSchema } from "../toStoredSchema.js"; | ||
| import { TreeViewConfigurationAlpha, TreeViewConfiguration } from "./configuration.js"; | ||
| import { SchemaCompatibilityTester } from "./schemaCompatibilityTester.js"; | ||
| import { generateSchemaFromSimpleSchema } from "./schemaFromSimple.js"; | ||
| import { decodeSimpleSchema, encodeSimpleSchema } from "./simpleSchemaCodec.js"; | ||
| import type { SchemaCompatibilityStatus } from "./tree.js"; | ||
| import { toSimpleTreeSchema } from "./viewSchemaToSimpleSchema.js"; | ||
|
|
||
| /** | ||
| * Compute the compatibility of using `view` to {@link ViewableTree.viewWith | view a tree} who's {@link ITreeAlpha.exportSimpleSchema | stored schema} could be derived from `viewWhichCreatedStoredSchema` via either {@link TreeView.initialize} or {@link TreeView.upgradeSchema}. | ||
| * | ||
| * @remarks See {@link SchemaCompatibilityStatus} for details on the compatibility results. | ||
| * | ||
| * @example This example demonstrates checking the compatibility of a historical schema against a current schema. | ||
| * In this case, the historical schema is a Point2D object with x and y fields, while the current schema is a Point3D object | ||
| * that adds an optional z field. | ||
| * | ||
| * ```ts | ||
| * // This snapshot is assumed to be the same as Point3D, except missing `z`. | ||
| * const encodedSchema = JSON.parse(fs.readFileSync("PointSchema.json", "utf8")); | ||
| * const oldViewSchema = importCompatibilitySchemaSnapshot(encodedSchema); | ||
| * | ||
| * // Build the current view schema | ||
| * class Point3D extends factory.object("Point", { | ||
| * x: factory.number, | ||
| * y: factory.number, | ||
| * | ||
| * // The current schema has a new optional field that was not present on Point2D | ||
| * z: factory.optional(factory.number), | ||
| * }) {} | ||
| * const currentViewSchema = new TreeViewConfiguration({ schema: Point3D }); | ||
| * | ||
| * // Check to see if the document created by the historical view schema can be opened with the current view schema | ||
| * const backwardsCompatibilityStatus = checkCompatibility(oldViewSchema, currentViewSchema); | ||
| * | ||
| * // z is not present in Point2D, so the schema must be upgraded | ||
| * assert.equal(backwardsCompatibilityStatus.canView, false); | ||
| * | ||
| * // The schema can be upgraded to add the new optional field | ||
| * assert.equal(backwardsCompatibilityStatus.canUpgrade, true); | ||
| * | ||
| * // Test what the old version of the application would do with a tree using the new schema: | ||
| * const forwardsCompatibilityStatus = checkCompatibility(currentViewSchema, oldViewSchema); | ||
| * | ||
| * // If the old schema set allowUnknownOptionalFields, this would be true, but since it did not, | ||
| * // this assert will fail, detecting the forwards compatibility break: | ||
| * // this means these two versions of the application cannot collaborate on content using these schema. | ||
| * assert.equal(forwardsCompatibilityStatus.canView, true); | ||
| * ``` | ||
| * | ||
| * @param viewWhichCreatedStoredSchema - From which to derive the stored schema, as if it initialized or upgraded a tree via {@link TreeView}. | ||
| * @param view - The view being tested to see if it could view tree created or initialized using `viewWhichCreatedStoredSchema`. | ||
| * @returns The compatibility status. | ||
| * | ||
| * @alpha | ||
| */ | ||
| export function checkCompatibility( | ||
| viewWhichCreatedStoredSchema: TreeViewConfiguration, | ||
| view: TreeViewConfiguration, | ||
| ): Omit<SchemaCompatibilityStatus, "canInitialize"> { | ||
| const viewAsAlpha = new TreeViewConfigurationAlpha({ schema: view.schema }); | ||
| const stored = toStoredSchema(viewWhichCreatedStoredSchema.schema, { | ||
| includeStaged: () => true, | ||
| }); | ||
| const tester = new SchemaCompatibilityTester(viewAsAlpha); | ||
| return tester.checkCompatibility(stored); | ||
| } | ||
|
|
||
| /** | ||
| * Returns a JSON compatible representation of the tree schema for snapshot compatibility checking. | ||
| * | ||
| * Snapshots can be loaded by the same or newer package versions, but not necessarily older versions. | ||
| * | ||
| * @see {@link importCompatibilitySchemaSnapshot} which loads these snapshots. | ||
| * | ||
| * @param config - The schema to snapshot. Only the schema field of the `TreeViewConfiguration` is used. | ||
| * @returns The JSON representation of the schema. | ||
| * | ||
| * @example This example creates and persists a snapshot of a Point2D schema. | ||
| * | ||
| * ```ts | ||
| * const schemaFactory = new SchemaFactory("test"); | ||
| * class Point2D extends schemaFactory.object("Point", { | ||
| * x: factory.number, | ||
| * y: factory.number, | ||
| * }) {} | ||
| * const viewSchema = new TreeViewConfiguration({ schema: Point2D }); | ||
| * const encodedSchema = JSON.stringify(exportCompatibilitySchemaSnapshot(viewSchema)); | ||
| * fs.writeFileSync("PointSchema.json", encodedSchema); | ||
| * ``` | ||
| * | ||
| * @alpha | ||
| */ | ||
| export function exportCompatibilitySchemaSnapshot( | ||
TommyBrosman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| config: Pick<TreeViewConfiguration, "schema">, | ||
| ): JsonCompatibleReadOnly { | ||
| const simpleSchema = toSimpleTreeSchema(config.schema, true); | ||
| return encodeSimpleSchema(simpleSchema); | ||
| } | ||
|
|
||
| /** | ||
| * Parse the format exported by {@link exportCompatibilitySchemaSnapshot} into a schema. | ||
| * | ||
| * Can load snapshots created by the same or older package versions, but not necessarily newer versions. | ||
| * | ||
| * @see {@link exportCompatibilitySchemaSnapshot} which creates these snapshots. | ||
| * | ||
| * @param config - The JSON representation of the schema. | ||
| * @returns The schema. Only the schema field of the {@link TreeViewConfiguration} is populated. | ||
| * @throws Will throw a usage error if the encoded schema is not in the expected format. | ||
| * | ||
| * @example This example loads and parses a snapshot of a Point2D schema. | ||
| * | ||
| * ```ts; | ||
| * const oldViewSchema = importCompatibilitySchemaSnapshot(fs.readFileSync("PointSchema.json", "utf8")); | ||
| * ``` | ||
| * | ||
| * @alpha | ||
| */ | ||
| export function importCompatibilitySchemaSnapshot( | ||
TommyBrosman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| config: JsonCompatibleReadOnly, | ||
| ): TreeViewConfiguration { | ||
| const simpleSchema = decodeSimpleSchema(config); | ||
| const viewSchema = generateSchemaFromSimpleSchema(simpleSchema); | ||
|
|
||
| // We construct a TreeViewConfiguration here with the default parameters. The default set of validation parameters are fine for | ||
| // a schema produced by `generateSchemaFromSimpleSchema`. | ||
| return new TreeViewConfiguration({ schema: viewSchema.root }); | ||
TommyBrosman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
102 changes: 102 additions & 0 deletions
102
packages/dds/tree/src/test/simple-tree/api/snapshotCompatibilityChecker.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| /*! | ||
| * Copyright (c) Microsoft Corporation and contributors. All rights reserved. | ||
| * Licensed under the MIT License. | ||
| */ | ||
|
|
||
| import { | ||
| checkCompatibility, | ||
| normalizeFieldSchema, | ||
| importCompatibilitySchemaSnapshot, | ||
| SchemaFactory, | ||
| exportCompatibilitySchemaSnapshot, | ||
| TreeViewConfiguration, | ||
| type SchemaCompatibilityStatus, | ||
| } from "../../../simple-tree/index.js"; | ||
| import { strict as assert } from "node:assert"; | ||
|
|
||
| describe("snapshotCompatibilityChecker", () => { | ||
TommyBrosman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| it("parse and snapshot can roundtrip schema", () => { | ||
| const factory = new SchemaFactory("test"); | ||
| const Schema = factory.optional(factory.string, {}); | ||
|
|
||
| const view = new TreeViewConfiguration({ schema: Schema }); | ||
| const snapshot = exportCompatibilitySchemaSnapshot(view); | ||
| const parsedView = importCompatibilitySchemaSnapshot(snapshot); | ||
|
|
||
| const normalizedView = normalizeFieldSchema(view.schema); | ||
|
|
||
| assert.equal(normalizedView.allowedTypeSet.size, 1); | ||
| assert.equal( | ||
| normalizedView.allowedTypesIdentifiers.has("com.fluidframework.leaf.string"), | ||
| true, | ||
| ); | ||
| }); | ||
|
|
||
| it("checkCompatibility detects incompatible schemas", () => { | ||
TommyBrosman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const factory = new SchemaFactory("test"); | ||
| class Point2D extends factory.object("Point", { | ||
| x: factory.number, | ||
| y: factory.number, | ||
| }) {} | ||
| class Point3D extends factory.object("Point", { | ||
| x: factory.number, | ||
| y: factory.number, | ||
| z: factory.optional(factory.number), | ||
| }) {} | ||
|
|
||
| const storedAsView = new TreeViewConfiguration({ schema: Point2D }); | ||
| const view = new TreeViewConfiguration({ schema: Point3D }); | ||
| const compatibility = checkCompatibility(storedAsView, view); | ||
|
|
||
| const expected: Omit<SchemaCompatibilityStatus, "canInitialize"> = { | ||
| canView: false, | ||
| canUpgrade: true, | ||
| isEquivalent: false, | ||
| }; | ||
| assert.deepEqual(compatibility, expected); | ||
| }); | ||
|
|
||
| it("checkCompatibility detects compatible schemas", () => { | ||
| const factory = new SchemaFactory("test"); | ||
|
|
||
| // The past view schema, for the purposes of illustration. This wouldn't normally appear as a concrete schema in the test | ||
| // checking compatibility, but rather would be loaded from a snapshot. | ||
| class Point2D extends factory.object("Point", { | ||
| x: factory.number, | ||
| y: factory.number, | ||
| }) {} | ||
| const viewSchema = new TreeViewConfiguration({ schema: Point2D }); | ||
| const encodedSchema = JSON.stringify(exportCompatibilitySchemaSnapshot(viewSchema)); | ||
|
|
||
| // Load the past view schema from the snapshot (in-memory for the purposes of this test) | ||
| // This snapshot is assumed to be the same as Point3D, except missing `z`. | ||
| const oldViewSchema = importCompatibilitySchemaSnapshot(JSON.parse(encodedSchema)); | ||
|
|
||
| // Build the current view schema | ||
| class Point3D extends factory.object("Point", { | ||
| x: factory.number, | ||
| y: factory.number, | ||
|
|
||
| // The current schema has a new optional field that was not present on Point2D | ||
| z: factory.optional(factory.number), | ||
| }) {} | ||
| const currentViewSchema = new TreeViewConfiguration({ schema: Point3D }); | ||
|
|
||
| // Check to see if the document created by the historical view schema can be opened with the current view schema | ||
| const backwardsCompatibilityStatus = checkCompatibility(oldViewSchema, currentViewSchema); | ||
|
|
||
| // z is not present in Point2D, so the schema must be upgraded | ||
| assert.equal(backwardsCompatibilityStatus.canView, false); | ||
|
|
||
| // The schema can be upgraded to add the new optional field | ||
| assert.equal(backwardsCompatibilityStatus.canUpgrade, true); | ||
|
|
||
| // Test what the old version of the application would do with a tree using the new schema: | ||
| const forwardsCompatibilityStatus = checkCompatibility(currentViewSchema, oldViewSchema); | ||
|
|
||
| // If the old schema set allowUnknownOptionalFields, this would be true, but since it did not, | ||
| // we assert that there is forwards compatibility break: | ||
| // this means these two versions of the application cannot collaborate on content using these schema. | ||
| assert.equal(forwardsCompatibilityStatus.canView, false); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.