Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions packages/dds/tree/api-report/tree.alpha.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -926,9 +926,9 @@ export class SchemaFactoryAlpha<out TScope extends string | undefined = string |

// @beta
export class SchemaFactoryBeta<out TScope extends string | undefined = string | undefined, TName extends number | string = string> extends SchemaFactory<TScope, TName> {
object<const Name extends TName, const T extends RestrictiveStringRecord<ImplicitFieldSchema>, const TCustomMetadata = unknown>(name: Name, fields: T, options?: ObjectSchemaOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Object, TreeObjectNode<T, ScopedSchemaName<TScope, Name>>, object & InsertableObjectFromSchemaRecord<T>, true, T>;
object<const Name extends TName, const T extends RestrictiveStringRecord<ImplicitFieldSchema>, const TCustomMetadata = unknown>(name: Name, fields: T, options?: ObjectSchemaOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Object, TreeObjectNode<T, ScopedSchemaName<TScope, Name>>, object & InsertableObjectFromSchemaRecord<T>, true, T, never, TCustomMetadata>;
// (undocumented)
objectRecursive<const Name extends TName, const T extends RestrictiveStringRecord<System_Unsafe.ImplicitFieldSchemaUnsafe>, const TCustomMetadata = unknown>(name: Name, t: T, options?: ObjectSchemaOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Object, System_Unsafe.TreeObjectNodeUnsafe<T, ScopedSchemaName<TScope, Name>>, object & System_Unsafe.InsertableObjectFromSchemaRecordUnsafe<T>, false, T>;
objectRecursive<const Name extends TName, const T extends RestrictiveStringRecord<System_Unsafe.ImplicitFieldSchemaUnsafe>, const TCustomMetadata = unknown>(name: Name, t: T, options?: ObjectSchemaOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Object, System_Unsafe.TreeObjectNodeUnsafe<T, ScopedSchemaName<TScope, Name>>, object & System_Unsafe.InsertableObjectFromSchemaRecordUnsafe<T>, false, T, never, TCustomMetadata>;
record<const T extends TreeNodeSchema | readonly TreeNodeSchema[]>(allowedTypes: T): TreeNodeSchemaNonClass<ScopedSchemaName<TScope, `Record<${string}>`>, NodeKind.Record, TreeRecordNode<T> & WithType<ScopedSchemaName<TScope, `Record<${string}>`>, NodeKind.Record>, RecordNodeInsertableData<T>, true, T, undefined>;
record<const Name extends TName, const T extends ImplicitAllowedTypes, const TCustomMetadata = unknown>(name: Name, allowedTypes: T, options?: NodeSchemaOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Record, TreeRecordNode<T> & WithType<ScopedSchemaName<TScope, Name>, NodeKind.Record>, RecordNodeInsertableData<T>, true, T, undefined, TCustomMetadata>;
recordRecursive<Name extends TName, const T extends System_Unsafe.ImplicitAllowedTypesUnsafe, const TCustomMetadata = unknown>(name: Name, allowedTypes: T, options?: NodeSchemaOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Record, TreeRecordNodeUnsafe<T> & WithType<ScopedSchemaName<TScope, Name>, NodeKind.Record, unknown>, {
Expand Down
4 changes: 2 additions & 2 deletions packages/dds/tree/api-report/tree.beta.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -483,9 +483,9 @@ export const SchemaFactory_base: SchemaStatics & (new () => SchemaStatics);

// @beta
export class SchemaFactoryBeta<out TScope extends string | undefined = string | undefined, TName extends number | string = string> extends SchemaFactory<TScope, TName> {
object<const Name extends TName, const T extends RestrictiveStringRecord<ImplicitFieldSchema>, const TCustomMetadata = unknown>(name: Name, fields: T, options?: ObjectSchemaOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Object, TreeObjectNode<T, ScopedSchemaName<TScope, Name>>, object & InsertableObjectFromSchemaRecord<T>, true, T>;
object<const Name extends TName, const T extends RestrictiveStringRecord<ImplicitFieldSchema>, const TCustomMetadata = unknown>(name: Name, fields: T, options?: ObjectSchemaOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Object, TreeObjectNode<T, ScopedSchemaName<TScope, Name>>, object & InsertableObjectFromSchemaRecord<T>, true, T, never, TCustomMetadata>;
// (undocumented)
objectRecursive<const Name extends TName, const T extends RestrictiveStringRecord<System_Unsafe.ImplicitFieldSchemaUnsafe>, const TCustomMetadata = unknown>(name: Name, t: T, options?: ObjectSchemaOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Object, System_Unsafe.TreeObjectNodeUnsafe<T, ScopedSchemaName<TScope, Name>>, object & System_Unsafe.InsertableObjectFromSchemaRecordUnsafe<T>, false, T>;
objectRecursive<const Name extends TName, const T extends RestrictiveStringRecord<System_Unsafe.ImplicitFieldSchemaUnsafe>, const TCustomMetadata = unknown>(name: Name, t: T, options?: ObjectSchemaOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Object, System_Unsafe.TreeObjectNodeUnsafe<T, ScopedSchemaName<TScope, Name>>, object & System_Unsafe.InsertableObjectFromSchemaRecordUnsafe<T>, false, T, never, TCustomMetadata>;
record<const T extends TreeNodeSchema | readonly TreeNodeSchema[]>(allowedTypes: T): TreeNodeSchemaNonClass<ScopedSchemaName<TScope, `Record<${string}>`>, NodeKind.Record, TreeRecordNode<T> & WithType<ScopedSchemaName<TScope, `Record<${string}>`>, NodeKind.Record>, RecordNodeInsertableData<T>, true, T, undefined>;
record<const Name extends TName, const T extends ImplicitAllowedTypes, const TCustomMetadata = unknown>(name: Name, allowedTypes: T, options?: NodeSchemaOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Record, TreeRecordNode<T> & WithType<ScopedSchemaName<TScope, Name>, NodeKind.Record>, RecordNodeInsertableData<T>, true, T, undefined, TCustomMetadata>;
recordRecursive<Name extends TName, const T extends System_Unsafe.ImplicitAllowedTypesUnsafe, const TCustomMetadata = unknown>(name: Name, allowedTypes: T, options?: NodeSchemaOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Record, TreeRecordNodeUnsafe<T> & WithType<ScopedSchemaName<TScope, Name>, NodeKind.Record, unknown>, {
Expand Down
4 changes: 2 additions & 2 deletions packages/dds/tree/api-report/tree.legacy.beta.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -486,9 +486,9 @@ export const SchemaFactory_base: SchemaStatics & (new () => SchemaStatics);

// @beta
export class SchemaFactoryBeta<out TScope extends string | undefined = string | undefined, TName extends number | string = string> extends SchemaFactory<TScope, TName> {
object<const Name extends TName, const T extends RestrictiveStringRecord<ImplicitFieldSchema>, const TCustomMetadata = unknown>(name: Name, fields: T, options?: ObjectSchemaOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Object, TreeObjectNode<T, ScopedSchemaName<TScope, Name>>, object & InsertableObjectFromSchemaRecord<T>, true, T>;
object<const Name extends TName, const T extends RestrictiveStringRecord<ImplicitFieldSchema>, const TCustomMetadata = unknown>(name: Name, fields: T, options?: ObjectSchemaOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Object, TreeObjectNode<T, ScopedSchemaName<TScope, Name>>, object & InsertableObjectFromSchemaRecord<T>, true, T, never, TCustomMetadata>;
// (undocumented)
objectRecursive<const Name extends TName, const T extends RestrictiveStringRecord<System_Unsafe.ImplicitFieldSchemaUnsafe>, const TCustomMetadata = unknown>(name: Name, t: T, options?: ObjectSchemaOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Object, System_Unsafe.TreeObjectNodeUnsafe<T, ScopedSchemaName<TScope, Name>>, object & System_Unsafe.InsertableObjectFromSchemaRecordUnsafe<T>, false, T>;
objectRecursive<const Name extends TName, const T extends RestrictiveStringRecord<System_Unsafe.ImplicitFieldSchemaUnsafe>, const TCustomMetadata = unknown>(name: Name, t: T, options?: ObjectSchemaOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Object, System_Unsafe.TreeObjectNodeUnsafe<T, ScopedSchemaName<TScope, Name>>, object & System_Unsafe.InsertableObjectFromSchemaRecordUnsafe<T>, false, T, never, TCustomMetadata>;
record<const T extends TreeNodeSchema | readonly TreeNodeSchema[]>(allowedTypes: T): TreeNodeSchemaNonClass<ScopedSchemaName<TScope, `Record<${string}>`>, NodeKind.Record, TreeRecordNode<T> & WithType<ScopedSchemaName<TScope, `Record<${string}>`>, NodeKind.Record>, RecordNodeInsertableData<T>, true, T, undefined>;
record<const Name extends TName, const T extends ImplicitAllowedTypes, const TCustomMetadata = unknown>(name: Name, allowedTypes: T, options?: NodeSchemaOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Record, TreeRecordNode<T> & WithType<ScopedSchemaName<TScope, Name>, NodeKind.Record>, RecordNodeInsertableData<T>, true, T, undefined, TCustomMetadata>;
recordRecursive<Name extends TName, const T extends System_Unsafe.ImplicitAllowedTypesUnsafe, const TCustomMetadata = unknown>(name: Name, allowedTypes: T, options?: NodeSchemaOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Record, TreeRecordNodeUnsafe<T> & WithType<ScopedSchemaName<TScope, Name>, NodeKind.Record, unknown>, {
Expand Down
8 changes: 6 additions & 2 deletions packages/dds/tree/src/simple-tree/api/schemaFactoryBeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ export class SchemaFactoryBeta<
TreeObjectNode<T, ScopedSchemaName<TScope, Name>>,
object & InsertableObjectFromSchemaRecord<T>,
true,
T
T,
never,
TCustomMetadata
> {
return objectSchema(scoped<TScope, TName, Name>(this, name), fields, true, {
...defaultSchemaFactoryObjectOptions,
Expand All @@ -110,7 +112,9 @@ export class SchemaFactoryBeta<
System_Unsafe.TreeObjectNodeUnsafe<T, ScopedSchemaName<TScope, Name>>,
object & System_Unsafe.InsertableObjectFromSchemaRecordUnsafe<T>,
false,
T
T,
never,
TCustomMetadata
> {
type TScopedName = ScopedSchemaName<TScope, Name>;
return this.object(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,42 @@ import { EmptyKey } from "../../../core/index.js";
type FromArray = TreeNodeFromImplicitAllowedTypes<[typeof Note, typeof Note]>;
type _check5 = requireTrue<areSafelyAssignable<FromArray, Note>>;
}
// TreeNodeFromImplicitAllowedTypes with a class
{
class NoteCustomized extends schema.object("Note", { text: schema.string }) {
public test: boolean = false;
}

type _check = requireAssignableTo<typeof NoteCustomized, TreeNodeSchema>;
type _checkNodeType = requireAssignableTo<
typeof NoteCustomized,
TreeNodeSchema<string, NodeKind, NoteCustomized>
>;

type Instance = InstanceType<typeof NoteCustomized>;
type _checkInstance = requireTrue<areSafelyAssignable<Instance, NoteCustomized>>;

type Test = TreeNodeFromImplicitAllowedTypes<typeof NoteCustomized>;
Copy link
Contributor

@Josmithr Josmithr Oct 23, 2025

Choose a reason for hiding this comment

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

Nit: any reason this type isn't in-lined like the subsequent test cases?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I probable needed to mouse over it to investigate things at some point

type _check2 = requireTrue<areSafelyAssignable<Test, NoteCustomized>>;

type _check3 = requireTrue<
areSafelyAssignable<
TreeNodeFromImplicitAllowedTypes<[typeof NoteCustomized]>,
NoteCustomized
>
>;
type _check4 = requireTrue<
areSafelyAssignable<
TreeNodeFromImplicitAllowedTypes<[() => typeof NoteCustomized]>,
NoteCustomized
>
>;

type FromArray = TreeNodeFromImplicitAllowedTypes<
[typeof NoteCustomized, typeof NoteCustomized]
>;
type _check5 = requireTrue<areSafelyAssignable<FromArray, NoteCustomized>>;
}

// TreeFieldFromImplicitField
{
Expand Down Expand Up @@ -383,29 +419,78 @@ describe("schemaFactory", () => {
);
});

it("Node schema metadata", () => {
it("Node schema metadata - beta", () => {
const factory = new SchemaFactoryBeta("");

const fooMetadata = {
description: "An object called Foo",
custom: {
baz: true,
},
};
} as const;

class Foo extends factory.object(
"Foo",
{ bar: factory.number },
{ metadata: fooMetadata },
) {}

// Ensure `Foo.metadata` is typed as we expect, and we can access its fields without casting.
const description = Foo.metadata.description;
type _check1 = requireTrue<areSafelyAssignable<typeof description, string | undefined>>;

const custom = Foo.metadata.custom;

// Currently it is impossible to have required custom metadata: it always includes undefined as an option.
// TODO: having a way to make metadata required would be nice.

type _check2 = requireTrue<
areSafelyAssignable<typeof custom, { baz: true } | undefined>
>;
assert(custom !== undefined);

const baz = custom.baz;
type _check3 = requireTrue<areSafelyAssignable<typeof baz, true>>;

// This must be checked after the types are checked to avoid it narrowing and making the type checks above not test anything.
assert.deepEqual(Foo.metadata, fooMetadata);
});

it("Node schema metadata - alpha", () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Not necessarily for this PR, but should we be running most of these tests over all of the applicable variants of SchemaFactory? Probably largely redundant, but might be nice to catch unexpected typing differences between the implementations.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There isn't really a good way to do that since we are testing typing so looping over them wouldn't work so we would have to write out multiple tests like I did here. I only added them because there was actually a bug, but yes this is something we might want to do when the typing for the overloads is different and complex.

Hopefulyl we can get away from having logic duplicated at multiple layers to reduce risk here.

const factory = new SchemaFactoryAlpha("");

const fooMetadata = {
description: "An object called Foo",
custom: {
baz: true,
},
} as const;

class Foo extends factory.objectAlpha(
"Foo",
{ bar: factory.number },
{ metadata: fooMetadata },
) {}

// Ensure `Foo.metadata` is typed as we expect, and we can access its fields without casting.
const description = Foo.metadata.description;
const baz = Foo.metadata.custom.baz;
type _check1 = requireTrue<areSafelyAssignable<typeof description, string>>;
type _check2 = requireTrue<areSafelyAssignable<typeof baz, boolean>>;
type _check1 = requireTrue<areSafelyAssignable<typeof description, string | undefined>>;

const custom = Foo.metadata.custom;

// Currently it is impossible to have required custom metadata: it always includes undefined as an option.
// TODO: having a way to make metadata required would be nice.

type _check2 = requireTrue<
areSafelyAssignable<typeof custom, { baz: true } | undefined>
>;
assert(custom !== undefined);

const baz = custom.baz;
type _check3 = requireTrue<areSafelyAssignable<typeof baz, true>>;

// This must be checked after the types are checked to avoid it narrowing and making the type checks above not test anything.
assert.deepEqual(Foo.metadata, fooMetadata);
});

it("Field schema metadata", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
type AllowedTypesFull,
type ImplicitAllowedTypes,
type AllowedTypes,
SchemaFactoryBeta,
} from "../../../simple-tree/index.js";
import {
allowUnused,
Expand Down Expand Up @@ -498,8 +499,8 @@ describe("SchemaFactory Recursive methods", () => {
}
});

it("Node schema metadata", () => {
const factory = new SchemaFactoryAlpha("");
it("Node schema metadata - beta", () => {
const factory = new SchemaFactoryBeta("");
class Foo extends factory.objectRecursive(
"Foo",
{ bar: [() => Foo] },
Expand All @@ -511,14 +512,59 @@ describe("SchemaFactory Recursive methods", () => {
},
) {}

const custom = Foo.metadata.custom;

// Currently it is impossible to have required custom metadata: it always includes undefined as an option.
// TODO: having a way to make metadata required would be nice.
type _check1 = requireTrue<
areSafelyAssignable<typeof custom, { baz: true } | undefined>
>;
assert(custom !== undefined);

// Ensure `Foo.metadata` is typed as we expect, and we can access its fields without casting.
const baz = custom.baz;

type _check2 = requireTrue<areSafelyAssignable<typeof baz, true>>;

// This must be checked after the types are checked to avoid it narrowing and making the type checks above not test anything.
assert.deepEqual(Foo.metadata, {
description: "A recursive object called Foo",
custom: { baz: true },
});
});

it("Node schema metadata - alpha", () => {
const factory = new SchemaFactoryAlpha("");
class Foo extends factory.objectRecursive(
"Foo",
{ bar: [() => Foo] },
{
metadata: {
description: "A recursive object called Foo",
custom: { baz: true },
},
},
) {}

const custom = Foo.metadata.custom;

// Currently it is impossible to have required custom metadata: it always includes undefined as an option.
// TODO: having a way to make metadata required would be nice.
type _check1 = requireTrue<
areSafelyAssignable<typeof custom, { baz: true } | undefined>
>;
assert(custom !== undefined);

// Ensure `Foo.metadata` is typed as we expect, and we can access its fields without casting.
const baz = Foo.metadata.custom.baz;
type _check1 = requireTrue<areSafelyAssignable<typeof baz, true>>;
const baz = custom.baz;

type _check2 = requireTrue<areSafelyAssignable<typeof baz, true>>;

// This must be checked after the types are checked to avoid it narrowing and making the type checks above not test anything.
assert.deepEqual(Foo.metadata, {
description: "A recursive object called Foo",
custom: { baz: true },
});
});
});
describe("ValidateRecursiveSchema", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1296,9 +1296,9 @@ export class SchemaFactoryAlpha<out TScope extends string | undefined = string |

// @beta
export class SchemaFactoryBeta<out TScope extends string | undefined = string | undefined, TName extends number | string = string> extends SchemaFactory<TScope, TName> {
object<const Name extends TName, const T extends RestrictiveStringRecord<ImplicitFieldSchema>, const TCustomMetadata = unknown>(name: Name, fields: T, options?: ObjectSchemaOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Object, TreeObjectNode<T, ScopedSchemaName<TScope, Name>>, object & InsertableObjectFromSchemaRecord<T>, true, T>;
object<const Name extends TName, const T extends RestrictiveStringRecord<ImplicitFieldSchema>, const TCustomMetadata = unknown>(name: Name, fields: T, options?: ObjectSchemaOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Object, TreeObjectNode<T, ScopedSchemaName<TScope, Name>>, object & InsertableObjectFromSchemaRecord<T>, true, T, never, TCustomMetadata>;
// (undocumented)
objectRecursive<const Name extends TName, const T extends RestrictiveStringRecord<System_Unsafe.ImplicitFieldSchemaUnsafe>, const TCustomMetadata = unknown>(name: Name, t: T, options?: ObjectSchemaOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Object, System_Unsafe.TreeObjectNodeUnsafe<T, ScopedSchemaName<TScope, Name>>, object & System_Unsafe.InsertableObjectFromSchemaRecordUnsafe<T>, false, T>;
objectRecursive<const Name extends TName, const T extends RestrictiveStringRecord<System_Unsafe.ImplicitFieldSchemaUnsafe>, const TCustomMetadata = unknown>(name: Name, t: T, options?: ObjectSchemaOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Object, System_Unsafe.TreeObjectNodeUnsafe<T, ScopedSchemaName<TScope, Name>>, object & System_Unsafe.InsertableObjectFromSchemaRecordUnsafe<T>, false, T, never, TCustomMetadata>;
record<const T extends TreeNodeSchema | readonly TreeNodeSchema[]>(allowedTypes: T): TreeNodeSchemaNonClass<ScopedSchemaName<TScope, `Record<${string}>`>, NodeKind.Record, TreeRecordNode<T> & WithType<ScopedSchemaName<TScope, `Record<${string}>`>, NodeKind.Record>, RecordNodeInsertableData<T>, true, T, undefined>;
record<const Name extends TName, const T extends ImplicitAllowedTypes, const TCustomMetadata = unknown>(name: Name, allowedTypes: T, options?: NodeSchemaOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Record, TreeRecordNode<T> & WithType<ScopedSchemaName<TScope, Name>, NodeKind.Record>, RecordNodeInsertableData<T>, true, T, undefined, TCustomMetadata>;
recordRecursive<Name extends TName, const T extends System_Unsafe.ImplicitAllowedTypesUnsafe, const TCustomMetadata = unknown>(name: Name, allowedTypes: T, options?: NodeSchemaOptions<TCustomMetadata>): TreeNodeSchemaClass<ScopedSchemaName<TScope, Name>, NodeKind.Record, TreeRecordNodeUnsafe<T> & WithType<ScopedSchemaName<TScope, Name>, NodeKind.Record, unknown>, {
Expand Down
Loading
Loading