@lobehub/lobehub 2.0.0-next.137 → 2.0.0-next.138
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/CHANGELOG.md +25 -0
- package/CLAUDE.md +38 -0
- package/changelog/v1.json +5 -0
- package/package.json +1 -1
- package/packages/conversation-flow/src/transformation/BranchResolver.ts +24 -14
- package/packages/conversation-flow/src/transformation/ContextTreeBuilder.ts +6 -1
- package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +15 -0
- package/packages/conversation-flow/src/transformation/__tests__/BranchResolver.test.ts +66 -3
- package/packages/conversation-flow/src/transformation/__tests__/ContextTreeBuilder.test.ts +64 -0
- package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +47 -0
- package/packages/database/src/models/__tests__/agent.test.ts +102 -3
- package/packages/database/src/models/__tests__/document.test.ts +163 -0
- package/packages/database/src/models/__tests__/embedding.test.ts +294 -0
- package/packages/database/src/models/__tests__/oauthHandoff.test.ts +261 -0
- package/packages/database/src/models/__tests__/thread.test.ts +327 -0
- package/packages/database/src/models/__tests__/user.test.ts +372 -0
|
@@ -54,6 +54,169 @@ const createTestDocument = async (model: DocumentModel, fModel: FileModel, conte
|
|
|
54
54
|
};
|
|
55
55
|
|
|
56
56
|
describe('DocumentModel', () => {
|
|
57
|
+
describe('create', () => {
|
|
58
|
+
it('should create a new document', async () => {
|
|
59
|
+
const { id: fileId } = await fileModel.create({
|
|
60
|
+
fileType: 'text/plain',
|
|
61
|
+
name: 'test.txt',
|
|
62
|
+
size: 100,
|
|
63
|
+
url: 'https://example.com/test.txt',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const file = await fileModel.findById(fileId);
|
|
67
|
+
if (!file) throw new Error('File not found');
|
|
68
|
+
|
|
69
|
+
const result = await documentModel.create({
|
|
70
|
+
content: 'Test content',
|
|
71
|
+
fileId: file.id,
|
|
72
|
+
fileType: 'text/plain',
|
|
73
|
+
source: file.url,
|
|
74
|
+
sourceType: 'file',
|
|
75
|
+
totalCharCount: 12,
|
|
76
|
+
totalLineCount: 1,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(result).toBeDefined();
|
|
80
|
+
expect(result.content).toBe('Test content');
|
|
81
|
+
expect(result.fileId).toBe(file.id);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('delete', () => {
|
|
86
|
+
it('should delete a document', async () => {
|
|
87
|
+
const { documentId } = await createTestDocument(documentModel, fileModel, 'Test content');
|
|
88
|
+
|
|
89
|
+
await documentModel.delete(documentId);
|
|
90
|
+
|
|
91
|
+
const deleted = await documentModel.findById(documentId);
|
|
92
|
+
expect(deleted).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should not delete document belonging to another user', async () => {
|
|
96
|
+
const { documentId } = await createTestDocument(documentModel, fileModel, 'Test content');
|
|
97
|
+
|
|
98
|
+
// Try to delete with another user's model
|
|
99
|
+
await documentModel2.delete(documentId);
|
|
100
|
+
|
|
101
|
+
// Document should still exist
|
|
102
|
+
const stillExists = await documentModel.findById(documentId);
|
|
103
|
+
expect(stillExists).toBeDefined();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('deleteAll', () => {
|
|
108
|
+
it('should delete all documents for the user', async () => {
|
|
109
|
+
await createTestDocument(documentModel, fileModel, 'First document');
|
|
110
|
+
await createTestDocument(documentModel, fileModel, 'Second document');
|
|
111
|
+
await createTestDocument(documentModel2, fileModel2, 'Other user document');
|
|
112
|
+
|
|
113
|
+
await documentModel.deleteAll();
|
|
114
|
+
|
|
115
|
+
const userDocs = await documentModel.query();
|
|
116
|
+
const otherUserDocs = await documentModel2.query();
|
|
117
|
+
|
|
118
|
+
expect(userDocs).toHaveLength(0);
|
|
119
|
+
expect(otherUserDocs).toHaveLength(1);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('query', () => {
|
|
124
|
+
it('should return all documents for the user', async () => {
|
|
125
|
+
await createTestDocument(documentModel, fileModel, 'First document');
|
|
126
|
+
await createTestDocument(documentModel, fileModel, 'Second document');
|
|
127
|
+
|
|
128
|
+
const result = await documentModel.query();
|
|
129
|
+
|
|
130
|
+
expect(result).toHaveLength(2);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should only return documents for the current user', async () => {
|
|
134
|
+
await createTestDocument(documentModel, fileModel, 'User 1 document');
|
|
135
|
+
await createTestDocument(documentModel2, fileModel2, 'User 2 document');
|
|
136
|
+
|
|
137
|
+
const result = await documentModel.query();
|
|
138
|
+
|
|
139
|
+
expect(result).toHaveLength(1);
|
|
140
|
+
expect(result[0].content).toBe('User 1 document');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should return documents ordered by updatedAt desc', async () => {
|
|
144
|
+
const { documentId: doc1Id } = await createTestDocument(
|
|
145
|
+
documentModel,
|
|
146
|
+
fileModel,
|
|
147
|
+
'First document',
|
|
148
|
+
);
|
|
149
|
+
const { documentId: doc2Id } = await createTestDocument(
|
|
150
|
+
documentModel,
|
|
151
|
+
fileModel,
|
|
152
|
+
'Second document',
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// Wait a bit to ensure timestamp difference
|
|
156
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
157
|
+
|
|
158
|
+
// Update first document to make it more recent
|
|
159
|
+
await documentModel.update(doc1Id, { content: 'Updated first document' });
|
|
160
|
+
|
|
161
|
+
const result = await documentModel.query();
|
|
162
|
+
|
|
163
|
+
expect(result[0].id).toBe(doc1Id);
|
|
164
|
+
expect(result[1].id).toBe(doc2Id);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('findById', () => {
|
|
169
|
+
it('should find document by id', async () => {
|
|
170
|
+
const { documentId } = await createTestDocument(documentModel, fileModel, 'Test content');
|
|
171
|
+
|
|
172
|
+
const found = await documentModel.findById(documentId);
|
|
173
|
+
|
|
174
|
+
expect(found).toBeDefined();
|
|
175
|
+
expect(found?.id).toBe(documentId);
|
|
176
|
+
expect(found?.content).toBe('Test content');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should return undefined for non-existent document', async () => {
|
|
180
|
+
const found = await documentModel.findById('non-existent-id');
|
|
181
|
+
|
|
182
|
+
expect(found).toBeUndefined();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should not find document belonging to another user', async () => {
|
|
186
|
+
const { documentId } = await createTestDocument(documentModel, fileModel, 'Test content');
|
|
187
|
+
|
|
188
|
+
const found = await documentModel2.findById(documentId);
|
|
189
|
+
|
|
190
|
+
expect(found).toBeUndefined();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('update', () => {
|
|
195
|
+
it('should update a document', async () => {
|
|
196
|
+
const { documentId } = await createTestDocument(documentModel, fileModel, 'Original content');
|
|
197
|
+
|
|
198
|
+
await documentModel.update(documentId, {
|
|
199
|
+
content: 'Updated content',
|
|
200
|
+
totalCharCount: 15,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const updated = await documentModel.findById(documentId);
|
|
204
|
+
|
|
205
|
+
expect(updated?.content).toBe('Updated content');
|
|
206
|
+
expect(updated?.totalCharCount).toBe(15);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should not update document belonging to another user', async () => {
|
|
210
|
+
const { documentId } = await createTestDocument(documentModel, fileModel, 'Original content');
|
|
211
|
+
|
|
212
|
+
await documentModel2.update(documentId, { content: 'Hacked content' });
|
|
213
|
+
|
|
214
|
+
const unchanged = await documentModel.findById(documentId);
|
|
215
|
+
|
|
216
|
+
expect(unchanged?.content).toBe('Original content');
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
57
220
|
describe('findByFileId', () => {
|
|
58
221
|
it('should find document by fileId', async () => {
|
|
59
222
|
const { documentId, file } = await createTestDocument(
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { chunks, embeddings, users } from '../../schemas';
|
|
5
|
+
import { LobeChatDatabase } from '../../type';
|
|
6
|
+
import { EmbeddingModel } from '../embedding';
|
|
7
|
+
import { getTestDB } from './_util';
|
|
8
|
+
import { designThinkingQuery } from './fixtures/embedding';
|
|
9
|
+
|
|
10
|
+
const userId = 'embedding-user-test';
|
|
11
|
+
const otherUserId = 'other-user-test';
|
|
12
|
+
|
|
13
|
+
const serverDB: LobeChatDatabase = await getTestDB();
|
|
14
|
+
const embeddingModel = new EmbeddingModel(serverDB, userId);
|
|
15
|
+
|
|
16
|
+
describe('EmbeddingModel', () => {
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
await serverDB.delete(users);
|
|
19
|
+
await serverDB.insert(users).values([{ id: userId }, { id: otherUserId }]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
await serverDB.delete(users);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('create', () => {
|
|
27
|
+
it('should create a new embedding', async () => {
|
|
28
|
+
// Create a chunk first
|
|
29
|
+
const [chunk] = await serverDB
|
|
30
|
+
.insert(chunks)
|
|
31
|
+
.values({ text: 'Test chunk', userId })
|
|
32
|
+
.returning();
|
|
33
|
+
|
|
34
|
+
const id = await embeddingModel.create({
|
|
35
|
+
chunkId: chunk.id,
|
|
36
|
+
embeddings: designThinkingQuery,
|
|
37
|
+
model: 'text-embedding-ada-002',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(id).toBeDefined();
|
|
41
|
+
|
|
42
|
+
const created = await serverDB.query.embeddings.findFirst({
|
|
43
|
+
where: eq(embeddings.id, id),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(created).toBeDefined();
|
|
47
|
+
expect(created?.chunkId).toBe(chunk.id);
|
|
48
|
+
expect(created?.model).toBe('text-embedding-ada-002');
|
|
49
|
+
expect(created?.userId).toBe(userId);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('bulkCreate', () => {
|
|
54
|
+
it('should create multiple embeddings', async () => {
|
|
55
|
+
// Create chunks first
|
|
56
|
+
const [chunk1, chunk2] = await serverDB
|
|
57
|
+
.insert(chunks)
|
|
58
|
+
.values([
|
|
59
|
+
{ text: 'Test chunk 1', userId },
|
|
60
|
+
{ text: 'Test chunk 2', userId },
|
|
61
|
+
])
|
|
62
|
+
.returning();
|
|
63
|
+
|
|
64
|
+
await embeddingModel.bulkCreate([
|
|
65
|
+
{ chunkId: chunk1.id, embeddings: designThinkingQuery, model: 'text-embedding-ada-002' },
|
|
66
|
+
{ chunkId: chunk2.id, embeddings: designThinkingQuery, model: 'text-embedding-ada-002' },
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
const created = await serverDB.query.embeddings.findMany({
|
|
70
|
+
where: eq(embeddings.userId, userId),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(created).toHaveLength(2);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should handle duplicate chunkId with onConflictDoNothing', async () => {
|
|
77
|
+
// Create a chunk
|
|
78
|
+
const [chunk] = await serverDB
|
|
79
|
+
.insert(chunks)
|
|
80
|
+
.values({ text: 'Test chunk', userId })
|
|
81
|
+
.returning();
|
|
82
|
+
|
|
83
|
+
// Create first embedding
|
|
84
|
+
await embeddingModel.create({
|
|
85
|
+
chunkId: chunk.id,
|
|
86
|
+
embeddings: designThinkingQuery,
|
|
87
|
+
model: 'text-embedding-ada-002',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Try to create duplicate
|
|
91
|
+
await embeddingModel.bulkCreate([
|
|
92
|
+
{ chunkId: chunk.id, embeddings: designThinkingQuery, model: 'text-embedding-3-small' },
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
const created = await serverDB.query.embeddings.findMany({
|
|
96
|
+
where: eq(embeddings.chunkId, chunk.id),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Should still have only 1 embedding due to unique constraint
|
|
100
|
+
expect(created).toHaveLength(1);
|
|
101
|
+
expect(created[0].model).toBe('text-embedding-ada-002');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('delete', () => {
|
|
106
|
+
it('should delete an embedding', async () => {
|
|
107
|
+
const [chunk] = await serverDB
|
|
108
|
+
.insert(chunks)
|
|
109
|
+
.values({ text: 'Test chunk', userId })
|
|
110
|
+
.returning();
|
|
111
|
+
|
|
112
|
+
const id = await embeddingModel.create({
|
|
113
|
+
chunkId: chunk.id,
|
|
114
|
+
embeddings: designThinkingQuery,
|
|
115
|
+
model: 'text-embedding-ada-002',
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await embeddingModel.delete(id);
|
|
119
|
+
|
|
120
|
+
const deleted = await serverDB.query.embeddings.findFirst({
|
|
121
|
+
where: eq(embeddings.id, id),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(deleted).toBeUndefined();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should not delete embedding belonging to another user', async () => {
|
|
128
|
+
// Create chunk and embedding for other user
|
|
129
|
+
const [chunk] = await serverDB
|
|
130
|
+
.insert(chunks)
|
|
131
|
+
.values({ text: 'Other user chunk', userId: otherUserId })
|
|
132
|
+
.returning();
|
|
133
|
+
|
|
134
|
+
const [otherEmbedding] = await serverDB
|
|
135
|
+
.insert(embeddings)
|
|
136
|
+
.values({
|
|
137
|
+
chunkId: chunk.id,
|
|
138
|
+
embeddings: designThinkingQuery,
|
|
139
|
+
model: 'text-embedding-ada-002',
|
|
140
|
+
userId: otherUserId,
|
|
141
|
+
})
|
|
142
|
+
.returning();
|
|
143
|
+
|
|
144
|
+
await embeddingModel.delete(otherEmbedding.id);
|
|
145
|
+
|
|
146
|
+
const stillExists = await serverDB.query.embeddings.findFirst({
|
|
147
|
+
where: eq(embeddings.id, otherEmbedding.id),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(stillExists).toBeDefined();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('query', () => {
|
|
155
|
+
it('should return all embeddings for the user', async () => {
|
|
156
|
+
const [chunk1, chunk2] = await serverDB
|
|
157
|
+
.insert(chunks)
|
|
158
|
+
.values([
|
|
159
|
+
{ text: 'Test chunk 1', userId },
|
|
160
|
+
{ text: 'Test chunk 2', userId },
|
|
161
|
+
])
|
|
162
|
+
.returning();
|
|
163
|
+
|
|
164
|
+
await serverDB.insert(embeddings).values([
|
|
165
|
+
{ chunkId: chunk1.id, embeddings: designThinkingQuery, userId },
|
|
166
|
+
{ chunkId: chunk2.id, embeddings: designThinkingQuery, userId },
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
const result = await embeddingModel.query();
|
|
170
|
+
|
|
171
|
+
expect(result).toHaveLength(2);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should only return embeddings for the current user', async () => {
|
|
175
|
+
const [chunk1] = await serverDB
|
|
176
|
+
.insert(chunks)
|
|
177
|
+
.values([{ text: 'Test chunk 1', userId }])
|
|
178
|
+
.returning();
|
|
179
|
+
|
|
180
|
+
const [chunk2] = await serverDB
|
|
181
|
+
.insert(chunks)
|
|
182
|
+
.values([{ text: 'Other user chunk', userId: otherUserId }])
|
|
183
|
+
.returning();
|
|
184
|
+
|
|
185
|
+
await serverDB.insert(embeddings).values([
|
|
186
|
+
{ chunkId: chunk1.id, embeddings: designThinkingQuery, userId },
|
|
187
|
+
{ chunkId: chunk2.id, embeddings: designThinkingQuery, userId: otherUserId },
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
const result = await embeddingModel.query();
|
|
191
|
+
|
|
192
|
+
expect(result).toHaveLength(1);
|
|
193
|
+
expect(result[0].userId).toBe(userId);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('findById', () => {
|
|
198
|
+
it('should return an embedding by id', async () => {
|
|
199
|
+
const [chunk] = await serverDB
|
|
200
|
+
.insert(chunks)
|
|
201
|
+
.values({ text: 'Test chunk', userId })
|
|
202
|
+
.returning();
|
|
203
|
+
|
|
204
|
+
const id = await embeddingModel.create({
|
|
205
|
+
chunkId: chunk.id,
|
|
206
|
+
embeddings: designThinkingQuery,
|
|
207
|
+
model: 'text-embedding-ada-002',
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const result = await embeddingModel.findById(id);
|
|
211
|
+
|
|
212
|
+
expect(result).toBeDefined();
|
|
213
|
+
expect(result?.id).toBe(id);
|
|
214
|
+
expect(result?.chunkId).toBe(chunk.id);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should return undefined for non-existent embedding', async () => {
|
|
218
|
+
// Use a valid UUID format that doesn't exist
|
|
219
|
+
const result = await embeddingModel.findById('00000000-0000-0000-0000-000000000000');
|
|
220
|
+
|
|
221
|
+
expect(result).toBeUndefined();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should not return embedding belonging to another user', async () => {
|
|
225
|
+
const [chunk] = await serverDB
|
|
226
|
+
.insert(chunks)
|
|
227
|
+
.values({ text: 'Other user chunk', userId: otherUserId })
|
|
228
|
+
.returning();
|
|
229
|
+
|
|
230
|
+
const [otherEmbedding] = await serverDB
|
|
231
|
+
.insert(embeddings)
|
|
232
|
+
.values({
|
|
233
|
+
chunkId: chunk.id,
|
|
234
|
+
embeddings: designThinkingQuery,
|
|
235
|
+
userId: otherUserId,
|
|
236
|
+
})
|
|
237
|
+
.returning();
|
|
238
|
+
|
|
239
|
+
const result = await embeddingModel.findById(otherEmbedding.id);
|
|
240
|
+
|
|
241
|
+
expect(result).toBeUndefined();
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('countUsage', () => {
|
|
246
|
+
it('should return the count of embeddings for the user', async () => {
|
|
247
|
+
const [chunk1, chunk2, chunk3] = await serverDB
|
|
248
|
+
.insert(chunks)
|
|
249
|
+
.values([
|
|
250
|
+
{ text: 'Test chunk 1', userId },
|
|
251
|
+
{ text: 'Test chunk 2', userId },
|
|
252
|
+
{ text: 'Test chunk 3', userId },
|
|
253
|
+
])
|
|
254
|
+
.returning();
|
|
255
|
+
|
|
256
|
+
await serverDB.insert(embeddings).values([
|
|
257
|
+
{ chunkId: chunk1.id, embeddings: designThinkingQuery, userId },
|
|
258
|
+
{ chunkId: chunk2.id, embeddings: designThinkingQuery, userId },
|
|
259
|
+
{ chunkId: chunk3.id, embeddings: designThinkingQuery, userId },
|
|
260
|
+
]);
|
|
261
|
+
|
|
262
|
+
const count = await embeddingModel.countUsage();
|
|
263
|
+
|
|
264
|
+
expect(count).toBe(3);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should return 0 when user has no embeddings', async () => {
|
|
268
|
+
const count = await embeddingModel.countUsage();
|
|
269
|
+
|
|
270
|
+
expect(count).toBe(0);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should only count embeddings for the current user', async () => {
|
|
274
|
+
const [chunk1] = await serverDB
|
|
275
|
+
.insert(chunks)
|
|
276
|
+
.values([{ text: 'Test chunk 1', userId }])
|
|
277
|
+
.returning();
|
|
278
|
+
|
|
279
|
+
const [chunk2] = await serverDB
|
|
280
|
+
.insert(chunks)
|
|
281
|
+
.values([{ text: 'Other user chunk', userId: otherUserId }])
|
|
282
|
+
.returning();
|
|
283
|
+
|
|
284
|
+
await serverDB.insert(embeddings).values([
|
|
285
|
+
{ chunkId: chunk1.id, embeddings: designThinkingQuery, userId },
|
|
286
|
+
{ chunkId: chunk2.id, embeddings: designThinkingQuery, userId: otherUserId },
|
|
287
|
+
]);
|
|
288
|
+
|
|
289
|
+
const count = await embeddingModel.countUsage();
|
|
290
|
+
|
|
291
|
+
expect(count).toBe(1);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
});
|