import { assert, beforeAll, describe, expect, expectTypeOf, test, } from "vitest"; import { Account, co, CoPlainText, CoValueLoadingState, Group, z, } from "../exports"; import { createJazzTestAccount, setupJazzTestSync } from "../testing"; import { assertLoaded, setupTwoNodes, waitFor } from "./utils"; describe("Schema.resolved()", () => { beforeAll(async () => { await setupJazzTestSync(); await createJazzTestAccount({ isCurrentActiveAccount: true, creationProps: { name: "Hermes Puggington" }, }); }); test("all CoValue schemas have a `true` resolve query by default", () => { const AllSchemas = [ co.plainText(), co.richText(), co.fileStream(), co.vector(1), co.group(), co.list(co.plainText()), co.feed(co.plainText()), co.map({ text: co.plainText() }), co.record(z.string(), co.plainText()), co.optional(co.plainText()), co.discriminatedUnion("type", [ co.map({ type: z.literal("a") }), co.map({ type: z.literal("b") }), ]), ]; for (const Schema of AllSchemas) { expect(Schema.resolveQuery).toBe(true); } }); describe("allows adding a default resolve query", () => { test("to a CoMap schema", () => { const TestMap = co.map({ name: co.plainText(), }); const TestMapWithName = TestMap.resolved({ name: true, }); expect(TestMapWithName.resolveQuery).toEqual({ name: true, }); }); test("to a CoRecord schema", () => { const TestRecord = co.record(z.string(), co.plainText()); const TestRecordWithName = TestRecord.resolved({ name: true, }); expect(TestRecordWithName.resolveQuery).toEqual({ name: true, }); }); test("to a CoList schema", () => { const TestList = co.list(co.plainText()); const TestListWithItems = TestList.resolved({ $each: true, }); expect(TestListWithItems.resolveQuery).toEqual({ $each: true, }); }); test("to a CoFeed schema", () => { const TestFeed = co.feed(co.plainText()); const TestFeedWithItems = TestFeed.resolved({ $each: true, }); expect(TestFeedWithItems.resolveQuery).toEqual({ $each: true, }); }); test("to an Account schema", () => { const TestAccount = co.account(); const TestAccountWithName = TestAccount.resolved({ profile: true, }); expect(TestAccountWithName.resolveQuery).toEqual({ profile: true, }); }); test("to a DiscriminatedUnion schema", () => { const Pet = co.discriminatedUnion("type", [ co.map({ type: z.literal("dog"), name: co.plainText(), }), co.map({ type: z.literal("cat"), name: co.plainText(), }), ]); const PetWithName = Pet.resolved({ name: true, }); expect(PetWithName.resolveQuery).toEqual({ name: true, }); }); }); describe("the schema's resolve query is used when loading CoValues", () => { let clientAccount: Account; let serverAccount: Account; let publicGroup: Group; beforeAll(async () => { ({ clientAccount, serverAccount } = await setupTwoNodes()); publicGroup = Group.create(serverAccount).makePublic("writer"); }); describe("on load()", () => { test("for CoMap", async () => { const TestMap = co.map({ name: co.plainText(), }); const TestMapWithName = TestMap.resolved({ name: true, }); const map = TestMapWithName.create( { name: "Test", }, publicGroup, ); const loadedMap = await TestMapWithName.load(map.$jazz.id, { loadAs: clientAccount, }); assertLoaded(loadedMap); expect(loadedMap.name.$isLoaded).toBe(true); expect(loadedMap.name.toUpperCase()).toBe("TEST"); }); test("for CoRecord", async () => { const TestRecord = co.record(z.string(), co.plainText()); const TestRecordWithName = TestRecord.resolved({ name: true, }); const record = TestRecordWithName.create( { name: "Test", }, publicGroup, ); const loadedRecord = await TestRecordWithName.load(record.$jazz.id, { loadAs: clientAccount, }); assertLoaded(loadedRecord); expect(loadedRecord.name?.$isLoaded).toBe(true); expect(loadedRecord.name?.toUpperCase()).toBe("TEST"); }); test("for CoList", async () => { const TestList = co.list(co.plainText()); const TestListWithItems = TestList.resolved({ $each: true, }); const list = TestListWithItems.create(["Test", "Test2"], publicGroup); const loadedList = await TestListWithItems.load(list.$jazz.id, { loadAs: clientAccount, }); assertLoaded(loadedList); expect(loadedList.length).toBe(2); expect(loadedList[0]?.$isLoaded).toBe(true); expect(loadedList[0]?.toUpperCase()).toBe("TEST"); }); // TODO fix - `$each` does not load nested CoValues when providing an explicit resolve query either: // TestFeed.load(feed.$jazz.id, { // loadAs: clientAccount, // resolve: { // $each: true, // }, // }) test.skip("for CoFeed", async () => { const TestFeed = co.feed(co.plainText()); const TestFeedWithItems = TestFeed.resolved({ $each: true, }); const feed = TestFeedWithItems.create(["Test"], publicGroup); const loadedFeed = await TestFeedWithItems.load(feed.$jazz.id, { loadAs: clientAccount, }); assertLoaded(loadedFeed); expect(loadedFeed.inCurrentSession?.value.$jazz.loadingState).toBe( CoValueLoadingState.LOADED, ); expect(loadedFeed.inCurrentSession?.value.toUpperCase()).toEqual( "HELLO", ); }); test("for Account", async () => { const AccountWithProfile = co.account().resolved({ profile: true, }); const { account } = await AccountWithProfile.createAs(serverAccount, { creationProps: { name: "Hermes Puggington" }, onCreate: async (account) => { account.$jazz.set( "profile", co.profile().create({ name: "Hermes Puggington" }, publicGroup), ); }, }); const loadedAccount = await AccountWithProfile.load(account.$jazz.id, { loadAs: clientAccount, }); assertLoaded(loadedAccount); expect(loadedAccount.profile.$isLoaded).toBe(true); expect(loadedAccount.profile.name).toBe("Hermes Puggington"); }); test("for DiscriminatedUnion", async () => { const Person = co.map({ name: co.plainText(), }); const Dog = co.map({ type: z.literal("dog"), name: co.plainText(), owner: Person, }); const Cat = co.map({ type: z.literal("cat"), name: co.plainText(), }); const Pet = co.discriminatedUnion("type", [Dog, Cat]).resolved({ name: true, owner: { name: true, }, }); const dog = Dog.create( { type: "dog", name: "Rex", owner: { name: "Lewis" }, }, publicGroup, ); const loadedDiscriminatedUnion = await Pet.load(dog.$jazz.id, { loadAs: clientAccount, }); assertLoaded(loadedDiscriminatedUnion); expect(loadedDiscriminatedUnion.name.$isLoaded).toBe(true); expect(loadedDiscriminatedUnion.name.toUpperCase()).toBe("REX"); assert(loadedDiscriminatedUnion.type === "dog"); expect(loadedDiscriminatedUnion.owner.name.toUpperCase()).toBe("LEWIS"); }); }); describe("on subscribe()", () => { test("for CoMap", async () => { const TestMap = co.map({ name: co.plainText() }); const TestMapWithName = TestMap.resolved({ name: true }); const map = TestMapWithName.create({ name: "Test" }, publicGroup); const updates: co.loaded[] = []; TestMapWithName.subscribe( map.$jazz.id, { loadAs: clientAccount, }, (map) => { expectTypeOf().toEqualTypeOf(); updates.push(map); }, ); await waitFor(() => expect(updates.length).toBe(1)); expect(updates[0]?.name.toUpperCase()).toEqual("TEST"); }); test("for CoRecord", async () => { const TestRecord = co.record(z.string(), co.plainText()); const TestRecordWithName = TestRecord.resolved({ name: true }); const record = TestRecordWithName.create({ name: "Test" }, publicGroup); const updates: co.loaded[] = []; TestRecordWithName.subscribe( record.$jazz.id, { loadAs: clientAccount, }, (record) => { expectTypeOf().toEqualTypeOf(); updates.push(record); }, ); await waitFor(() => expect(updates.length).toBe(1)); expect(updates[0]?.name.toUpperCase()).toEqual("TEST"); }); test("for CoList", async () => { const TestList = co.list(co.plainText()); const TestListWithItems = TestList.resolved({ $each: true }); const list = TestListWithItems.create(["Test"], publicGroup); const updates: co.loaded[] = []; TestListWithItems.subscribe( list.$jazz.id, { loadAs: clientAccount, }, (list) => { expectTypeOf<(typeof list)[0]>().toEqualTypeOf(); updates.push(list); }, ); await waitFor(() => expect(updates.length).toBe(1)); expect(updates[0]?.[0]?.toUpperCase()).toEqual("TEST"); }); // TODO fix - `$each` does not load nested CoValues when providing an explicit resolve query either test.skip("for CoFeed", async () => { const TestFeed = co.feed(co.plainText()); const TestFeedWithItems = TestFeed.resolved({ $each: true }); const feed = TestFeedWithItems.create(["Test"], publicGroup); const updates: co.loaded[] = []; TestFeedWithItems.subscribe( feed.$jazz.id, { loadAs: clientAccount, }, (feed) => { updates.push(feed); }, ); await waitFor(() => expect(updates.length).toBe(1)); expect(updates[0]?.inCurrentSession?.value.toUpperCase()).toEqual( "TEST", ); }); test("for Account", async () => { const AccountWithProfile = co.account().resolved({ profile: true }); const { account } = await AccountWithProfile.createAs(serverAccount, { creationProps: { name: "Hermes Puggington" }, onCreate: async (account) => { account.$jazz.set( "profile", co.profile().create({ name: "Hermes Puggington" }, publicGroup), ); }, }); const updates: co.loaded[] = []; AccountWithProfile.subscribe( account.$jazz.id, { loadAs: clientAccount, }, (account) => { updates.push(account); }, ); await waitFor(() => expect(updates.length).toBe(1)); expect(updates[0]?.profile.name).toBe("Hermes Puggington"); }); }); describe("on merge()", () => { test("for CoMap", async () => { const TestMap = co.map({ name: co.plainText() }); const TestMapWithName = TestMap.resolved({ name: true }); const map = TestMap.create({ name: "Test" }, publicGroup); const branchMap = await TestMapWithName.load(map.$jazz.id, { unstable_branch: { name: "test-merge-coMap", owner: publicGroup }, loadAs: clientAccount, }); assertLoaded(branchMap); branchMap.name.insertAfter(branchMap.name.length, "!!"); await TestMapWithName.unstable_merge(map.$jazz.id, { branch: { name: "test-merge-coMap", owner: publicGroup }, loadAs: clientAccount, }); const mergedMap = await TestMapWithName.load(map.$jazz.id, { loadAs: clientAccount, }); assertLoaded(mergedMap); expect(mergedMap.name.toUpperCase()).toEqual("TEST!!"); }); test("for CoRecord", async () => { const TestRecord = co.record(z.string(), co.plainText()); const TestRecordWithName = TestRecord.resolved({ name: true }); const record = TestRecordWithName.create({ name: "Test" }, publicGroup); const branchRecord = await TestRecordWithName.load(record.$jazz.id, { unstable_branch: { name: "test-merge-coRecord", owner: publicGroup }, loadAs: clientAccount, }); assertLoaded(branchRecord); branchRecord.name.insertAfter(branchRecord.name.length, "!!"); await TestRecordWithName.unstable_merge(record.$jazz.id, { branch: { name: "test-merge-coRecord", owner: publicGroup }, loadAs: clientAccount, }); const mergedRecord = await TestRecordWithName.load(record.$jazz.id, { loadAs: clientAccount, }); assertLoaded(mergedRecord); expect(mergedRecord.name.toUpperCase()).toEqual("TEST!!"); }); test("for CoList", async () => { const TestList = co.list(co.plainText()); const TestListWithItems = TestList.resolved({ $each: true }); const list = TestListWithItems.create(["Test"], publicGroup); const branchList = await TestListWithItems.load(list.$jazz.id, { unstable_branch: { name: "test-merge-coList", owner: publicGroup }, loadAs: clientAccount, }); assertLoaded(branchList); branchList[0]?.insertAfter(branchList[0].length, "!!"); await TestListWithItems.unstable_merge(list.$jazz.id, { branch: { name: "test-merge-coList", owner: publicGroup }, loadAs: clientAccount, }); const mergedList = await TestListWithItems.load(list.$jazz.id, { loadAs: clientAccount, }); assertLoaded(mergedList); expect(mergedList[0]?.toUpperCase()).toEqual("TEST!!"); }); // TODO fix - `$each` does not load nested CoValues when providing an explicit resolve query either test.skip("for CoFeed", async () => { const TestFeed = co.feed(co.plainText()); const TestFeedWithItems = TestFeed.resolved({ $each: true }); const feed = TestFeedWithItems.create(["Test"], publicGroup); const branchFeed = await TestFeedWithItems.load(feed.$jazz.id, { unstable_branch: { name: "test-merge-coFeed", owner: publicGroup }, loadAs: clientAccount, }); assertLoaded(branchFeed); branchFeed.inCurrentSession?.value.insertAfter( branchFeed.inCurrentSession.value.length, "!!", ); await TestFeedWithItems.unstable_merge(feed.$jazz.id, { branch: { name: "test-merge-coFeed", owner: publicGroup }, loadAs: clientAccount, }); const mergedFeed = await TestFeedWithItems.load(feed.$jazz.id, { loadAs: clientAccount, }); assertLoaded(mergedFeed); expect(mergedFeed.inCurrentSession?.value.toUpperCase()).toEqual( "TEST!!", ); }); test("for Account", async () => { const TestAccount = co .account({ profile: co.profile(), root: co.map({ text: co.plainText() }), }) .resolved({ profile: true, root: { text: true } }); const AccountList = co .list(TestAccount) .resolved({ $each: TestAccount.resolveQuery }); const { account } = await TestAccount.createAs(serverAccount, { creationProps: { name: "Hermes Puggington" }, onCreate: async (account) => { account.$jazz.set( "profile", TestAccount.shape.profile.create( { name: "Hermes Puggington" }, publicGroup, ), ); account.$jazz.set( "root", TestAccount.shape.root.create({ text: "Test" }, publicGroup), ); }, }); const accountList = AccountList.create([account], publicGroup); const branchAccountList = await AccountList.load(accountList.$jazz.id, { unstable_branch: { name: "test-merge-account", owner: publicGroup, }, loadAs: clientAccount, }); assertLoaded(branchAccountList); branchAccountList[0]?.root.text.insertAfter( branchAccountList[0].root.text.length, "!!", ); await TestAccount.unstable_merge(account.$jazz.id, { branch: { name: "test-merge-account", owner: publicGroup }, loadAs: clientAccount, }); const mergedAccount = await TestAccount.load(account.$jazz.id, { loadAs: clientAccount, }); assertLoaded(mergedAccount); expect(mergedAccount.root.text.toUpperCase()).toEqual("TEST!!"); }); }); describe("on upsertUnique()", () => { test("for CoMap", async () => { const TestMap = co.map({ name: co.plainText() }); const TestMapWithName = TestMap.resolved({ name: true }); const map = await TestMapWithName.upsertUnique({ value: { name: "Test" }, unique: "test-upsertUnique-coList", owner: publicGroup, }); assertLoaded(map); expect(map.name.toUpperCase()).toEqual("TEST"); }); test("for CoRecord", async () => { const TestRecord = co.record(z.string(), co.plainText()); const TestRecordWithName = TestRecord.resolved({ name: true }); const record = await TestRecordWithName.upsertUnique({ value: { name: "Test" }, unique: "test-upsertUnique-coRecord", owner: publicGroup, }); assertLoaded(record); expect(record.name?.toUpperCase()).toEqual("TEST"); }); test("for CoList", async () => { const TestList = co.list(co.plainText()); const TestListWithItems = TestList.resolved({ $each: true }); const list = await TestListWithItems.upsertUnique({ value: ["Test"], unique: "test-upsertUnique-coList", owner: publicGroup, }); assertLoaded(list); expect(list[0]?.toUpperCase()).toEqual("TEST"); }); }); describe("on loadUnique()", () => { let group: Group; beforeAll(async () => { group = Group.create(); }); test("for CoMap", async () => { const TestMap = co.map({ name: co.plainText() }); const TestMapWithName = TestMap.resolved({ name: true }); const map = TestMapWithName.create( { name: "Test" }, { unique: "test-loadUnique-coMap", owner: group, }, ); const loadedMap = await TestMapWithName.loadUnique( "test-loadUnique-coMap", group.$jazz.id, ); assertLoaded(loadedMap); expect(loadedMap.name.toUpperCase()).toEqual("TEST"); }); test("for CoRecord", async () => { const TestRecord = co.record(z.string(), co.plainText()); const TestRecordWithName = TestRecord.resolved({ name: true }); const record = TestRecordWithName.create( { name: "Test" }, { unique: "test-loadUnique-coRecord", owner: group, }, ); const loadedRecord = await TestRecordWithName.loadUnique( "test-loadUnique-coRecord", group.$jazz.id, ); assertLoaded(loadedRecord); expect(loadedRecord.name?.toUpperCase()).toEqual("TEST"); }); test("for CoList", async () => { const TestList = co.list(co.plainText()); const TestListWithItems = TestList.resolved({ $each: true }); const list = TestListWithItems.create(["Test"], { unique: "test-loadUnique-coList", owner: group, }); const loadedList = await TestListWithItems.loadUnique( "test-loadUnique-coList", group.$jazz.id, ); assertLoaded(loadedList); expect(loadedList[0]?.toUpperCase()).toEqual("TEST"); }); }); test("the default resolve query is overridden with provided resolve queries", async () => { const TestMap = co.map({ name: co.plainText() }); const TestMapWithName = TestMap.resolved({ name: true }); const map = TestMapWithName.create({ name: "Test" }, publicGroup); const loadedMap = await TestMapWithName.load(map.$jazz.id, { loadAs: clientAccount, resolve: true, }); assertLoaded(loadedMap); expect(loadedMap.name.$jazz.loadingState).toEqual( CoValueLoadingState.LOADING, ); }); test("nested schemas' resolve queries are ignored when loading the parent schema", async () => { const TestMap = co.map({ name: co.plainText(), }); const TestMapWithName = TestMap.resolved({ name: true }); const ParentMap = co.map({ child: TestMapWithName, }); const ParentMapWithChild = ParentMap.resolved({ child: true }); const parentMap = ParentMapWithChild.create( { child: { name: "Test" } }, publicGroup, ); const loadedParentMap = await ParentMapWithChild.load( parentMap.$jazz.id, { loadAs: clientAccount, }, ); assertLoaded(loadedParentMap); expect(loadedParentMap.child.$isLoaded).toEqual(true); expect(loadedParentMap.child.name.$isLoaded).toEqual(false); }); test("works with recursive schemas", async () => { const Person = co.map({ get friends(): co.List { return Friends; }, }); const Friends = co.list(Person).resolved({ $each: true }); const PersonWithFriends = Person.resolved({ friends: { $each: true }, }); const person = Person.create( { friends: [{ friends: [] }], }, publicGroup, ); const loadedPerson = await PersonWithFriends.load(person.$jazz.id, { loadAs: clientAccount, }); assertLoaded(loadedPerson); expect(loadedPerson.friends.$isLoaded).toBe(true); expect(loadedPerson.friends.length).toBe(1); expect(loadedPerson.friends[0]?.friends.$isLoaded).toBe(false); }); }); test("resolved() does not override previous schema configuration", () => { const TestMap = co .map({ name: co.plainText() }) .withPermissions({ onInlineCreate: "sameAsContainer" }); const TestMapWithName = TestMap.resolved({ name: true }); expect(TestMapWithName.permissions).toEqual({ onInlineCreate: "sameAsContainer", }); }); });