jazz-tools 0.19.14 → 0.19.16

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 (37) hide show
  1. package/.turbo/turbo-build.log +56 -56
  2. package/CHANGELOG.md +25 -0
  3. package/dist/{chunk-GAPMDNJY.js → chunk-R3KIZG4P.js} +47 -27
  4. package/dist/chunk-R3KIZG4P.js.map +1 -0
  5. package/dist/index.js +22 -14
  6. package/dist/index.js.map +1 -1
  7. package/dist/react-native/index.js +18 -0
  8. package/dist/react-native/index.js.map +1 -1
  9. package/dist/react-native-core/ReactNativeSessionProvider.d.ts.map +1 -1
  10. package/dist/react-native-core/index.js +18 -0
  11. package/dist/react-native-core/index.js.map +1 -1
  12. package/dist/testing.js +1 -1
  13. package/dist/tools/auth/clerk/index.d.ts +1 -0
  14. package/dist/tools/auth/clerk/index.d.ts.map +1 -1
  15. package/dist/tools/auth/clerk/tests/isClerkAuthStateEqual.test.d.ts +2 -0
  16. package/dist/tools/auth/clerk/tests/isClerkAuthStateEqual.test.d.ts.map +1 -0
  17. package/dist/tools/auth/clerk/types.d.ts +2 -2
  18. package/dist/tools/auth/clerk/types.d.ts.map +1 -1
  19. package/dist/tools/coValues/CoValueBase.d.ts +13 -0
  20. package/dist/tools/coValues/CoValueBase.d.ts.map +1 -1
  21. package/dist/tools/subscribe/SubscriptionScope.d.ts +33 -35
  22. package/dist/tools/subscribe/SubscriptionScope.d.ts.map +1 -1
  23. package/package.json +4 -4
  24. package/src/react-native-core/ReactNativeSessionProvider.ts +29 -3
  25. package/src/react-native-core/tests/ReactNativeSessionProvider.test.ts +175 -3
  26. package/src/tools/auth/clerk/index.ts +21 -11
  27. package/src/tools/auth/clerk/tests/JazzClerkAuth.test.ts +77 -0
  28. package/src/tools/auth/clerk/tests/isClerkAuthStateEqual.test.ts +124 -0
  29. package/src/tools/auth/clerk/types.ts +23 -6
  30. package/src/tools/coValues/CoValueBase.ts +24 -0
  31. package/src/tools/coValues/coMap.ts +1 -1
  32. package/src/tools/implementation/createContext.ts +2 -2
  33. package/src/tools/subscribe/SubscriptionScope.ts +43 -34
  34. package/src/tools/tests/account.test.ts +19 -0
  35. package/src/tools/tests/coMap.record.test.ts +43 -0
  36. package/src/tools/tests/coMap.test.ts +67 -1
  37. package/dist/chunk-GAPMDNJY.js.map +0 -1
@@ -0,0 +1,124 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { isClerkAuthStateEqual } from "../types";
3
+
4
+ describe("isClerkAuthStateEqual", () => {
5
+ const validCredentials = {
6
+ jazzAccountID: "account-123",
7
+ jazzAccountSecret: "secret-123",
8
+ jazzAccountSeed: [1, 2, 3],
9
+ };
10
+
11
+ const differentCredentials = {
12
+ jazzAccountID: "account-456",
13
+ jazzAccountSecret: "secret-456",
14
+ jazzAccountSeed: [4, 5, 6],
15
+ };
16
+
17
+ describe("both users null/undefined", () => {
18
+ it.each([
19
+ { previous: null, next: null, description: "both null" },
20
+ { previous: undefined, next: undefined, description: "both undefined" },
21
+ { previous: null, next: undefined, description: "null and undefined" },
22
+ { previous: undefined, next: null, description: "undefined and null" },
23
+ ])("returns true when $description", ({ previous, next }) => {
24
+ expect(isClerkAuthStateEqual(previous, next)).toBe(true);
25
+ });
26
+ });
27
+
28
+ describe("one user null, other exists", () => {
29
+ it.each([
30
+ {
31
+ previous: null,
32
+ next: { unsafeMetadata: validCredentials },
33
+ description: "previous null, next exists",
34
+ },
35
+ {
36
+ previous: { unsafeMetadata: validCredentials },
37
+ next: null,
38
+ description: "previous exists, next null",
39
+ },
40
+ {
41
+ previous: undefined,
42
+ next: { unsafeMetadata: validCredentials },
43
+ description: "previous undefined, next exists",
44
+ },
45
+ {
46
+ previous: { unsafeMetadata: validCredentials },
47
+ next: undefined,
48
+ description: "previous exists, next undefined",
49
+ },
50
+ ])("returns false when $description", ({ previous, next }) => {
51
+ expect(isClerkAuthStateEqual(previous, next)).toBe(false);
52
+ });
53
+ });
54
+
55
+ describe("same jazzAccountID", () => {
56
+ it("returns true when both users have the same jazzAccountID", () => {
57
+ const previous = { unsafeMetadata: validCredentials };
58
+ const next = {
59
+ unsafeMetadata: {
60
+ ...validCredentials,
61
+ jazzAccountSecret: "different-secret",
62
+ },
63
+ };
64
+ expect(isClerkAuthStateEqual(previous, next)).toBe(true);
65
+ });
66
+ });
67
+
68
+ describe("different jazzAccountID", () => {
69
+ it("returns false when users have different jazzAccountID", () => {
70
+ const previous = { unsafeMetadata: validCredentials };
71
+ const next = { unsafeMetadata: differentCredentials };
72
+ expect(isClerkAuthStateEqual(previous, next)).toBe(false);
73
+ });
74
+ });
75
+
76
+ describe("neither user has valid credentials", () => {
77
+ it.each([
78
+ {
79
+ previous: { unsafeMetadata: {} },
80
+ next: { unsafeMetadata: {} },
81
+ description: "both have empty metadata",
82
+ },
83
+ {
84
+ previous: { unsafeMetadata: { someOtherField: "value" } },
85
+ next: { unsafeMetadata: { anotherField: "value" } },
86
+ description: "both have non-credential metadata",
87
+ },
88
+ {
89
+ previous: { unsafeMetadata: { jazzAccountID: "123" } },
90
+ next: { unsafeMetadata: { jazzAccountSecret: "456" } },
91
+ description: "both have incomplete credentials",
92
+ },
93
+ ])("returns true when $description", ({ previous, next }) => {
94
+ expect(isClerkAuthStateEqual(previous, next)).toBe(true);
95
+ });
96
+ });
97
+
98
+ describe("one has credentials, other doesn't", () => {
99
+ it.each([
100
+ {
101
+ previous: { unsafeMetadata: validCredentials },
102
+ next: { unsafeMetadata: {} },
103
+ description: "previous has credentials, next empty",
104
+ },
105
+ {
106
+ previous: { unsafeMetadata: {} },
107
+ next: { unsafeMetadata: validCredentials },
108
+ description: "previous empty, next has credentials",
109
+ },
110
+ {
111
+ previous: { unsafeMetadata: validCredentials },
112
+ next: { unsafeMetadata: { jazzAccountID: "123" } },
113
+ description: "previous has credentials, next has incomplete",
114
+ },
115
+ {
116
+ previous: { unsafeMetadata: { jazzAccountSecret: "456" } },
117
+ next: { unsafeMetadata: validCredentials },
118
+ description: "previous has incomplete, next has credentials",
119
+ },
120
+ ])("returns false when $description", ({ previous, next }) => {
121
+ expect(isClerkAuthStateEqual(previous, next)).toBe(false);
122
+ });
123
+ });
124
+ });
@@ -36,21 +36,38 @@ export type ClerkCredentials = {
36
36
  * **Note**: It does not validate the credentials, only checks if the necessary fields are present in the metadata object.
37
37
  */
38
38
  export function isClerkCredentials(
39
- data: NonNullable<MinimalClerkClient["user"]>["unsafeMetadata"] | undefined,
39
+ data:
40
+ | NonNullable<MinimalClerkClient["user"]>["unsafeMetadata"]
41
+ | null
42
+ | undefined,
40
43
  ): data is ClerkCredentials {
41
44
  return !!data && "jazzAccountID" in data && "jazzAccountSecret" in data;
42
45
  }
43
46
 
44
47
  export function isClerkAuthStateEqual(
45
- previousUser: MinimalClerkClient["user"] | null | undefined,
46
- newUser: MinimalClerkClient["user"] | null | undefined,
48
+ previousUser:
49
+ | Pick<NonNullable<MinimalClerkClient["user"]>, "unsafeMetadata">
50
+ | null
51
+ | undefined,
52
+ newUser:
53
+ | Pick<NonNullable<MinimalClerkClient["user"]>, "unsafeMetadata">
54
+ | null
55
+ | undefined,
47
56
  ) {
48
57
  if (Boolean(previousUser) !== Boolean(newUser)) {
49
58
  return false;
50
59
  }
51
60
 
52
- const previousCredentials = isClerkCredentials(previousUser?.unsafeMetadata);
53
- const newCredentials = isClerkCredentials(newUser?.unsafeMetadata);
61
+ const previousCredentials = isClerkCredentials(previousUser?.unsafeMetadata)
62
+ ? previousUser?.unsafeMetadata
63
+ : null;
64
+ const newCredentials = isClerkCredentials(newUser?.unsafeMetadata)
65
+ ? newUser?.unsafeMetadata
66
+ : null;
67
+
68
+ if (!previousCredentials || !newCredentials) {
69
+ return previousCredentials === newCredentials;
70
+ }
54
71
 
55
- return previousCredentials === newCredentials;
72
+ return previousCredentials.jazzAccountID === newCredentials.jazzAccountID;
56
73
  }
@@ -112,6 +112,30 @@ export abstract class CoValueJazzApi<V extends CoValue> {
112
112
  return this.raw.core.earliestTxMadeAt;
113
113
  }
114
114
 
115
+ /**
116
+ * Returns the account ID of the user who created this CoValue.
117
+ *
118
+ * Creation is determined by inspecting the earliest valid transaction,
119
+ * Note: Where the author is a sealer/signer identifiers (e.g. accounts)
120
+ * nothing is returned intentionally
121
+ *
122
+ * @returns {string | undefined} The creating user's account ID, or
123
+ * `undefined` if no author can be determined
124
+ *
125
+ * @category Content
126
+ */
127
+ get createdBy(): string | undefined {
128
+ const createdBy = this.raw.core.getValidSortedTransactions({
129
+ ignorePrivateTransactions: false,
130
+ })[0]?.author;
131
+
132
+ // Only return accounts, not sealer/signer strings
133
+ if (typeof createdBy === "string" && createdBy.startsWith("co_z"))
134
+ return createdBy;
135
+
136
+ return undefined;
137
+ }
138
+
115
139
  /**
116
140
  * The timestamp of the last updated time of the CoValue
117
141
  *
@@ -825,7 +825,7 @@ class CoMapJazzApi<M extends CoMap> extends CoValueJazzApi<M> {
825
825
  };
826
826
  },
827
827
  ownKeys(_target) {
828
- return map.$jazz.raw.keys();
828
+ return Object.keys(map.$jazz.raw.ops);
829
829
  },
830
830
  getOwnPropertyDescriptor(target, key) {
831
831
  return {
@@ -274,8 +274,8 @@ export async function createJazzContext<
274
274
  crypto,
275
275
  AccountSchema: options.AccountSchema,
276
276
  sessionProvider: options.sessionProvider,
277
- onLogOut: () => {
278
- authSecretStorage.clearWithoutNotify();
277
+ onLogOut: async () => {
278
+ await authSecretStorage.clearWithoutNotify();
279
279
  },
280
280
  storage: options.storage,
281
281
  asActiveAccount: true,
@@ -29,7 +29,6 @@ import {
29
29
  } from "./errorReporting.js";
30
30
  import {
31
31
  createCoValue,
32
- isEqualRefsToResolve,
33
32
  myRoleForRawValue,
34
33
  PromiseWithStatus,
35
34
  rejectedPromise,
@@ -49,25 +48,25 @@ export class SubscriptionScope<D extends CoValue> {
49
48
  /**
50
49
  * Autoloaded child ids that are unloaded
51
50
  */
52
- pendingAutoloadedChildren: Set<string> = new Set();
51
+ private pendingAutoloadedChildren: Set<string> = new Set();
53
52
  value: SubscriptionValue<D, any> | SubscriptionValueLoading;
54
- childErrors: Map<string, JazzError> = new Map();
55
- validationErrors: Map<string, JazzError> = new Map();
53
+ private childErrors: Map<string, JazzError> = new Map();
54
+ private validationErrors: Map<string, JazzError> = new Map();
56
55
  errorFromChildren: JazzError | undefined;
57
- subscription: CoValueCoreSubscription;
58
- dirty = false;
59
- resolve: RefsToResolve<any>;
60
- idsSubscribed = new Set<string>();
61
- autoloaded = new Set<string>();
62
- autoloadedKeys = new Set<string>();
63
- skipInvalidKeys = new Set<string>();
64
- totalValidTransactions = 0;
65
- version = 0;
66
- migrated = false;
67
- migrating = false;
56
+ private subscription: CoValueCoreSubscription;
57
+ private dirty = false;
58
+ private resolve: RefsToResolve<any>;
59
+ private idsSubscribed = new Set<string>();
60
+ private autoloaded = new Set<string>();
61
+ private autoloadedKeys = new Set<string>();
62
+ private skipInvalidKeys = new Set<string>();
63
+ private totalValidTransactions = 0;
64
+ private version = 0;
65
+ private migrated = false;
66
+ private migrating = false;
68
67
  closed = false;
69
68
 
70
- silenceUpdates = false;
69
+ private silenceUpdates = false;
71
70
 
72
71
  /**
73
72
  * Stack trace captured at subscription creation time.
@@ -145,7 +144,9 @@ export class SubscriptionScope<D extends CoValue> {
145
144
  this.dirty = true;
146
145
  }
147
146
 
148
- handleUpdate(update: RawCoValue | typeof CoValueLoadingState.UNAVAILABLE) {
147
+ private handleUpdate(
148
+ update: RawCoValue | typeof CoValueLoadingState.UNAVAILABLE,
149
+ ) {
149
150
  if (update === CoValueLoadingState.UNAVAILABLE) {
150
151
  if (this.value.type === CoValueLoadingState.LOADING) {
151
152
  const error = new JazzError(this.id, CoValueLoadingState.UNAVAILABLE, [
@@ -213,7 +214,7 @@ export class SubscriptionScope<D extends CoValue> {
213
214
  this.triggerUpdate();
214
215
  }
215
216
 
216
- computeChildErrors() {
217
+ private computeChildErrors() {
217
218
  let issues: JazzErrorIssue[] = [];
218
219
  let errorType: JazzError["type"] = CoValueLoadingState.UNAVAILABLE;
219
220
 
@@ -259,11 +260,11 @@ export class SubscriptionScope<D extends CoValue> {
259
260
  return undefined;
260
261
  }
261
262
 
262
- handleChildUpdate = (
263
+ handleChildUpdate(
263
264
  id: string,
264
265
  value: SubscriptionValue<any, any> | SubscriptionValueLoading,
265
266
  key?: string,
266
- ) => {
267
+ ) {
267
268
  if (value.type === CoValueLoadingState.LOADING) {
268
269
  return;
269
270
  }
@@ -296,9 +297,9 @@ export class SubscriptionScope<D extends CoValue> {
296
297
  }
297
298
 
298
299
  this.triggerUpdate();
299
- };
300
+ }
300
301
 
301
- shouldSendUpdates() {
302
+ private shouldSendUpdates() {
302
303
  if (this.value.type === CoValueLoadingState.LOADING) return false;
303
304
 
304
305
  // If the value is in error, we send the update regardless of the children statuses
@@ -309,9 +310,9 @@ export class SubscriptionScope<D extends CoValue> {
309
310
 
310
311
  unloadedValue: NotLoaded<D> | undefined;
311
312
 
312
- lastPromise: PromiseWithStatus<D> | undefined;
313
+ private lastPromise: PromiseWithStatus<D> | undefined;
313
314
 
314
- getPromise() {
315
+ private getPromise() {
315
316
  const currentValue = this.getCurrentValue();
316
317
 
317
318
  if (currentValue.$isLoaded) {
@@ -403,7 +404,7 @@ export class SubscriptionScope<D extends CoValue> {
403
404
  return unloadedValue;
404
405
  }
405
406
 
406
- lastErrorLogged: JazzError | undefined;
407
+ private lastErrorLogged: JazzError | undefined;
407
408
 
408
409
  getCurrentValue(): MaybeLoaded<D> {
409
410
  const rawValue = this.getCurrentRawValue();
@@ -420,7 +421,7 @@ export class SubscriptionScope<D extends CoValue> {
420
421
  return rawValue;
421
422
  }
422
423
 
423
- getCurrentRawValue(): D | NotLoadedCoValueState {
424
+ private getCurrentRawValue(): D | NotLoadedCoValueState {
424
425
  if (
425
426
  this.value.type === CoValueLoadingState.UNAUTHORIZED ||
426
427
  this.value.type === CoValueLoadingState.UNAVAILABLE
@@ -443,7 +444,7 @@ export class SubscriptionScope<D extends CoValue> {
443
444
  return CoValueLoadingState.LOADING;
444
445
  }
445
446
 
446
- getCreationStackLines() {
447
+ private getCreationStackLines() {
447
448
  const stack = this.callerStack?.stack;
448
449
 
449
450
  if (!stack) {
@@ -474,7 +475,7 @@ export class SubscriptionScope<D extends CoValue> {
474
475
  return result;
475
476
  }
476
477
 
477
- getError() {
478
+ private getError() {
478
479
  if (
479
480
  this.value.type === CoValueLoadingState.UNAUTHORIZED ||
480
481
  this.value.type === CoValueLoadingState.UNAVAILABLE
@@ -487,7 +488,7 @@ export class SubscriptionScope<D extends CoValue> {
487
488
  }
488
489
  }
489
490
 
490
- logError() {
491
+ private logError() {
491
492
  const error = this.getError();
492
493
 
493
494
  if (!error || this.lastErrorLogged === error) {
@@ -510,7 +511,7 @@ export class SubscriptionScope<D extends CoValue> {
510
511
  }
511
512
  }
512
513
 
513
- triggerUpdate() {
514
+ private triggerUpdate() {
514
515
  if (!this.shouldSendUpdates()) return;
515
516
  if (!this.dirty) return;
516
517
  if (this.subscribers.size === 0) return;
@@ -700,7 +701,7 @@ export class SubscriptionScope<D extends CoValue> {
700
701
  this.silenceUpdates = false;
701
702
  }
702
703
 
703
- loadChildren() {
704
+ private loadChildren() {
704
705
  const { resolve } = this;
705
706
 
706
707
  if (this.value.type !== CoValueLoadingState.LOADED) {
@@ -816,7 +817,11 @@ export class SubscriptionScope<D extends CoValue> {
816
817
  return hasChanged;
817
818
  }
818
819
 
819
- loadCoMapKey(map: CoMap, key: string, depth: Record<string, any> | true) {
820
+ private loadCoMapKey(
821
+ map: CoMap,
822
+ key: string,
823
+ depth: Record<string, any> | true,
824
+ ) {
820
825
  if (key === "$onError") {
821
826
  return undefined;
822
827
  }
@@ -858,7 +863,11 @@ export class SubscriptionScope<D extends CoValue> {
858
863
  return undefined;
859
864
  }
860
865
 
861
- loadCoListKey(list: CoList, key: string, depth: Record<string, any> | true) {
866
+ private loadCoListKey(
867
+ list: CoList,
868
+ key: string,
869
+ depth: Record<string, any> | true,
870
+ ) {
862
871
  const descriptor = list.$jazz.getItemsDescriptor();
863
872
 
864
873
  if (!descriptor || !isRefEncoded(descriptor)) {
@@ -896,7 +905,7 @@ export class SubscriptionScope<D extends CoValue> {
896
905
  return undefined;
897
906
  }
898
907
 
899
- loadChildNode(
908
+ private loadChildNode(
900
909
  id: string,
901
910
  query: RefsToResolve<any>,
902
911
  descriptor: RefEncoded<any>,
@@ -506,4 +506,23 @@ describe("createAs", () => {
506
506
  // Verify execution order
507
507
  expect(executionOrder).toEqual(["migration", "onCreate"]);
508
508
  });
509
+ test("createdBy returns undefined", async () => {
510
+ const CustomAccount = co.account({
511
+ profile: co.profile({
512
+ name: z.string(),
513
+ }),
514
+ root: co.map({}),
515
+ });
516
+
517
+ const worker = await createJazzTestAccount({
518
+ isCurrentActiveAccount: true,
519
+ });
520
+
521
+ const createdAccount = await CustomAccount.createAs(worker, {
522
+ creationProps: { name: "Test Account" },
523
+ });
524
+
525
+ assertLoaded(createdAccount.account);
526
+ expect(createdAccount.account.$jazz.createdBy).toBe(undefined);
527
+ });
509
528
  });
@@ -194,6 +194,49 @@ describe("CoMap.Record", async () => {
194
194
  $jazz: expect.objectContaining({ id: me.$jazz.id }),
195
195
  });
196
196
  });
197
+
198
+ test("getEdits() keys should return deleted keys", () => {
199
+ const me = Account.getMe();
200
+
201
+ const Person = co.record(z.string(), z.string());
202
+ const person = Person.create({ name: "John" });
203
+ person.$jazz.set("name", "Jane");
204
+ person.$jazz.delete("name");
205
+
206
+ expect(Object.keys(person.$jazz.getEdits())).toEqual(["name"]);
207
+
208
+ const edits = person.$jazz.getEdits().name?.all;
209
+
210
+ expect(edits).toEqual([
211
+ expect.objectContaining({
212
+ value: "John",
213
+ key: "name",
214
+ ref: undefined,
215
+ madeAt: expect.any(Date),
216
+ }),
217
+ expect.objectContaining({
218
+ value: "Jane",
219
+ key: "name",
220
+ ref: undefined,
221
+ madeAt: expect.any(Date),
222
+ }),
223
+ expect.objectContaining({
224
+ value: undefined,
225
+ key: "name",
226
+ ref: undefined,
227
+ madeAt: expect.any(Date),
228
+ }),
229
+ ]);
230
+
231
+ expect(edits?.[0]?.by).toMatchObject({
232
+ [TypeSym]: "Account",
233
+ $jazz: expect.objectContaining({ id: me.$jazz.id }),
234
+ });
235
+ expect(edits?.[1]?.by).toMatchObject({
236
+ [TypeSym]: "Account",
237
+ $jazz: expect.objectContaining({ id: me.$jazz.id }),
238
+ });
239
+ });
197
240
  });
198
241
 
199
242
  describe("Record resolution", async () => {
@@ -26,6 +26,7 @@ import {
26
26
  disableJazzTestSync,
27
27
  getPeerConnectedToTestSyncServer,
28
28
  runWithoutActiveAccount,
29
+ setActiveAccount,
29
30
  setupJazzTestSync,
30
31
  } from "../testing.js";
31
32
  import { assertLoaded, setupTwoNodes, waitFor } from "./utils.js";
@@ -2316,28 +2317,93 @@ describe("CoMap migration", () => {
2316
2317
  });
2317
2318
  });
2318
2319
 
2319
- describe("createdAt & lastUpdatedAt", () => {
2320
+ describe("createdAt, lastUpdatedAt, createdBy", () => {
2320
2321
  test("empty map created time", () => {
2321
2322
  const emptyMap = co.map({}).create({});
2322
2323
 
2323
2324
  expect(emptyMap.$jazz.lastUpdatedAt).toEqual(emptyMap.$jazz.createdAt);
2324
2325
  });
2325
2326
 
2327
+ test("empty map created by", () => {
2328
+ const emptyMap = co.map({}).create({});
2329
+ const me = Account.getMe();
2330
+ expect(emptyMap.$jazz.createdBy).toEqual(me.$jazz.id);
2331
+ });
2332
+
2326
2333
  test("created time and last updated time", async () => {
2327
2334
  const Person = co.map({
2328
2335
  name: z.string(),
2329
2336
  });
2337
+ const me = Account.getMe();
2330
2338
 
2331
2339
  const person = Person.create({ name: "John" });
2332
2340
 
2333
2341
  const createdAt = person.$jazz.createdAt;
2334
2342
  expect(person.$jazz.lastUpdatedAt).toEqual(createdAt);
2335
2343
 
2344
+ const createdBy = person.$jazz.createdBy;
2345
+ expect(createdBy).toEqual(me.$jazz.id);
2346
+
2336
2347
  await new Promise((r) => setTimeout(r, 10));
2337
2348
  person.$jazz.set("name", "Jane");
2338
2349
 
2339
2350
  expect(person.$jazz.createdAt).toEqual(createdAt);
2340
2351
  expect(person.$jazz.lastUpdatedAt).not.toEqual(createdAt);
2352
+
2353
+ // Double check after update.
2354
+ expect(createdBy).toEqual(me.$jazz.id);
2355
+ });
2356
+
2357
+ test("createdBy does not change when updated", async () => {
2358
+ const Person = co.map({
2359
+ name: z.string(),
2360
+ });
2361
+ const me = Account.getMe();
2362
+
2363
+ const person = Person.create({ name: "John" });
2364
+
2365
+ const createdBy = person.$jazz.createdBy;
2366
+ expect(createdBy).toEqual(me.$jazz.id);
2367
+
2368
+ await new Promise((r) => setTimeout(r, 10));
2369
+ person.$jazz.set("name", "Jane");
2370
+
2371
+ // Double check after update.
2372
+ expect(createdBy).toEqual(me.$jazz.id);
2373
+ });
2374
+
2375
+ test("createdBy is after key rotation", async () => {
2376
+ const Person = co.map({
2377
+ name: z.string(),
2378
+ });
2379
+ const me = Account.getMe();
2380
+
2381
+ // Create person
2382
+ const person = Person.create({ name: "John" });
2383
+
2384
+ // True created by
2385
+ const createdBy = person.$jazz.createdBy;
2386
+
2387
+ // Create a user, grant access, then kick to trigger key rotation.
2388
+ const newUser = await createJazzTestAccount();
2389
+
2390
+ person.$jazz.owner.addMember(newUser, "reader");
2391
+
2392
+ // This should trigger read key rotation
2393
+ person.$jazz.owner.removeMember(newUser);
2394
+
2395
+ // Now create a new user and grant access
2396
+ const newUser2 = await createJazzTestAccount();
2397
+ person.$jazz.owner.addMember(newUser2, "reader");
2398
+
2399
+ // Load the CoValue as the new user:
2400
+ setActiveAccount(newUser2);
2401
+
2402
+ const personLoadedAsUser2 = await Person.load(person.$jazz.id);
2403
+ assertLoaded(personLoadedAsUser2);
2404
+ const createdByPerUser2 = personLoadedAsUser2.$jazz.createdBy;
2405
+ // Double check after update.
2406
+ expect(createdBy).toEqual(createdByPerUser2);
2341
2407
  });
2342
2408
  });
2343
2409