Skip to content

Commit 02ecf8d

Browse files
Align object schema with simple object node schema, and expose as alpha (#24143)
## Description Several tweaks to the typing of object node schema have been made to allow exposing an `@alpha` `ObjectNodeSchema` type. See changeset for details. This is part of a larger effort to align the various TreeNodeSchema with the SimpleNodeSchema interfaces to provide friendly ways to inspect schema information. ## Breaking Changes The typing tweaks are not expected to break any existing valid use, bit it is possible some exceptionally picky code could be impacted. See the changeset for exact details.
1 parent eb14b89 commit 02ecf8d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+800
-516
lines changed

.changeset/strong-dragons-build.md

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
"fluid-framework": minor
3+
"@fluidframework/tree": minor
4+
---
5+
---
6+
"section": tree
7+
---
8+
9+
Improvements to typing of Object Node Schema, and expose related `@alpha` types
10+
11+
Several tweaks to the typing of object node schema have been made to allow exposing an `@alpha` `ObjectNodeSchema` type.
12+
13+
[SchemaFactoryAlpha](https://fluidframework.com/docs/api/fluid-framework/schemafactoryalpha-class)'s `object` and `objectRecursive` now return schema which are compatible with the new `ObjectNodeSchema` type.
14+
This new `ObjectNodeSchema` type exposes a `fields: ReadonlyMap<string, FieldSchemaAlpha & SimpleObjectFieldSchema>` property which provides an easy way to get information about the object's fields.
15+
16+
Additionally an alpha `ObjectNodeSchema` object is added to enable support for `schema instanceof ObjectNodeSchema` to safely narrow `TreeNodeSchema` to this new type.
17+
18+
In support of this work, several typing details were fixed including:
19+
20+
- `info` field of `[typeSchemaSymbol]` type brand on recursive object schema was specified to match non-recursive variants.
21+
- Type of field metadata was correctly plumbed through `optionalReclusive` and `requiredRecursive`.
22+
- When fields object provided to [SchemaFactory.object](https://fluidframework.com/docs/api/fluid-framework/schemafactory-class#object-method) is typed as `RestrictiveStringRecord<ImplicitFieldSchema>` the resulting [TreeObjectNode](https://fluidframework.com/docs/api/fluid-framework/treeobjectnode-typealias) no longer gets a `Record<string, TreeNode | TreeLeafValue>` signature which could incorrectly conflict with custom members added to the object. Instead `{}` is used to provide no information about felids on the type when the schema provides no information about them. Additionally this case is explicitly made non-constructable: the constructor takes in `never` instead of a `Record<string,never>` which could be erroneously satisfied with an empty object due to how TypeScript assignability rules consider records to have all allowed fields, but also allow objects missing those fields to be assigned to them.
23+
24+
Lastly `metadata` on the various schema types has been made required instead of optional.
25+
This does not impact the APis for constructing schema: when `undefined` is provided the schema not defaults to `{}` instead of `undefined`.
26+
This reduces the number of cases code reading metadata from schema has to handle.

packages/dds/tree/api-report/tree.alpha.api.md

+50-18
Original file line numberDiff line numberDiff line change
@@ -152,26 +152,32 @@ export class FieldSchema<out Kind extends FieldKind = FieldKind, out Types exten
152152
readonly allowedTypes: Types;
153153
get allowedTypeSet(): ReadonlySet<TreeNodeSchema>;
154154
readonly kind: Kind;
155-
get metadata(): FieldSchemaMetadata<TCustomMetadata> | undefined;
155+
get metadata(): FieldSchemaMetadata<TCustomMetadata>;
156156
readonly props?: FieldProps<TCustomMetadata> | undefined;
157157
readonly requiresValue: boolean;
158158
protected _typeCheck: MakeNominal;
159159
}
160160

161161
// @alpha @sealed
162162
export class FieldSchemaAlpha<Kind extends FieldKind = FieldKind, Types extends ImplicitAllowedTypes = ImplicitAllowedTypes, TCustomMetadata = unknown> extends FieldSchema<Kind, Types, TCustomMetadata> implements SimpleFieldSchema {
163+
protected constructor(kind: Kind, allowedTypes: Types, props?: FieldProps<TCustomMetadata>);
163164
// (undocumented)
164165
get allowedTypesIdentifiers(): ReadonlySet<string>;
165166
}
166167

168+
// @alpha @sealed
169+
export interface FieldSchemaAlphaUnsafe<out Kind extends FieldKind, out Types extends ImplicitAllowedTypesUnsafe, out TCustomMetadata = unknown> extends FieldSchemaAlpha<Kind, any, TCustomMetadata>, FieldSchemaUnsafe<Kind, Types, TCustomMetadata> {
170+
readonly allowedTypes: Types;
171+
}
172+
167173
// @public @sealed
168174
export interface FieldSchemaMetadata<TCustomMetadata = unknown> {
169175
readonly custom?: TCustomMetadata;
170176
readonly description?: string | undefined;
171177
}
172178

173179
// @public
174-
export interface FieldSchemaUnsafe<out Kind extends FieldKind, out Types extends ImplicitAllowedTypesUnsafe> extends FieldSchema<Kind, any> {
180+
export interface FieldSchemaUnsafe<out Kind extends FieldKind, out Types extends ImplicitAllowedTypesUnsafe, out TCustomMetadata = unknown> extends FieldSchema<Kind, any, TCustomMetadata> {
175181
readonly allowedTypes: Types;
176182
readonly allowedTypeSet: ReadonlySet<TreeNodeSchema>;
177183
readonly kind: Kind;
@@ -272,7 +278,9 @@ TSchema
272278
] extends [ImplicitFieldSchema] ? InsertableTreeFieldFromImplicitField<TSchema> : [TSchema] extends [UnsafeUnknownSchema] ? InsertableContent | undefined : never;
273279

274280
// @public
275-
type InsertableObjectFromSchemaRecord<T extends RestrictiveStringRecord<ImplicitFieldSchema>> = Record<string, never> extends T ? Record<string, never> : FlattenKeys<{
281+
type InsertableObjectFromSchemaRecord<T extends RestrictiveStringRecord<ImplicitFieldSchema>> = RestrictiveStringRecord<ImplicitFieldSchema> extends T ? {
282+
arbitraryKey: "arbitraryValue";
283+
} extends T ? Record<string, never> : never : FlattenKeys<{
276284
readonly [Property in keyof T]?: InsertableTreeFieldFromImplicitField<T[Property & string]>;
277285
} & {
278286
readonly [Property in keyof T as FieldHasDefault<T[Property & string]> extends false ? Property : never]: InsertableTreeFieldFromImplicitField<T[Property & string]>;
@@ -554,7 +562,7 @@ export interface NodeSchemaOptions<out TCustomMetadata = unknown> {
554562
export const noopValidator: JsonValidator;
555563

556564
// @public
557-
type ObjectFromSchemaRecord<T extends RestrictiveStringRecord<ImplicitFieldSchema>> = {
565+
type ObjectFromSchemaRecord<T extends RestrictiveStringRecord<ImplicitFieldSchema>> = RestrictiveStringRecord<ImplicitFieldSchema> extends T ? {} : {
558566
-readonly [Property in keyof T]: Property extends string ? TreeFieldFromImplicitField<T[Property]> : unknown;
559567
};
560568

@@ -563,6 +571,16 @@ type ObjectFromSchemaRecordUnsafe<T extends Unenforced<RestrictiveStringRecord<I
563571
-readonly [Property in keyof T]: TreeFieldFromImplicitFieldUnsafe<T[Property]>;
564572
};
565573

574+
// @alpha @sealed
575+
export interface ObjectNodeSchema<out TName extends string = string, in out T extends RestrictiveStringRecord<ImplicitFieldSchema> = RestrictiveStringRecord<ImplicitFieldSchema>, ImplicitlyConstructable extends boolean = boolean, out TCustomMetadata = unknown> extends TreeNodeSchemaClass<TName, NodeKind.Object, TreeObjectNode<T, TName>, InsertableObjectFromSchemaRecord<T>, ImplicitlyConstructable, T, never, TCustomMetadata>, SimpleObjectNodeSchema<TCustomMetadata> {
576+
readonly fields: ReadonlyMap<string, FieldSchemaAlpha & SimpleObjectFieldSchema>;
577+
}
578+
579+
// @alpha (undocumented)
580+
export const ObjectNodeSchema: {
581+
readonly [Symbol.hasInstance]: (value: TreeNodeSchema) => value is ObjectNodeSchema<string, RestrictiveStringRecord<ImplicitFieldSchema>, boolean, unknown>;
582+
};
583+
566584
// @public @deprecated
567585
export type Off = Off_2;
568586

@@ -715,12 +733,12 @@ export class SchemaFactory<out TScope extends string | undefined = string | unde
715733
objectRecursive<const Name extends TName, const T extends RestrictiveStringRecord<Unenforced<ImplicitFieldSchema>>>(name: Name, t: T): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Object, TreeObjectNodeUnsafe<T, ScopedSchemaName<TScope, Name>>, object & InsertableObjectFromSchemaRecordUnsafe<T>, false, T>;
716734
readonly optional: <const T extends ImplicitAllowedTypes, const TCustomMetadata = unknown>(t: T, props?: Omit<FieldProps<TCustomMetadata>, "defaultProvider"> | undefined) => FieldSchema<FieldKind.Optional, T, TCustomMetadata>;
717735
static readonly optional: <const T extends ImplicitAllowedTypes, const TCustomMetadata = unknown>(t: T, props?: Omit<FieldProps<TCustomMetadata>, "defaultProvider"> | undefined) => FieldSchema<FieldKind.Optional, T, TCustomMetadata>;
718-
readonly optionalRecursive: <const T extends ImplicitAllowedTypesUnsafe>(t: T, props?: Omit<FieldProps<unknown>, "defaultProvider"> | undefined) => FieldSchemaUnsafe<FieldKind.Optional, T>;
719-
static readonly optionalRecursive: <const T extends ImplicitAllowedTypesUnsafe>(t: T, props?: Omit<FieldProps<unknown>, "defaultProvider"> | undefined) => FieldSchemaUnsafe<FieldKind.Optional, T>;
736+
readonly optionalRecursive: <const T extends ImplicitAllowedTypesUnsafe, const TCustomMetadata = unknown>(t: T, props?: Omit<FieldProps<TCustomMetadata>, "defaultProvider"> | undefined) => FieldSchemaUnsafe<FieldKind.Optional, T, TCustomMetadata>;
737+
static readonly optionalRecursive: <const T extends ImplicitAllowedTypesUnsafe, const TCustomMetadata = unknown>(t: T, props?: Omit<FieldProps<TCustomMetadata>, "defaultProvider"> | undefined) => FieldSchemaUnsafe<FieldKind.Optional, T, TCustomMetadata>;
720738
readonly required: <const T extends ImplicitAllowedTypes, const TCustomMetadata = unknown>(t: T, props?: Omit<FieldProps<TCustomMetadata>, "defaultProvider"> | undefined) => FieldSchema<FieldKind.Required, T, TCustomMetadata>;
721739
static readonly required: <const T extends ImplicitAllowedTypes, const TCustomMetadata = unknown>(t: T, props?: Omit<FieldProps<TCustomMetadata>, "defaultProvider"> | undefined) => FieldSchema<FieldKind.Required, T, TCustomMetadata>;
722-
readonly requiredRecursive: <const T extends ImplicitAllowedTypesUnsafe>(t: T, props?: Omit<FieldProps<unknown>, "defaultProvider"> | undefined) => FieldSchemaUnsafe<FieldKind.Required, T>;
723-
static readonly requiredRecursive: <const T extends ImplicitAllowedTypesUnsafe>(t: T, props?: Omit<FieldProps<unknown>, "defaultProvider"> | undefined) => FieldSchemaUnsafe<FieldKind.Required, T>;
740+
readonly requiredRecursive: <const T extends ImplicitAllowedTypesUnsafe, const TCustomMetadata = unknown>(t: T, props?: Omit<FieldProps<TCustomMetadata>, "defaultProvider"> | undefined) => FieldSchemaUnsafe<FieldKind.Required, T, TCustomMetadata>;
741+
static readonly requiredRecursive: <const T extends ImplicitAllowedTypesUnsafe, const TCustomMetadata = unknown>(t: T, props?: Omit<FieldProps<TCustomMetadata>, "defaultProvider"> | undefined) => FieldSchemaUnsafe<FieldKind.Required, T, TCustomMetadata>;
724742
readonly scope: TScope;
725743
readonly string: LeafSchema_2<"string", string>;
726744
static readonly string: LeafSchema_2<"string", string>;
@@ -741,8 +759,9 @@ export class SchemaFactoryAlpha<out TScope extends string | undefined = string |
741759
} | {
742760
readonly [x: string]: InsertableTreeNodeFromImplicitAllowedTypesUnsafe<T>;
743761
}, false, T, undefined, TCustomMetadata>;
744-
object<const Name extends TName, const T extends RestrictiveStringRecord<ImplicitFieldSchema>, const TCustomMetadata = unknown>(name: Name, fields: T, options?: SchemaFactoryObjectOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Object, TreeObjectNode<T, ScopedSchemaName<TScope, Name>>, object & InsertableObjectFromSchemaRecord<T>, true, T, never, TCustomMetadata>;
745-
objectRecursive<const Name extends TName, const T extends Unenforced<RestrictiveStringRecord<ImplicitFieldSchema>>, const TCustomMetadata = unknown>(name: Name, t: T, options?: SchemaFactoryObjectOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Object, TreeObjectNodeUnsafe<T, ScopedSchemaName<TScope, Name>>, object & InsertableObjectFromSchemaRecordUnsafe<T>, false, T, never, TCustomMetadata>;
762+
object<const Name extends TName, const T extends RestrictiveStringRecord<ImplicitFieldSchema>, const TCustomMetadata = unknown>(name: Name, fields: T, options?: SchemaFactoryObjectOptions<TCustomMetadata>): ObjectNodeSchema<ScopedSchemaName<TScope, Name>, T, true, TCustomMetadata>;
763+
objectRecursive<const Name extends TName, const T extends Unenforced<RestrictiveStringRecord<ImplicitFieldSchema>>, const TCustomMetadata = unknown>(name: Name, t: T, options?: SchemaFactoryObjectOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Object, TreeObjectNodeUnsafe<T, ScopedSchemaName<TScope, Name>>, object & InsertableObjectFromSchemaRecordUnsafe<T>, false, T, never, TCustomMetadata> & SimpleObjectNodeSchema<TCustomMetadata> & Pick<ObjectNodeSchema, "fields">;
764+
readonly optionalRecursive: <const T extends ImplicitAllowedTypesUnsafe, const TCustomMetadata = unknown>(t: T, props?: Omit<FieldProps<TCustomMetadata>, "defaultProvider">) => FieldSchemaAlphaUnsafe<FieldKind.Optional, T, TCustomMetadata>;
746765
scopedFactory<const T extends TName, TNameInner extends number | string = string>(name: T): SchemaFactoryAlpha<ScopedSchemaName<TScope, T>, TNameInner>;
747766
}
748767

@@ -761,8 +780,8 @@ export const schemaStatics: {
761780
readonly leaves: readonly [LeafSchema_2<"string", string>, LeafSchema_2<"number", number>, LeafSchema_2<"boolean", boolean>, LeafSchema_2<"null", null>, LeafSchema_2<"handle", IFluidHandle<unknown>>];
762781
readonly optional: <const T extends ImplicitAllowedTypes, const TCustomMetadata = unknown>(t: T, props?: Omit<FieldProps<TCustomMetadata>, "defaultProvider">) => FieldSchema<FieldKind.Optional, T, TCustomMetadata>;
763782
readonly required: <const T_1 extends ImplicitAllowedTypes, const TCustomMetadata_1 = unknown>(t: T_1, props?: Omit<FieldProps<TCustomMetadata_1>, "defaultProvider"> | undefined) => FieldSchema<FieldKind.Required, T_1, TCustomMetadata_1>;
764-
readonly optionalRecursive: <const T_2 extends ImplicitAllowedTypesUnsafe>(t: T_2, props?: Omit<FieldProps, "defaultProvider">) => FieldSchemaUnsafe<FieldKind.Optional, T_2>;
765-
readonly requiredRecursive: <const T_3 extends ImplicitAllowedTypesUnsafe>(t: T_3, props?: Omit<FieldProps, "defaultProvider">) => FieldSchemaUnsafe<FieldKind.Required, T_3>;
783+
readonly optionalRecursive: <const T_2 extends ImplicitAllowedTypesUnsafe, const TCustomMetadata_2 = unknown>(t: T_2, props?: Omit<FieldProps<TCustomMetadata_2>, "defaultProvider"> | undefined) => FieldSchemaUnsafe<FieldKind.Optional, T_2, TCustomMetadata_2>;
784+
readonly requiredRecursive: <const T_3 extends ImplicitAllowedTypesUnsafe, const TCustomMetadata_3 = unknown>(t: T_3, props?: Omit<FieldProps<TCustomMetadata_3>, "defaultProvider"> | undefined) => FieldSchemaUnsafe<FieldKind.Required, T_3, TCustomMetadata_3>;
766785
};
767786

768787
// @alpha
@@ -796,7 +815,23 @@ export type SharedTreeOptions = Partial<ICodecOptions> & Partial<SharedTreeForma
796815
export interface SimpleFieldSchema {
797816
readonly allowedTypesIdentifiers: ReadonlySet<string>;
798817
readonly kind: FieldKind;
799-
readonly metadata?: FieldSchemaMetadata | undefined;
818+
readonly metadata: FieldSchemaMetadata;
819+
}
820+
821+
// @public @sealed
822+
export interface SimpleNodeSchemaBase<out TNodeKind extends NodeKind, out TCustomMetadata = unknown> {
823+
readonly kind: TNodeKind;
824+
readonly metadata: NodeSchemaMetadata<TCustomMetadata>;
825+
}
826+
827+
// @alpha @sealed
828+
export interface SimpleObjectFieldSchema extends SimpleFieldSchema {
829+
readonly storedKey: string;
830+
}
831+
832+
// @alpha @sealed
833+
export interface SimpleObjectNodeSchema<out TCustomMetadata = unknown> extends SimpleNodeSchemaBase<NodeKind.Object, TCustomMetadata> {
834+
readonly fields: ReadonlyMap<string, SimpleObjectFieldSchema>;
800835
}
801836

802837
// @alpha
@@ -1020,16 +1055,13 @@ export interface TreeNodeSchemaClassUnsafe<out Name extends string, out Kind ext
10201055
}
10211056

10221057
// @public @sealed
1023-
export interface TreeNodeSchemaCore<out Name extends string, out Kind extends NodeKind, out ImplicitlyConstructable extends boolean, out Info = unknown, out TInsertable = never, out TCustomMetadata = unknown> {
1058+
export interface TreeNodeSchemaCore<out Name extends string, out Kind extends NodeKind, out ImplicitlyConstructable extends boolean, out Info = unknown, out TInsertable = never, out TCustomMetadata = unknown> extends SimpleNodeSchemaBase<Kind, TCustomMetadata> {
10241059
readonly childTypes: ReadonlySet<TreeNodeSchema>;
10251060
// @sealed
10261061
createFromInsertable(data: TInsertable): Unhydrated<TreeNode | TreeLeafValue>;
10271062
readonly identifier: Name;
10281063
readonly implicitlyConstructable: ImplicitlyConstructable;
10291064
readonly info: Info;
1030-
// (undocumented)
1031-
readonly kind: Kind;
1032-
readonly metadata?: NodeSchemaMetadata<TCustomMetadata> | undefined;
10331065
}
10341066

10351067
// @public @sealed
@@ -1052,7 +1084,7 @@ type TreeNodeSchemaUnsafe<Name extends string = string, Kind extends NodeKind =
10521084
export type TreeObjectNode<T extends RestrictiveStringRecord<ImplicitFieldSchema>, TypeName extends string = string> = TreeNode & ObjectFromSchemaRecord<T> & WithType<TypeName, NodeKind.Object, T>;
10531085

10541086
// @public
1055-
export type TreeObjectNodeUnsafe<T extends Unenforced<RestrictiveStringRecord<ImplicitFieldSchema>>, TypeName extends string = string> = TreeNode & ObjectFromSchemaRecordUnsafe<T> & WithType<TypeName, NodeKind.Object>;
1087+
export type TreeObjectNodeUnsafe<T extends Unenforced<RestrictiveStringRecord<ImplicitFieldSchema>>, TypeName extends string = string> = TreeNode & ObjectFromSchemaRecordUnsafe<T> & WithType<TypeName, NodeKind.Object, T>;
10561088

10571089
// @public
10581090
export enum TreeStatus {

0 commit comments

Comments
 (0)