diff --git a/api-report/tree.api.md b/api-report/tree.api.md index a05e2b3a1b2d..b3da989957a4 100644 --- a/api-report/tree.api.md +++ b/api-report/tree.api.md @@ -132,6 +132,9 @@ export function createEmitter>(): ISubscribable & IEmitte // @alpha export const createField: unique symbol; +// @alpha +export function createSchemaRepository(schemaPolicy?: FullSchemaPolicy, data?: SchemaData): StoredSchemaRepository; + // @alpha export interface CrossFieldManager { get(target: CrossFieldTarget, revision: RevisionTag | undefined, id: ChangesetLocalId): T | undefined; @@ -697,6 +700,12 @@ export function keyFromSymbol(key: GlobalFieldKeySymbol): GlobalFieldKey; // @alpha export type LocalFieldKey = Brand; +// @alpha +export function lookupGlobalFieldSchema(data: SchemaDataAndPolicy, identifier: GlobalFieldKey): FieldSchema; + +// @alpha +export function lookupTreeSchema(data: SchemaDataAndPolicy, identifier: TreeSchemaIdentifier): TreeSchema; + // @alpha export interface MakeNominal { } diff --git a/experimental/PropertyDDS/packages/property-properties/src/index.d.ts b/experimental/PropertyDDS/packages/property-properties/src/index.d.ts index 3298b6850cd9..0e56018458cd 100644 --- a/experimental/PropertyDDS/packages/property-properties/src/index.d.ts +++ b/experimental/PropertyDDS/packages/property-properties/src/index.d.ts @@ -2274,6 +2274,8 @@ declare module "@fluid-experimental/property-properties" { */ public extractDependencies(): Array; + public static extractDependencies(template: PropertyTemplateType): Array; + constants: any[]; context: string; diff --git a/experimental/PropertyDDS/packages/property-properties/src/index.ts b/experimental/PropertyDDS/packages/property-properties/src/index.ts index d7a65a8cc611..7a62e1ccb6f9 100644 --- a/experimental/PropertyDDS/packages/property-properties/src/index.ts +++ b/experimental/PropertyDDS/packages/property-properties/src/index.ts @@ -4,6 +4,7 @@ */ import { PropertyFactory } from "./propertyFactory"; +import { PropertyTemplate } from "./propertyTemplate"; import { PropertyUtils } from "./propertyUtils"; import { BaseProperty } from "./properties/baseProperty"; import { ContainerProperty } from "./properties/containerProperty"; @@ -25,6 +26,7 @@ import { enableValidations } from "./enableValidations"; export { PropertyFactory, + PropertyTemplate, PropertyUtils, BaseProperty, ContainerProperty, diff --git a/experimental/PropertyDDS/packages/property-shared-tree-interop/.eslintignore b/experimental/PropertyDDS/packages/property-shared-tree-interop/.eslintignore new file mode 100644 index 000000000000..ca340747a7cc --- /dev/null +++ b/experimental/PropertyDDS/packages/property-shared-tree-interop/.eslintignore @@ -0,0 +1,5 @@ +doc +node_modules +dist +jest.config.js +src/index.d.ts diff --git a/experimental/PropertyDDS/packages/property-shared-tree-interop/.eslintrc.js b/experimental/PropertyDDS/packages/property-shared-tree-interop/.eslintrc.js new file mode 100644 index 000000000000..e1b89d050b79 --- /dev/null +++ b/experimental/PropertyDDS/packages/property-shared-tree-interop/.eslintrc.js @@ -0,0 +1,26 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +module.exports = { + extends: ["@fluidframework/eslint-config-fluid/minimal", "prettier"], + parserOptions: { + project: ["./tsconfig.json", "./src/test/tsconfig.json"], + }, + rules: { + "@typescript-eslint/strict-boolean-expressions": "off", + "unicorn/filename-case": "off", + "import/no-default-export": "off", + }, + overrides: [ + { + // Rules only for test files + files: ["*.spec.ts", "src/test/**"], + rules: { + // Test files are run in node only so additional node libraries can be used. + "import/no-nodejs-modules": ["error", { allow: ["assert", "events"] }], + }, + }, + ], +}; diff --git a/experimental/PropertyDDS/packages/property-shared-tree-interop/.gitignore b/experimental/PropertyDDS/packages/property-shared-tree-interop/.gitignore new file mode 100644 index 000000000000..6d4486933c45 --- /dev/null +++ b/experimental/PropertyDDS/packages/property-shared-tree-interop/.gitignore @@ -0,0 +1,4 @@ +dist +node_modules +*.log +**/*.tsbuildinfo diff --git a/experimental/PropertyDDS/packages/property-shared-tree-interop/.npmignore b/experimental/PropertyDDS/packages/property-shared-tree-interop/.npmignore new file mode 100644 index 000000000000..a40f882cf599 --- /dev/null +++ b/experimental/PropertyDDS/packages/property-shared-tree-interop/.npmignore @@ -0,0 +1,6 @@ +nyc +*.log +**/*.tsbuildinfo +src/test +dist/test +**/_api-extractor-temp/** diff --git a/experimental/PropertyDDS/packages/property-shared-tree-interop/LICENSE b/experimental/PropertyDDS/packages/property-shared-tree-interop/LICENSE new file mode 100644 index 000000000000..60af0a6a40e9 --- /dev/null +++ b/experimental/PropertyDDS/packages/property-shared-tree-interop/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) Microsoft Corporation and contributors. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/experimental/PropertyDDS/packages/property-shared-tree-interop/README.md b/experimental/PropertyDDS/packages/property-shared-tree-interop/README.md new file mode 100644 index 000000000000..2e0d5456ccdb --- /dev/null +++ b/experimental/PropertyDDS/packages/property-shared-tree-interop/README.md @@ -0,0 +1,3 @@ +# @fluid-experimental/property-dds-new-tree + +This package contains schemas that are shared between the example projects. diff --git a/experimental/PropertyDDS/packages/property-shared-tree-interop/jest.config.js b/experimental/PropertyDDS/packages/property-shared-tree-interop/jest.config.js new file mode 100644 index 000000000000..7d90f57448ad --- /dev/null +++ b/experimental/PropertyDDS/packages/property-shared-tree-interop/jest.config.js @@ -0,0 +1,18 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +module.exports = { + preset: "ts-jest", + testEnvironment: "node", + transform: { + "^.+\\.ts$": "ts-jest", + }, + globals: { + "ts-jest": { + tsconfig: "src/test/tsconfig.json", + }, + }, + testPathIgnorePatterns: ["/node_modules/", "dist"], +}; diff --git a/experimental/PropertyDDS/packages/property-shared-tree-interop/package.json b/experimental/PropertyDDS/packages/property-shared-tree-interop/package.json new file mode 100644 index 000000000000..098277c6e387 --- /dev/null +++ b/experimental/PropertyDDS/packages/property-shared-tree-interop/package.json @@ -0,0 +1,67 @@ +{ + "name": "@fluid-experimental/property-shared-tree-interop", + "version": "2.0.0-internal.3.2.0", + "description": "Utilities for migration from PropertyDDS to the new SharedTree DDS", + "homepage": "https://fluidframework.com", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/FluidFramework.git", + "directory": "experimental/PropertyDDS/packages/property-shared-tree-interop" + }, + "license": "MIT", + "author": "Microsoft and contributors", + "main": "dist/index.js", + "module": "lib/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "concurrently npm:build:compile npm:lint", + "build:commonjs": "npm run tsc && npm run build:test", + "build:compile": "concurrently npm:build:commonjs npm:build:esnext", + "build:esnext": "tsc --project ./tsconfig.esnext.json", + "build:full": "npm run build", + "build:full:compile": "npm run build:compile", + "build:test": "tsc --project ./src/test/tsconfig.json", + "clean": "rimraf dist *.tsbuildinfo *.build.log", + "eslint": "eslint --format stylish src", + "eslint:fix": "eslint --format stylish src --fix --fix-type problem,suggestion,layout", + "format": "npm run prettier:fix", + "lint": "npm run prettier && npm run eslint", + "lint:fix": "npm run prettier:fix && npm run eslint:fix", + "prettier": "prettier --check . --ignore-path ../../../../.prettierignore", + "prettier:fix": "prettier --write . --ignore-path ../../../../.prettierignore", + "test": "npm run test:jest", + "test:coverage": "jest --coverage --ci --reporters=default --reporters=jest-junit", + "test:jest": "jest", + "tsc": "tsc" + }, + "devDependencies": { + "@fluidframework/build-common": "^1.1.0", + "@rushstack/eslint-config": "^2.5.1", + "@types/jest": "22.2.3", + "@types/node": "^14.18.36", + "concurrently": "^6.2.0", + "eslint": "~8.6.0", + "jest": "^26.6.3", + "jest-junit": "^10.0.0", + "prettier": "~2.6.2", + "rimraf": "^2.6.2", + "ts-jest": "^26.4.4", + "typescript": "~4.5.5" + }, + "dependencies": { + "@fluid-experimental/property-properties": ">=2.0.0-internal.3.2.0 <2.0.0-internal.4.0.0", + "@fluid-experimental/property-changeset": ">=2.0.0-internal.3.2.0 <2.0.0-internal.4.0.0", + "@fluid-internal/tree": ">=2.0.0-internal.3.2.0 <2.0.0-internal.4.0.0" + }, + "jest-junit": { + "outputDirectory": "nyc", + "outputName": "jest-junit-report.xml" + }, + "typeValidation": { + "disabled": true, + "version": "2.0.0-internal.2.2.0", + "baselineRange": ">=2.0.0-internal.2.1.0 <2.0.0-internal.2.2.0", + "baselineVersion": "2.0.0-internal.2.1.0", + "broken": {} + } +} diff --git a/experimental/PropertyDDS/packages/property-shared-tree-interop/prettier.config.cjs b/experimental/PropertyDDS/packages/property-shared-tree-interop/prettier.config.cjs new file mode 100644 index 000000000000..d4870022599f --- /dev/null +++ b/experimental/PropertyDDS/packages/property-shared-tree-interop/prettier.config.cjs @@ -0,0 +1,8 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +module.exports = { + ...require("@fluidframework/build-common/prettier.config.cjs"), +}; diff --git a/experimental/PropertyDDS/packages/property-shared-tree-interop/src/index.ts b/experimental/PropertyDDS/packages/property-shared-tree-interop/src/index.ts new file mode 100644 index 000000000000..a3963a71dfe2 --- /dev/null +++ b/experimental/PropertyDDS/packages/property-shared-tree-interop/src/index.ts @@ -0,0 +1,6 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +export { convertPSetSchemaToSharedTreeLls as convertPSetSchema } from "./schemaConverter"; diff --git a/experimental/PropertyDDS/packages/property-shared-tree-interop/src/schemaConverter.ts b/experimental/PropertyDDS/packages/property-shared-tree-interop/src/schemaConverter.ts new file mode 100644 index 000000000000..1c6a3c7d2639 --- /dev/null +++ b/experimental/PropertyDDS/packages/property-shared-tree-interop/src/schemaConverter.ts @@ -0,0 +1,270 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { + fail, + emptyField, + FieldKinds, + FieldSchema, + NamedTreeSchema, + neverTree, + rootFieldKey, + SchemaData, + StoredSchemaRepository, + TreeSchemaIdentifier, + ValueSchema, + lookupTreeSchema, + fieldSchema, + namedTreeSchema, + brand, + EmptyKey, +} from "@fluid-internal/tree"; +import { PropertyFactory, PropertyTemplate } from "@fluid-experimental/property-properties"; +import { TypeIdHelper } from "@fluid-experimental/property-changeset"; + +const booleanTypes = new Set(["Bool"]); +const numberTypes = new Set([ + "Int8", + "Uint8", + "Int16", + "Uint16", + "Int32", + "Int64", + "Uint64", + "Uint32", + "Float32", + "Float64", +]); +const primitiveTypes = new Set([ + "Bool", + "String", + "Int8", + "Uint8", + "Int16", + "Uint16", + "Int32", + "Int64", + "Uint64", + "Uint32", + "Float32", + "Float64", +]); + +export function convertPSetSchemaToSharedTreeLls( + repository: StoredSchemaRepository, + rootFieldSchema: FieldSchema, +): void { + const globalTreeSchema: Map = new Map(); + // Extract all referenced typeids for the schema + const unprocessedTypeIds: TreeSchemaIdentifier[] = []; + for (const type of rootFieldSchema.types ?? fail("expected at least one root type")) { + unprocessedTypeIds.push(type); + } + const referencedTypeIDs = new Set(); + + while (unprocessedTypeIds.length > 0) { + const unprocessedTypeID = unprocessedTypeIds.pop(); + + if (!unprocessedTypeID) { + return; + } + + referencedTypeIDs.add(unprocessedTypeID); + + const schemaTemplate = PropertyFactory.getTemplate(unprocessedTypeID); + if (schemaTemplate === undefined) { + throw new Error(`Unknown typeid: ${unprocessedTypeID}`); + } + const dependencies = PropertyTemplate.extractDependencies( + schemaTemplate, + ) as TreeSchemaIdentifier[]; + for (const dependencyTypeId of dependencies) { + if (!referencedTypeIDs.has(dependencyTypeId)) { + unprocessedTypeIds.push(dependencyTypeId); + } + } + + // Extract context information (i.e. array, map and set types) + const extractContexts = (properties: any[]): void => { + for (const property of properties || []) { + if (property.properties) { + // We have a nested set of properties + // TODO: We have to create a corresponding nested type + extractContexts(property.properties); + } + if (property.context && property.context !== "single") { + referencedTypeIDs.add( + `${property.context}<${property.typeid}>` as TreeSchemaIdentifier, + ); + } + if (TypeIdHelper.isPrimitiveType(property.typeid)) { + referencedTypeIDs.add(property.typeid); + } + } + }; + extractContexts(schemaTemplate.properties); + } + + for (const type of primitiveTypes) { + const typeid: TreeSchemaIdentifier = brand(type); + if (!referencedTypeIDs.has(typeid)) { + referencedTypeIDs.add(typeid); + } + } + + // Now we create the actual schemas, since we are now able to reference the dependent types + for (const referencedTypeId of referencedTypeIDs.values()) { + if (lookupTreeSchema(repository, referencedTypeId) !== neverTree) { + continue; + } + + const splitTypeId = TypeIdHelper.extractContext(referencedTypeId); + let typeSchema: NamedTreeSchema | undefined; + + if (splitTypeId.context === "single") { + if (TypeIdHelper.isPrimitiveType(splitTypeId.typeid)) { + // @TODO for simplicity we convert it to native string + // if (splitTypeId.typeid === "String") { + // // String is a special case, we actually have to represent it as a sequence + // typeSchema = { + // name: referencedTypeId, + // localFields: new Map([ + // // TODO: What should be the key we use for the entries? Should this be standardized? + // ["entries" as LocalFieldKey, { + // kind: FieldKinds.sequence.identifier, + // types: new Set([ + // // TODO: Which type do we use for characters? + // ]), + // }], + // ]), + // globalFields: new Set(), + // extraLocalFields: emptyField, + // extraGlobalFields: false, + // value: ValueSchema.Nothing, + // }; + // } else { + let valueType: ValueSchema; + if (splitTypeId.isEnum) { + valueType = ValueSchema.Number; + } else if ( + splitTypeId.typeid === "String" || + splitTypeId.typeid.startsWith("Reference<") + ) { + valueType = ValueSchema.String; + } else if (booleanTypes.has(splitTypeId.typeid)) { + valueType = ValueSchema.Boolean; + } else if (numberTypes.has(splitTypeId.typeid)) { + valueType = ValueSchema.Number; + } else { + throw new Error(`Unknown primitive typeid: ${splitTypeId.typeid}`); + } + + typeSchema = namedTreeSchema({ + name: referencedTypeId, + extraLocalFields: emptyField, + value: valueType, + }); + // } + } else { + if (splitTypeId.typeid === "NodeProperty") { + typeSchema = namedTreeSchema({ + name: referencedTypeId, + extraLocalFields: fieldSchema(FieldKinds.optional), + }); + } else { + const localFields = {}; + const inheritanceChain = PropertyFactory.getAllParentsForTemplate( + splitTypeId.typeid, + ); + inheritanceChain.push(splitTypeId.typeid); + + for (const typeIdInInheritanceChain of inheritanceChain) { + if (typeIdInInheritanceChain === "NodeProperty") { + continue; + } + + const schema = PropertyFactory.getTemplate(typeIdInInheritanceChain); + if (schema === undefined) { + throw new Error( + `Unknown typeid referenced: ${typeIdInInheritanceChain}`, + ); + } + for (const property of schema.properties) { + if (property.properties) { + // TODO: Handle nested properties + } else { + let currentTypeid = property.typeid as string; + if (property.context && property.context !== "single") { + currentTypeid = `${property.context}<${property.typeid || ""}>`; + } + + localFields[property.id] = fieldSchema( + property.optional ? FieldKinds.optional : FieldKinds.value, + [brand(currentTypeid)], + ); + } + } + } + + typeSchema = namedTreeSchema({ + name: referencedTypeId, + localFields, + extraLocalFields: PropertyFactory.inheritsFrom( + splitTypeId.typeid, + "NodeProperty", + ) + ? fieldSchema(FieldKinds.optional) + : emptyField, + }); + } + } + } else { + const fieldKind = + splitTypeId.context === "array" ? FieldKinds.sequence : FieldKinds.optional; + + const fieldType = fieldSchema( + fieldKind, + splitTypeId.typeid !== "" && splitTypeId.typeid !== "BaseProperty" + ? [brand(splitTypeId.typeid)] + : undefined, + ); + switch (splitTypeId.context) { + case "map": + case "set": + typeSchema = namedTreeSchema({ + name: referencedTypeId, + extraLocalFields: fieldType, + }); + + break; + case "array": + typeSchema = namedTreeSchema({ + name: referencedTypeId, + localFields: { + [EmptyKey]: fieldType, + }, + extraLocalFields: emptyField, + }); + break; + default: + throw new Error(`Unknown context in typeid: ${splitTypeId.context}`); + } + } + globalTreeSchema.set(referencedTypeId, typeSchema); + } + const fullSchemaData: SchemaData = { + treeSchema: globalTreeSchema, + globalFieldSchema: new Map([[rootFieldKey, rootFieldSchema]]), + }; + repository.update(fullSchemaData); +} + +// Concepts currently not mapped / represented in the compiled schema: +// +// * Annotations +// * Length constraints for arrays / strings +// * Constants +// * Values for enums +// * Default values diff --git a/experimental/PropertyDDS/packages/property-shared-tree-interop/src/test/personSchema.ts b/experimental/PropertyDDS/packages/property-shared-tree-interop/src/test/personSchema.ts new file mode 100644 index 000000000000..b0644f5bc57d --- /dev/null +++ b/experimental/PropertyDDS/packages/property-shared-tree-interop/src/test/personSchema.ts @@ -0,0 +1,74 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +export default { + geodesicLocation: { + typeid: "Test:GeodesicLocation-1.0.0", + properties: [ + { id: "lat", typeid: "Float64" }, + { id: "lon", typeid: "Float64" }, + ], + }, + cartesianLocation: { + typeid: "Test:CartesianLocation-1.0.0", + properties: [{ id: "coords", typeid: "Float64", context: "array" }], + }, + simplePhones: { + typeid: "Test:SimplePhones-1.0.0", + properties: [{ id: "phone", typeid: "String", context: "array" }], + }, + complexPhone: { + typeid: "Test:ComplexPhone-1.0.0", + properties: [ + { id: "number", typeid: "String" }, + { id: "prefix", typeid: "String" }, + { + id: "extraPhones", + typeid: "Test:SimplePhones-1.0.0", + optional: true, + }, + ], + }, + phone: { + typeid: "Test:Phone-1.0.0", + properties: [ + { id: "phoneAsString", typeid: "String", context: "array" }, + { id: "phoneAsInt32", typeid: "Int32", context: "array" }, + { + id: "complexPhones", + typeid: "Test:ComplexPhone-1.0.0", + context: "array", + }, + { + id: "phonesAsArray", + typeid: "Test:SimplePhones-1.0.0", + }, + ], + }, + address: { + typeid: "Test:Address-1.0.0", + inherits: ["NodeProperty", "Test:GeodesicLocation-1.0.0", "Test:CartesianLocation-1.0.0"], + properties: [ + { id: "zip", typeid: "String" }, + { id: "street", typeid: "String", optional: true }, + { id: "city", typeid: "String", optional: true }, + { id: "country", typeid: "String", optional: true }, + { id: "phones", typeid: "Test:Phone-1.0.0", optional: true, context: "array" }, + ], + }, + person: { + typeid: "Test:Person-1.0.0", + inherits: ["NodeProperty"], + properties: [ + { id: "name", typeid: "String" }, + { id: "age", typeid: "Int32", optional: true }, + { id: "adult", typeid: "Bool", optional: true }, + { id: "salary", typeid: "Float64", optional: true }, + { id: "salary", typeid: "Int32", optional: true }, + { id: "address", typeid: "Test:Address-1.0.0", optional: true }, + { id: "friends", typeid: "String", context: "map", optional: true }, + ], + }, +}; diff --git a/experimental/PropertyDDS/packages/property-shared-tree-interop/src/test/schemaConverter.spec.ts b/experimental/PropertyDDS/packages/property-shared-tree-interop/src/test/schemaConverter.spec.ts new file mode 100644 index 000000000000..9aafe15a20e9 --- /dev/null +++ b/experimental/PropertyDDS/packages/property-shared-tree-interop/src/test/schemaConverter.spec.ts @@ -0,0 +1,69 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { + brand, + FieldKinds, + createSchemaRepository, + fieldSchema, + lookupGlobalFieldSchema, + lookupTreeSchema, + ValueSchema, + rootFieldKey, +} from "@fluid-internal/tree"; +import { PropertyFactory } from "@fluid-experimental/property-properties"; +import { convertPSetSchemaToSharedTreeLls } from "../schemaConverter"; +import personSchema from "./personSchema"; + +describe("schema converter", () => { + beforeAll(() => { + PropertyFactory.register(Object.values(personSchema)); + }); + + it(`can use "NodeProperty" as root`, () => { + const rootFieldSchema = fieldSchema(FieldKinds.optional, [brand("NodeProperty")]); + const schemaRepository = createSchemaRepository(); + convertPSetSchemaToSharedTreeLls(schemaRepository, rootFieldSchema); + + // 12 basic types + NodeProperty + expect(schemaRepository.globalFieldSchema.size).toEqual(1); + expect(lookupGlobalFieldSchema(schemaRepository, rootFieldKey)).toEqual(rootFieldSchema); + + expect(schemaRepository.treeSchema.size).toEqual(13); + const nodePropertySchema = lookupTreeSchema(schemaRepository, brand("NodeProperty")); + expect(nodePropertySchema).toEqual({ + name: "NodeProperty", + localFields: new Map(), + extraLocalFields: { kind: "Optional" }, + globalFields: new Set(), + extraGlobalFields: false, + value: ValueSchema.Nothing, + }); + }); + + it("can convert property with array context", () => { + const rootFieldSchema = fieldSchema(FieldKinds.optional, [brand("Test:Person-1.0.0")]); + const schemaRepository = createSchemaRepository(); + convertPSetSchemaToSharedTreeLls(schemaRepository, rootFieldSchema); + const addressSchema = lookupTreeSchema(schemaRepository, brand("Test:Address-1.0.0")); + expect(addressSchema).toMatchObject({ + name: "Test:Address-1.0.0", + localFields: new Map([ + ["lat", fieldSchema(FieldKinds.value, [brand("Float64")])], + ["lon", fieldSchema(FieldKinds.value, [brand("Float64")])], + ["coords", fieldSchema(FieldKinds.value, [brand("array")])], + ["zip", fieldSchema(FieldKinds.value, [brand("String")])], + ["street", fieldSchema(FieldKinds.optional, [brand("String")])], + ["city", fieldSchema(FieldKinds.optional, [brand("String")])], + ["country", fieldSchema(FieldKinds.optional, [brand("String")])], + ["phones", fieldSchema(FieldKinds.optional, [brand("array")])], + ]), + extraLocalFields: { kind: "Optional" }, + globalFields: new Set(), + extraGlobalFields: false, + value: ValueSchema.Nothing, + }); + }); +}); diff --git a/experimental/PropertyDDS/packages/property-shared-tree-interop/src/test/tsconfig.json b/experimental/PropertyDDS/packages/property-shared-tree-interop/src/test/tsconfig.json new file mode 100644 index 000000000000..df8b508e0cfe --- /dev/null +++ b/experimental/PropertyDDS/packages/property-shared-tree-interop/src/test/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "@fluidframework/build-common/ts-common-config.json", + "compilerOptions": { + "rootDir": "./", + "outDir": "../../dist/test", + "types": ["node", "jest"], + "declaration": false, + "declarationMap": false, + "allowJs": true, + }, + "include": ["./**/*"], + "references": [ + { + "path": "../..", + }, + ], +} diff --git a/experimental/PropertyDDS/packages/property-shared-tree-interop/tsconfig.esnext.json b/experimental/PropertyDDS/packages/property-shared-tree-interop/tsconfig.esnext.json new file mode 100644 index 000000000000..1ea62d46b20b --- /dev/null +++ b/experimental/PropertyDDS/packages/property-shared-tree-interop/tsconfig.esnext.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./lib", + "module": "esnext", + }, +} diff --git a/experimental/PropertyDDS/packages/property-shared-tree-interop/tsconfig.json b/experimental/PropertyDDS/packages/property-shared-tree-interop/tsconfig.json new file mode 100644 index 000000000000..9b43542b6220 --- /dev/null +++ b/experimental/PropertyDDS/packages/property-shared-tree-interop/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@fluidframework/build-common/ts-common-config.json", + "exclude": ["src/test/**/*"], + "compilerOptions": { + "composite": true, + "rootDir": "./src", + "outDir": "./dist", + }, + "include": ["src/**/*"], +} diff --git a/packages/dds/tree/src/core/schema-stored/storedSchemaRepository.ts b/packages/dds/tree/src/core/schema-stored/storedSchemaRepository.ts index 17ee35776af4..67d019ac4d1c 100644 --- a/packages/dds/tree/src/core/schema-stored/storedSchemaRepository.ts +++ b/packages/dds/tree/src/core/schema-stored/storedSchemaRepository.ts @@ -126,6 +126,11 @@ interface MutableSchemaData extends SchemaData { treeSchema: Map; } +/** + * Helper for getting a global {@link FieldSchema} from a stored schema. + * Defaults to the FieldSchema defined with a stored schema policy. + * @alpha + */ export function lookupGlobalFieldSchema( data: SchemaDataAndPolicy, identifier: GlobalFieldKey, @@ -133,6 +138,11 @@ export function lookupGlobalFieldSchema( return data.globalFieldSchema.get(identifier) ?? data.policy.defaultGlobalFieldSchema; } +/** + * Helper for getting a {@link TreeSchema} from a stored schema. + * Defaults to the TreeSchema defined with a stored schema policy. + * @alpha + */ export function lookupTreeSchema( data: SchemaDataAndPolicy, identifier: TreeSchemaIdentifier, diff --git a/packages/dds/tree/src/feature-libraries/defaultSchema.ts b/packages/dds/tree/src/feature-libraries/defaultSchema.ts index c563a9364aaa..2e349c568126 100644 --- a/packages/dds/tree/src/feature-libraries/defaultSchema.ts +++ b/packages/dds/tree/src/feature-libraries/defaultSchema.ts @@ -3,7 +3,16 @@ * Licensed under the MIT License. */ -import { fieldSchema, emptyMap, emptySet, ValueSchema, TreeSchema } from "../core"; +import { + fieldSchema, + emptyMap, + emptySet, + ValueSchema, + TreeSchema, + InMemoryStoredSchemaRepository, + StoredSchemaRepository, + SchemaData, +} from "../core"; import { value, forbidden, fieldKinds } from "./defaultFieldKinds"; import { FullSchemaPolicy } from "./modular-schema"; @@ -43,3 +52,14 @@ export const defaultSchemaPolicy: FullSchemaPolicy = { defaultTreeSchema: neverTree, defaultGlobalFieldSchema: emptyField, }; + +/** + * Helper for building {@link StoredSchemaRepository}. + * @alpha + */ +export function createSchemaRepository( + schemaPolicy = defaultSchemaPolicy, + data?: SchemaData, +): StoredSchemaRepository { + return new InMemoryStoredSchemaRepository(schemaPolicy, data); +} diff --git a/packages/dds/tree/src/feature-libraries/index.ts b/packages/dds/tree/src/feature-libraries/index.ts index 3904d02681e7..42be58d90818 100644 --- a/packages/dds/tree/src/feature-libraries/index.ts +++ b/packages/dds/tree/src/feature-libraries/index.ts @@ -60,7 +60,13 @@ export { singleTextCursor, jsonableTreeFromCursor } from "./treeTextCursor"; import * as SequenceField from "./sequence-field"; export { SequenceField }; -export { defaultSchemaPolicy, emptyField, neverField, neverTree } from "./defaultSchema"; +export { + defaultSchemaPolicy, + emptyField, + neverField, + neverTree, + createSchemaRepository, +} from "./defaultSchema"; export { ChangesetLocalId, diff --git a/packages/dds/tree/src/index.ts b/packages/dds/tree/src/index.ts index 4887ca12cedd..1444d2229a6b 100644 --- a/packages/dds/tree/src/index.ts +++ b/packages/dds/tree/src/index.ts @@ -67,6 +67,8 @@ export { SchemaData, SchemaPolicy, SchemaDataAndPolicy, + lookupGlobalFieldSchema, + lookupTreeSchema, ChangeEncoder, ChangeFamily, ProgressiveEditBuilder, @@ -177,6 +179,7 @@ export { isWritableArrayLike, isContextuallyTypedNodeDataObject, defaultSchemaPolicy, + createSchemaRepository, jsonableTreeFromCursor, PrimitiveValue, IDefaultEditBuilder,