@omnistreamai/data-core 0.1.0

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.
@@ -0,0 +1,452 @@
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import { Store } from "./store.js";
3
+ import { AdapterType, type QueryOptions, type QueryResult } from "./types.js";
4
+ import type { LocalAdapter, RemoteAdapter } from "./types.js";
5
+
6
+ // Mock adapters
7
+ class MockLocalAdapter implements LocalAdapter {
8
+ readonly type = AdapterType.Local;
9
+ readonly name = "MockLocalAdapter";
10
+ private data: Map<string, Record<string, unknown>> = new Map();
11
+
12
+ async add<T extends Record<string, unknown>>(
13
+ storeName: string,
14
+ data: T,
15
+ idKey?: string
16
+ ): Promise<T> {
17
+ const key = String(data[idKey ?? "id"]);
18
+ this.data.set(key, data);
19
+ return data;
20
+ }
21
+
22
+ async update<T extends Record<string, unknown>>(
23
+ storeName: string,
24
+ id: string,
25
+ data: Partial<T>,
26
+ idKey?: string
27
+ ): Promise<T> {
28
+ const existing = this.data.get(id) as T | undefined;
29
+ if (!existing) {
30
+ throw new Error(`Data with id ${id} not found`);
31
+ }
32
+ const updated = { ...existing, ...data } as T;
33
+ this.data.set(id, updated);
34
+ return updated;
35
+ }
36
+
37
+ async delete(storeName: string, id: string, idKey?: string): Promise<void> {
38
+ this.data.delete(id);
39
+ }
40
+
41
+ async getData<T extends Record<string, unknown>>(
42
+ storeName: string,
43
+ id: string,
44
+ idKey?: string
45
+ ): Promise<T | null> {
46
+ return (this.data.get(id) as T) || null;
47
+ }
48
+
49
+ async getList<T extends Record<string, unknown>>(
50
+ storeName: string,
51
+ options?: unknown,
52
+ idKey?: string
53
+ ): Promise<T[]> {
54
+ return Array.from(this.data.values()) as T[];
55
+ }
56
+
57
+
58
+
59
+ async clear(storeName: string): Promise<void> {
60
+ this.data.clear();
61
+ }
62
+
63
+ async initStore(
64
+ storeName: string,
65
+ indexes?: string[],
66
+ idKey?: string
67
+ ): Promise<void> {
68
+ // Mock implementation
69
+ }
70
+ }
71
+
72
+ class MockRemoteAdapter implements RemoteAdapter {
73
+ readonly type = AdapterType.Remote;
74
+ readonly name = "MockRemoteAdapter";
75
+ private data: Map<string, Record<string, unknown>> = new Map();
76
+
77
+ async add<T extends Record<string, unknown>>(
78
+ storeName: string,
79
+ data: T,
80
+ idKey?: string
81
+ ): Promise<T> {
82
+ const key = String(data[idKey ?? "id"]);
83
+ this.data.set(key, data);
84
+ return data;
85
+ }
86
+
87
+ async update<T extends Record<string, unknown>>(
88
+ storeName: string,
89
+ id: string,
90
+ data: Partial<T>,
91
+ idKey?: string
92
+ ): Promise<T> {
93
+ const existing = this.data.get(id) as T | undefined;
94
+ if (!existing) {
95
+ throw new Error(`Data with id ${id} not found`);
96
+ }
97
+ const updated = { ...existing, ...data } as T;
98
+ this.data.set(id, updated);
99
+ return updated;
100
+ }
101
+
102
+ async delete(storeName: string, id: string, idKey?: string): Promise<void> {
103
+ this.data.delete(id);
104
+ }
105
+
106
+ async getData<T extends Record<string, unknown>>(
107
+ storeName: string,
108
+ id: string,
109
+ idKey?: string
110
+ ): Promise<T | null> {
111
+ return (this.data.get(id) as T) || null;
112
+ }
113
+
114
+ async getList<T extends Record<string, unknown>>(
115
+ storeName: string,
116
+ options?: QueryOptions<T>,
117
+ idKey?: string
118
+ ) {
119
+ let allData = Array.from(this.data.values()) as T[];
120
+
121
+ // 应用条件筛选
122
+ if (options?.where) {
123
+ const whereKeys = Object.keys(options.where);
124
+ allData = allData.filter((item) => {
125
+ return whereKeys.every((key) => {
126
+ return item[key] === options.where?.[key];
127
+ });
128
+ });
129
+ }
130
+
131
+ // 应用分页
132
+ let page = options?.page ?? 1;
133
+ let limit = options?.limit ?? (allData.length || 10);
134
+ const start = (page - 1) * limit;
135
+ const end = start + limit;
136
+ const paginatedData = allData.slice(start, end);
137
+
138
+ return {
139
+ data: paginatedData,
140
+ totalCount: allData.length,
141
+ page,
142
+ limit,
143
+ };
144
+ }
145
+
146
+ async getListQuery<T extends Record<string, unknown>>(
147
+ storeName: string,
148
+ options?: QueryOptions<T>,
149
+ idKey?: string
150
+ ): Promise<QueryResult<T[]>> {
151
+ return {
152
+ queryKey: ["store", storeName, "remote", options],
153
+ queryFn: async () => {
154
+ const result = await this.getList<T>(storeName, options, idKey);
155
+ return result.data;
156
+ },
157
+ getInitialData: async () => [],
158
+ };
159
+ }
160
+
161
+ async clear(storeName: string): Promise<void> {
162
+ this.data.clear();
163
+ }
164
+
165
+ async initStore(
166
+ storeName: string,
167
+ indexes?: string[],
168
+ idKey?: string
169
+ ): Promise<void> {
170
+ // Mock implementation
171
+ }
172
+
173
+ setService(service: unknown): void {
174
+ // Mock implementation
175
+ }
176
+ }
177
+
178
+ describe("Store", () => {
179
+ let localAdapter: LocalAdapter;
180
+ let remoteAdapter: RemoteAdapter;
181
+ let store: Store<{ id: string; username: string; email?: string }>;
182
+
183
+ beforeEach(async () => {
184
+ localAdapter = new MockLocalAdapter();
185
+ remoteAdapter = new MockRemoteAdapter();
186
+
187
+ store = new Store("user", {
188
+ localAdapter,
189
+ remoteAdapter,
190
+ idKey: "id",
191
+ indexes: ["username"],
192
+ maxLocalEntries: 100,
193
+ notThrowLocalError: false,
194
+ });
195
+
196
+ await store.init();
197
+ });
198
+
199
+ describe("add", () => {
200
+ it("应该能够添加数据", async () => {
201
+ const data = {
202
+ id: "1",
203
+ username: "test-user",
204
+ email: "test@example.com",
205
+ };
206
+
207
+ const result = await store.add(data);
208
+ expect(result).toEqual(data);
209
+ });
210
+
211
+ it("应该同时更新本地和远程适配器", async () => {
212
+ const data = {
213
+ id: "1",
214
+ username: "test-user",
215
+ };
216
+
217
+ await store.add(data);
218
+
219
+ const localData = await localAdapter.getData("user", "1", "id");
220
+ const remoteData = await remoteAdapter.getData("user", "1", "id");
221
+
222
+ expect(localData).toEqual(data);
223
+ expect(remoteData).toEqual(data);
224
+ });
225
+ });
226
+
227
+ describe("getData", () => {
228
+ it("应该能够根据 ID 获取数据", async () => {
229
+ const data = {
230
+ id: "1",
231
+ username: "test-user",
232
+ email: "test@example.com",
233
+ };
234
+
235
+ await store.add(data);
236
+ const result = await store.getData("1");
237
+ expect(result).toEqual(data);
238
+ });
239
+
240
+ it("当数据不存在时应该返回 null", async () => {
241
+ const result = await store.getData("non-existent");
242
+ expect(result).toBeNull();
243
+ });
244
+ });
245
+
246
+ describe("getList", () => {
247
+ it("应该能够获取列表数据", async () => {
248
+ const data1 = { id: "1", username: "user1" };
249
+ const data2 = { id: "2", username: "user2" };
250
+
251
+ await store.add(data1);
252
+ await store.add(data2);
253
+
254
+ const result = await store.getList();
255
+ expect(result).toHaveLength(2);
256
+ });
257
+
258
+ it("应该支持从本地源读取数据", async () => {
259
+ const data = { id: "1", username: "user1" };
260
+ await store.add(data);
261
+
262
+ const result = await store.getList({ source: "local" });
263
+ expect(result).toHaveLength(1);
264
+ });
265
+
266
+ it("应该支持分页查询", async () => {
267
+ const data1 = { id: "1", username: "user1" };
268
+ const data2 = { id: "2", username: "user2" };
269
+
270
+ await store.add(data1);
271
+ await store.add(data2);
272
+
273
+ const result = await store.getList({ page: 1, limit: 1 });
274
+ expect(result).toHaveLength(1);
275
+ });
276
+
277
+ it("应该支持条件筛选", async () => {
278
+ const data1 = { id: "1", username: "user1" };
279
+ const data2 = { id: "2", username: "user2" };
280
+
281
+ await store.add(data1);
282
+ await store.add(data2);
283
+
284
+ const result = await store.getList({ where: { username: "user1" } });
285
+ expect(result).toHaveLength(1);
286
+ expect(result[0]?.username).toBe("user1");
287
+ });
288
+
289
+ it("应该返回 queryKey 和 queryFn", async () => {
290
+ const result = await store.getListQuery({
291
+ where: { username: "user1" },
292
+ });
293
+
294
+ expect(result.queryKey).toBeDefined();
295
+ expect(result.queryFn).toBeDefined();
296
+ expect(result.getInitialData).toBeDefined();
297
+
298
+ const data = await result.queryFn();
299
+ expect(Array.isArray(data)).toBe(true);
300
+ });
301
+ });
302
+
303
+ describe("update", () => {
304
+ it("应该能够更新数据", async () => {
305
+ const data = {
306
+ id: "1",
307
+ username: "test-user",
308
+ email: "test@example.com",
309
+ };
310
+
311
+ await store.add(data);
312
+ const updated = await store.update("1", { email: "updated@example.com" });
313
+
314
+ expect(updated.email).toBe("updated@example.com");
315
+ expect(updated.username).toBe("test-user");
316
+ });
317
+
318
+ it("应该同时更新本地和远程适配器", async () => {
319
+ const data = { id: "1", username: "test-user" };
320
+ await store.add(data);
321
+
322
+ await store.update("1", { email: "updated@example.com" });
323
+
324
+ const localData = await localAdapter.getData("user", "1", "id");
325
+ const remoteData = await remoteAdapter.getData("user", "1", "id");
326
+
327
+ expect(localData?.email).toBe("updated@example.com");
328
+ expect(remoteData?.email).toBe("updated@example.com");
329
+ });
330
+ });
331
+
332
+ describe("delete", () => {
333
+ it("应该能够删除数据", async () => {
334
+ const data = {
335
+ id: "1",
336
+ username: "test-user",
337
+ };
338
+
339
+ await store.add(data);
340
+ await store.delete("1");
341
+
342
+ const result = await store.getData("1");
343
+ expect(result).toBeNull();
344
+ });
345
+
346
+ it("应该同时从本地和远程适配器删除", async () => {
347
+ const data = { id: "1", username: "test-user" };
348
+ await store.add(data);
349
+
350
+ await store.delete("1");
351
+
352
+ const localData = await localAdapter.getData("user", "1", "id");
353
+ const remoteData = await remoteAdapter.getData("user", "1", "id");
354
+
355
+ expect(localData).toBeNull();
356
+ expect(remoteData).toBeNull();
357
+ });
358
+ });
359
+
360
+ describe("onChange", () => {
361
+ it("应该在数据变更时触发 onChange 回调", async () => {
362
+ const onChange = vi.fn();
363
+ const storeWithCallback = new Store("user2", {
364
+ localAdapter,
365
+ remoteAdapter,
366
+ idKey: "id",
367
+ onChange,
368
+ notThrowLocalError: true,
369
+ });
370
+
371
+ await storeWithCallback.init();
372
+ await storeWithCallback.add({ id: "1", username: "test" });
373
+
374
+ expect(onChange).toHaveBeenCalled();
375
+ });
376
+ });
377
+
378
+ describe("maxLocalEntries", () => {
379
+ it("应该限制本地存储的条目数量", async () => {
380
+ const limitedStore = new Store<{ id: string; name: string }>("test-store", {
381
+ localAdapter,
382
+ remoteAdapter: null, // 只使用本地适配器
383
+ idKey: "id",
384
+ maxLocalEntries: 3,
385
+ notThrowLocalError: false,
386
+ });
387
+
388
+ await limitedStore.init();
389
+
390
+ // 添加超过限制的条目
391
+ await limitedStore.add({ id: "1", name: "Item 1" });
392
+ await limitedStore.add({ id: "2", name: "Item 2" });
393
+ await limitedStore.add({ id: "3", name: "Item 3" });
394
+ await limitedStore.add({ id: "4", name: "Item 4" });
395
+ await limitedStore.add({ id: "5", name: "Item 5" });
396
+
397
+ // 检查本地存储中的条目数量
398
+ const localData = await localAdapter.getList("test-store");
399
+ expect(localData.length).toBeLessThanOrEqual(3);
400
+ });
401
+
402
+ it("当 maxLocalEntries 未设置时不应该限制条目数量", async () => {
403
+ const unlimitedStore = new Store<{ id: string; name: string }>("test-store-unlimited", {
404
+ localAdapter,
405
+ remoteAdapter: null,
406
+ idKey: "id",
407
+ notThrowLocalError: false,
408
+ });
409
+
410
+ await unlimitedStore.init();
411
+
412
+ // 添加多个条目
413
+ await unlimitedStore.add({ id: "1", name: "Item 1" });
414
+ await unlimitedStore.add({ id: "2", name: "Item 2" });
415
+ await unlimitedStore.add({ id: "3", name: "Item 3" });
416
+ await unlimitedStore.add({ id: "4", name: "Item 4" });
417
+ await unlimitedStore.add({ id: "5", name: "Item 5" });
418
+
419
+ // 检查本地存储中的条目数量
420
+ const localData = await localAdapter.getList("test-store-unlimited");
421
+ expect(localData.length).toBe(5);
422
+ });
423
+
424
+ it("应该根据 sortByKey 参数排序后删除最旧的条目", async () => {
425
+ const sortedStore = new Store<{ id: string; name: string; timestamp: number }>("test-store-sorted", {
426
+ localAdapter,
427
+ remoteAdapter: null, // 只使用本地适配器
428
+ idKey: "id",
429
+ maxLocalEntries: 3,
430
+ sortByKey: "timestamp", // 按时间戳排序
431
+ notThrowLocalError: false,
432
+ });
433
+
434
+ await sortedStore.init();
435
+
436
+ // 添加带时间戳的条目(时间戳从小到大)
437
+ await sortedStore.add({ id: "1", name: "Item 1", timestamp: 1000 });
438
+ await sortedStore.add({ id: "2", name: "Item 2", timestamp: 2000 });
439
+ await sortedStore.add({ id: "3", name: "Item 3", timestamp: 3000 });
440
+ await sortedStore.add({ id: "4", name: "Item 4", timestamp: 4000 });
441
+ await sortedStore.add({ id: "5", name: "Item 5", timestamp: 5000 });
442
+
443
+ // 检查本地存储中的条目数量和内容
444
+ const localData = await localAdapter.getList("test-store-sorted");
445
+ expect(localData.length).toBe(3);
446
+
447
+ // 应该保留时间戳最大的3个条目(删除了 timestamp: 1000, 2000 的条目)
448
+ const timestamps = localData.map(item => item.timestamp).sort((a, b) => a - b);
449
+ expect(timestamps).toEqual([3000, 4000, 5000]);
450
+ });
451
+ });
452
+ });