jazz-tools 0.15.0 → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.turbo/turbo-build.log +47 -47
  2. package/CHANGELOG.md +23 -0
  3. package/dist/{chunk-FSIM7N33.js → chunk-VBDJM6Z5.js} +142 -31
  4. package/dist/chunk-VBDJM6Z5.js.map +1 -0
  5. package/dist/index.js +1 -1
  6. package/dist/inspector/index.js +147 -102
  7. package/dist/inspector/index.js.map +1 -1
  8. package/dist/inspector/viewer/co-plain-text-view.d.ts +5 -0
  9. package/dist/inspector/viewer/co-plain-text-view.d.ts.map +1 -0
  10. package/dist/inspector/viewer/new-app.d.ts.map +1 -1
  11. package/dist/inspector/viewer/page.d.ts.map +1 -1
  12. package/dist/inspector/viewer/type-icon.d.ts.map +1 -1
  13. package/dist/inspector/viewer/use-open-inspector.d.ts +2 -0
  14. package/dist/inspector/viewer/use-open-inspector.d.ts.map +1 -0
  15. package/dist/inspector/viewer/use-page-path.d.ts.map +1 -1
  16. package/dist/inspector/viewer/use-resolve-covalue.d.ts +1 -1
  17. package/dist/inspector/viewer/use-resolve-covalue.d.ts.map +1 -1
  18. package/dist/inspector/viewer/value-renderer.d.ts.map +1 -1
  19. package/dist/react-native-core/index.js +2 -18
  20. package/dist/react-native-core/index.js.map +1 -1
  21. package/dist/react-native-core/media.d.ts.map +1 -1
  22. package/dist/testing.js +1 -1
  23. package/dist/tools/coValues/coFeed.d.ts +9 -0
  24. package/dist/tools/coValues/coFeed.d.ts.map +1 -1
  25. package/dist/tools/coValues/coMap.d.ts +98 -2
  26. package/dist/tools/coValues/coMap.d.ts.map +1 -1
  27. package/dist/tools/coValues/interfaces.d.ts +3 -0
  28. package/dist/tools/coValues/interfaces.d.ts.map +1 -1
  29. package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts +12 -0
  30. package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts.map +1 -1
  31. package/dist/tools/implementation/zodSchema/zodCo.d.ts.map +1 -1
  32. package/dist/tools/subscribe/CoValueCoreSubscription.d.ts +2 -1
  33. package/dist/tools/subscribe/CoValueCoreSubscription.d.ts.map +1 -1
  34. package/dist/tools/subscribe/SubscriptionScope.d.ts +2 -1
  35. package/dist/tools/subscribe/SubscriptionScope.d.ts.map +1 -1
  36. package/package.json +5 -5
  37. package/src/inspector/viewer/co-plain-text-view.tsx +17 -0
  38. package/src/inspector/viewer/new-app.tsx +2 -1
  39. package/src/inspector/viewer/page.tsx +5 -0
  40. package/src/inspector/viewer/type-icon.tsx +1 -0
  41. package/src/inspector/viewer/use-open-inspector.ts +18 -0
  42. package/src/inspector/viewer/use-page-path.ts +14 -1
  43. package/src/inspector/viewer/use-resolve-covalue.ts +1 -1
  44. package/src/inspector/viewer/value-renderer.tsx +4 -0
  45. package/src/react-native-core/media.tsx +2 -22
  46. package/src/tools/coValues/coFeed.ts +38 -0
  47. package/src/tools/coValues/coMap.ts +118 -14
  48. package/src/tools/coValues/interfaces.ts +14 -4
  49. package/src/tools/implementation/zodSchema/schemaTypes/CoMapSchema.ts +38 -0
  50. package/src/tools/implementation/zodSchema/zodCo.ts +6 -0
  51. package/src/tools/subscribe/CoValueCoreSubscription.ts +12 -9
  52. package/src/tools/subscribe/SubscriptionScope.ts +31 -19
  53. package/src/tools/tests/coFeed.test.ts +69 -0
  54. package/src/tools/tests/coMap.test.ts +480 -4
  55. package/src/tools/tests/load.test.ts +2 -1
  56. package/dist/chunk-FSIM7N33.js.map +0 -1
@@ -44,34 +44,46 @@ export class SubscriptionScope<D extends CoValue> {
44
44
  resolve: RefsToResolve<D>,
45
45
  public id: ID<D>,
46
46
  public schema: RefEncoded<D>,
47
+ public skipRetry?: boolean,
47
48
  ) {
48
49
  this.resolve = resolve;
49
50
  this.value = { type: "unloaded", id };
50
51
 
51
52
  let lastUpdate: RawCoValue | "unavailable" | undefined;
52
- this.subscription = new CoValueCoreSubscription(node, id, (value) => {
53
- lastUpdate = value;
54
-
55
- // Need all these checks because the migration can trigger new syncronous updates
56
- //
57
- // We want to:
58
- // - Run the migration only once
59
- // - Skip all the updates until the migration is done
60
- // - Trigger handleUpdate only with the final value
61
- if (!this.migrated && value !== "unavailable") {
62
- if (this.migrating) {
53
+ this.subscription = new CoValueCoreSubscription(
54
+ node,
55
+ id,
56
+ (value) => {
57
+ lastUpdate = value;
58
+
59
+ if (skipRetry && value === "unavailable") {
60
+ this.handleUpdate(value);
61
+ this.destroy();
63
62
  return;
64
63
  }
65
64
 
66
- this.migrating = true;
67
- applyCoValueMigrations(instantiateRefEncoded(this.schema, value));
68
- this.migrated = true;
69
- this.handleUpdate(lastUpdate);
70
- return;
71
- }
65
+ // Need all these checks because the migration can trigger new syncronous updates
66
+ //
67
+ // We want to:
68
+ // - Run the migration only once
69
+ // - Skip all the updates until the migration is done
70
+ // - Trigger handleUpdate only with the final value
71
+ if (!this.migrated && value !== "unavailable") {
72
+ if (this.migrating) {
73
+ return;
74
+ }
75
+
76
+ this.migrating = true;
77
+ applyCoValueMigrations(instantiateRefEncoded(this.schema, value));
78
+ this.migrated = true;
79
+ this.handleUpdate(lastUpdate);
80
+ return;
81
+ }
72
82
 
73
- this.handleUpdate(value);
74
- });
83
+ this.handleUpdate(value);
84
+ },
85
+ skipRetry,
86
+ );
75
87
  }
76
88
 
77
89
  updateValue(value: SubscriptionValue<D, any>) {
@@ -688,6 +688,75 @@ describe("FileStream.loadAsBlob", async () => {
688
688
  });
689
689
  });
690
690
 
691
+ describe("FileStream.loadAsBase64", async () => {
692
+ async function setup() {
693
+ const me = await Account.create({
694
+ creationProps: { name: "Hermes Puggington" },
695
+ crypto: Crypto,
696
+ });
697
+
698
+ const stream = FileStream.create({ owner: me });
699
+
700
+ stream.start({ mimeType: "text/plain" });
701
+
702
+ return { stream, me };
703
+ }
704
+
705
+ test("resolves only when the stream is ended", async () => {
706
+ const { stream, me } = await setup();
707
+ stream.push(new Uint8Array([1]));
708
+
709
+ const promise = FileStream.loadAsBase64(stream.id, { loadAs: me });
710
+
711
+ stream.push(new Uint8Array([2]));
712
+ stream.end();
713
+
714
+ const base64 = await promise;
715
+
716
+ // The promise resolves only when the stream is ended
717
+ // so we get a blob with all the chunks
718
+ expect(base64).toBe("AQI=");
719
+ });
720
+
721
+ test("resolves with a data URL if dataURL: true", async () => {
722
+ const { stream, me } = await setup();
723
+ stream.push(new Uint8Array([1]));
724
+
725
+ const promise = FileStream.loadAsBase64(stream.id, {
726
+ loadAs: me,
727
+ dataURL: true,
728
+ });
729
+
730
+ stream.push(new Uint8Array([2]));
731
+ stream.end();
732
+
733
+ const base64 = await promise;
734
+
735
+ // The promise resolves only when the stream is ended
736
+ // so we get a blob with all the chunks
737
+ expect(base64).toBe("data:text/plain;base64,AQI=");
738
+ });
739
+
740
+ test("resolves with a partial base64 if allowUnfinished: true", async () => {
741
+ const { stream, me } = await setup();
742
+ stream.push(new Uint8Array([1]));
743
+
744
+ const promise = FileStream.loadAsBase64(stream.id, {
745
+ loadAs: me,
746
+ allowUnfinished: true,
747
+ });
748
+
749
+ const base64 = await promise;
750
+
751
+ stream.push(new Uint8Array([2]));
752
+ stream.end();
753
+
754
+ // The promise resolves before the stream is ended
755
+ // so we get a blob only with the first chunk
756
+ expect(base64).toBe("AQ==");
757
+ });
758
+ });
759
+
691
760
  describe("FileStream progress tracking", async () => {
692
761
  test("createFromBlob should report upload progress correctly", async () => {
693
762
  // Create 5MB test blob
@@ -1,3 +1,4 @@
1
+ import { cojsonInternals } from "cojson";
1
2
  import { WasmCrypto } from "cojson/crypto/WasmCrypto";
2
3
  import {
3
4
  assert,
@@ -11,13 +12,19 @@ import {
11
12
  } from "vitest";
12
13
  import { Group, co, subscribeToCoValue, z } from "../exports.js";
13
14
  import { Account } from "../index.js";
14
- import { Loaded, zodSchemaToCoSchema } from "../internal.js";
15
- import { createJazzTestAccount, setupJazzTestSync } from "../testing.js";
15
+ import { ID, Loaded, zodSchemaToCoSchema } from "../internal.js";
16
+ import {
17
+ createJazzTestAccount,
18
+ getPeerConnectedToTestSyncServer,
19
+ setupJazzTestSync,
20
+ } from "../testing.js";
16
21
  import { setupTwoNodes, waitFor } from "./utils.js";
17
22
 
18
23
  const Crypto = await WasmCrypto.create();
19
24
 
20
25
  beforeEach(async () => {
26
+ cojsonInternals.CO_VALUE_LOADING_CONFIG.RETRY_DELAY = 1000;
27
+
21
28
  await setupJazzTestSync();
22
29
 
23
30
  await createJazzTestAccount({
@@ -543,6 +550,115 @@ describe("CoMap resolution", async () => {
543
550
  expect(loadedPerson.dog?.name).toEqual("Rex");
544
551
  });
545
552
 
553
+ test("loading a remotely available map with skipRetry set to true", async () => {
554
+ // Make the retry delay extra long to ensure that it's not used
555
+ cojsonInternals.CO_VALUE_LOADING_CONFIG.RETRY_DELAY = 100_000_000;
556
+
557
+ const Dog = co.map({
558
+ name: z.string(),
559
+ breed: z.string(),
560
+ });
561
+
562
+ const Person = co.map({
563
+ name: z.string(),
564
+ age: z.number(),
565
+ dog: Dog,
566
+ });
567
+
568
+ const currentAccount = Account.getMe();
569
+
570
+ // Disconnect the current account
571
+ currentAccount._raw.core.node.syncManager.getPeers().forEach((peer) => {
572
+ peer.gracefulShutdown();
573
+ });
574
+
575
+ const group = Group.create();
576
+ group.addMember("everyone", "writer");
577
+
578
+ const person = Person.create(
579
+ {
580
+ name: "John",
581
+ age: 20,
582
+ dog: Dog.create({ name: "Rex", breed: "Labrador" }, group),
583
+ },
584
+ group,
585
+ );
586
+
587
+ const userB = await createJazzTestAccount();
588
+
589
+ // We expect that the test doesn't hang here and immediately returns null
590
+ const loadedPerson = await Person.load(person.id, {
591
+ loadAs: userB,
592
+ skipRetry: true,
593
+ });
594
+
595
+ expect(loadedPerson).toBeNull();
596
+ });
597
+
598
+ test("loading a remotely available map with skipRetry set to false", async () => {
599
+ // Make the retry delay extra long to avoid flakyness in the resolved checks
600
+ cojsonInternals.CO_VALUE_LOADING_CONFIG.RETRY_DELAY = 100_000_000;
601
+
602
+ const Dog = co.map({
603
+ name: z.string(),
604
+ breed: z.string(),
605
+ });
606
+
607
+ const Person = co.map({
608
+ name: z.string(),
609
+ age: z.number(),
610
+ dog: Dog,
611
+ });
612
+
613
+ const currentAccount = Account.getMe();
614
+
615
+ // Disconnect the current account
616
+ currentAccount._raw.core.node.syncManager.getPeers().forEach((peer) => {
617
+ peer.gracefulShutdown();
618
+ });
619
+
620
+ const group = Group.create();
621
+ group.addMember("everyone", "writer");
622
+
623
+ const person = Person.create(
624
+ {
625
+ name: "John",
626
+ age: 20,
627
+ dog: Dog.create({ name: "Rex", breed: "Labrador" }, group),
628
+ },
629
+ group,
630
+ );
631
+
632
+ const userB = await createJazzTestAccount();
633
+ let resolved = false;
634
+ const promise = Person.load(person.id, {
635
+ loadAs: userB,
636
+ skipRetry: false,
637
+ });
638
+ promise.then(() => {
639
+ resolved = true;
640
+ });
641
+
642
+ await new Promise((resolve) => setTimeout(resolve, 100));
643
+
644
+ expect(resolved).toBe(false);
645
+
646
+ // Reconnect the current account
647
+ currentAccount._raw.core.node.syncManager.addPeer(
648
+ getPeerConnectedToTestSyncServer(),
649
+ );
650
+
651
+ const loadedPerson = await promise;
652
+
653
+ expect(resolved).toBe(true);
654
+ assert(loadedPerson);
655
+ expect(loadedPerson.dog).toBe(null);
656
+
657
+ await waitFor(() => expect(loadedPerson.dog).toBeTruthy());
658
+
659
+ expect(loadedPerson.dog?.name).toEqual("Rex");
660
+ });
661
+
546
662
  test("accessing the value refs", async () => {
547
663
  const Dog = co.map({
548
664
  name: z.string(),
@@ -1197,8 +1313,368 @@ describe("Creating and finding unique CoMaps", async () => {
1197
1313
  { owner: group, unique: { name: "Alice" } },
1198
1314
  );
1199
1315
 
1200
- const foundAlice = Person.findUnique({ name: "Alice" }, group.id);
1201
- expect(foundAlice).toEqual(alice.id);
1316
+ const foundAlice = await Person.loadUnique({ name: "Alice" }, group.id);
1317
+ expect(foundAlice).toEqual(alice);
1318
+ });
1319
+
1320
+ test("manual upserting pattern", async () => {
1321
+ // Schema
1322
+ const Event = co.map({
1323
+ title: z.string(),
1324
+ identifier: z.string(),
1325
+ external_id: z.string(),
1326
+ });
1327
+
1328
+ // Data
1329
+ const sourceData = {
1330
+ title: "Test Event Title",
1331
+ identifier: "test-event-identifier",
1332
+ _id: "test-event-external-id",
1333
+ };
1334
+ const workspace = Group.create();
1335
+
1336
+ // Pattern
1337
+ let activeEvent = await Event.loadUnique(
1338
+ { identifier: sourceData.identifier },
1339
+ workspace.id,
1340
+ );
1341
+ if (!activeEvent) {
1342
+ activeEvent = Event.create(
1343
+ {
1344
+ title: sourceData.title,
1345
+ identifier: sourceData.identifier,
1346
+ external_id: sourceData._id,
1347
+ },
1348
+ workspace,
1349
+ );
1350
+ } else {
1351
+ activeEvent.applyDiff({
1352
+ title: sourceData.title,
1353
+ identifier: sourceData.identifier,
1354
+ external_id: sourceData._id,
1355
+ });
1356
+ }
1357
+ expect(activeEvent).toEqual({
1358
+ title: sourceData.title,
1359
+ identifier: sourceData.identifier,
1360
+ external_id: sourceData._id,
1361
+ });
1362
+ });
1363
+
1364
+ test("upserting a non-existent value", async () => {
1365
+ // Schema
1366
+ const Event = co.map({
1367
+ title: z.string(),
1368
+ identifier: z.string(),
1369
+ external_id: z.string(),
1370
+ });
1371
+
1372
+ // Data
1373
+ const sourceData = {
1374
+ title: "Test Event Title",
1375
+ identifier: "test-event-identifier",
1376
+ _id: "test-event-external-id",
1377
+ };
1378
+ const workspace = Group.create();
1379
+
1380
+ // Upserting
1381
+ const activeEvent = await Event.upsertUnique({
1382
+ value: {
1383
+ title: sourceData.title,
1384
+ identifier: sourceData.identifier,
1385
+ external_id: sourceData._id,
1386
+ },
1387
+ unique: sourceData.identifier,
1388
+ owner: workspace,
1389
+ });
1390
+ expect(activeEvent).toEqual({
1391
+ title: sourceData.title,
1392
+ identifier: sourceData.identifier,
1393
+ external_id: sourceData._id,
1394
+ });
1395
+ });
1396
+
1397
+ test("upserting an existing value", async () => {
1398
+ // Schema
1399
+ const Event = co.map({
1400
+ title: z.string(),
1401
+ identifier: z.string(),
1402
+ external_id: z.string(),
1403
+ });
1404
+
1405
+ // Data
1406
+ const oldSourceData = {
1407
+ title: "Old Event Title",
1408
+ identifier: "test-event-identifier",
1409
+ _id: "test-event-external-id",
1410
+ };
1411
+ const newSourceData = {
1412
+ title: "New Event Title",
1413
+ identifier: "test-event-identifier",
1414
+ _id: "test-event-external-id",
1415
+ };
1416
+ expect(oldSourceData.identifier).toEqual(newSourceData.identifier);
1417
+ const workspace = Group.create();
1418
+ const oldActiveEvent = Event.create(
1419
+ {
1420
+ title: oldSourceData.title,
1421
+ identifier: oldSourceData.identifier,
1422
+ external_id: oldSourceData._id,
1423
+ },
1424
+ workspace,
1425
+ );
1426
+
1427
+ // Upserting
1428
+ const activeEvent = await Event.upsertUnique({
1429
+ value: {
1430
+ title: newSourceData.title,
1431
+ identifier: newSourceData.identifier,
1432
+ external_id: newSourceData._id,
1433
+ },
1434
+ unique: newSourceData.identifier,
1435
+ owner: workspace,
1436
+ });
1437
+ expect(activeEvent).toEqual({
1438
+ title: newSourceData.title,
1439
+ identifier: newSourceData.identifier,
1440
+ external_id: newSourceData._id,
1441
+ });
1442
+ expect(activeEvent).not.toEqual(oldActiveEvent);
1443
+ });
1444
+
1445
+ test("upserting a non-existent value with resolve", async () => {
1446
+ const Project = co.map({
1447
+ name: z.string(),
1448
+ });
1449
+ const Organisation = co.map({
1450
+ name: z.string(),
1451
+ projects: co.list(Project),
1452
+ });
1453
+ const workspace = Group.create();
1454
+
1455
+ const myOrg = await Organisation.upsertUnique({
1456
+ value: {
1457
+ name: "My organisation",
1458
+ projects: co.list(Project).create(
1459
+ [
1460
+ Project.create(
1461
+ {
1462
+ name: "My project",
1463
+ },
1464
+ workspace,
1465
+ ),
1466
+ ],
1467
+ workspace,
1468
+ ),
1469
+ },
1470
+ unique: { name: "My organisation" },
1471
+ owner: workspace,
1472
+ resolve: {
1473
+ projects: {
1474
+ $each: true,
1475
+ },
1476
+ },
1477
+ });
1478
+ assert(myOrg);
1479
+ expect(myOrg).not.toBeNull();
1480
+ expect(myOrg.name).toEqual("My organisation");
1481
+ expect(myOrg.projects.length).toBe(1);
1482
+ expect(myOrg.projects[0]).toMatchObject({
1483
+ name: "My project",
1484
+ });
1485
+ });
1486
+
1487
+ test("upserting an existing value with resolve", async () => {
1488
+ const Project = co.map({
1489
+ name: z.string(),
1490
+ });
1491
+ const Organisation = co.map({
1492
+ name: z.string(),
1493
+ projects: co.list(Project),
1494
+ });
1495
+ const workspace = Group.create();
1496
+ const initialProject = await Project.upsertUnique({
1497
+ value: {
1498
+ name: "My project",
1499
+ },
1500
+ unique: { unique: "First project" },
1501
+ owner: workspace,
1502
+ });
1503
+ assert(initialProject);
1504
+ expect(initialProject).not.toBeNull();
1505
+ expect(initialProject.name).toEqual("My project");
1506
+
1507
+ const myOrg = await Organisation.upsertUnique({
1508
+ value: {
1509
+ name: "My organisation",
1510
+ projects: co.list(Project).create([initialProject], workspace),
1511
+ },
1512
+ unique: { name: "My organisation" },
1513
+ owner: workspace,
1514
+ resolve: {
1515
+ projects: {
1516
+ $each: true,
1517
+ },
1518
+ },
1519
+ });
1520
+ assert(myOrg);
1521
+ expect(myOrg).not.toBeNull();
1522
+ expect(myOrg.name).toEqual("My organisation");
1523
+ expect(myOrg.projects.length).toBe(1);
1524
+ expect(myOrg.projects.at(0)?.name).toEqual("My project");
1525
+
1526
+ const updatedProject = await Project.upsertUnique({
1527
+ value: {
1528
+ name: "My updated project",
1529
+ },
1530
+ unique: { unique: "First project" },
1531
+ owner: workspace,
1532
+ });
1533
+
1534
+ assert(updatedProject);
1535
+ expect(updatedProject).not.toBeNull();
1536
+ expect(updatedProject).toEqual(initialProject);
1537
+ expect(updatedProject.name).toEqual("My updated project");
1538
+ expect(myOrg.projects.length).toBe(1);
1539
+ expect(myOrg.projects.at(0)?.name).toEqual("My updated project");
1540
+ });
1541
+
1542
+ test("upserting a partially loaded value on an new value with resolve", async () => {
1543
+ const Project = co.map({
1544
+ name: z.string(),
1545
+ });
1546
+ const Organisation = co.map({
1547
+ name: z.string(),
1548
+ projects: co.list(Project),
1549
+ });
1550
+ const publicAccess = Group.create();
1551
+ publicAccess.addMember("everyone", "writer");
1552
+
1553
+ const initialProject = await Project.upsertUnique({
1554
+ value: {
1555
+ name: "My project",
1556
+ },
1557
+ unique: { unique: "First project" },
1558
+ owner: publicAccess,
1559
+ });
1560
+ assert(initialProject);
1561
+ expect(initialProject).not.toBeNull();
1562
+ expect(initialProject.name).toEqual("My project");
1563
+
1564
+ const fullProjectList = co
1565
+ .list(Project)
1566
+ .create([initialProject], publicAccess);
1567
+
1568
+ const account = await createJazzTestAccount({
1569
+ isCurrentActiveAccount: true,
1570
+ });
1571
+
1572
+ const shallowProjectList = await co.list(Project).load(fullProjectList.id, {
1573
+ loadAs: account,
1574
+ });
1575
+ assert(shallowProjectList);
1576
+
1577
+ const publicAccessAsNewAccount = await Group.load(publicAccess.id, {
1578
+ loadAs: account,
1579
+ });
1580
+ assert(publicAccessAsNewAccount);
1581
+
1582
+ const updatedOrg = await Organisation.upsertUnique({
1583
+ value: {
1584
+ name: "My organisation",
1585
+ projects: shallowProjectList,
1586
+ },
1587
+ unique: { name: "My organisation" },
1588
+ owner: publicAccessAsNewAccount,
1589
+ resolve: {
1590
+ projects: {
1591
+ $each: true,
1592
+ },
1593
+ },
1594
+ });
1595
+
1596
+ assert(updatedOrg);
1597
+
1598
+ expect(updatedOrg.projects.id).toEqual(fullProjectList.id);
1599
+ expect(updatedOrg.projects.length).toBe(1);
1600
+ expect(updatedOrg.projects.at(0)?.name).toEqual("My project");
1601
+ });
1602
+
1603
+ test("upserting a partially loaded value on an existing value with resolve", async () => {
1604
+ const Project = co.map({
1605
+ name: z.string(),
1606
+ });
1607
+ const Organisation = co.map({
1608
+ name: z.string(),
1609
+ projects: co.list(Project),
1610
+ });
1611
+ const publicAccess = Group.create();
1612
+ publicAccess.addMember("everyone", "writer");
1613
+
1614
+ const initialProject = await Project.upsertUnique({
1615
+ value: {
1616
+ name: "My project",
1617
+ },
1618
+ unique: { unique: "First project" },
1619
+ owner: publicAccess,
1620
+ });
1621
+ assert(initialProject);
1622
+ expect(initialProject).not.toBeNull();
1623
+ expect(initialProject.name).toEqual("My project");
1624
+
1625
+ const myOrg = await Organisation.upsertUnique({
1626
+ value: {
1627
+ name: "My organisation",
1628
+ projects: co.list(Project).create([], publicAccess),
1629
+ },
1630
+ unique: { name: "My organisation" },
1631
+ owner: publicAccess,
1632
+ resolve: {
1633
+ projects: {
1634
+ $each: true,
1635
+ },
1636
+ },
1637
+ });
1638
+ assert(myOrg);
1639
+
1640
+ const fullProjectList = co
1641
+ .list(Project)
1642
+ .create([initialProject], publicAccess);
1643
+
1644
+ const account = await createJazzTestAccount({
1645
+ isCurrentActiveAccount: true,
1646
+ });
1647
+
1648
+ const shallowProjectList = await co.list(Project).load(fullProjectList.id, {
1649
+ loadAs: account,
1650
+ });
1651
+ assert(shallowProjectList);
1652
+
1653
+ const publicAccessAsNewAccount = await Group.load(publicAccess.id, {
1654
+ loadAs: account,
1655
+ });
1656
+ assert(publicAccessAsNewAccount);
1657
+
1658
+ const updatedOrg = await Organisation.upsertUnique({
1659
+ value: {
1660
+ name: "My organisation",
1661
+ projects: shallowProjectList,
1662
+ },
1663
+ unique: { name: "My organisation" },
1664
+ owner: publicAccessAsNewAccount,
1665
+ resolve: {
1666
+ projects: {
1667
+ $each: true,
1668
+ },
1669
+ },
1670
+ });
1671
+
1672
+ assert(updatedOrg);
1673
+
1674
+ expect(updatedOrg.projects.id).toEqual(fullProjectList.id);
1675
+ expect(updatedOrg.projects.length).toBe(1);
1676
+ expect(updatedOrg.projects.at(0)?.name).toEqual("My project");
1677
+ expect(updatedOrg.id).toEqual(myOrg.id);
1202
1678
  });
1203
1679
 
1204
1680
  test("complex discriminated union", () => {
@@ -30,7 +30,7 @@ test("load a value", async () => {
30
30
  expect(john?.name).toBe("John");
31
31
  });
32
32
 
33
- test("retry an unavailable a value", async () => {
33
+ test("retry an unavailable value", async () => {
34
34
  const Person = co.map({
35
35
  name: z.string(),
36
36
  });
@@ -69,6 +69,7 @@ test("retry an unavailable a value", async () => {
69
69
  );
70
70
 
71
71
  const john = await promise;
72
+ expect(resolved).toBe(true);
72
73
  expect(john).not.toBeNull();
73
74
  expect(john?.name).toBe("John");
74
75
  });