prototypey.org - atproto lexicon typescript toolkit - mirror https://github.com/tylersayshi/prototypey
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

add type level and runtime error for nested objects (#94)

* add type level and runtime error for nested objects

and recommend use of lx.ref instead

resolves #89

* changeset

* formatting

* fix tests

* link to atproto docs

authored by

Tyler Lawson and committed by
GitHub
9b28e82e 0cea3c43

+185 -233
+5
.changeset/stale-hoops-grab.md
··· 1 + --- 2 + "prototypey": minor 3 + --- 4 + 5 + this errors on nested objects for lexicon correctness. more info in #89
+1 -1
.node-version
··· 1 - 25 1 + 24
+25 -7
packages/prototypey/core/infer.ts
··· 1 1 import { Prettify } from "./type-utils.ts"; 2 2 3 3 /* eslint-disable @typescript-eslint/no-empty-object-type */ 4 + 5 + /** 6 + * Error message displayed when an object is nested inside another object. 7 + * Lexicon definitions do not support inline nested objects — each object 8 + * should be its own definition, referenced via lx.ref(). 9 + */ 10 + type NestedObjectError = 11 + '[Nested objects are not supported in lexicon definitions. Per the Lexicon spec, objects can be "nested inside other definitions by reference" (https://atproto.com/specs/lexicon#object). Define each object in its own lexicon def and use lx.ref() instead.]'; 12 + 13 + /** Resolves property type, returning NestedObjectError for inline nested objects. */ 14 + type InferPropertyType<T> = T extends { type: "object" } 15 + ? NestedObjectError 16 + : InferType<T>; 17 + 4 18 type InferType<T> = T extends { type: "record" } 5 19 ? InferRecord<T> 6 20 : T extends { type: "object" } ··· 64 78 > = Prettify< 65 79 T extends { properties: infer P } 66 80 ? { 67 - -readonly [K in Normal]?: InferType<P[K & keyof P]>; 81 + -readonly [K in Normal]?: InferPropertyType<P[K & keyof P]>; 68 82 } & { 69 - -readonly [K in Exclude<Required, NullableAndRequired>]-?: InferType< 70 - P[K & keyof P] 71 - >; 83 + -readonly [K in Exclude< 84 + Required, 85 + NullableAndRequired 86 + >]-?: InferPropertyType<P[K & keyof P]>; 72 87 } & { 73 - -readonly [K in Exclude<Nullable, NullableAndRequired>]?: InferType< 88 + -readonly [K in Exclude< 89 + Nullable, 90 + NullableAndRequired 91 + >]?: InferPropertyType<P[K & keyof P]> | null; 92 + } & { 93 + -readonly [K in NullableAndRequired]: InferPropertyType< 74 94 P[K & keyof P] 75 95 > | null; 76 - } & { 77 - -readonly [K in NullableAndRequired]: InferType<P[K & keyof P]> | null; 78 96 } 79 97 : {} 80 98 >;
+15 -1
packages/prototypey/core/lib.ts
··· 211 211 } 212 212 >; 213 213 214 + /** Resolves to an error string for nested objects, or passes through unchanged. */ 215 + type CheckNotObject<T> = T extends { type: "object" } 216 + ? '❌ Nested objects are not supported. Per the Lexicon spec, objects can be "nested inside other definitions by reference" (https://atproto.com/specs/lexicon#object). Use lx.ref() instead.' 217 + : T; 218 + 214 219 /** 215 220 * Object-level options (not property-level). 216 221 * @see https://atproto.com/specs/lexicon#object ··· 692 697 * @see https://atproto.com/specs/lexicon#object 693 698 */ 694 699 object<T extends ObjectProperties, O extends ObjectOptions>( 695 - properties: T, 700 + properties: { [K in keyof T]: CheckNotObject<T[K]> }, 696 701 options?: O, 697 702 ): ObjectResult<T, O> { 703 + // Nested objects are not supported in lexicon definitions. 704 + // Each object should be its own definition, referenced via lx.ref(). 705 + for (const [key, value] of Object.entries(properties)) { 706 + if ((value as { type: string }).type === "object") { 707 + throw new Error( 708 + `Nested objects are not supported in lexicon definitions. Property "${key}" is an inline object. Per the Lexicon spec, objects can be "nested inside other definitions by reference" (https://atproto.com/specs/lexicon#object). Define it as its own lexicon def and use lx.ref() instead.`, 709 + ); 710 + } 711 + } 698 712 const { 699 713 required: propRequired, 700 714 nullable: propNullable,
+22 -29
packages/prototypey/core/tests/from-json-infer.test.ts
··· 637 637 // NESTED OBJECTS TESTS 638 638 // ============================================================================ 639 639 640 - test("fromJSON InferObject handles nested objects", () => { 640 + test("fromJSON InferObject shows error for nested objects", () => { 641 641 const lexicon = fromJSON({ 642 642 id: "test.nested", 643 643 defs: { ··· 659 659 660 660 attest(lexicon["~infer"]).type.toString.snap(`{ 661 661 $type: "test.nested" 662 - user?: { email: string; name: string } | undefined 662 + user?: 663 + | '[Nested objects are not supported in lexicon definitions. Per the Lexicon spec, objects can be "nested inside other definitions by reference" (https://atproto.com/specs/lexicon#object). Define each object in its own lexicon def and use lx.ref() instead.]' 664 + | undefined 663 665 }`); 664 666 }); 665 667 666 - test("fromJSON InferObject handles deeply nested objects", () => { 668 + test("fromJSON InferObject shows error for deeply nested objects", () => { 667 669 const lexicon = fromJSON({ 668 670 id: "test.deepNested", 669 671 defs: { ··· 695 697 attest(lexicon["~infer"]).type.toString.snap(`{ 696 698 $type: "test.deepNested" 697 699 data?: 698 - | { 699 - user?: 700 - | { profile?: { name: string } | undefined } 701 - | undefined 702 - } 700 + | '[Nested objects are not supported in lexicon definitions. Per the Lexicon spec, objects can be "nested inside other definitions by reference" (https://atproto.com/specs/lexicon#object). Define each object in its own lexicon def and use lx.ref() instead.]' 703 701 | undefined 704 702 }`); 705 703 }); ··· 708 706 // NESTED ARRAYS TESTS 709 707 // ============================================================================ 710 708 711 - test("fromJSON InferArray handles arrays of objects", () => { 709 + test("fromJSON InferArray handles arrays of objects via refs", () => { 712 710 const lexicon = fromJSON({ 713 711 id: "test.arrayOfObjects", 714 712 defs: { 713 + user: { 714 + type: "object", 715 + properties: { 716 + id: { type: "string", required: true }, 717 + name: { type: "string", required: true }, 718 + }, 719 + required: ["id", "name"], 720 + }, 715 721 main: { 716 722 type: "object", 717 723 properties: { 718 724 users: { 719 725 type: "array", 720 - items: { 721 - type: "object", 722 - properties: { 723 - id: { type: "string", required: true }, 724 - name: { type: "string", required: true }, 725 - }, 726 - required: ["id", "name"], 727 - }, 726 + items: { type: "ref", ref: "#user" }, 728 727 }, 729 728 }, 730 729 }, ··· 733 732 734 733 attest(lexicon["~infer"]).type.toString.snap(`{ 735 734 $type: "test.arrayOfObjects" 736 - users?: { id: string; name: string }[] | undefined 735 + users?: 736 + | { id: string; name: string; $type: "#user" }[] 737 + | undefined 737 738 }`); 738 739 }); 739 740 ··· 795 796 // COMPLEX NESTED STRUCTURES 796 797 // ============================================================================ 797 798 798 - test("fromJSON InferObject handles complex nested structure", () => { 799 + test("fromJSON InferObject shows error for inline nested objects in complex structure", () => { 799 800 const lexicon = fromJSON({ 800 801 id: "test.complex", 801 802 defs: { ··· 858 859 } 859 860 | undefined 860 861 author?: 861 - | { 862 - avatar?: string | undefined 863 - did: string 864 - handle: string 865 - } 862 + | '[Nested objects are not supported in lexicon definitions. Per the Lexicon spec, objects can be "nested inside other definitions by reference" (https://atproto.com/specs/lexicon#object). Define each object in its own lexicon def and use lx.ref() instead.]' 866 863 | undefined 867 864 metadata?: 868 - | { 869 - likes?: number | undefined 870 - views?: number | undefined 871 - shares?: number | undefined 872 - } 865 + | '[Nested objects are not supported in lexicon definitions. Per the Lexicon spec, objects can be "nested inside other definitions by reference" (https://atproto.com/specs/lexicon#object). Define each object in its own lexicon def and use lx.ref() instead.]' 873 866 | undefined 874 867 id: string 875 868 }`);
+27 -33
packages/prototypey/core/tests/infer.bench.ts
··· 10 10 }), 11 11 }); 12 12 return schema["~infer"]; 13 - }).types([803, "instantiations"]); 13 + }).types([1044, "instantiations"]); 14 14 15 15 bench("infer with complex nested structure", () => { 16 16 const schema = lx.lexicon("test.complex", { ··· 33 33 }), 34 34 }); 35 35 return schema["~infer"]; 36 - }).types([1110, "instantiations"]); 36 + }).types([1819, "instantiations"]); 37 37 38 38 bench("infer with circular reference", () => { 39 39 const ns = lx.lexicon("test", { ··· 50 50 }), 51 51 }); 52 52 return ns["~infer"]; 53 - }).types([781, "instantiations"]); 53 + }).types([1148, "instantiations"]); 54 54 55 55 bench("infer with app.bsky.feed.defs lexicon", () => { 56 56 const schema = lx.lexicon("app.bsky.feed.defs", { ··· 117 117 interactionShare: lx.token("User shared the feed item"), 118 118 }); 119 119 return schema["~infer"]; 120 - }).types([1437, "instantiations"]); 120 + }).types([2592, "instantiations"]); 121 121 122 - bench("infer with required/nullable nested objects", () => { 122 + bench("infer with required/nullable refs to objects", () => { 123 123 const schema = lx.lexicon("test.nestedFlags", { 124 + profile: lx.object({ 125 + name: lx.string({ required: true }), 126 + bio: lx.string(), 127 + }), 128 + settings: lx.object({ 129 + theme: lx.string(), 130 + }), 131 + metadata: lx.object({ 132 + source: lx.string({ required: true }), 133 + }), 124 134 main: lx.object({ 125 - profile: lx.object( 126 - { 127 - name: lx.string({ required: true }), 128 - bio: lx.string(), 129 - }, 130 - { required: true }, 131 - ), 132 - settings: lx.object( 133 - { 134 - theme: lx.string(), 135 - }, 136 - { nullable: true }, 137 - ), 138 - metadata: lx.object( 139 - { 140 - source: lx.string({ required: true }), 141 - }, 142 - { required: true, nullable: true }, 143 - ), 135 + profile: lx.ref("#profile", { required: true }), 136 + settings: lx.ref("#settings", { nullable: true }), 137 + metadata: lx.ref("#metadata", { required: true, nullable: true }), 144 138 }), 145 139 }); 146 140 return schema["~infer"]; 147 - }).types([1334, "instantiations"]); 141 + }).types([1350, "instantiations"]); 148 142 149 143 bench("fromJSON infer with simple object", () => { 150 144 const schema = fromJSON({ ··· 161 155 }, 162 156 }); 163 157 return schema["~infer"]; 164 - }).types([504, "instantiations"]); 158 + }).types([512, "instantiations"]); 165 159 166 160 bench("fromJSON infer with complex nested structure", () => { 167 161 const schema = fromJSON({ ··· 204 198 }, 205 199 }); 206 200 return schema["~infer"]; 207 - }).types([565, "instantiations"]); 201 + }).types([573, "instantiations"]); 208 202 209 203 bench("fromJSON infer with circular reference", () => { 210 204 const ns = fromJSON({ ··· 235 229 }, 236 230 }); 237 231 return ns["~infer"]; 238 - }).types([477, "instantiations"]); 232 + }).types([485, "instantiations"]); 239 233 240 234 bench("fromJSON infer with app.bsky.feed.defs lexicon", () => { 241 235 const schema = fromJSON({ ··· 352 346 }, 353 347 }); 354 348 return schema["~infer"]; 355 - }).types([579, "instantiations"]); 349 + }).types([587, "instantiations"]); 356 350 357 351 bench("infer with simple permission set", () => { 358 352 const schema = lx.lexicon("com.example.authCore", { ··· 368 362 }), 369 363 }); 370 364 return schema["~infer"]; 371 - }).types([312, "instantiations"]); 365 + }).types([320, "instantiations"]); 372 366 373 367 bench("infer with complex permission set", () => { 374 368 const schema = lx.lexicon("com.example.fullPerms", { ··· 398 392 }), 399 393 }); 400 394 return schema["~infer"]; 401 - }).types([318, "instantiations"]); 395 + }).types([326, "instantiations"]); 402 396 403 397 bench("fromJSON infer with simple permission set", () => { 404 398 const schema = fromJSON({ ··· 421 415 }, 422 416 }); 423 417 return schema["~infer"]; 424 - }).types([329, "instantiations"]); 418 + }).types([337, "instantiations"]); 425 419 426 420 bench("fromJSON infer with complex permission set", () => { 427 421 const schema = fromJSON({ ··· 466 460 }, 467 461 }); 468 462 return schema["~infer"]; 469 - }).types([377, "instantiations"]); 463 + }).types([385, "instantiations"]);
+49 -91
packages/prototypey/core/tests/infer.test.ts
··· 1 - import { test } from "vitest"; 1 + import { test, expect } from "vitest"; 2 2 import { attest } from "@ark/attest"; 3 3 import { lx } from "../lib.ts"; 4 4 ··· 622 622 // NESTED OBJECTS TESTS 623 623 // ============================================================================ 624 624 625 - test("InferObject handles nested objects", () => { 626 - const lexicon = lx.lexicon("test.nested", { 627 - main: lx.object({ 625 + test("InferObject throws for nested objects", () => { 626 + expect(() => 627 + lx.object({ 628 + // @ts-expect-error - nested objects are intentionally invalid 628 629 user: lx.object({ 629 630 name: lx.string({ required: true }), 630 631 email: lx.string({ required: true }), 631 632 }), 632 633 }), 633 - }); 634 - 635 - attest(lexicon["~infer"]).type.toString.snap(`{ 636 - $type: "test.nested" 637 - user?: { email: string; name: string } | undefined 638 - }`); 634 + ).toThrow( 635 + 'Nested objects are not supported in lexicon definitions. Property "user" is an inline object. Per the Lexicon spec, objects can be "nested inside other definitions by reference" (https://atproto.com/specs/lexicon#object). Define it as its own lexicon def and use lx.ref() instead.', 636 + ); 639 637 }); 640 638 641 - test("InferObject handles deeply nested objects", () => { 642 - const lexicon = lx.lexicon("test.deepNested", { 643 - main: lx.object({ 644 - data: lx.object({ 645 - user: lx.object({ 646 - profile: lx.object({ 647 - name: lx.string({ required: true }), 648 - }), 649 - }), 650 - }), 651 - }), 652 - }); 653 - 654 - attest(lexicon["~infer"]).type.toString.snap(`{ 655 - $type: "test.deepNested" 656 - data?: 657 - | { 658 - user?: 659 - | { profile?: { name: string } | undefined } 660 - | undefined 661 - } 662 - | undefined 663 - }`); 639 + test("nested object type error message", () => { 640 + attest(() => 641 + // @ts-expect-error - nested objects are intentionally invalid 642 + lx.object({ user: lx.object({ name: lx.string() }) }), 643 + ).type.errors.snap( 644 + 'Type \'ObjectResult<{ name: LexiconItemCommonOptions & { format?: "at-identifier" | "at-uri" | "cid" | "datetime" | "did" | "handle" | "nsid" | "tid" | "record-key" | "uri" | "language" | undefined; maxLength?: number | undefined; minLength?: number | undefined; maxGraphemes?: number | undefined; minGraphemes?: number | undefined; knownValues?: string[] | undefined; enum?: string[] | undefined; default?: string | undefined; const?: string | undefined; } & { type: "string"; }; }, ObjectOptions>\' is not assignable to type \'"❌ Nested objects are not supported. Per the Lexicon spec, objects can be \\"nested inside other definitions by reference\\" (https://atproto.com/specs/lexicon#object). Use lx.ref() instead."\'.', 645 + ); 664 646 }); 665 647 666 - test("InferObject handles required nested object", () => { 667 - const lexicon = lx.lexicon("test.requiredNested", { 668 - main: lx.object({ 669 - user: lx.object( 670 - { 671 - name: lx.string({ required: true }), 672 - email: lx.string(), 673 - }, 674 - { required: true }, 675 - ), 676 - }), 677 - }); 678 - 679 - attest(lexicon["~infer"]).type.toString.snap(`{ 680 - $type: "test.requiredNested" 681 - user: { email?: string | undefined; name: string } 682 - }`); 683 - }); 684 - 685 - test("InferObject handles nullable nested object", () => { 686 - const lexicon = lx.lexicon("test.nullableNested", { 687 - main: lx.object({ 688 - meta: lx.object({ tag: lx.string() }, { nullable: true }), 648 + test("InferObject uses refs instead of nested objects", () => { 649 + const lexicon = lx.lexicon("test.nested", { 650 + user: lx.object({ 651 + name: lx.string({ required: true }), 652 + email: lx.string({ required: true }), 689 653 }), 690 - }); 691 - 692 - attest(lexicon["~infer"]).type.toString.snap(`{ 693 - $type: "test.nullableNested" 694 - meta?: { tag?: string | undefined } | null | undefined 695 - }`); 696 - }); 697 - 698 - test("InferObject handles required+nullable nested object", () => { 699 - const lexicon = lx.lexicon("test.requiredNullableNested", { 700 654 main: lx.object({ 701 - data: lx.object( 702 - { value: lx.string() }, 703 - { required: true, nullable: true }, 704 - ), 655 + user: lx.ref("#user"), 705 656 }), 706 657 }); 707 658 708 659 attest(lexicon["~infer"]).type.toString.snap(`{ 709 - $type: "test.requiredNullableNested" 710 - data: { value?: string | undefined } | null 660 + $type: "test.nested" 661 + user?: 662 + | { email: string; name: string; $type: "#user" } 663 + | undefined 711 664 }`); 712 665 }); 713 666 ··· 715 668 // NESTED ARRAYS TESTS 716 669 // ============================================================================ 717 670 718 - test("InferArray handles arrays of objects", () => { 671 + test("InferArray handles arrays of objects via refs", () => { 719 672 const lexicon = lx.lexicon("test.arrayOfObjects", { 673 + user: lx.object({ 674 + id: lx.string({ required: true }), 675 + name: lx.string({ required: true }), 676 + }), 720 677 main: lx.object({ 721 - users: lx.array( 722 - lx.object({ 723 - id: lx.string({ required: true }), 724 - name: lx.string({ required: true }), 725 - }), 726 - ), 678 + users: lx.array(lx.ref("#user")), 727 679 }), 728 680 }); 729 681 730 682 attest(lexicon["~infer"]).type.toString.snap(`{ 731 683 $type: "test.arrayOfObjects" 732 - users?: { id: string; name: string }[] | undefined 684 + users?: 685 + | { id: string; name: string; $type: "#user" }[] 686 + | undefined 733 687 }`); 734 688 }); 735 689 ··· 767 721 // COMPLEX NESTED STRUCTURES 768 722 // ============================================================================ 769 723 770 - test("InferObject handles complex nested structure", () => { 724 + test("InferObject handles complex structure with refs instead of nested objects", () => { 771 725 const lexicon = lx.lexicon("test.complex", { 726 + author: lx.object({ 727 + did: lx.string({ required: true, format: "did" }), 728 + handle: lx.string({ required: true, format: "handle" }), 729 + avatar: lx.string(), 730 + }), 731 + metadata: lx.object({ 732 + views: lx.integer(), 733 + likes: lx.integer(), 734 + shares: lx.integer(), 735 + }), 772 736 main: lx.object({ 773 737 id: lx.string({ required: true }), 774 - author: lx.object({ 775 - did: lx.string({ required: true, format: "did" }), 776 - handle: lx.string({ required: true, format: "handle" }), 777 - avatar: lx.string(), 778 - }), 738 + author: lx.ref("#author"), 779 739 content: lx.union(["com.example.text", "com.example.image"]), 780 740 tags: lx.array(lx.string(), { maxLength: 10 }), 781 - metadata: lx.object({ 782 - views: lx.integer(), 783 - likes: lx.integer(), 784 - shares: lx.integer(), 785 - }), 741 + metadata: lx.ref("#metadata"), 786 742 }), 787 743 }); 788 744 ··· 798 754 avatar?: string | undefined 799 755 did: string 800 756 handle: string 757 + $type: "#author" 801 758 } 802 759 | undefined 803 760 metadata?: ··· 805 762 likes?: number | undefined 806 763 views?: number | undefined 807 764 shares?: number | undefined 765 + $type: "#metadata" 808 766 } 809 767 | undefined 810 768 id: string
+20 -53
packages/prototypey/core/tests/primitives.test.ts
··· 256 256 }); 257 257 }); 258 258 259 - test("lx.object() with required option marks object as required in parent", () => { 260 - const result = lx.object({ 261 - foo: lx.object({ bar: lx.string({ required: true }) }, { required: true }), 262 - }); 263 - expect(result).toEqual({ 264 - type: "object", 265 - required: ["foo"], 266 - properties: { 267 - foo: { 268 - type: "object", 269 - required: ["bar"], 270 - properties: { 271 - bar: { type: "string" }, 272 - }, 273 - }, 274 - }, 275 - }); 259 + test("lx.object() throws when nesting an object inside another object", () => { 260 + expect(() => 261 + lx.object({ 262 + // @ts-expect-error - nested objects are intentionally invalid 263 + foo: lx.object( 264 + { bar: lx.string({ required: true }) }, 265 + { required: true }, 266 + ), 267 + }), 268 + ).toThrow( 269 + 'Nested objects are not supported in lexicon definitions. Property "foo" is an inline object. Per the Lexicon spec, objects can be "nested inside other definitions by reference" (https://atproto.com/specs/lexicon#object). Define it as its own lexicon def and use lx.ref() instead.', 270 + ); 276 271 }); 277 272 278 - test("lx.object() with nullable option marks object as nullable in parent", () => { 279 - const result = lx.object({ 280 - foo: lx.object({ bar: lx.string() }, { nullable: true }), 281 - }); 282 - expect(result).toEqual({ 283 - type: "object", 284 - nullable: ["foo"], 285 - properties: { 286 - foo: { 287 - type: "object", 288 - properties: { 289 - bar: { type: "string" }, 290 - }, 291 - }, 292 - }, 293 - }); 294 - }); 295 - 296 - test("lx.object() nested with own required fields is not falsely required in parent", () => { 297 - const result = lx.object({ 298 - foo: lx.object({ 299 - bar: lx.string({ required: true }), 273 + test("lx.object() throws for nullable nested object", () => { 274 + expect(() => 275 + lx.object({ 276 + // @ts-expect-error - nested objects are intentionally invalid 277 + foo: lx.object({ bar: lx.string() }, { nullable: true }), 300 278 }), 301 - }); 302 - // foo should NOT appear in parent's required array 303 - expect(result).toEqual({ 304 - type: "object", 305 - properties: { 306 - foo: { 307 - type: "object", 308 - required: ["bar"], 309 - properties: { 310 - bar: { type: "string" }, 311 - }, 312 - }, 313 - }, 314 - }); 279 + ).toThrow( 280 + 'Nested objects are not supported in lexicon definitions. Property "foo" is an inline object. Per the Lexicon spec, objects can be "nested inside other definitions by reference" (https://atproto.com/specs/lexicon#object). Define it as its own lexicon def and use lx.ref() instead.', 281 + ); 315 282 }); 316 283 317 284 test("lx.token() with interaction event", () => {
+21 -18
packages/prototypey/core/tests/validation.test.ts
··· 387 387 expect(result.success).toBe(false); 388 388 }); 389 389 390 - it("should handle deeply nested nulls", () => { 391 - const nestedSchema = lx.lexicon("test.nested", { 392 - main: lx.object({ 393 - user: lx.object({ 394 - name: lx.string({ required: true }), 390 + it("should throw when nesting objects inline", () => { 391 + expect(() => 392 + lx.lexicon("test.nested", { 393 + main: lx.object({ 394 + // @ts-expect-error - nested objects are intentionally invalid 395 + user: lx.object({ 396 + name: lx.string({ required: true }), 397 + }), 395 398 }), 396 399 }), 397 - }); 398 - const result = nestedSchema.validate({ 399 - user: null, 400 - }); 401 - expect(result.success).toBe(false); 400 + ).toThrow( 401 + 'Nested objects are not supported in lexicon definitions. Property "user" is an inline object. Per the Lexicon spec, objects can be "nested inside other definitions by reference" (https://atproto.com/specs/lexicon#object). Define it as its own lexicon def and use lx.ref() instead.', 402 + ); 402 403 }); 403 404 }); 404 405 ··· 750 751 expect(result.success).toBe(false); 751 752 }); 752 753 753 - it("should handle arrays of deeply nested objects", () => { 754 + it("should handle arrays of deeply nested objects via refs", () => { 754 755 const arraySchema = lx.lexicon("test.array-deep", { 756 + itemData: lx.object({ 757 + value: lx.string({ required: true }), 758 + }), 755 759 item: lx.object({ 756 - data: lx.object({ 757 - value: lx.string({ required: true }), 758 - }), 760 + data: lx.ref("#itemData"), 759 761 }), 760 762 main: lx.object({ 761 763 items: lx.array(lx.ref("#item"), { required: true }), ··· 772 774 expect(result.success).toBe(true); 773 775 }); 774 776 775 - it("should reject invalid item in array of nested objects", () => { 777 + it("should reject invalid item in array of nested objects via refs", () => { 776 778 const arraySchema = lx.lexicon("test.array-deep-invalid", { 779 + itemData: lx.object({ 780 + value: lx.string({ required: true }), 781 + }), 777 782 item: lx.object({ 778 - data: lx.object({ 779 - value: lx.string({ required: true }), 780 - }), 783 + data: lx.ref("#itemData"), 781 784 }), 782 785 main: lx.object({ 783 786 items: lx.array(lx.ref("#item"), { required: true }),