Skip to content

feat: customizeSchemaTyping #23084

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

Draft
wants to merge 45 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
Show all changes
45 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
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
---

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
@@ -19,6 +19,7 @@
"contravariance",
"contravariantly",
"covariantly",
"Customizer",
"deprioritized",
"endregion",
"fluidframework",
145 changes: 135 additions & 10 deletions packages/dds/tree/api-report/tree.alpha.api.md
Original file line number Diff line number Diff line change
@@ -28,11 +28,22 @@ type ApplyKind<T, Kind extends FieldKind> = {
[FieldKind.Identifier]: T;
}[Kind];

// @public
export type ApplyKindAssignment<T, Kind extends FieldKind> = [Kind] extends [
FieldKind.Required
] ? T : [Kind] extends [FieldKind.Optional] ? T | undefined : never;

// @public
type ApplyKindInput<T, Kind extends FieldKind, DefaultsAreOptional extends boolean> = [
Kind
] extends [FieldKind.Required] ? T : [Kind] extends [FieldKind.Optional] ? T | undefined : [Kind] extends [FieldKind.Identifier] ? DefaultsAreOptional extends true ? T | undefined : T : never;

// @public
export type AssignableTreeFieldFromImplicitField<TSchemaInput extends ImplicitFieldSchema, TSchema = SchemaUnionToIntersection<TSchemaInput>> = [TSchema] extends [FieldSchema<infer Kind, infer Types>] ? ApplyKindAssignment<GetTypes<Types>["readWrite"], Kind> : [TSchema] extends [ImplicitAllowedTypes] ? GetTypes<TSchema>["readWrite"] : never;

// @public
export type AssignableTreeFieldFromImplicitFieldUnsafe<TSchema extends Unenforced<ImplicitFieldSchema>> = TSchema extends FieldSchemaUnsafe<infer Kind, infer Types> ? ApplyKindAssignment<GetTypesUnsafe<Types>["readWrite"], Kind> : GetTypesUnsafe<TSchema>["readWrite"];

// @alpha
export function asTreeViewAlpha<TSchema extends ImplicitFieldSchema>(view: TreeView<TSchema>): TreeViewAlpha<TSchema>;

@@ -66,6 +77,13 @@ export interface CommitMetadata {
// @alpha
export function comparePersistedSchema(persisted: JsonCompatible, view: ImplicitFieldSchema, options: ICodecOptions, canInitialize: boolean): SchemaCompatibilityStatus;

// @alpha
export namespace Component {
export type ComponentSchemaCollection<TConfig, TSchema> = (lazyConfiguration: () => TConfig) => LazyArray<TSchema>;
export function composeComponentSchema<TConfig, TItem>(allComponents: readonly ComponentSchemaCollection<TConfig, TItem>[], lazyConfiguration: () => TConfig): (() => TItem)[];
export type LazyArray<T> = readonly (() => T)[];
}

// @alpha
export type ConciseTree<THandle = IFluidHandle> = Exclude<TreeLeafValue, IFluidHandle> | THandle | ConciseTree<THandle>[] | {
[key: string]: ConciseTree<THandle>;
@@ -86,10 +104,66 @@ export function createSimpleTreeIndex<TFieldSchema extends ImplicitFieldSchema,
// @alpha
export function createSimpleTreeIndex<TFieldSchema extends ImplicitFieldSchema, TKey extends TreeIndexKey, TValue, TSchema extends TreeNodeSchema>(view: TreeView<TFieldSchema>, indexer: Map<TreeNodeSchema, string>, getValue: (nodes: TreeIndexNodes<NodeFromSchema<TSchema>>) => TValue, isKeyValid: (key: TreeIndexKey) => key is TKey, indexableSchema: readonly TSchema[]): SimpleTreeIndex<TKey, TValue>;

// @public
export type CustomizedSchemaTyping<TSchema, TCustom extends CustomTypes> = TSchema & {
[CustomizedTyping]: TCustom;
};

// @public
export const CustomizedTyping: unique symbol;

// @public
export type CustomizedTyping = typeof CustomizedTyping;

// @alpha @sealed
export interface Customizer<TSchema extends ImplicitAllowedTypes> {
custom<T extends Partial<CustomTypes>>(): CustomizedSchemaTyping<TSchema, {
[Property in keyof CustomTypes]: Property extends keyof T ? T[Property] extends CustomTypes[Property] ? T[Property] : GetTypes<TSchema>[Property] : GetTypes<TSchema>[Property];
}>;
relaxed(): CustomizedSchemaTyping<TSchema, {
input: TreeNodeSchema extends TSchema ? InsertableContent : TSchema extends TreeNodeSchema ? InsertableTypedNode<TSchema> : TSchema extends AllowedTypes ? TSchema[number] extends LazyItem<infer TSchemaInner extends TreeNodeSchema> ? InsertableTypedNode<TSchemaInner, TSchemaInner> : never : never;
readWrite: TreeNodeFromImplicitAllowedTypes<TSchema>;
output: TreeNodeFromImplicitAllowedTypes<TSchema>;
}>;
simplified<T extends TreeNodeFromImplicitAllowedTypes<TSchema>>(): CustomizedSchemaTyping<TSchema, {
input: T;
readWrite: T;
output: T;
}>;
simplifiedUnrestricted<T extends TreeNode | TreeLeafValue>(): CustomizedSchemaTyping<TSchema, {
input: T;
readWrite: T;
output: T;
}>;
strict(): CustomizedSchemaTyping<TSchema, StrictTypes<TSchema>>;
}

// @alpha
export function customizeSchemaTyping<const TSchema extends ImplicitAllowedTypes>(schema: TSchema): Customizer<TSchema>;

// @public @sealed
export interface CustomTypes {
readonly input: unknown;
readonly output: TreeLeafValue | TreeNode;
readonly readWrite: TreeLeafValue | TreeNode;
}

// @public
export type DefaultInsertableTreeNodeFromImplicitAllowedTypes<TSchema extends ImplicitAllowedTypes> = [TSchema] extends [TreeNodeSchema] ? InsertableTypedNode<TSchema> : [TSchema] extends [AllowedTypes] ? InsertableTreeNodeFromAllowedTypes<TSchema> : never;

// @public
export type DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe<TSchema extends Unenforced<ImplicitAllowedTypes>> = [TSchema] extends [TreeNodeSchemaUnsafe] ? InsertableTypedNodeUnsafe<TSchema> : [TSchema] extends [AllowedTypesUnsafe] ? InsertableTreeNodeFromAllowedTypesUnsafe<TSchema> : never;

// @public @sealed
interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> {
}

// @public
export type DefaultTreeNodeFromImplicitAllowedTypes<TSchema extends ImplicitAllowedTypes = TreeNodeSchema> = TSchema extends TreeNodeSchema ? NodeFromSchema<TSchema> : TSchema extends AllowedTypes ? NodeFromSchema<FlexListToUnion<TSchema>> : unknown;

// @public
export type DefaultTreeNodeFromImplicitAllowedTypesUnsafe<TSchema extends Unenforced<ImplicitAllowedTypes>> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe<TSchema> : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe<FlexListToUnion<TSchema>> : unknown;

// @alpha
export interface EncodeOptions {
readonly useStoredKeys?: boolean;
@@ -216,6 +290,16 @@ export function getBranch<T extends ImplicitFieldSchema | UnsafeUnknownSchema>(v
// @alpha
export function getJsonSchema(schema: ImplicitFieldSchema): JsonTreeSchema;

// @public
export type GetTypes<TSchema extends ImplicitAllowedTypes> = [TSchema] extends [
CustomizedSchemaTyping<unknown, infer TCustom>
] ? TCustom : StrictTypes<TSchema>;

// @public
export type GetTypesUnsafe<TSchema extends Unenforced<ImplicitAllowedTypes>> = [
TSchema
] extends [CustomizedSchemaTyping<unknown, infer TCustom>] ? TCustom : StrictTypesUnsafe<TSchema>;

// @alpha
export type HandleConverter<TCustom> = (data: IFluidHandle) => TCustom;

@@ -248,7 +332,7 @@ type _InlineTrick = 0;
export type Input<T extends never> = T;

// @alpha
export type Insertable<TSchema extends ImplicitAllowedTypes | UnsafeUnknownSchema> = TSchema extends ImplicitAllowedTypes ? InsertableTreeNodeFromImplicitAllowedTypes<TSchema> : InsertableContent;
export type Insertable<TSchema extends ImplicitAllowedTypes> = InsertableTreeNodeFromImplicitAllowedTypes<TSchema>;

// @alpha
export type InsertableContent = Unhydrated<TreeNode> | FactoryContent;
@@ -273,7 +357,7 @@ export type InsertableObjectFromSchemaRecordUnsafe<T extends Unenforced<Restrict
};

// @public
export type InsertableTreeFieldFromImplicitField<TSchemaInput extends ImplicitFieldSchema, TSchema = UnionToIntersection<TSchemaInput>> = [TSchema] extends [FieldSchema<infer Kind, infer Types>] ? ApplyKindInput<InsertableTreeNodeFromImplicitAllowedTypes<Types>, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypes<TSchema> : never;
export type InsertableTreeFieldFromImplicitField<TSchemaInput extends ImplicitFieldSchema, TSchema = [TSchemaInput] extends [CustomizedSchemaTyping<unknown, CustomTypes>] ? TSchemaInput : SchemaUnionToIntersection<TSchemaInput>> = [TSchema] extends [FieldSchema<infer Kind, infer Types>] ? ApplyKindInput<InsertableTreeNodeFromImplicitAllowedTypes<Types>, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypes<TSchema> : never;

// @public
export type InsertableTreeFieldFromImplicitFieldUnsafe<TSchemaInput extends Unenforced<ImplicitFieldSchema>, TSchema = UnionToIntersection<TSchemaInput>> = [TSchema] extends [FieldSchemaUnsafe<infer Kind, infer Types>] ? ApplyKindInput<InsertableTreeNodeFromImplicitAllowedTypesUnsafe<Types>, Kind, true> : [TSchema] extends [ImplicitAllowedTypes] ? InsertableTreeNodeFromImplicitAllowedTypesUnsafe<TSchema> : never;
@@ -291,12 +375,10 @@ LazyItem<infer TSchema extends TreeNodeSchemaUnsafe>,
] ? InsertableTypedNodeUnsafe<TSchema> | InsertableTreeNodeFromAllowedTypesUnsafe<Rest> : never;

// @public
export type InsertableTreeNodeFromImplicitAllowedTypes<TSchema extends ImplicitAllowedTypes> = [
TSchema
] extends [TreeNodeSchema] ? InsertableTypedNode<TSchema> : [TSchema] extends [AllowedTypes] ? InsertableTreeNodeFromAllowedTypes<TSchema> : never;
export type InsertableTreeNodeFromImplicitAllowedTypes<TSchema extends ImplicitAllowedTypes> = GetTypes<TSchema>["input"];

// @public
export type InsertableTreeNodeFromImplicitAllowedTypesUnsafe<TSchema extends Unenforced<ImplicitAllowedTypes>> = [TSchema] extends [TreeNodeSchemaUnsafe] ? InsertableTypedNodeUnsafe<TSchema> : [TSchema] extends [AllowedTypesUnsafe] ? InsertableTreeNodeFromAllowedTypesUnsafe<TSchema> : never;
export type InsertableTreeNodeFromImplicitAllowedTypesUnsafe<TSchema extends Unenforced<ImplicitAllowedTypes>> = GetTypesUnsafe<TSchema>["input"];

// @public
export type InsertableTypedNode<TSchema extends TreeNodeSchema, T = UnionToIntersection<TSchema>> = (T extends TreeNodeSchema<string, NodeKind, TreeNode | TreeLeafValue, never, true> ? NodeBuilderData<T> : never) | (T extends TreeNodeSchema ? Unhydrated<TreeNode extends NodeFromSchema<T> ? never : NodeFromSchema<T>> : never);
@@ -544,12 +626,30 @@ export const noopValidator: JsonValidator;

// @public
type ObjectFromSchemaRecord<T extends RestrictiveStringRecord<ImplicitFieldSchema>> = {
-readonly [Property in keyof T]: Property extends string ? TreeFieldFromImplicitField<T[Property]> : unknown;
-readonly [Property in keyof T as [
AssignableTreeFieldFromImplicitField<T[Property & string]>
] extends [never | undefined] ? never : Property]: AssignableTreeFieldFromImplicitField<T[Property & string]>;
} & {
readonly [Property in keyof T]: TreeFieldFromImplicitField<T[Property & string]>;
};

// @public
type ObjectFromSchemaRecordUnsafe<T extends Unenforced<RestrictiveStringRecord<ImplicitFieldSchema>>> = {
-readonly [Property in keyof T]: TreeFieldFromImplicitFieldUnsafe<T[Property]>;
-readonly [Property in keyof T as [T[Property]] extends [
CustomizedSchemaTyping<unknown, {
readonly readWrite: never;
readonly input: unknown;
readonly output: TreeNode | TreeLeafValue;
}>
] ? never : Property]: AssignableTreeFieldFromImplicitFieldUnsafe<T[Property]>;
} & {
readonly [Property in keyof T as [T[Property]] extends [
CustomizedSchemaTyping<unknown, {
readonly readWrite: never;
readonly input: unknown;
readonly output: TreeNode | TreeLeafValue;
}>
] ? Property : never]: TreeFieldFromImplicitFieldUnsafe<T[Property]>;
};

// @public @deprecated
@@ -754,6 +854,11 @@ export const schemaStatics: {
readonly requiredRecursive: <const T_3 extends unknown>(t: T_3, props?: Omit<FieldProps, "defaultProvider">) => FieldSchemaUnsafe<FieldKind.Required, T_3>;
};

// @public
export type SchemaUnionToIntersection<T> = [T] extends [
CustomizedSchemaTyping<unknown, CustomTypes>
] ? T : UnionToIntersection<T>;

// @alpha
export interface SchemaValidationFunction<Schema extends TSchema> {
check(data: unknown): data is Static<Schema>;
@@ -789,6 +894,26 @@ export function singletonSchema<TScope extends string, TName extends string | nu
readonly value: TName;
}, Record<string, never>, true, Record<string, never>, undefined>;

// @public @sealed
export interface StrictTypes<TSchema extends ImplicitAllowedTypes, TInput = DefaultInsertableTreeNodeFromImplicitAllowedTypes<TSchema>, TOutput extends TreeNode | TreeLeafValue = DefaultTreeNodeFromImplicitAllowedTypes<TSchema>> {
// (undocumented)
input: TInput;
// (undocumented)
output: TOutput;
// (undocumented)
readWrite: TInput extends never ? never : TOutput;
}

// @public
export interface StrictTypesUnsafe<TSchema extends Unenforced<ImplicitAllowedTypes>, TInput = DefaultInsertableTreeNodeFromImplicitAllowedTypesUnsafe<TSchema>, TOutput = DefaultTreeNodeFromImplicitAllowedTypesUnsafe<TSchema>> {
// (undocumented)
input: TInput;
// (undocumented)
output: TOutput;
// (undocumented)
readWrite: TOutput;
}

// @alpha
export type TransactionCallbackStatus<TSuccessValue, TFailureValue> = ({
rollback?: false;
@@ -980,10 +1105,10 @@ export interface TreeNodeApi {
}

// @public
export type TreeNodeFromImplicitAllowedTypes<TSchema extends ImplicitAllowedTypes = TreeNodeSchema> = TSchema extends TreeNodeSchema ? NodeFromSchema<TSchema> : TSchema extends AllowedTypes ? NodeFromSchema<FlexListToUnion<TSchema>> : unknown;
export type TreeNodeFromImplicitAllowedTypes<TSchema extends ImplicitAllowedTypes = TreeNodeSchema> = GetTypes<TSchema>["output"];

// @public
type TreeNodeFromImplicitAllowedTypesUnsafe<TSchema extends Unenforced<ImplicitAllowedTypes>> = TSchema extends TreeNodeSchemaUnsafe ? NodeFromSchemaUnsafe<TSchema> : TSchema extends AllowedTypesUnsafe ? NodeFromSchemaUnsafe<FlexListToUnion<TSchema>> : unknown;
type TreeNodeFromImplicitAllowedTypesUnsafe<TSchema extends Unenforced<ImplicitAllowedTypes>> = GetTypesUnsafe<TSchema>["output"];

// @public @sealed
export type TreeNodeSchema<Name extends string = string, Kind extends NodeKind = NodeKind, TNode extends TreeNode | TreeLeafValue = TreeNode | TreeLeafValue, TBuild = never, ImplicitlyConstructable extends boolean = boolean, Info = unknown, TCustomMetadata = unknown> = (TNode extends TreeNode ? TreeNodeSchemaClass<Name, Kind, TNode, TBuild, ImplicitlyConstructable, Info, never, TCustomMetadata> : never) | TreeNodeSchemaNonClass<Name, Kind, TNode, TBuild, ImplicitlyConstructable, Info, never, TCustomMetadata>;
Loading
Oops, something went wrong.
Loading
Oops, something went wrong.