Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
8c91051
Initial example
CraigMacomber Nov 11, 2024
aea7d62
Make fields safer
CraigMacomber Nov 11, 2024
73d05f5
Merge branch 'fieldSafe' into inversion
CraigMacomber Nov 11, 2024
ec64de4
Update packages/dds/tree/src/simple-tree/objectNode.ts
CraigMacomber Nov 12, 2024
395c688
typeNarrow asserts
CraigMacomber Nov 12, 2024
4119174
Fixes for optional
CraigMacomber Nov 12, 2024
a90e167
Merge branch 'main' of https://github.com/microsoft/FluidFramework in…
CraigMacomber Nov 12, 2024
90a79f5
FIx build
CraigMacomber Nov 12, 2024
494dde2
Merge branch 'fieldSafe' into inversion
CraigMacomber Nov 12, 2024
6997880
Export ObjectNodeSchema
CraigMacomber Nov 12, 2024
735622f
customizeSchemaTyping
CraigMacomber Nov 13, 2024
a1c3bd0
Add customized narrowing example
CraigMacomber Nov 13, 2024
beffae5
add another example, and more docs
CraigMacomber Nov 14, 2024
2836d9b
add branding example
CraigMacomber Nov 14, 2024
7ebff45
Merge branch 'main' of https://github.com/microsoft/FluidFramework in…
CraigMacomber Nov 14, 2024
1dcd254
Merge branch 'main' of https://github.com/microsoft/FluidFramework in…
CraigMacomber Nov 14, 2024
eb49097
Remove unneeded changes
CraigMacomber Nov 15, 2024
10c5ef8
Fix package API
CraigMacomber Nov 15, 2024
9645aa6
Recursive type support
CraigMacomber Dec 4, 2024
2a91ad2
Merge branch 'main' of https://github.com/microsoft/FluidFramework in…
CraigMacomber Dec 4, 2024
a3981a8
Merge branch 'main' of https://github.com/microsoft/FluidFramework in…
CraigMacomber Jan 13, 2025
773afb9
Fix merge
CraigMacomber Jan 13, 2025
d7cf0e5
Cleanup CustomizerUnsafe
CraigMacomber Jan 13, 2025
cc2684a
Cleanup and docs
CraigMacomber Jan 14, 2025
ec90211
Merge branch 'main' of https://github.com/microsoft/FluidFramework in…
CraigMacomber Jan 14, 2025
b3dffdc
Merge branch 'main' of https://github.com/microsoft/FluidFramework in…
CraigMacomber Jan 22, 2025
6e4faa2
Fix export and reports
CraigMacomber Jan 22, 2025
a6df85c
Fixes tests and cleanup
CraigMacomber Jan 23, 2025
14893ad
Merge branch 'main' of https://github.com/microsoft/FluidFramework in…
CraigMacomber Jan 23, 2025
bdc6959
update reports
CraigMacomber Jan 23, 2025
86a4b5d
Better document and test ObjectFromSchemaRecordUnsafe
CraigMacomber Jan 23, 2025
8ab3281
Merge branch 'main' of https://github.com/microsoft/FluidFramework in…
CraigMacomber Jan 23, 2025
addb022
Skip failing/broken/hanging test
CraigMacomber Jan 23, 2025
91fe629
Merge branch 'main' of https://github.com/microsoft/FluidFramework in…
CraigMacomber Jan 23, 2025
3b33fea
Add "components" example
CraigMacomber Mar 4, 2025
224c7b5
add components example
CraigMacomber Mar 4, 2025
9e1328b
Add generic components system
CraigMacomber Mar 4, 2025
b1b2c41
Apply suggestions from code review
CraigMacomber Mar 4, 2025
23487bf
More consistent and robust handling of lazy schema
CraigMacomber Mar 4, 2025
13beb5e
Fix infinite recursion and broken test
CraigMacomber Mar 4, 2025
3da123f
Expose evaluateLazySchema
CraigMacomber Mar 4, 2025
41d2c5c
Export Component
CraigMacomber Mar 4, 2025
d8681cb
Merge branch 'main' of https://github.com/microsoft/FluidFramework in…
CraigMacomber Mar 11, 2025
ff69090
Merge branch 'main' of https://github.com/microsoft/FluidFramework in…
CraigMacomber Mar 19, 2025
94b9178
Fix merge
CraigMacomber Mar 19, 2025
89a5756
Merge
CraigMacomber Oct 9, 2025
1828f00
fix non-table stuff
CraigMacomber Oct 9, 2025
bbb0ef6
Fix build
CraigMacomber Oct 10, 2025
fbe06f6
Add options for type customization and readonly field inclusion
CraigMacomber Oct 21, 2025
77b0e2f
Fix build
CraigMacomber Oct 21, 2025
9a2c09c
Merge branch 'main' of https://github.com/microsoft/FluidFramework in…
CraigMacomber Oct 21, 2025
fa17c25
Merge branch 'main' of https://github.com/microsoft/FluidFramework in…
CraigMacomber Oct 22, 2025
73fa9dc
Merge branch 'main' of https://github.com/microsoft/FluidFramework in…
CraigMacomber Nov 4, 2025
951d9ce
Fix from merge
CraigMacomber Nov 4, 2025
3096691
Merge branch 'main' of https://github.com/microsoft/FluidFramework in…
CraigMacomber Nov 4, 2025
24cf057
Update for changes on main
CraigMacomber Nov 5, 2025
2b51d0d
Merge branch 'main' of https://github.com/microsoft/FluidFramework in…
CraigMacomber Nov 5, 2025
a956b61
unify components
CraigMacomber Nov 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .changeset/metal-sloths-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
"fluid-framework": minor
"@fluidframework/tree": minor
---
---
"section": tree

Check failure on line 6 in .changeset/metal-sloths-join.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Microsoft.HeadingColons] Capitalize ': t'. Raw Output: {"message": "[Microsoft.HeadingColons] Capitalize ': t'.", "location": {"path": ".changeset/metal-sloths-join.md", "range": {"start": {"line": 6, "column": 10}}}, "severity": "ERROR"}

Check warning on line 6 in .changeset/metal-sloths-join.md

View workflow job for this annotation

GitHub Actions / vale

[vale] reported by reviewdog 🐶 [Microsoft.Headings] '"section": tree' should use sentence-style capitalization. Raw Output: {"message": "[Microsoft.Headings] '\"section\": tree' should use sentence-style capitalization.", "location": {"path": ".changeset/metal-sloths-join.md", "range": {"start": {"line": 6, "column": 1}}}, "severity": "INFO"}
---

Disallow some invalid and unsafe ObjectNode field assignments at compile time
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR builds upon #23053 . This changeset will probably need to be rewritten based on the major changes.


The compile time validation of the type of values assigned to ObjectNode fields is limited by TypeScript's limitations.
Two cases which were actually possible to disallow and should be disallowed for consistency with runtime behavior and similar APIs were being allowed:

1. [Identifier fields](https://fluidframework.com/docs/api/v2/fluid-framework/schemafactory-class#identifier-property):
Identifier fields are immutable, and setting them produces a runtime error.
This changes fixes them to no longer be typed as assignable.

2. Fields with non-exact schema:
When non-exact scheme is used for a field (for example the schema is either a schema only allowing numbers or a schema only allowing strings) the field is no longer typed as assignable.
This matches how constructors and implicit node construction work.
For example when a node `Foo` has such an non-exact schema for field `bar`, you can no longer unsafely do `foo.bar = 5` just like how you could already not do `new Foo({bar: 5})`.

This fix only applies to [`SchemaFactory.object`](https://fluidframework.com/docs/api/v2/fluid-framework/schemafactory-class#object-method).
[`SchemaFactory.objectRecursive`](https://fluidframework.com/docs/api/v2/fluid-framework/schemafactory-class#objectrecursive-method) was unable to be updated to match due to TypeScript limitations on recursive types.

An `@alpha` API, `customizeSchemaTyping` has been added to allow control over the types generated from schema.
For example code relying on the unsound typing fixed above can restore the behavior using `customizeSchemaTyping`:
1 change: 1 addition & 0 deletions packages/dds/tree/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"contravariance",
"contravariantly",
"covariantly",
"Customizer",
"deprioritized",
"endregion",
"fluidframework",
Expand Down
207 changes: 181 additions & 26 deletions packages/dds/tree/api-report/tree.alpha.api.md

Large diffs are not rendered by default.

155 changes: 142 additions & 13 deletions packages/dds/tree/api-report/tree.beta.api.md

Large diffs are not rendered by default.

155 changes: 142 additions & 13 deletions packages/dds/tree/api-report/tree.legacy.beta.api.md

Large diffs are not rendered by default.

141 changes: 130 additions & 11 deletions packages/dds/tree/api-report/tree.legacy.public.api.md

Large diffs are not rendered by default.

141 changes: 130 additions & 11 deletions packages/dds/tree/api-report/tree.public.api.md

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion packages/dds/tree/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,11 @@
}
},
"typeValidation": {
"broken": {},
"broken": {
"TypeAlias_InsertableTreeNodeFromImplicitAllowedTypes": {
"backCompat": false
}
},
"entrypoint": "public"
}
}
17 changes: 17 additions & 0 deletions packages/dds/tree/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,17 @@ export {
type NodeSchemaOptions,
type NodeSchemaOptionsAlpha,
type NodeSchemaMetadata,
type AssignableTreeFieldFromImplicitField,
type ApplyKindAssignment,
type Customizer,
type GetTypes,
type StrictTypes,
type CustomTypes,
type CustomizedSchemaTyping,
CustomizedTyping,
type DefaultInsertableTreeNodeFromImplicitAllowedTypes,
customizeSchemaTyping,
type SchemaUnionToIntersection,
type SchemaStatics,
type ITreeAlpha,
type TransactionConstraint,
Expand Down Expand Up @@ -283,6 +294,11 @@ export {
type SchemaFactory_base,
type NumberKeys,
type SimpleAllowedTypeAttributes,
type DefaultTreeNodeFromImplicitAllowedTypes,
type ObjectFromSchemaRecordRelaxed,
type ObjectSchemaTypingOptions,
type AssignableTreeFieldFromImplicitFieldDefault,
type TreeFieldFromImplicitFieldDefault,
} from "./simple-tree/index.js";
export {
SharedTree,
Expand Down Expand Up @@ -323,6 +339,7 @@ export type {
JsonCompatibleObject,
JsonCompatibleReadOnly,
JsonCompatibleReadOnlyObject,
PreventExtraProperties,
} from "./util/index.js";
export { cloneWithReplacements } from "./util/index.js";

Expand Down
7 changes: 6 additions & 1 deletion packages/dds/tree/src/simple-tree/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ export {
type SchemaFactory_base,
} from "./schemaFactory.js";
export { SchemaFactoryBeta } from "./schemaFactoryBeta.js";
export { SchemaFactoryAlpha, type SchemaStaticsAlpha } from "./schemaFactoryAlpha.js";
export {
SchemaFactoryAlpha,
type SchemaStaticsAlpha,
relaxObject,
} from "./schemaFactoryAlpha.js";
export type {
ValidateRecursiveSchema,
FixRecursiveArraySchema,
Expand Down Expand Up @@ -99,6 +103,7 @@ export type {
AllowedTypesFullFromMixedUnsafe,
UnannotateAllowedTypesListUnsafe,
AnnotateAllowedTypesListUnsafe,
customizeSchemaTypingUnsafe,
} from "./typesUnsafe.js";

export {
Expand Down
18 changes: 4 additions & 14 deletions packages/dds/tree/src/simple-tree/api/schemaFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
type InsertableObjectFromSchemaRecord,
type TreeMapNode,
type TreeObjectNode,
type ObjectSchemaTypingOptions,
} from "../node-kinds/index.js";
import {
FieldKind,
Expand Down Expand Up @@ -100,7 +101,8 @@ export function schemaFromValue(value: TreeValue): TreeNodeSchema {
* @beta
*/
export interface ObjectSchemaOptions<TCustomMetadata = unknown>
extends NodeSchemaOptions<TCustomMetadata> {
extends NodeSchemaOptions<TCustomMetadata>,
ObjectSchemaTypingOptions {
/**
* Allow nodes typed with this object node schema to contain optional fields that are not present in the schema declaration.
* Such nodes can come into existence either via import APIs (see remarks) or by way of collaboration with another client
Expand Down Expand Up @@ -159,16 +161,6 @@ export interface ObjectSchemaOptionsAlpha<TCustomMetadata = unknown>
extends ObjectSchemaOptions<TCustomMetadata>,
NodeSchemaOptionsAlpha<TCustomMetadata> {}

/**
* Default options for Object node schema creation.
* @remarks Omits parameters that are not relevant for common use cases.
*/
export const defaultSchemaFactoryObjectOptions: Required<
Pick<ObjectSchemaOptions, "allowUnknownOptionalFields">
> = {
allowUnknownOptionalFields: false,
};

/**
* The name of a schema produced by {@link SchemaFactory}, including its optional scope prefix.
*
Expand Down Expand Up @@ -397,9 +389,7 @@ export class SchemaFactory<
true,
T
> {
return objectSchema(scoped(this, name), fields, true, {
...defaultSchemaFactoryObjectOptions,
});
return objectSchema(scoped(this, name), fields, true);
}

/**
Expand Down
70 changes: 55 additions & 15 deletions packages/dds/tree/src/simple-tree/api/schemaFactoryAlpha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,21 @@ import {
arraySchema,
type MapNodeCustomizableSchema,
mapSchema,
type ObjectFromSchemaRecordRelaxed,
type ObjectNodeSchema,
objectSchema,
type RecordNodeCustomizableSchema,
recordSchema,
} from "../node-kinds/index.js";
import {
defaultSchemaFactoryObjectOptions,
scoped,
type NodeSchemaOptionsAlpha,
type ObjectSchemaOptionsAlpha,
type ScopedSchemaName,
} from "./schemaFactory.js";
import { schemaStatics } from "./schemaStatics.js";
import type { ImplicitFieldSchema } from "../fieldSchema.js";
import type { RestrictiveStringRecord } from "../../util/index.js";
import type { PreventExtraProperties, RestrictiveStringRecord } from "../../util/index.js";
import type {
NodeKind,
TreeNodeSchema,
Expand All @@ -33,6 +33,7 @@ import type {
WithType,
AllowedTypesMetadata,
AllowedTypesFullFromMixed,
TreeNode,
} from "../core/index.js";
import {
normalizeToAnnotatedAllowedType,
Expand Down Expand Up @@ -200,12 +201,12 @@ export class SchemaFactoryAlpha<
public objectAlpha<
const Name extends TName,
const T extends RestrictiveStringRecord<ImplicitFieldSchema>,
const TCustomMetadata = unknown,
const TOptions extends ObjectSchemaOptionsAlpha = ObjectSchemaOptionsAlpha,
>(
name: Name,
fields: T,
options?: ObjectSchemaOptionsAlpha<TCustomMetadata>,
): ObjectNodeSchema<ScopedSchemaName<TScope, Name>, T, true, TCustomMetadata> & {
options?: PreventExtraProperties<TOptions, ObjectSchemaOptionsAlpha>,
): ObjectNodeSchema<ScopedSchemaName<TScope, Name>, T, true, TOptions> & {
/**
* Typing checking workaround: not for for actual use.
* @remarks
Expand All @@ -218,10 +219,7 @@ export class SchemaFactoryAlpha<
*/
readonly createFromInsertable: unknown;
} {
return objectSchema(scoped<TScope, TName, Name>(this, name), fields, true, {
...defaultSchemaFactoryObjectOptions,
...(options ?? {}),
});
return objectSchema(scoped<TScope, TName, Name>(this, name), fields, true, options);
}

/**
Expand All @@ -230,11 +228,11 @@ export class SchemaFactoryAlpha<
public override objectRecursive<
const Name extends TName,
const T extends RestrictiveStringRecord<System_Unsafe.ImplicitFieldSchemaUnsafe>,
const TCustomMetadata = unknown,
const TOptions extends ObjectSchemaOptionsAlpha = ObjectSchemaOptionsAlpha,
>(
name: Name,
t: T,
options?: ObjectSchemaOptionsAlpha<TCustomMetadata>,
options?: PreventExtraProperties<TOptions, ObjectSchemaOptionsAlpha>,
): TreeNodeSchemaClass<
ScopedSchemaName<TScope, Name>,
NodeKind.Object,
Expand All @@ -243,9 +241,15 @@ export class SchemaFactoryAlpha<
false,
T,
never,
TCustomMetadata
TOptions extends ObjectSchemaOptionsAlpha<infer TCustomMetadataX>
? TCustomMetadataX
: unknown
> &
SimpleObjectNodeSchema<TCustomMetadata> &
SimpleObjectNodeSchema<
TOptions extends ObjectSchemaOptionsAlpha<infer TCustomMetadataX>
? TCustomMetadataX
: unknown
> &
// We can't just use non generic `ObjectNodeSchema` here since "Base constructors must all have the same return type".
// We also can't just use generic `ObjectNodeSchema` here and not `TreeNodeSchemaClass` since that doesn't work with unsafe recursive types.
// ObjectNodeSchema<
Expand All @@ -270,13 +274,15 @@ export class SchemaFactoryAlpha<
false,
T,
never,
TCustomMetadata
TOptions extends ObjectSchemaOptionsAlpha<infer TCustomMetadataX>
? TCustomMetadataX
: unknown
> &
ObjectNodeSchema<
ScopedSchemaName<TScope, Name>,
RestrictiveStringRecord<ImplicitFieldSchema>,
false,
TCustomMetadata
TOptions
>;
}

Expand Down Expand Up @@ -559,3 +565,37 @@ export class SchemaFactoryAlpha<
return new SchemaFactoryAlpha(scoped<TScope, TName, T>(this, name));
}
}

/**
* Convert an object node to a version with a relaxed types for its fields.
* @remarks
* This can help get TypeScript to allow sub-classing it in generic contexts.
* This must be to the class from the SchemaFactory then subclassed.
* @alpha
*/
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function relaxObject<const T extends TreeNodeSchemaClass<string, NodeKind.Object>>(
t: T,
) {
return t as T extends TreeNodeSchemaClass<
infer Name,
NodeKind.Object,
TreeNode,
infer TInsertable,
infer ImplicitlyConstructable,
infer Info extends RestrictiveStringRecord<ImplicitFieldSchema>,
infer TConstructorExtra,
infer TCustomMetadata
>
? TreeNodeSchemaClass<
Name,
NodeKind.Object,
TreeNode & WithType<Name, NodeKind.Object, T> & ObjectFromSchemaRecordRelaxed<Info>,
TInsertable,
ImplicitlyConstructable,
Info,
TConstructorExtra,
TCustomMetadata
>
: T;
}
32 changes: 22 additions & 10 deletions packages/dds/tree/src/simple-tree/api/schemaFactoryBeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ import {
type TreeRecordNode,
} from "../node-kinds/index.js";
import {
defaultSchemaFactoryObjectOptions,
SchemaFactory,
scoped,
structuralName,
type NodeSchemaOptions,
type ObjectSchemaOptions,
type ObjectSchemaOptionsAlpha,
type ScopedSchemaName,
} from "./schemaFactory.js";
import type { System_Unsafe, TreeRecordNodeUnsafe } from "./typesUnsafe.js";
Expand All @@ -43,8 +43,8 @@ import type {
} from "../fieldSchema.js";
import type { LeafSchema } from "../leafNodeSchema.js";
import type { SimpleLeafNodeSchema } from "../simpleSchema.js";
import type { RestrictiveStringRecord } from "../../util/index.js";
/* eslint-enable unused-imports/no-unused-imports, @typescript-eslint/no-unused-vars, import/no-duplicates */
import type { PreventExtraProperties, RestrictiveStringRecord } from "../../util/index.js";

/**
* {@link SchemaFactory} with additional beta APIs.
Expand Down Expand Up @@ -77,25 +77,37 @@ export class SchemaFactoryBeta<
public override object<
const Name extends TName,
const T extends RestrictiveStringRecord<ImplicitFieldSchema>,
const TCustomMetadata = unknown,
const TOptions extends ObjectSchemaOptions = ObjectSchemaOptions,
>(
name: Name,
fields: T,
options?: ObjectSchemaOptions<TCustomMetadata>,
options?: PreventExtraProperties<TOptions, ObjectSchemaOptions>,
): TreeNodeSchemaClass<
ScopedSchemaName<TScope, Name>,
NodeKind.Object,
TreeObjectNode<T, ScopedSchemaName<TScope, Name>>,
TreeObjectNode<T, ScopedSchemaName<TScope, Name>, TOptions>,
object & InsertableObjectFromSchemaRecord<T>,
true,
T,
never,
TCustomMetadata
TOptions extends ObjectSchemaOptions<infer TCustomMetadata> ? TCustomMetadata : unknown
> {
return objectSchema(scoped<TScope, TName, Name>(this, name), fields, true, {
...defaultSchemaFactoryObjectOptions,
...(options ?? {}),
});
// TODO: make type safe
return objectSchema(
scoped<TScope, TName, Name>(this, name),
fields,
true,
options as PreventExtraProperties<TOptions, ObjectSchemaOptionsAlpha>,
) as TreeNodeSchemaClass<
ScopedSchemaName<TScope, Name>,
NodeKind.Object,
TreeObjectNode<T, ScopedSchemaName<TScope, Name>, TOptions>,
object & InsertableObjectFromSchemaRecord<T>,
true,
T,
never,
TOptions extends ObjectSchemaOptions<infer TCustomMetadata> ? TCustomMetadata : unknown
>;
}

public override objectRecursive<
Expand Down
Loading
Loading