@palantir/pack.state.core 0.0.1-beta.1
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.
- package/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-transpileBrowser.log +5 -0
- package/.turbo/turbo-transpileCjs.log +5 -0
- package/.turbo/turbo-transpileEsm.log +5 -0
- package/.turbo/turbo-transpileTypes.log +5 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/LICENSE.txt +13 -0
- package/README.md +55 -0
- package/build/browser/index.js +1257 -0
- package/build/browser/index.js.map +1 -0
- package/build/cjs/index.cjs +1298 -0
- package/build/cjs/index.cjs.map +1 -0
- package/build/cjs/index.d.cts +272 -0
- package/build/esm/index.js +1257 -0
- package/build/esm/index.js.map +1 -0
- package/build/types/DocumentServiceModule.d.ts +6 -0
- package/build/types/DocumentServiceModule.d.ts.map +1 -0
- package/build/types/__tests__/DocumentStatusTracking.test.d.ts +1 -0
- package/build/types/__tests__/DocumentStatusTracking.test.d.ts.map +1 -0
- package/build/types/__tests__/RefStability.test.d.ts +1 -0
- package/build/types/__tests__/RefStability.test.d.ts.map +1 -0
- package/build/types/__tests__/StateModule.integration.test.d.ts +1 -0
- package/build/types/__tests__/StateModule.integration.test.d.ts.map +1 -0
- package/build/types/__tests__/testUtils.d.ts +7 -0
- package/build/types/__tests__/testUtils.d.ts.map +1 -0
- package/build/types/index.d.ts +11 -0
- package/build/types/index.d.ts.map +1 -0
- package/build/types/service/BaseYjsDocumentService.d.ts +155 -0
- package/build/types/service/BaseYjsDocumentService.d.ts.map +1 -0
- package/build/types/service/InMemoryDocumentService.d.ts +12 -0
- package/build/types/service/InMemoryDocumentService.d.ts.map +1 -0
- package/build/types/service/YjsSchemaMapper.d.ts +9 -0
- package/build/types/service/YjsSchemaMapper.d.ts.map +1 -0
- package/build/types/types/DocumentRefImpl.d.ts +5 -0
- package/build/types/types/DocumentRefImpl.d.ts.map +1 -0
- package/build/types/types/DocumentService.d.ts +62 -0
- package/build/types/types/DocumentService.d.ts.map +1 -0
- package/build/types/types/DocumentServiceConfig.d.ts +5 -0
- package/build/types/types/DocumentServiceConfig.d.ts.map +1 -0
- package/build/types/types/RecordCollectionRefImpl.d.ts +5 -0
- package/build/types/types/RecordCollectionRefImpl.d.ts.map +1 -0
- package/build/types/types/RecordRefImpl.d.ts +5 -0
- package/build/types/types/RecordRefImpl.d.ts.map +1 -0
- package/build/types/types/StateModule.d.ts +59 -0
- package/build/types/types/StateModule.d.ts.map +1 -0
- package/package.json +71 -0
- package/src/DocumentServiceModule.ts +53 -0
- package/src/__tests__/DocumentStatusTracking.test.ts +229 -0
- package/src/__tests__/RefStability.test.ts +441 -0
- package/src/__tests__/StateModule.integration.test.ts +1187 -0
- package/src/__tests__/testUtils.ts +106 -0
- package/src/index.ts +38 -0
- package/src/service/BaseYjsDocumentService.ts +1277 -0
- package/src/service/InMemoryDocumentService.ts +162 -0
- package/src/service/YjsSchemaMapper.ts +194 -0
- package/src/types/DocumentRefImpl.ts +98 -0
- package/src/types/DocumentService.ts +210 -0
- package/src/types/DocumentServiceConfig.ts +22 -0
- package/src/types/RecordCollectionRefImpl.ts +124 -0
- package/src/types/RecordRefImpl.ts +106 -0
- package/src/types/StateModule.ts +329 -0
- package/tsconfig.json +21 -0
- package/vitest.config.mjs +26 -0
|
@@ -0,0 +1,1187 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Palantir Technologies, Inc. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { PackAppInternal } from "@palantir/pack.core";
|
|
18
|
+
import type {
|
|
19
|
+
DocumentId,
|
|
20
|
+
DocumentMetadata,
|
|
21
|
+
DocumentSchema,
|
|
22
|
+
Model,
|
|
23
|
+
RecordId,
|
|
24
|
+
} from "@palantir/pack.document-schema.model-types";
|
|
25
|
+
import { Metadata } from "@palantir/pack.document-schema.model-types";
|
|
26
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
27
|
+
import * as Y from "yjs";
|
|
28
|
+
import { z } from "zod";
|
|
29
|
+
import { DOCUMENT_SERVICE_MODULE_KEY } from "../DocumentServiceModule.js";
|
|
30
|
+
import type { BaseYjsDocumentService } from "../service/BaseYjsDocumentService.js";
|
|
31
|
+
import { createDocRef } from "../types/DocumentRefImpl.js";
|
|
32
|
+
import { getStateModule } from "../types/StateModule.js";
|
|
33
|
+
import { createTestApp, createTestAppNoAutocreate } from "./testUtils.js";
|
|
34
|
+
|
|
35
|
+
const TEST_SECURITY = {
|
|
36
|
+
mandatory: {
|
|
37
|
+
classification: ["MU"],
|
|
38
|
+
markings: [],
|
|
39
|
+
},
|
|
40
|
+
discretionary: {
|
|
41
|
+
owners: [],
|
|
42
|
+
editors: [],
|
|
43
|
+
viewers: [],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const createTestSchema = (): DocumentSchema => ({
|
|
48
|
+
[Metadata]: {
|
|
49
|
+
version: 1,
|
|
50
|
+
},
|
|
51
|
+
} as const satisfies DocumentSchema);
|
|
52
|
+
|
|
53
|
+
// Test data types
|
|
54
|
+
interface User {
|
|
55
|
+
id: string;
|
|
56
|
+
name: string;
|
|
57
|
+
email: string;
|
|
58
|
+
age: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface Address {
|
|
62
|
+
street: string;
|
|
63
|
+
city: string;
|
|
64
|
+
zipCode: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const createSchemaWithRecords = () => {
|
|
68
|
+
const userSchema = z.object({
|
|
69
|
+
id: z.string(),
|
|
70
|
+
name: z.string(),
|
|
71
|
+
email: z.email(),
|
|
72
|
+
age: z.number().int().positive(),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const addressSchema = z.object({
|
|
76
|
+
street: z.string(),
|
|
77
|
+
city: z.string(),
|
|
78
|
+
zipCode: z.string(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const UserModel: Model<User, typeof userSchema> = {
|
|
82
|
+
__type: {} as User,
|
|
83
|
+
zodSchema: userSchema,
|
|
84
|
+
[Metadata]: {
|
|
85
|
+
name: "User",
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const AddressModel: Model<Address, typeof addressSchema> = {
|
|
90
|
+
__type: {} as Address,
|
|
91
|
+
zodSchema: addressSchema,
|
|
92
|
+
[Metadata]: {
|
|
93
|
+
name: "Address",
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
User: UserModel,
|
|
99
|
+
Address: AddressModel,
|
|
100
|
+
[Metadata]: {
|
|
101
|
+
version: 1,
|
|
102
|
+
},
|
|
103
|
+
} as const satisfies DocumentSchema;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
describe("State Module Integration", () => {
|
|
107
|
+
let app: PackAppInternal;
|
|
108
|
+
beforeEach(() => {
|
|
109
|
+
app = createTestAppNoAutocreate();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should initialize app with state module and create/load document", async () => {
|
|
113
|
+
const stateModule = getStateModule(app);
|
|
114
|
+
expect(stateModule).toBeDefined();
|
|
115
|
+
|
|
116
|
+
const metadata: DocumentMetadata = {
|
|
117
|
+
name: "Test Document",
|
|
118
|
+
documentTypeName: "TestType",
|
|
119
|
+
ontologyRid: "test-ontology-rid",
|
|
120
|
+
security: TEST_SECURITY,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const schema = createTestSchema();
|
|
124
|
+
const docRef = await stateModule.createDocument(metadata, schema);
|
|
125
|
+
|
|
126
|
+
expect(docRef).toBeDefined();
|
|
127
|
+
expect(docRef.id).toBeDefined();
|
|
128
|
+
expect(typeof docRef.id).toBe("string");
|
|
129
|
+
|
|
130
|
+
let metadataCallbackCalled = false;
|
|
131
|
+
let stateCallbackCalled = false;
|
|
132
|
+
|
|
133
|
+
const unsubscribeMetadata = stateModule.onMetadataChange(
|
|
134
|
+
docRef,
|
|
135
|
+
(doc, receivedMetadata) => {
|
|
136
|
+
expect(doc.id).toBe(docRef.id);
|
|
137
|
+
expect(receivedMetadata.name).toBe("Test Document");
|
|
138
|
+
metadataCallbackCalled = true;
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const unsubscribeState = stateModule.onStateChange(docRef, doc => {
|
|
143
|
+
expect(doc.id).toBe(docRef.id);
|
|
144
|
+
stateCallbackCalled = true;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
148
|
+
|
|
149
|
+
expect(metadataCallbackCalled).toBe(true);
|
|
150
|
+
expect(stateCallbackCalled).toBe(true);
|
|
151
|
+
|
|
152
|
+
unsubscribeMetadata();
|
|
153
|
+
unsubscribeState();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("should load via docRef", async () => {
|
|
157
|
+
const stateModule = getStateModule(app);
|
|
158
|
+
const metadata: DocumentMetadata = {
|
|
159
|
+
name: "Persistent Document",
|
|
160
|
+
documentTypeName: "TestType",
|
|
161
|
+
ontologyRid: "test-ontology-rid",
|
|
162
|
+
security: TEST_SECURITY,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Create document and capture the ID, then discard the ref
|
|
166
|
+
const schema = createTestSchema();
|
|
167
|
+
const originalDocRef = await stateModule.createDocument(metadata, schema);
|
|
168
|
+
const documentId = originalDocRef.id;
|
|
169
|
+
|
|
170
|
+
// Create new docRef using the same ID and schema
|
|
171
|
+
const newDocRef = createDocRef(app, documentId, schema);
|
|
172
|
+
|
|
173
|
+
// Use the new docRef to load metadata via subscription
|
|
174
|
+
let loadedMetadata: DocumentMetadata | null = null;
|
|
175
|
+
let metadataLoadComplete = false;
|
|
176
|
+
|
|
177
|
+
const unsubscribeMetadata = newDocRef.onMetadataChange(
|
|
178
|
+
(doc, receivedMetadata) => {
|
|
179
|
+
expect(doc.id).toBe(documentId);
|
|
180
|
+
expect(receivedMetadata.name).toBe("Persistent Document");
|
|
181
|
+
loadedMetadata = receivedMetadata;
|
|
182
|
+
metadataLoadComplete = true;
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Use the new docRef to subscribe to state changes
|
|
187
|
+
let stateLoadComplete = false;
|
|
188
|
+
const unsubscribeState = newDocRef.onStateChange(doc => {
|
|
189
|
+
expect(doc.id).toBe(documentId);
|
|
190
|
+
stateLoadComplete = true;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
194
|
+
|
|
195
|
+
expect(metadataLoadComplete).toBe(true);
|
|
196
|
+
expect(stateLoadComplete).toBe(true);
|
|
197
|
+
expect(loadedMetadata).toEqual(metadata);
|
|
198
|
+
|
|
199
|
+
unsubscribeMetadata();
|
|
200
|
+
unsubscribeState();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("should set and get record values", async () => {
|
|
204
|
+
const stateModule = getStateModule(app);
|
|
205
|
+
const schema = createSchemaWithRecords();
|
|
206
|
+
const metadata: DocumentMetadata = {
|
|
207
|
+
name: "Document with Records",
|
|
208
|
+
documentTypeName: "TestType",
|
|
209
|
+
ontologyRid: "test-ontology-rid",
|
|
210
|
+
security: TEST_SECURITY,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const docRef = await stateModule.createDocument(metadata, schema);
|
|
214
|
+
|
|
215
|
+
const userSchema = schema.User;
|
|
216
|
+
const recordsCollection = docRef.getRecords(userSchema);
|
|
217
|
+
|
|
218
|
+
const userId = "user_1" as RecordId;
|
|
219
|
+
const userData = {
|
|
220
|
+
id: "user_1",
|
|
221
|
+
name: "John Doe",
|
|
222
|
+
email: "john@example.com",
|
|
223
|
+
age: 30,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Set a record
|
|
227
|
+
await recordsCollection.set(userId, userData);
|
|
228
|
+
|
|
229
|
+
// Get the record back
|
|
230
|
+
const recordRef = recordsCollection.get(userId);
|
|
231
|
+
expect(recordRef).toBeDefined();
|
|
232
|
+
|
|
233
|
+
if (recordRef) {
|
|
234
|
+
const snapshot = await recordRef.getSnapshot();
|
|
235
|
+
expect(snapshot).toEqual(userData);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("should trigger onStateChange when record is modified", async () => {
|
|
240
|
+
const stateModule = getStateModule(app);
|
|
241
|
+
const schema = createSchemaWithRecords();
|
|
242
|
+
const metadata: DocumentMetadata = {
|
|
243
|
+
name: "Document with State Changes",
|
|
244
|
+
documentTypeName: "TestType",
|
|
245
|
+
ontologyRid: "test-ontology-rid",
|
|
246
|
+
security: TEST_SECURITY,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const docRef = await stateModule.createDocument(metadata, schema);
|
|
250
|
+
|
|
251
|
+
let stateChangeCount = 0;
|
|
252
|
+
const unsubscribe = stateModule.onStateChange(docRef, () => {
|
|
253
|
+
stateChangeCount++;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Initial callback should fire immediately
|
|
257
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
258
|
+
expect(stateChangeCount).toBe(1);
|
|
259
|
+
|
|
260
|
+
const userSchema = schema.User;
|
|
261
|
+
const recordsCollection = docRef.getRecords(userSchema);
|
|
262
|
+
|
|
263
|
+
const userId = "user_1" as RecordId;
|
|
264
|
+
const userData = {
|
|
265
|
+
id: "user_1",
|
|
266
|
+
name: "Jane Doe",
|
|
267
|
+
email: "jane@example.com",
|
|
268
|
+
age: 28,
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// Setting a record should trigger state change
|
|
272
|
+
await recordsCollection.set(userId, userData);
|
|
273
|
+
|
|
274
|
+
// Verify the data was actually stored
|
|
275
|
+
const recordRef = recordsCollection.get(userId);
|
|
276
|
+
expect(recordRef).toBeDefined();
|
|
277
|
+
const snapshot = await recordRef!.getSnapshot();
|
|
278
|
+
expect(snapshot).toEqual(userData);
|
|
279
|
+
|
|
280
|
+
// Wait for async callback
|
|
281
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
282
|
+
// Y.Doc may fire multiple events during record creation
|
|
283
|
+
expect(stateChangeCount).toBeGreaterThanOrEqual(2);
|
|
284
|
+
|
|
285
|
+
const countBeforeUpdate = stateChangeCount;
|
|
286
|
+
|
|
287
|
+
// Update the record
|
|
288
|
+
const updatedData = { ...userData, age: 29 };
|
|
289
|
+
await recordsCollection.set(userId, updatedData);
|
|
290
|
+
|
|
291
|
+
// Verify the update was stored
|
|
292
|
+
const updatedSnapshot = await recordRef!.getSnapshot();
|
|
293
|
+
expect(updatedSnapshot).toEqual(updatedData);
|
|
294
|
+
expect(updatedSnapshot.age).toBe(29);
|
|
295
|
+
|
|
296
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
297
|
+
expect(stateChangeCount).toBeGreaterThan(countBeforeUpdate);
|
|
298
|
+
|
|
299
|
+
unsubscribe();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("should handle multiple records and collections", async () => {
|
|
303
|
+
const stateModule = getStateModule(app);
|
|
304
|
+
const schema = createSchemaWithRecords();
|
|
305
|
+
const metadata: DocumentMetadata = {
|
|
306
|
+
name: "Multi-Record Document",
|
|
307
|
+
documentTypeName: "TestType",
|
|
308
|
+
ontologyRid: "test-ontology-rid",
|
|
309
|
+
security: TEST_SECURITY,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const docRef = await stateModule.createDocument(metadata, schema);
|
|
313
|
+
|
|
314
|
+
const userSchema = schema.User;
|
|
315
|
+
const addressSchema = schema.Address;
|
|
316
|
+
|
|
317
|
+
const usersCollection = docRef.getRecords(userSchema);
|
|
318
|
+
const addressesCollection = docRef.getRecords(addressSchema);
|
|
319
|
+
|
|
320
|
+
// Add multiple users
|
|
321
|
+
const user1Id = "user_1" as RecordId;
|
|
322
|
+
const user2Id = "user_2" as RecordId;
|
|
323
|
+
|
|
324
|
+
const user1Data = {
|
|
325
|
+
id: "user_1",
|
|
326
|
+
name: "Alice",
|
|
327
|
+
email: "alice@example.com",
|
|
328
|
+
age: 25,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const user2Data = {
|
|
332
|
+
id: "user_2",
|
|
333
|
+
name: "Bob",
|
|
334
|
+
email: "bob@example.com",
|
|
335
|
+
age: 32,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
await usersCollection.set(user1Id, user1Data);
|
|
339
|
+
await usersCollection.set(user2Id, user2Data);
|
|
340
|
+
|
|
341
|
+
// Add an address
|
|
342
|
+
const addressId = "addr_1" as RecordId;
|
|
343
|
+
const addressData = {
|
|
344
|
+
street: "123 Main St",
|
|
345
|
+
city: "New York",
|
|
346
|
+
zipCode: "10001",
|
|
347
|
+
};
|
|
348
|
+
await addressesCollection.set(addressId, addressData);
|
|
349
|
+
|
|
350
|
+
// Verify collections maintain separate data
|
|
351
|
+
expect(usersCollection.size).toBe(2);
|
|
352
|
+
expect(addressesCollection.size).toBe(1);
|
|
353
|
+
|
|
354
|
+
// Verify we can retrieve each user correctly
|
|
355
|
+
const user1Ref = usersCollection.get(user1Id);
|
|
356
|
+
const user2Ref = usersCollection.get(user2Id);
|
|
357
|
+
expect(user1Ref).toBeDefined();
|
|
358
|
+
expect(user2Ref).toBeDefined();
|
|
359
|
+
|
|
360
|
+
const user1Snapshot = await user1Ref!.getSnapshot();
|
|
361
|
+
const user2Snapshot = await user2Ref!.getSnapshot();
|
|
362
|
+
expect(user1Snapshot).toEqual(user1Data);
|
|
363
|
+
expect(user2Snapshot).toEqual(user2Data);
|
|
364
|
+
|
|
365
|
+
// Verify address data
|
|
366
|
+
const addressRef = addressesCollection.get(addressId);
|
|
367
|
+
expect(addressRef).toBeDefined();
|
|
368
|
+
const addressSnapshot = await addressRef!.getSnapshot();
|
|
369
|
+
expect(addressSnapshot).toEqual(addressData);
|
|
370
|
+
|
|
371
|
+
// Verify we can iterate over records
|
|
372
|
+
const userIds: string[] = [];
|
|
373
|
+
for (const recordRef of usersCollection) {
|
|
374
|
+
userIds.push(recordRef.id as string);
|
|
375
|
+
// Also verify each iterated record can be read
|
|
376
|
+
const snapshot = await recordRef.getSnapshot();
|
|
377
|
+
expect(snapshot).toBeDefined();
|
|
378
|
+
expect(snapshot.id).toBeDefined();
|
|
379
|
+
}
|
|
380
|
+
expect(userIds).toContain("user_1");
|
|
381
|
+
expect(userIds).toContain("user_2");
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
describe("Lazy Document Creation", () => {
|
|
386
|
+
let app: PackAppInternal;
|
|
387
|
+
|
|
388
|
+
beforeEach(() => {
|
|
389
|
+
app = createTestAppNoAutocreate();
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("should create document lazily when metadata subscription is made", async () => {
|
|
393
|
+
const schema = createTestSchema();
|
|
394
|
+
const documentId = "lazy-metadata-doc" as DocumentId;
|
|
395
|
+
const testDocRef = createDocRef(app, documentId, schema);
|
|
396
|
+
|
|
397
|
+
let callbackCalled = false;
|
|
398
|
+
|
|
399
|
+
// Register metadata listener - this should create the document lazily
|
|
400
|
+
const unsubscribe = testDocRef.onMetadataChange((doc, metadata) => {
|
|
401
|
+
callbackCalled = true;
|
|
402
|
+
expect(doc.id).toBe(documentId);
|
|
403
|
+
// Initially no metadata since document was created lazily
|
|
404
|
+
expect(metadata).toBeUndefined();
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// Wait for async operations
|
|
408
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
409
|
+
|
|
410
|
+
// Callback should not be called since metadata is undefined
|
|
411
|
+
expect(callbackCalled).toBe(false);
|
|
412
|
+
|
|
413
|
+
// Verify the document was created internally (getDocSnapshot should work)
|
|
414
|
+
const snapshot = await testDocRef.getDocSnapshot();
|
|
415
|
+
expect(snapshot).toBeDefined();
|
|
416
|
+
|
|
417
|
+
unsubscribe();
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("should create document lazily when state subscription is made", async () => {
|
|
421
|
+
const schema = createTestSchema();
|
|
422
|
+
const documentId = "lazy-state-doc" as DocumentId;
|
|
423
|
+
const testDocRef = createDocRef(app, documentId, schema);
|
|
424
|
+
|
|
425
|
+
let callbackCalled = false;
|
|
426
|
+
|
|
427
|
+
// Register state listener - this should create the document lazily
|
|
428
|
+
const unsubscribe = testDocRef.onStateChange(doc => {
|
|
429
|
+
callbackCalled = true;
|
|
430
|
+
expect(doc.id).toBe(documentId);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// Wait for async operations
|
|
434
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
435
|
+
|
|
436
|
+
// Callback should be called immediately
|
|
437
|
+
expect(callbackCalled).toBe(true);
|
|
438
|
+
|
|
439
|
+
// Verify the document was created internally
|
|
440
|
+
const snapshot = await testDocRef.getDocSnapshot();
|
|
441
|
+
expect(snapshot).toBeDefined();
|
|
442
|
+
|
|
443
|
+
unsubscribe();
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("should create document lazily when collection subscription is made", async () => {
|
|
447
|
+
const schema = createSchemaWithRecords();
|
|
448
|
+
const documentId = "lazy-collection-doc" as DocumentId;
|
|
449
|
+
const testDocRef = createDocRef(app, documentId, schema);
|
|
450
|
+
|
|
451
|
+
const usersCollection = testDocRef.getRecords(schema.User);
|
|
452
|
+
|
|
453
|
+
let addedCallbackCalled = false;
|
|
454
|
+
let changedCallbackCalled = false;
|
|
455
|
+
let deletedCallbackCalled = false;
|
|
456
|
+
|
|
457
|
+
// Register collection listeners - these should create the document lazily
|
|
458
|
+
const unsubscribeAdded = usersCollection.onItemsAdded(() => {
|
|
459
|
+
addedCallbackCalled = true;
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const unsubscribeChanged = usersCollection.onItemsChanged(() => {
|
|
463
|
+
changedCallbackCalled = true;
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const unsubscribeDeleted = usersCollection.onItemsDeleted(() => {
|
|
467
|
+
deletedCallbackCalled = true;
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// Verify the document was created internally
|
|
471
|
+
const snapshot = await testDocRef.getDocSnapshot();
|
|
472
|
+
expect(snapshot).toBeDefined();
|
|
473
|
+
|
|
474
|
+
// Add a record to test the subscriptions work
|
|
475
|
+
const userId = "user_1" as RecordId;
|
|
476
|
+
const userData = {
|
|
477
|
+
id: "user_1",
|
|
478
|
+
name: "John Doe",
|
|
479
|
+
email: "john@example.com",
|
|
480
|
+
age: 30,
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
await usersCollection.set(userId, userData);
|
|
484
|
+
|
|
485
|
+
// Wait for async operations
|
|
486
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
487
|
+
|
|
488
|
+
expect(addedCallbackCalled).toBe(true);
|
|
489
|
+
|
|
490
|
+
// Update the record
|
|
491
|
+
await usersCollection.set(userId, { ...userData, age: 31 });
|
|
492
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
493
|
+
|
|
494
|
+
expect(changedCallbackCalled).toBe(true);
|
|
495
|
+
|
|
496
|
+
// Delete the record
|
|
497
|
+
await usersCollection.delete(userId);
|
|
498
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
499
|
+
|
|
500
|
+
expect(deletedCallbackCalled).toBe(true);
|
|
501
|
+
|
|
502
|
+
unsubscribeAdded();
|
|
503
|
+
unsubscribeChanged();
|
|
504
|
+
unsubscribeDeleted();
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("should create document lazily when record subscription is made", async () => {
|
|
508
|
+
const schema = createSchemaWithRecords();
|
|
509
|
+
const documentId = "lazy-record-doc" as DocumentId;
|
|
510
|
+
const testDocRef = createDocRef(app, documentId, schema);
|
|
511
|
+
|
|
512
|
+
const usersCollection = testDocRef.getRecords(schema.User);
|
|
513
|
+
const userId = "user_1" as RecordId;
|
|
514
|
+
const userRecord = usersCollection.get(userId);
|
|
515
|
+
|
|
516
|
+
// This should return undefined since record doesn't exist yet
|
|
517
|
+
expect(userRecord).toBeUndefined();
|
|
518
|
+
|
|
519
|
+
// Create a record ref directly using the recordRef function
|
|
520
|
+
const stateModule = getStateModule(app);
|
|
521
|
+
const directRecordRef = stateModule.createRecordRef(testDocRef, userId, schema.User);
|
|
522
|
+
|
|
523
|
+
let changedCallbackCalled = false;
|
|
524
|
+
let deletedCallbackCalled = false;
|
|
525
|
+
|
|
526
|
+
// Register record listeners - these should create the document lazily
|
|
527
|
+
const unsubscribeChanged = stateModule.onRecordChanged(directRecordRef, record => {
|
|
528
|
+
changedCallbackCalled = true;
|
|
529
|
+
expect(record.id).toBe(userId);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const unsubscribeDeleted = stateModule.onRecordDeleted(directRecordRef, record => {
|
|
533
|
+
deletedCallbackCalled = true;
|
|
534
|
+
expect(record.id).toBe(userId);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// Verify the document was created internally
|
|
538
|
+
const snapshot = await testDocRef.getDocSnapshot();
|
|
539
|
+
expect(snapshot).toBeDefined();
|
|
540
|
+
|
|
541
|
+
// Add a record to test the subscriptions work
|
|
542
|
+
const userData = {
|
|
543
|
+
id: "user_1",
|
|
544
|
+
name: "Jane Doe",
|
|
545
|
+
email: "jane@example.com",
|
|
546
|
+
age: 25,
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
await stateModule.setRecord(directRecordRef, userData);
|
|
550
|
+
|
|
551
|
+
// Wait for async operations
|
|
552
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
553
|
+
|
|
554
|
+
expect(changedCallbackCalled).toBe(true);
|
|
555
|
+
|
|
556
|
+
// Delete the record
|
|
557
|
+
await usersCollection.delete(userId);
|
|
558
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
559
|
+
|
|
560
|
+
expect(deletedCallbackCalled).toBe(true);
|
|
561
|
+
|
|
562
|
+
unsubscribeChanged();
|
|
563
|
+
unsubscribeDeleted();
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it("should handle mixed subscription scenarios with lazy creation", async () => {
|
|
567
|
+
const schema = createSchemaWithRecords();
|
|
568
|
+
const documentId = "mixed-lazy-doc" as DocumentId;
|
|
569
|
+
const testDocRef = createDocRef(app, documentId, schema);
|
|
570
|
+
|
|
571
|
+
const usersCollection = testDocRef.getRecords(schema.User);
|
|
572
|
+
const userId = "user_1" as RecordId;
|
|
573
|
+
|
|
574
|
+
let stateChangeCount = 0;
|
|
575
|
+
let collectionAddedCount = 0;
|
|
576
|
+
let recordChangedCount = 0;
|
|
577
|
+
|
|
578
|
+
// Subscribe to state changes first
|
|
579
|
+
const unsubscribeState = testDocRef.onStateChange(() => {
|
|
580
|
+
stateChangeCount++;
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// Subscribe to collection changes
|
|
584
|
+
const unsubscribeCollection = usersCollection.onItemsAdded(() => {
|
|
585
|
+
collectionAddedCount++;
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// Create record ref and subscribe to it using StateModule API
|
|
589
|
+
const stateModule = getStateModule(app);
|
|
590
|
+
const directRecordRef = stateModule.createRecordRef(testDocRef, userId, schema.User);
|
|
591
|
+
const unsubscribeRecord = stateModule.onRecordChanged(directRecordRef, () => {
|
|
592
|
+
recordChangedCount++;
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// Wait for initial callbacks
|
|
596
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
597
|
+
|
|
598
|
+
// State change should be called immediately
|
|
599
|
+
expect(stateChangeCount).toBe(1);
|
|
600
|
+
|
|
601
|
+
// Verify the document was created internally
|
|
602
|
+
const snapshot = await testDocRef.getDocSnapshot();
|
|
603
|
+
expect(snapshot).toBeDefined();
|
|
604
|
+
|
|
605
|
+
// Add a record - this should trigger multiple subscriptions
|
|
606
|
+
const userData = {
|
|
607
|
+
id: "user_1",
|
|
608
|
+
name: "Mixed Test User",
|
|
609
|
+
email: "mixed@example.com",
|
|
610
|
+
age: 40,
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
await usersCollection.set(userId, userData);
|
|
614
|
+
|
|
615
|
+
// Wait for async operations
|
|
616
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
617
|
+
|
|
618
|
+
// All subscriptions should have been notified
|
|
619
|
+
expect(stateChangeCount).toBeGreaterThan(1); // State change from record addition
|
|
620
|
+
expect(collectionAddedCount).toBeGreaterThanOrEqual(1); // Collection item added (may be multiple events)
|
|
621
|
+
expect(recordChangedCount).toBe(1); // Record changed
|
|
622
|
+
|
|
623
|
+
unsubscribeState();
|
|
624
|
+
unsubscribeCollection();
|
|
625
|
+
unsubscribeRecord();
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it("should preserve existing behavior when document already exists", async () => {
|
|
629
|
+
const schema = createTestSchema();
|
|
630
|
+
const metadata: DocumentMetadata = {
|
|
631
|
+
name: "Pre-existing Document",
|
|
632
|
+
documentTypeName: "TestType",
|
|
633
|
+
ontologyRid: "test-ontology-rid",
|
|
634
|
+
security: TEST_SECURITY,
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
// Create document normally first
|
|
638
|
+
const stateModule = getStateModule(app);
|
|
639
|
+
const docRef = await stateModule.createDocument(metadata, schema);
|
|
640
|
+
|
|
641
|
+
let metadataCallbackCount = 0;
|
|
642
|
+
let receivedMetadata: DocumentMetadata | null = null;
|
|
643
|
+
|
|
644
|
+
// Subscribe to existing document - should get immediate callback with metadata
|
|
645
|
+
const unsubscribe = docRef.onMetadataChange((doc, meta) => {
|
|
646
|
+
metadataCallbackCount++;
|
|
647
|
+
receivedMetadata = meta;
|
|
648
|
+
expect(doc.id).toBe(docRef.id);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
// Wait for async operations
|
|
652
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
653
|
+
|
|
654
|
+
// Should get immediate callback with existing metadata
|
|
655
|
+
expect(metadataCallbackCount).toBe(1);
|
|
656
|
+
expect(receivedMetadata).toEqual(metadata);
|
|
657
|
+
|
|
658
|
+
unsubscribe();
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
it("should handle updateRecord with partial updates", async () => {
|
|
662
|
+
const stateModule = getStateModule(app);
|
|
663
|
+
const schema = createSchemaWithRecords();
|
|
664
|
+
const metadata: DocumentMetadata = {
|
|
665
|
+
name: "Update Record Test",
|
|
666
|
+
documentTypeName: "TestType",
|
|
667
|
+
ontologyRid: "test-ontology-rid",
|
|
668
|
+
security: TEST_SECURITY,
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
const docRef = await stateModule.createDocument(metadata, schema);
|
|
672
|
+
const usersCollection = docRef.getRecords(schema.User);
|
|
673
|
+
|
|
674
|
+
const userId = "user_1" as RecordId;
|
|
675
|
+
const initialData = {
|
|
676
|
+
id: "user_1",
|
|
677
|
+
name: "John Doe",
|
|
678
|
+
email: "john@example.com",
|
|
679
|
+
age: 30,
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
// Set initial record
|
|
683
|
+
await usersCollection.set(userId, initialData);
|
|
684
|
+
|
|
685
|
+
const recordRef = usersCollection.get(userId)!;
|
|
686
|
+
let changeNotifications = 0;
|
|
687
|
+
let lastSnapshot: User | null = null;
|
|
688
|
+
|
|
689
|
+
// Subscribe to record changes
|
|
690
|
+
const unsubscribe = stateModule.onRecordChanged(recordRef, snapshot => {
|
|
691
|
+
changeNotifications++;
|
|
692
|
+
lastSnapshot = snapshot;
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
// Wait for initial callback
|
|
696
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
697
|
+
expect(changeNotifications).toBe(1);
|
|
698
|
+
expect(lastSnapshot).toEqual(initialData);
|
|
699
|
+
|
|
700
|
+
// Update only age using updateRecord
|
|
701
|
+
await stateModule.updateRecord(recordRef, { age: 31 });
|
|
702
|
+
|
|
703
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
704
|
+
expect(changeNotifications).toBe(2);
|
|
705
|
+
expect(lastSnapshot).toEqual({
|
|
706
|
+
id: "user_1",
|
|
707
|
+
name: "John Doe", // Should remain unchanged
|
|
708
|
+
email: "john@example.com", // Should remain unchanged
|
|
709
|
+
age: 31, // Should be updated
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// Update multiple fields
|
|
713
|
+
await stateModule.updateRecord(recordRef, { name: "Jane Doe", age: 32 });
|
|
714
|
+
|
|
715
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
716
|
+
expect(changeNotifications).toBe(3);
|
|
717
|
+
expect(lastSnapshot).toEqual({
|
|
718
|
+
id: "user_1",
|
|
719
|
+
name: "Jane Doe", // Updated
|
|
720
|
+
email: "john@example.com", // Unchanged
|
|
721
|
+
age: 32, // Updated
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
// Verify final snapshot via direct get
|
|
725
|
+
const finalSnapshot = await recordRef.getSnapshot();
|
|
726
|
+
expect(finalSnapshot).toEqual(lastSnapshot);
|
|
727
|
+
|
|
728
|
+
unsubscribe();
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it("should handle updateRecord on non-existent record", async () => {
|
|
732
|
+
const stateModule = getStateModule(app);
|
|
733
|
+
const schema = createSchemaWithRecords();
|
|
734
|
+
const metadata: DocumentMetadata = {
|
|
735
|
+
name: "Update Non-Existent Record Test",
|
|
736
|
+
documentTypeName: "TestType",
|
|
737
|
+
ontologyRid: "test-ontology-rid",
|
|
738
|
+
security: TEST_SECURITY,
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
const docRef = await stateModule.createDocument(metadata, schema);
|
|
742
|
+
|
|
743
|
+
const userId = "non_existent_user" as RecordId;
|
|
744
|
+
const nonExistentRecord = stateModule.createRecordRef(docRef, userId, schema.User);
|
|
745
|
+
|
|
746
|
+
// Try to update non-existent record - should reject
|
|
747
|
+
await expect(stateModule.updateRecord(nonExistentRecord, { age: 25 }))
|
|
748
|
+
.rejects.toThrow("Record not found for update");
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it("should handle deleteRecord method", async () => {
|
|
752
|
+
const stateModule = getStateModule(app);
|
|
753
|
+
const schema = createSchemaWithRecords();
|
|
754
|
+
const metadata: DocumentMetadata = {
|
|
755
|
+
name: "Delete Record Test",
|
|
756
|
+
documentTypeName: "TestType",
|
|
757
|
+
ontologyRid: "test-ontology-rid",
|
|
758
|
+
security: TEST_SECURITY,
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
const docRef = await stateModule.createDocument(metadata, schema);
|
|
762
|
+
const usersCollection = docRef.getRecords(schema.User);
|
|
763
|
+
|
|
764
|
+
const userId = "user_to_delete" as RecordId;
|
|
765
|
+
const userData = {
|
|
766
|
+
id: "user_to_delete",
|
|
767
|
+
name: "Delete Me",
|
|
768
|
+
email: "delete@example.com",
|
|
769
|
+
age: 25,
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
// Create record
|
|
773
|
+
await usersCollection.set(userId, userData);
|
|
774
|
+
expect(usersCollection.has(userId)).toBe(true);
|
|
775
|
+
expect(usersCollection.size).toBe(1);
|
|
776
|
+
|
|
777
|
+
// Get record ref and delete it
|
|
778
|
+
const recordRef = usersCollection.get(userId)!;
|
|
779
|
+
let deletedCallbackCalled = false;
|
|
780
|
+
|
|
781
|
+
const unsubscribe = stateModule.onRecordDeleted(recordRef, deletedRecord => {
|
|
782
|
+
deletedCallbackCalled = true;
|
|
783
|
+
expect(deletedRecord.id).toBe(userId);
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
// Delete using StateModule deleteRecord method
|
|
787
|
+
await stateModule.deleteRecord(recordRef);
|
|
788
|
+
|
|
789
|
+
// Verify deletion
|
|
790
|
+
expect(usersCollection.has(userId)).toBe(false);
|
|
791
|
+
expect(usersCollection.size).toBe(0);
|
|
792
|
+
expect(usersCollection.get(userId)).toBeUndefined();
|
|
793
|
+
|
|
794
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
795
|
+
expect(deletedCallbackCalled).toBe(true);
|
|
796
|
+
|
|
797
|
+
// Try to delete again - should be a no-op (no error)
|
|
798
|
+
await stateModule.deleteRecord(recordRef);
|
|
799
|
+
|
|
800
|
+
unsubscribe();
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
it("should handle setRecord as full replacement (clearing missing fields)", async () => {
|
|
804
|
+
const stateModule = getStateModule(app);
|
|
805
|
+
|
|
806
|
+
// Use a schema with optional fields to test field removal
|
|
807
|
+
const userWithOptionalFields = z.object({
|
|
808
|
+
id: z.string(),
|
|
809
|
+
name: z.string(),
|
|
810
|
+
email: z.string().optional(),
|
|
811
|
+
age: z.number().optional(),
|
|
812
|
+
bio: z.string().optional(),
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
type UserWithOptional = z.infer<typeof userWithOptionalFields>;
|
|
816
|
+
|
|
817
|
+
const UserOptionalModel: Model<UserWithOptional, typeof userWithOptionalFields> = {
|
|
818
|
+
__type: {} as UserWithOptional,
|
|
819
|
+
zodSchema: userWithOptionalFields,
|
|
820
|
+
[Metadata]: {
|
|
821
|
+
name: "UserOptional",
|
|
822
|
+
},
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
const schema = {
|
|
826
|
+
UserOptional: UserOptionalModel,
|
|
827
|
+
[Metadata]: { version: 1 },
|
|
828
|
+
} as const satisfies DocumentSchema;
|
|
829
|
+
|
|
830
|
+
const metadata: DocumentMetadata = {
|
|
831
|
+
name: "Full Replacement Test",
|
|
832
|
+
documentTypeName: "TestType",
|
|
833
|
+
ontologyRid: "test-ontology-rid",
|
|
834
|
+
security: TEST_SECURITY,
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
const docRef = await stateModule.createDocument(metadata, schema);
|
|
838
|
+
const usersCollection = docRef.getRecords(schema.UserOptional);
|
|
839
|
+
|
|
840
|
+
const userId = "user_replace" as RecordId;
|
|
841
|
+
const initialData: UserWithOptional = {
|
|
842
|
+
id: "user_replace",
|
|
843
|
+
name: "John Doe",
|
|
844
|
+
email: "john@example.com",
|
|
845
|
+
age: 30,
|
|
846
|
+
bio: "Software Engineer",
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
// Set initial record with all fields
|
|
850
|
+
await usersCollection.set(userId, initialData);
|
|
851
|
+
|
|
852
|
+
let snapshot = await usersCollection.get(userId)!.getSnapshot();
|
|
853
|
+
expect(snapshot).toEqual(initialData);
|
|
854
|
+
|
|
855
|
+
// Replace with record missing some fields - setRecord should clear missing fields
|
|
856
|
+
const replacementData: UserWithOptional = {
|
|
857
|
+
id: "user_replace",
|
|
858
|
+
name: "Jane Doe", // Changed
|
|
859
|
+
email: "jane@example.com", // Changed
|
|
860
|
+
// age and bio are missing - should be cleared by setRecord
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
await usersCollection.set(userId, replacementData);
|
|
864
|
+
|
|
865
|
+
snapshot = await usersCollection.get(userId)!.getSnapshot();
|
|
866
|
+
expect(snapshot).toEqual({
|
|
867
|
+
id: "user_replace",
|
|
868
|
+
name: "Jane Doe",
|
|
869
|
+
email: "jane@example.com",
|
|
870
|
+
// age and bio should be undefined/missing
|
|
871
|
+
});
|
|
872
|
+
expect(snapshot.age).toBeUndefined();
|
|
873
|
+
expect(snapshot.bio).toBeUndefined();
|
|
874
|
+
});
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
describe("InMemoryDocumentService with autoCreateDocuments", () => {
|
|
878
|
+
let app: PackAppInternal;
|
|
879
|
+
|
|
880
|
+
beforeEach(() => {
|
|
881
|
+
app = createTestApp();
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
it("should auto-create document when metadata listener is registered for non-existent document", async () => {
|
|
885
|
+
const schema = createTestSchema();
|
|
886
|
+
|
|
887
|
+
// Create a document reference but don't create the actual document
|
|
888
|
+
const documentId = "auto-created-doc" as DocumentId;
|
|
889
|
+
const testDocRef = createDocRef(app, documentId, schema);
|
|
890
|
+
|
|
891
|
+
let callbackCalled = false;
|
|
892
|
+
|
|
893
|
+
// Register metadata listener - document should be created but callback should NOT be called
|
|
894
|
+
// since autoCreateDocuments is enabled but no metadata is set initially
|
|
895
|
+
const unsubscribe = testDocRef.onMetadataChange((doc, metadata) => {
|
|
896
|
+
callbackCalled = true;
|
|
897
|
+
expect(doc.id).toBe(documentId);
|
|
898
|
+
expect(metadata).toBeDefined(); // If callback is called, metadata should exist
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
// Wait for async operations
|
|
902
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
903
|
+
|
|
904
|
+
// With the new base implementation, callback is not called unless metadata exists
|
|
905
|
+
expect(callbackCalled).toBe(false);
|
|
906
|
+
|
|
907
|
+
// But the document should still be created internally
|
|
908
|
+
const snapshot = await testDocRef.getDocSnapshot();
|
|
909
|
+
expect(snapshot).toBeDefined();
|
|
910
|
+
|
|
911
|
+
// Now actually create the document with metadata to trigger the callback
|
|
912
|
+
const stateModule = getStateModule(app);
|
|
913
|
+
const metadata: DocumentMetadata = {
|
|
914
|
+
name: "Auto-created document",
|
|
915
|
+
documentTypeName: "auto-generated",
|
|
916
|
+
ontologyRid: "auto-ontology",
|
|
917
|
+
security: {
|
|
918
|
+
discretionary: {
|
|
919
|
+
owners: [{
|
|
920
|
+
"type": "userId",
|
|
921
|
+
"userId": "system",
|
|
922
|
+
}],
|
|
923
|
+
},
|
|
924
|
+
mandatory: {},
|
|
925
|
+
},
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
// Create document properly which should trigger metadata callback
|
|
929
|
+
const docRef2 = await stateModule.createDocument(metadata, schema);
|
|
930
|
+
|
|
931
|
+
// Register listener on properly created document
|
|
932
|
+
let callbackCalled2 = false;
|
|
933
|
+
let metadataReceived2: DocumentMetadata | null = null;
|
|
934
|
+
|
|
935
|
+
const unsubscribe2 = docRef2.onMetadataChange((doc, meta) => {
|
|
936
|
+
callbackCalled2 = true;
|
|
937
|
+
metadataReceived2 = meta;
|
|
938
|
+
expect(doc.id).toBe(docRef2.id);
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
942
|
+
|
|
943
|
+
expect(callbackCalled2).toBe(true);
|
|
944
|
+
expect(metadataReceived2).toBeDefined();
|
|
945
|
+
expect(metadataReceived2!.name).toBe("Auto-created document");
|
|
946
|
+
|
|
947
|
+
unsubscribe();
|
|
948
|
+
unsubscribe2();
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
it("should not auto-create document when autoCreateDocuments is disabled", async () => {
|
|
952
|
+
// Create a new app with autoCreateDocuments disabled
|
|
953
|
+
const appWithoutAutoCreate = createTestAppNoAutocreate();
|
|
954
|
+
|
|
955
|
+
const schema = createTestSchema();
|
|
956
|
+
|
|
957
|
+
const documentId = "non-auto-created-doc" as DocumentId;
|
|
958
|
+
const testDocRef = createDocRef(appWithoutAutoCreate, documentId, schema);
|
|
959
|
+
|
|
960
|
+
let callbackCalled = false;
|
|
961
|
+
|
|
962
|
+
// Register metadata listener - this should NOT trigger auto-creation
|
|
963
|
+
const unsubscribe = testDocRef.onMetadataChange(() => {
|
|
964
|
+
callbackCalled = true;
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
// Wait for async operations
|
|
968
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
969
|
+
|
|
970
|
+
// Callback should not be called because document doesn't exist
|
|
971
|
+
expect(callbackCalled).toBe(false);
|
|
972
|
+
|
|
973
|
+
unsubscribe();
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
it("should receive Y.js updates via record subscription without collection subscription", async () => {
|
|
977
|
+
const schema = createSchemaWithRecords();
|
|
978
|
+
const documentId = "yjs-update-test-doc" as DocumentId;
|
|
979
|
+
const testDocRef = createDocRef(app, documentId, schema);
|
|
980
|
+
const stateModule = getStateModule(app);
|
|
981
|
+
|
|
982
|
+
const userId = "user-yjs-test" as RecordId;
|
|
983
|
+
const userRecord = stateModule.createRecordRef(testDocRef, userId, schema.User);
|
|
984
|
+
|
|
985
|
+
let changeCallbackCount = 0;
|
|
986
|
+
let lastReceivedData: User | null = null;
|
|
987
|
+
|
|
988
|
+
const unsubscribe = userRecord.onChange(data => {
|
|
989
|
+
changeCallbackCount++;
|
|
990
|
+
lastReceivedData = data;
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
994
|
+
expect(changeCallbackCount).toBe(0);
|
|
995
|
+
|
|
996
|
+
const userData: User = {
|
|
997
|
+
id: userId,
|
|
998
|
+
name: "Test User",
|
|
999
|
+
email: "test@example.com",
|
|
1000
|
+
age: 30,
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
await userRecord.set(userData);
|
|
1004
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1005
|
+
expect(changeCallbackCount).toBe(1);
|
|
1006
|
+
expect(lastReceivedData).toMatchObject(userData);
|
|
1007
|
+
|
|
1008
|
+
const updatedData: User = {
|
|
1009
|
+
...userData,
|
|
1010
|
+
age: 31,
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
await userRecord.set(updatedData);
|
|
1014
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1015
|
+
expect(changeCallbackCount).toBe(2);
|
|
1016
|
+
expect(lastReceivedData).toMatchObject(updatedData);
|
|
1017
|
+
expect((lastReceivedData as User | null)?.age).toBe(31);
|
|
1018
|
+
|
|
1019
|
+
unsubscribe();
|
|
1020
|
+
});
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
describe("Remote Y.js Updates", () => {
|
|
1024
|
+
let app: PackAppInternal;
|
|
1025
|
+
|
|
1026
|
+
beforeEach(() => {
|
|
1027
|
+
app = createTestApp();
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
function simulateRemoteUpdate(
|
|
1031
|
+
app: PackAppInternal,
|
|
1032
|
+
documentId: DocumentId,
|
|
1033
|
+
updateFn: (yDoc: Y.Doc) => void,
|
|
1034
|
+
) {
|
|
1035
|
+
const docService = app.getModule(DOCUMENT_SERVICE_MODULE_KEY);
|
|
1036
|
+
const yDoc = (docService as BaseYjsDocumentService).getYDocForTesting(documentId);
|
|
1037
|
+
if (!yDoc) {
|
|
1038
|
+
throw new Error("Y.Doc not found for document ID: " + documentId);
|
|
1039
|
+
}
|
|
1040
|
+
yDoc.transact(() => {
|
|
1041
|
+
updateFn(yDoc);
|
|
1042
|
+
}, "remote");
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
it("should handle remote record property updates", async () => {
|
|
1046
|
+
const schema = createSchemaWithRecords();
|
|
1047
|
+
const documentId = "remote-update-doc" as DocumentId;
|
|
1048
|
+
const testDocRef = createDocRef(app, documentId, schema);
|
|
1049
|
+
const stateModule = getStateModule(app);
|
|
1050
|
+
|
|
1051
|
+
const userId = "user-1" as RecordId;
|
|
1052
|
+
const userRecord = stateModule.createRecordRef(testDocRef, userId, schema.User);
|
|
1053
|
+
|
|
1054
|
+
await userRecord.set({
|
|
1055
|
+
id: userId,
|
|
1056
|
+
name: "Alice",
|
|
1057
|
+
email: "alice@example.com",
|
|
1058
|
+
age: 25,
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
let changeCount = 0;
|
|
1062
|
+
let lastAge: number | undefined;
|
|
1063
|
+
|
|
1064
|
+
const unsubscribe = userRecord.onChange(data => {
|
|
1065
|
+
changeCount++;
|
|
1066
|
+
lastAge = data.age;
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1070
|
+
expect(changeCount).toBe(1);
|
|
1071
|
+
expect(lastAge).toBe(25);
|
|
1072
|
+
|
|
1073
|
+
simulateRemoteUpdate(app, documentId, yDoc => {
|
|
1074
|
+
const collection = yDoc.getMap("User");
|
|
1075
|
+
const record = collection.get(userId) as Y.Map<unknown>;
|
|
1076
|
+
record.set("age", 26);
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1080
|
+
expect(changeCount).toBe(2);
|
|
1081
|
+
expect(lastAge).toBe(26);
|
|
1082
|
+
|
|
1083
|
+
unsubscribe();
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
it("should handle remote record additions", async () => {
|
|
1087
|
+
const schema = createSchemaWithRecords();
|
|
1088
|
+
const documentId = "remote-add-doc" as DocumentId;
|
|
1089
|
+
const testDocRef = createDocRef(app, documentId, schema);
|
|
1090
|
+
|
|
1091
|
+
const usersCollection = testDocRef.getRecords(schema.User);
|
|
1092
|
+
|
|
1093
|
+
let addedCount = 0;
|
|
1094
|
+
const unsubscribe = usersCollection.onItemsAdded(records => {
|
|
1095
|
+
addedCount += records.length;
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
const userId = "user-remote" as RecordId;
|
|
1099
|
+
simulateRemoteUpdate(app, documentId, yDoc => {
|
|
1100
|
+
const collection = yDoc.getMap("User");
|
|
1101
|
+
const record = new Y.Map();
|
|
1102
|
+
record.set("id", userId);
|
|
1103
|
+
record.set("name", "Bob");
|
|
1104
|
+
record.set("email", "bob@example.com");
|
|
1105
|
+
record.set("age", 30);
|
|
1106
|
+
collection.set(userId, record);
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1110
|
+
expect(addedCount).toBe(1);
|
|
1111
|
+
|
|
1112
|
+
unsubscribe();
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
it("should handle remote record deletions", async () => {
|
|
1116
|
+
const schema = createSchemaWithRecords();
|
|
1117
|
+
const documentId = "remote-delete-doc" as DocumentId;
|
|
1118
|
+
const testDocRef = createDocRef(app, documentId, schema);
|
|
1119
|
+
const stateModule = getStateModule(app);
|
|
1120
|
+
|
|
1121
|
+
const userId = "user-to-delete" as RecordId;
|
|
1122
|
+
const userRecord = stateModule.createRecordRef(testDocRef, userId, schema.User);
|
|
1123
|
+
|
|
1124
|
+
await userRecord.set({
|
|
1125
|
+
id: userId,
|
|
1126
|
+
name: "Charlie",
|
|
1127
|
+
email: "charlie@example.com",
|
|
1128
|
+
age: 28,
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
const usersCollection = testDocRef.getRecords(schema.User);
|
|
1132
|
+
|
|
1133
|
+
let deletedCount = 0;
|
|
1134
|
+
const unsubscribe = usersCollection.onItemsDeleted(records => {
|
|
1135
|
+
deletedCount += records.length;
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
simulateRemoteUpdate(app, documentId, yDoc => {
|
|
1139
|
+
const collection = yDoc.getMap("User");
|
|
1140
|
+
collection.delete(userId);
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1144
|
+
expect(deletedCount).toBe(1);
|
|
1145
|
+
|
|
1146
|
+
unsubscribe();
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
it("should handle multiple property updates in single transaction", async () => {
|
|
1150
|
+
const schema = createSchemaWithRecords();
|
|
1151
|
+
const documentId = "multi-prop-doc" as DocumentId;
|
|
1152
|
+
const testDocRef = createDocRef(app, documentId, schema);
|
|
1153
|
+
const stateModule = getStateModule(app);
|
|
1154
|
+
|
|
1155
|
+
const userId = "user-multi" as RecordId;
|
|
1156
|
+
const userRecord = stateModule.createRecordRef(testDocRef, userId, schema.User);
|
|
1157
|
+
|
|
1158
|
+
await userRecord.set({
|
|
1159
|
+
id: userId,
|
|
1160
|
+
name: "Diana",
|
|
1161
|
+
email: "diana@example.com",
|
|
1162
|
+
age: 32,
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
let changeCount = 0;
|
|
1166
|
+
|
|
1167
|
+
const unsubscribe = userRecord.onChange(() => {
|
|
1168
|
+
changeCount++;
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1172
|
+
expect(changeCount).toBe(1);
|
|
1173
|
+
|
|
1174
|
+
simulateRemoteUpdate(app, documentId, yDoc => {
|
|
1175
|
+
const collection = yDoc.getMap("User");
|
|
1176
|
+
const record = collection.get(userId) as Y.Map<unknown>;
|
|
1177
|
+
record.set("name", "Diana Updated");
|
|
1178
|
+
record.set("age", 33);
|
|
1179
|
+
record.set("email", "diana.updated@example.com");
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
1183
|
+
expect(changeCount).toBe(2);
|
|
1184
|
+
|
|
1185
|
+
unsubscribe();
|
|
1186
|
+
});
|
|
1187
|
+
});
|