Skip to content

Commit 04f5283

Browse files
committed
Add ensureSchema
1 parent bc3a1a8 commit 04f5283

File tree

20 files changed

+615
-146
lines changed

20 files changed

+615
-146
lines changed

.changeset/tricky-shirts-sleep.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
"@fluidframework/tree": minor
3+
"__section": feature
4+
---
5+
Added `Tree.ensureSchema`
6+
7+
This helper function allows content to be tagged with a schema type before being inserted into the tree.
8+
This allows content that would otherwise be ambiguous to be well-defined, without having to wrap it in a node constructor.
9+
10+
Example:
11+
12+
```typescript
13+
const sf = new SchemaFactory("example");
14+
class Dog extends sf.object("Dog", { name: sf.string() }) {}
15+
class Cat extends sf.object("Cat", { name: sf.string() }) {}
16+
class Root extends sf.object("Root", { pet: [Dog, Cat] }) {}
17+
// ...
18+
const pet = { name: "Max" };
19+
view.root.pet = pet; // Error: `pet` is ambiguous - is it a Dog or a Cat?
20+
view.root.pet = new Dog(pet); // This works, but has the overhead of creating a Dog node before the insertion actually happens.
21+
TreeAlpha.ensureSchema(Dog, pet); // Instead, this tags the `pet` object as a Dog...
22+
view.root.pet = pet; // So now there is no error for a normal insertion - it's a Dog.
23+
```
24+
25+
This function works by leveraging the new `schemaSymbol`, which is also available for use.
26+
See its documentation for more information.

packages/common/core-interfaces/api-report/core-interfaces.legacy.beta.api.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
// @public
88
export type ConfigTypes = string | number | boolean | number[] | string[] | boolean[] | undefined;
99

10+
// @beta @sealed @system
11+
export abstract class ErasedBaseType<out Name = unknown> {
12+
protected constructor();
13+
protected abstract brand(dummy: never): Name;
14+
}
15+
1016
// @public @sealed
1117
export abstract class ErasedType<out Name = unknown> {
1218
static [Symbol.hasInstance](value: never): value is never;

packages/common/core-interfaces/src/erasedType.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,10 @@ export abstract class ErasedType<out Name = unknown> {
9696
*
9797
* This class should only be a `type` package export, preventing users from extending it directly.
9898
*
99-
* Since {@link ErasedTypeImplementation} is exported as `@internal`, this restricts implementations of the sealed interfaces to users of `@internal` APIs, which should be anything withing this release group.
99+
* Since {@link ErasedTypeImplementation} is exported as `@internal`, this restricts implementations of the sealed interfaces to users of `@internal` APIs, which should be anything within this release group.
100100
* Any finer grained restrictions can be done as documentation, but not type enforced.
101101
* @sealed
102-
* @alpha
102+
* @beta
103103
* @system
104104
*/
105105
export abstract class ErasedBaseType<out Name = unknown> {

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

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export function adaptEnum<TScope extends string, const TEnum extends Record<stri
1515
}, Record<string, never>, true, Record<string, never>, undefined>; }[keyof TEnum]>;
1616
};
1717

18-
// @alpha @input
18+
// @beta @input
1919
export interface AllowedTypeMetadata {
2020
readonly custom?: unknown;
2121
readonly stagedSchemaUpgrade?: SchemaUpgrade;
@@ -24,13 +24,13 @@ export interface AllowedTypeMetadata {
2424
// @public @system
2525
export type AllowedTypes = readonly LazyItem<TreeNodeSchema>[];
2626

27-
// @alpha @sealed
27+
// @beta @sealed
2828
export type AllowedTypesFull<T extends readonly AnnotatedAllowedType[] = readonly AnnotatedAllowedType[]> = AnnotatedAllowedTypes<T> & UnannotateAllowedTypesList<T>;
2929

30-
// @alpha @sealed
30+
// @beta @sealed
3131
export type AllowedTypesFullEvaluated = AllowedTypesFull<readonly AnnotatedAllowedType<TreeNodeSchema>[]>;
3232

33-
// @alpha @sealed
33+
// @beta @sealed @system
3434
export type AllowedTypesFullFromMixed<T extends readonly (AnnotatedAllowedType | LazyItem<TreeNodeSchema>)[]> = UnannotateAllowedTypesList<T> & AnnotatedAllowedTypes<AnnotateAllowedTypesList<T>>;
3535

3636
// @alpha @sealed @system
@@ -39,15 +39,15 @@ export type AllowedTypesFullFromMixedUnsafe<T extends readonly Unenforced<Annota
3939
// @alpha @sealed @system
4040
export type AllowedTypesFullUnsafe<T extends readonly AnnotatedAllowedTypeUnsafe[] = readonly AnnotatedAllowedTypeUnsafe[]> = AnnotatedAllowedTypes<T> & UnannotateAllowedTypesListUnsafe<T>;
4141

42-
// @alpha @input
42+
// @beta @input
4343
export interface AllowedTypesMetadata {
4444
readonly custom?: unknown;
4545
}
4646

4747
// @alpha
4848
export function allowUnused<T>(t?: T): void;
4949

50-
// @alpha @system
50+
// @beta @system
5151
export type AnnotateAllowedTypesList<T extends readonly (AnnotatedAllowedType | LazyItem<TreeNodeSchema>)[]> = {
5252
[I in keyof T]: T[I] extends AnnotatedAllowedType<unknown> ? T[I] : AnnotatedAllowedType<T[I]>;
5353
};
@@ -57,13 +57,13 @@ export type AnnotateAllowedTypesListUnsafe<T extends readonly Unenforced<Annotat
5757
[I in keyof T]: T[I] extends AnnotatedAllowedTypeUnsafe ? T[I] : AnnotatedAllowedTypeUnsafe<T[I]>;
5858
};
5959

60-
// @alpha @sealed
60+
// @beta @sealed
6161
export interface AnnotatedAllowedType<T = LazyItem<TreeNodeSchema>> {
6262
readonly metadata: AllowedTypeMetadata;
6363
readonly type: T;
6464
}
6565

66-
// @alpha @sealed
66+
// @beta @sealed
6767
export interface AnnotatedAllowedTypes<T = readonly AnnotatedAllowedType[]> extends ErasedBaseType<"tree.AnnotatedAllowedTypes"> {
6868
evaluate(): AllowedTypesFullEvaluated;
6969
evaluateIdentifiers(): ReadonlySet<string>;
@@ -965,7 +965,10 @@ export interface SchemaStaticsAlpha {
965965
readonly typesRecursive: <const T extends readonly Unenforced<AnnotatedAllowedType | LazyItem<TreeNodeSchema>>[]>(t: T, metadata?: AllowedTypesMetadata) => AllowedTypesFullFromMixedUnsafe<T>;
966966
}
967967

968-
// @alpha @sealed
968+
// @alpha
969+
export const schemaSymbol: unique symbol;
970+
971+
// @beta @sealed
969972
export class SchemaUpgrade {
970973
// (undocumented)
971974
protected _typeCheck: MakeNominal;
@@ -1348,6 +1351,7 @@ export interface TreeAlpha {
13481351
child(node: TreeNode, key: string | number): TreeNode | TreeLeafValue | undefined;
13491352
children(node: TreeNode): Iterable<[propertyKey: string | number, child: TreeNode | TreeLeafValue]>;
13501353
create<const TSchema extends ImplicitFieldSchema | UnsafeUnknownSchema>(schema: UnsafeUnknownSchema extends TSchema ? ImplicitFieldSchema : TSchema & ImplicitFieldSchema, data: InsertableField<TSchema>): Unhydrated<TSchema extends ImplicitFieldSchema ? TreeFieldFromImplicitField<TSchema> : TreeNode | TreeLeafValue | undefined>;
1354+
ensureSchema<TSchema extends TreeNodeSchema, TContent extends InsertableField<TSchema>>(schema: TSchema, content: TContent): TContent;
13511355
exportCompressed(tree: TreeNode | TreeLeafValue, options: {
13521356
idCompressor?: IIdCompressor;
13531357
} & Pick<CodecWriteOptions, "minVersionForCollab">): JsonCompatible<IFluidHandle>;
@@ -1638,7 +1642,7 @@ const typeNameSymbol: unique symbol;
16381642
// @public @system
16391643
export const typeSchemaSymbol: unique symbol;
16401644

1641-
// @alpha @system
1645+
// @beta @system
16421646
export type UnannotateAllowedTypesList<T extends readonly (AnnotatedAllowedType | LazyItem<TreeNodeSchema>)[]> = {
16431647
[I in keyof T]: T[I] extends AnnotatedAllowedType<infer X> ? X : T[I];
16441648
};

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,49 @@ export function adaptEnum<TScope extends string, const TEnum extends Record<stri
1515
}, Record<string, never>, true, Record<string, never>, undefined>; }[keyof TEnum]>;
1616
};
1717

18+
// @beta @input
19+
export interface AllowedTypeMetadata {
20+
readonly custom?: unknown;
21+
readonly stagedSchemaUpgrade?: SchemaUpgrade;
22+
}
23+
1824
// @public @system
1925
export type AllowedTypes = readonly LazyItem<TreeNodeSchema>[];
2026

27+
// @beta @sealed
28+
export type AllowedTypesFull<T extends readonly AnnotatedAllowedType[] = readonly AnnotatedAllowedType[]> = AnnotatedAllowedTypes<T> & UnannotateAllowedTypesList<T>;
29+
30+
// @beta @sealed
31+
export type AllowedTypesFullEvaluated = AllowedTypesFull<readonly AnnotatedAllowedType<TreeNodeSchema>[]>;
32+
33+
// @beta @sealed @system
34+
export type AllowedTypesFullFromMixed<T extends readonly (AnnotatedAllowedType | LazyItem<TreeNodeSchema>)[]> = UnannotateAllowedTypesList<T> & AnnotatedAllowedTypes<AnnotateAllowedTypesList<T>>;
35+
36+
// @beta @input
37+
export interface AllowedTypesMetadata {
38+
readonly custom?: unknown;
39+
}
40+
41+
// @beta @system
42+
export type AnnotateAllowedTypesList<T extends readonly (AnnotatedAllowedType | LazyItem<TreeNodeSchema>)[]> = {
43+
[I in keyof T]: T[I] extends AnnotatedAllowedType<unknown> ? T[I] : AnnotatedAllowedType<T[I]>;
44+
};
45+
46+
// @beta @sealed
47+
export interface AnnotatedAllowedType<T = LazyItem<TreeNodeSchema>> {
48+
readonly metadata: AllowedTypeMetadata;
49+
readonly type: T;
50+
}
51+
52+
// @beta @sealed
53+
export interface AnnotatedAllowedTypes<T = readonly AnnotatedAllowedType[]> extends ErasedBaseType<"tree.AnnotatedAllowedTypes"> {
54+
evaluate(): AllowedTypesFullEvaluated;
55+
evaluateIdentifiers(): ReadonlySet<string>;
56+
evaluateSet(): ReadonlySet<TreeNodeSchema>;
57+
readonly metadata: AllowedTypesMetadata;
58+
readonly types: T;
59+
}
60+
2161
// @public @system
2262
type ApplyKind<T, Kind extends FieldKind> = {
2363
[FieldKind.Required]: T;
@@ -474,6 +514,12 @@ export interface SchemaStatics {
474514
readonly string: LeafSchema<"string", string>;
475515
}
476516

517+
// @beta @sealed
518+
export class SchemaUpgrade {
519+
// (undocumented)
520+
protected _typeCheck: MakeNominal;
521+
}
522+
477523
// @public @system
478524
type ScopedSchemaName<TScope extends string | undefined, TName extends number | string> = TScope extends undefined ? `${TName}` : `${TScope}.${TName}`;
479525

@@ -773,6 +819,11 @@ const typeNameSymbol: unique symbol;
773819
// @public @system
774820
export const typeSchemaSymbol: unique symbol;
775821

822+
// @beta @system
823+
export type UnannotateAllowedTypesList<T extends readonly (AnnotatedAllowedType | LazyItem<TreeNodeSchema>)[]> = {
824+
[I in keyof T]: T[I] extends AnnotatedAllowedType<infer X> ? X : T[I];
825+
};
826+
776827
// @public
777828
export type Unenforced<_DesiredExtendsConstraint> = unknown;
778829

packages/dds/tree/api-report/tree.legacy.beta.api.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,49 @@ export function adaptEnum<TScope extends string, const TEnum extends Record<stri
1515
}, Record<string, never>, true, Record<string, never>, undefined>; }[keyof TEnum]>;
1616
};
1717

18+
// @beta @input
19+
export interface AllowedTypeMetadata {
20+
readonly custom?: unknown;
21+
readonly stagedSchemaUpgrade?: SchemaUpgrade;
22+
}
23+
1824
// @public @system
1925
export type AllowedTypes = readonly LazyItem<TreeNodeSchema>[];
2026

27+
// @beta @sealed
28+
export type AllowedTypesFull<T extends readonly AnnotatedAllowedType[] = readonly AnnotatedAllowedType[]> = AnnotatedAllowedTypes<T> & UnannotateAllowedTypesList<T>;
29+
30+
// @beta @sealed
31+
export type AllowedTypesFullEvaluated = AllowedTypesFull<readonly AnnotatedAllowedType<TreeNodeSchema>[]>;
32+
33+
// @beta @sealed @system
34+
export type AllowedTypesFullFromMixed<T extends readonly (AnnotatedAllowedType | LazyItem<TreeNodeSchema>)[]> = UnannotateAllowedTypesList<T> & AnnotatedAllowedTypes<AnnotateAllowedTypesList<T>>;
35+
36+
// @beta @input
37+
export interface AllowedTypesMetadata {
38+
readonly custom?: unknown;
39+
}
40+
41+
// @beta @system
42+
export type AnnotateAllowedTypesList<T extends readonly (AnnotatedAllowedType | LazyItem<TreeNodeSchema>)[]> = {
43+
[I in keyof T]: T[I] extends AnnotatedAllowedType<unknown> ? T[I] : AnnotatedAllowedType<T[I]>;
44+
};
45+
46+
// @beta @sealed
47+
export interface AnnotatedAllowedType<T = LazyItem<TreeNodeSchema>> {
48+
readonly metadata: AllowedTypeMetadata;
49+
readonly type: T;
50+
}
51+
52+
// @beta @sealed
53+
export interface AnnotatedAllowedTypes<T = readonly AnnotatedAllowedType[]> extends ErasedBaseType<"tree.AnnotatedAllowedTypes"> {
54+
evaluate(): AllowedTypesFullEvaluated;
55+
evaluateIdentifiers(): ReadonlySet<string>;
56+
evaluateSet(): ReadonlySet<TreeNodeSchema>;
57+
readonly metadata: AllowedTypesMetadata;
58+
readonly types: T;
59+
}
60+
2161
// @public @system
2262
type ApplyKind<T, Kind extends FieldKind> = {
2363
[FieldKind.Required]: T;
@@ -477,6 +517,12 @@ export interface SchemaStatics {
477517
readonly string: LeafSchema<"string", string>;
478518
}
479519

520+
// @beta @sealed
521+
export class SchemaUpgrade {
522+
// (undocumented)
523+
protected _typeCheck: MakeNominal;
524+
}
525+
480526
// @public @system
481527
type ScopedSchemaName<TScope extends string | undefined, TName extends number | string> = TScope extends undefined ? `${TName}` : `${TScope}.${TName}`;
482528

@@ -785,6 +831,11 @@ const typeNameSymbol: unique symbol;
785831
// @public @system
786832
export const typeSchemaSymbol: unique symbol;
787833

834+
// @beta @system
835+
export type UnannotateAllowedTypesList<T extends readonly (AnnotatedAllowedType | LazyItem<TreeNodeSchema>)[]> = {
836+
[I in keyof T]: T[I] extends AnnotatedAllowedType<infer X> ? X : T[I];
837+
};
838+
788839
// @public
789840
export type Unenforced<_DesiredExtendsConstraint> = unknown;
790841

packages/dds/tree/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@
211211
"concurrently": "^8.2.1",
212212
"copyfiles": "^2.4.1",
213213
"cross-env": "^7.0.3",
214-
"dependency-cruiser": "^14.1.0",
214+
"dependency-cruiser": "^17.1.0",
215215
"diff": "^3.5.0",
216216
"easy-table": "^1.1.1",
217217
"eslint": "~8.57.1",

packages/dds/tree/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ export {
123123
type WithType,
124124
type NodeChangedData,
125125
type SchemaUpgrade,
126+
schemaSymbol,
126127
// Types not really intended for public use, but used in links.
127128
// Can not be moved to internalTypes since doing so causes app code to throw errors like:
128129
// Error: src/simple-tree/objectNode.ts:72:1 - (ae-unresolved-link) The @link reference could not be resolved: The package "@fluidframework/tree" does not have an export "TreeNodeApi"

packages/dds/tree/src/shared-tree/treeAlpha.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ import {
5858
importConcise,
5959
exportConcise,
6060
borrowCursorFromTreeNodeOrValue,
61+
schemaSymbol,
62+
type TreeNodeSchema,
6163
} from "../simple-tree/index.js";
6264
import { brand, extractFromOpaque, type JsonCompatible } from "../util/index.js";
6365
import {
@@ -519,6 +521,31 @@ export interface TreeAlpha {
519521
onInvalidation: () => void,
520522
trackDuring: () => TResult,
521523
): ObservationResults<TResult>;
524+
525+
/**
526+
* Ensures that the provided content will be interpreted as the given schema when inserting into the tree.
527+
* @returns `content`, for convenience.
528+
* @remarks
529+
* If applicable, this will tag the given content with a {@link schemaSymbol | special property} that indicates its intended schema.
530+
* The `content` will be interpreted as the given `schema` when later inserted into the tree.
531+
* This is particularly useful when the content's schema cannot be inferred from its structure alone because it is compatible with multiple schemas.
532+
* @example
533+
* ```typescript
534+
* const sf = new SchemaFactory("example");
535+
* class Dog extends sf.object("Dog", { name: sf.string() }) {}
536+
* class Cat extends sf.object("Cat", { name: sf.string() }) {}
537+
* class Root extends sf.object("Root", { pet: [Dog, Cat] }) {}
538+
* // ...
539+
* const pet = { name: "Max" };
540+
* view.root.pet = pet; // Error: ambiguous schema - is it a Dog or a Cat?
541+
* TreeAlpha.ensureSchema(Dog, pet); // Tags `pet` as a Dog.
542+
* view.root.pet = pet; // No error - it's a Dog.
543+
* ```
544+
*/
545+
ensureSchema<TSchema extends TreeNodeSchema, TContent extends InsertableField<TSchema>>(
546+
schema: TSchema,
547+
content: TContent,
548+
): TContent;
522549
}
523550

524551
/**
@@ -1003,6 +1030,21 @@ export const TreeAlpha: TreeAlpha = {
10031030
}
10041031
return result;
10051032
},
1033+
1034+
ensureSchema<TSchema extends TreeNodeSchema, TNode extends InsertableField<TSchema>>(
1035+
schema: TSchema,
1036+
node: TNode,
1037+
): TNode {
1038+
if (typeof node === "object" && node !== null) {
1039+
Reflect.defineProperty(node, schemaSymbol, {
1040+
configurable: false,
1041+
enumerable: false,
1042+
writable: true,
1043+
value: schema.identifier,
1044+
});
1045+
}
1046+
return node;
1047+
},
10061048
};
10071049

10081050
/**

0 commit comments

Comments
 (0)