nosible 0.1.5
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/README.md +438 -0
- package/dist/index.cjs +1841 -0
- package/dist/index.cjs.map +29 -0
- package/dist/index.js +1815 -0
- package/dist/index.js.map +29 -0
- package/package.json +63 -0
- package/src/api/api.test.ts +366 -0
- package/src/api/index.ts +179 -0
- package/src/api/schemas.ts +152 -0
- package/src/client.test.ts +685 -0
- package/src/client.ts +762 -0
- package/src/index.ts +4 -0
- package/src/scrape/types.ts +119 -0
- package/src/scrape/webPageData.test.ts +302 -0
- package/src/scrape/webPageData.ts +103 -0
- package/src/search/analyze.test.ts +396 -0
- package/src/search/analyze.ts +151 -0
- package/src/search/bulkSearch.ts +62 -0
- package/src/search/result.test.ts +423 -0
- package/src/search/result.ts +391 -0
- package/src/search/result.types.ts +32 -0
- package/src/search/resultFactory.ts +21 -0
- package/src/search/resultSet.io.test.ts +320 -0
- package/src/search/resultSet.test.ts +368 -0
- package/src/search/resultSet.ts +387 -0
- package/src/search/resultSet.types.ts +3 -0
- package/src/search/search.test.ts +299 -0
- package/src/search/search.ts +187 -0
- package/src/search/searchSet.io.test.ts +321 -0
- package/src/search/searchSet.ts +122 -0
- package/src/search/sqlFilter.test.ts +129 -0
- package/src/search/sqlFilter.ts +147 -0
- package/src/test-utils/mocks.ts +159 -0
- package/src/topicTrend/topicTrend.ts +53 -0
- package/src/utils/browser.test.ts +209 -0
- package/src/utils/browser.ts +21 -0
- package/src/utils/fernet.ts +47 -0
- package/src/utils/file.test.ts +81 -0
- package/src/utils/file.ts +195 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/llm.test.ts +279 -0
- package/src/utils/llm.ts +244 -0
- package/src/utils/userPlan.test.ts +332 -0
- package/src/utils/userPlan.ts +211 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import {describe, expect, test} from "bun:test";
|
|
2
|
+
import {analyzeResults, AnalyzeBy} from "./analyze";
|
|
3
|
+
import {Result} from "./result";
|
|
4
|
+
import {NosibleClient} from "../client";
|
|
5
|
+
import {createMockClient} from "../test-utils/mocks";
|
|
6
|
+
import type {SearchResponse} from "../api/schemas";
|
|
7
|
+
|
|
8
|
+
describe("analyzeResults", () => {
|
|
9
|
+
const mockSearchResults: SearchResponse[] = [
|
|
10
|
+
{
|
|
11
|
+
url_hash: "hash1",
|
|
12
|
+
url: "https://example.com/article1",
|
|
13
|
+
netloc: "example.com",
|
|
14
|
+
published: "2023-01-15T10:00:00Z",
|
|
15
|
+
visited: "2023-01-16T12:00:00Z",
|
|
16
|
+
language: "en",
|
|
17
|
+
author: "John Doe",
|
|
18
|
+
title: "Test Article 1",
|
|
19
|
+
description: "Description 1",
|
|
20
|
+
content: "Content 1",
|
|
21
|
+
best_chunk: "Chunk 1",
|
|
22
|
+
semantics: {
|
|
23
|
+
origin_shard: 1,
|
|
24
|
+
chunks_total: 10,
|
|
25
|
+
chunks_matched: 5,
|
|
26
|
+
chunks_kept: 3,
|
|
27
|
+
similarity: 0.85,
|
|
28
|
+
bitstring: "101010",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
url_hash: "hash2",
|
|
33
|
+
url: "https://test.com/article2",
|
|
34
|
+
netloc: "test.com",
|
|
35
|
+
published: "2023-02-20T14:30:00Z",
|
|
36
|
+
visited: "2023-02-21T16:00:00Z",
|
|
37
|
+
language: "en",
|
|
38
|
+
author: "Jane Smith",
|
|
39
|
+
title: "Test Article 2",
|
|
40
|
+
description: "Description 2",
|
|
41
|
+
content: "Content 2",
|
|
42
|
+
best_chunk: "Chunk 2",
|
|
43
|
+
semantics: {
|
|
44
|
+
origin_shard: 2,
|
|
45
|
+
chunks_total: 8,
|
|
46
|
+
chunks_matched: 4,
|
|
47
|
+
chunks_kept: 2,
|
|
48
|
+
similarity: 0.92,
|
|
49
|
+
bitstring: "110011",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
url_hash: "hash3",
|
|
54
|
+
url: "https://example.com/article3",
|
|
55
|
+
netloc: "example.com",
|
|
56
|
+
published: "2023-03-10T09:15:00Z",
|
|
57
|
+
visited: "2023-03-11T11:30:00Z",
|
|
58
|
+
language: "fr",
|
|
59
|
+
author: "",
|
|
60
|
+
title: "Test Article 3",
|
|
61
|
+
description: "Description 3",
|
|
62
|
+
content: "Content 3",
|
|
63
|
+
best_chunk: "Chunk 3",
|
|
64
|
+
semantics: {
|
|
65
|
+
origin_shard: 1,
|
|
66
|
+
chunks_total: 12,
|
|
67
|
+
chunks_matched: 6,
|
|
68
|
+
chunks_kept: 4,
|
|
69
|
+
similarity: 0.78,
|
|
70
|
+
bitstring: "111000",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
// Convert SearchResponse objects to Result instances
|
|
76
|
+
const mockClient = createMockClient() as unknown as NosibleClient;
|
|
77
|
+
const mockResults = mockSearchResults.map(
|
|
78
|
+
(searchResponse) => new Result({client: mockClient, result: searchResponse})
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
test("analyzes by netloc", () => {
|
|
82
|
+
const result = analyzeResults(mockResults, AnalyzeBy.netloc);
|
|
83
|
+
expect(result).toEqual({
|
|
84
|
+
"example.com": 2,
|
|
85
|
+
"test.com": 1,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("analyzes by published date", () => {
|
|
90
|
+
const result = analyzeResults(mockResults, AnalyzeBy.published);
|
|
91
|
+
expect(result).toEqual({
|
|
92
|
+
"2023-01": 1,
|
|
93
|
+
"2023-02": 1,
|
|
94
|
+
"2023-03": 1,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("analyzes by visited date", () => {
|
|
99
|
+
const result = analyzeResults(mockResults, AnalyzeBy.visited);
|
|
100
|
+
expect(result).toEqual({
|
|
101
|
+
"2023-01": 1,
|
|
102
|
+
"2023-02": 1,
|
|
103
|
+
"2023-03": 1,
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("analyzes by author", () => {
|
|
108
|
+
const result = analyzeResults(mockResults, AnalyzeBy.author);
|
|
109
|
+
expect(result).toEqual({
|
|
110
|
+
"John Doe": 1,
|
|
111
|
+
"Jane Smith": 1,
|
|
112
|
+
"Unknown Author": 1,
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("analyzes by language", () => {
|
|
117
|
+
const result = analyzeResults(mockResults, AnalyzeBy.language);
|
|
118
|
+
expect(result).toEqual({
|
|
119
|
+
en: 2,
|
|
120
|
+
fr: 1,
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("analyzes by similarity", () => {
|
|
125
|
+
const result = analyzeResults(mockResults, AnalyzeBy.similarity);
|
|
126
|
+
expect(result).toHaveProperty("count", 3);
|
|
127
|
+
expect(result).toHaveProperty("null_count", 0);
|
|
128
|
+
expect(result).toHaveProperty("mean");
|
|
129
|
+
expect(result).toHaveProperty("std");
|
|
130
|
+
expect(result).toHaveProperty("min");
|
|
131
|
+
expect(result).toHaveProperty("q25");
|
|
132
|
+
expect(result).toHaveProperty("q50");
|
|
133
|
+
expect(result).toHaveProperty("q75");
|
|
134
|
+
expect(result).toHaveProperty("max");
|
|
135
|
+
|
|
136
|
+
// Check that the statistics are reasonable
|
|
137
|
+
expect((result as any).mean).toBeCloseTo(0.85, 1);
|
|
138
|
+
expect((result as any).min).toBe(0.78);
|
|
139
|
+
expect((result as any).max).toBe(0.92);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("handles empty results array", () => {
|
|
143
|
+
const result = analyzeResults([], AnalyzeBy.netloc);
|
|
144
|
+
expect(result).toEqual({});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("handles results with same month across different years", () => {
|
|
148
|
+
const resultsWithDifferentYears: SearchResponse[] = [
|
|
149
|
+
{
|
|
150
|
+
url_hash: "hash1",
|
|
151
|
+
url: "https://example.com/article1",
|
|
152
|
+
netloc: "example.com",
|
|
153
|
+
published: "2022-12-15T10:00:00Z",
|
|
154
|
+
visited: "2023-01-16T12:00:00Z",
|
|
155
|
+
language: "en",
|
|
156
|
+
author: "John Doe",
|
|
157
|
+
title: "Test Article 1",
|
|
158
|
+
description: "Description 1",
|
|
159
|
+
content: "Content 1",
|
|
160
|
+
best_chunk: "Chunk 1",
|
|
161
|
+
semantics: {
|
|
162
|
+
origin_shard: 1,
|
|
163
|
+
chunks_total: 10,
|
|
164
|
+
chunks_matched: 5,
|
|
165
|
+
chunks_kept: 3,
|
|
166
|
+
similarity: 0.85,
|
|
167
|
+
bitstring: "101010",
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
url_hash: "hash2",
|
|
172
|
+
url: "https://test.com/article2",
|
|
173
|
+
netloc: "test.com",
|
|
174
|
+
published: "2023-12-20T14:30:00Z",
|
|
175
|
+
visited: "2023-02-21T16:00:00Z",
|
|
176
|
+
language: "en",
|
|
177
|
+
author: "Jane Smith",
|
|
178
|
+
title: "Test Article 2",
|
|
179
|
+
description: "Description 2",
|
|
180
|
+
content: "Content 2",
|
|
181
|
+
best_chunk: "Chunk 2",
|
|
182
|
+
semantics: {
|
|
183
|
+
origin_shard: 2,
|
|
184
|
+
chunks_total: 8,
|
|
185
|
+
chunks_matched: 4,
|
|
186
|
+
chunks_kept: 2,
|
|
187
|
+
similarity: 0.92,
|
|
188
|
+
bitstring: "110011",
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
// Convert to Result instances
|
|
194
|
+
const resultInstances = resultsWithDifferentYears.map(
|
|
195
|
+
(searchResponse) => new Result({client: mockClient, result: searchResponse})
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const result = analyzeResults(
|
|
199
|
+
resultInstances,
|
|
200
|
+
AnalyzeBy.published
|
|
201
|
+
);
|
|
202
|
+
expect(result).toEqual({
|
|
203
|
+
"2022-12": 1,
|
|
204
|
+
"2023-01": 0,
|
|
205
|
+
"2023-02": 0,
|
|
206
|
+
"2023-03": 0,
|
|
207
|
+
"2023-04": 0,
|
|
208
|
+
"2023-05": 0,
|
|
209
|
+
"2023-06": 0,
|
|
210
|
+
"2023-07": 0,
|
|
211
|
+
"2023-08": 0,
|
|
212
|
+
"2023-09": 0,
|
|
213
|
+
"2023-10": 0,
|
|
214
|
+
"2023-11": 0,
|
|
215
|
+
"2023-12": 1,
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("handles results with missing author", () => {
|
|
220
|
+
const resultsWithMissingAuthor: SearchResponse[] = [
|
|
221
|
+
{
|
|
222
|
+
url_hash: "hash1",
|
|
223
|
+
url: "https://example.com/article1",
|
|
224
|
+
netloc: "example.com",
|
|
225
|
+
published: "2023-01-15T10:00:00Z",
|
|
226
|
+
visited: "2023-01-16T12:00:00Z",
|
|
227
|
+
language: "en",
|
|
228
|
+
author: "",
|
|
229
|
+
title: "Test Article 1",
|
|
230
|
+
description: "Description 1",
|
|
231
|
+
content: "Content 1",
|
|
232
|
+
best_chunk: "Chunk 1",
|
|
233
|
+
semantics: {
|
|
234
|
+
origin_shard: 1,
|
|
235
|
+
chunks_total: 10,
|
|
236
|
+
chunks_matched: 5,
|
|
237
|
+
chunks_kept: 3,
|
|
238
|
+
similarity: 0.85,
|
|
239
|
+
bitstring: "101010",
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
url_hash: "hash2",
|
|
244
|
+
url: "https://test.com/article2",
|
|
245
|
+
netloc: "test.com",
|
|
246
|
+
published: "2023-02-20T14:30:00Z",
|
|
247
|
+
visited: "2023-02-21T16:00:00Z",
|
|
248
|
+
language: "en",
|
|
249
|
+
author: "",
|
|
250
|
+
title: "Test Article 2",
|
|
251
|
+
description: "Description 2",
|
|
252
|
+
content: "Content 2",
|
|
253
|
+
best_chunk: "Chunk 2",
|
|
254
|
+
semantics: {
|
|
255
|
+
origin_shard: 2,
|
|
256
|
+
chunks_total: 8,
|
|
257
|
+
chunks_matched: 4,
|
|
258
|
+
chunks_kept: 2,
|
|
259
|
+
similarity: 0.92,
|
|
260
|
+
bitstring: "110011",
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
// Convert to Result instances
|
|
266
|
+
const resultInstances = resultsWithMissingAuthor.map(
|
|
267
|
+
(searchResponse) => new Result({client: mockClient, result: searchResponse})
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const result = analyzeResults(resultInstances, AnalyzeBy.author);
|
|
271
|
+
expect(result).toEqual({
|
|
272
|
+
"Unknown Author": 2,
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("handles similarity with null values", () => {
|
|
277
|
+
const resultsWithNullSimilarity: SearchResponse[] = [
|
|
278
|
+
{
|
|
279
|
+
url_hash: "hash1",
|
|
280
|
+
url: "https://example.com/article1",
|
|
281
|
+
netloc: "example.com",
|
|
282
|
+
published: "2023-01-15T10:00:00Z",
|
|
283
|
+
visited: "2023-01-16T12:00:00Z",
|
|
284
|
+
language: "en",
|
|
285
|
+
author: "John Doe",
|
|
286
|
+
title: "Test Article 1",
|
|
287
|
+
description: "Description 1",
|
|
288
|
+
content: "Content 1",
|
|
289
|
+
best_chunk: "Chunk 1",
|
|
290
|
+
semantics: {
|
|
291
|
+
origin_shard: 1,
|
|
292
|
+
chunks_total: 10,
|
|
293
|
+
chunks_matched: 5,
|
|
294
|
+
chunks_kept: 3,
|
|
295
|
+
similarity: 0.85,
|
|
296
|
+
bitstring: "101010",
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
url_hash: "hash2",
|
|
301
|
+
url: "https://test.com/article2",
|
|
302
|
+
netloc: "test.com",
|
|
303
|
+
published: "2023-02-20T14:30:00Z",
|
|
304
|
+
visited: "2023-02-21T16:00:00Z",
|
|
305
|
+
language: "en",
|
|
306
|
+
author: "Jane Smith",
|
|
307
|
+
title: "Test Article 2",
|
|
308
|
+
description: "Description 2",
|
|
309
|
+
content: "Content 2",
|
|
310
|
+
best_chunk: "Chunk 2",
|
|
311
|
+
semantics: {
|
|
312
|
+
origin_shard: 2,
|
|
313
|
+
chunks_total: 8,
|
|
314
|
+
chunks_matched: 4,
|
|
315
|
+
chunks_kept: 2,
|
|
316
|
+
similarity: null as any,
|
|
317
|
+
bitstring: "110011",
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
];
|
|
321
|
+
|
|
322
|
+
// Convert to Result instances
|
|
323
|
+
const resultInstances = resultsWithNullSimilarity.map(
|
|
324
|
+
(searchResponse) => new Result({client: mockClient, result: searchResponse})
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
const result = analyzeResults(
|
|
328
|
+
resultInstances,
|
|
329
|
+
AnalyzeBy.similarity
|
|
330
|
+
);
|
|
331
|
+
expect((result as any).count).toBe(2);
|
|
332
|
+
expect((result as any).null_count).toBe(1);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("handles invalid dates gracefully", () => {
|
|
336
|
+
const resultsWithInvalidDates: SearchResponse[] = [
|
|
337
|
+
{
|
|
338
|
+
url_hash: "hash1",
|
|
339
|
+
url: "https://example.com/article1",
|
|
340
|
+
netloc: "example.com",
|
|
341
|
+
published: "invalid-date",
|
|
342
|
+
visited: "2023-01-16T12:00:00Z",
|
|
343
|
+
language: "en",
|
|
344
|
+
author: "John Doe",
|
|
345
|
+
title: "Test Article 1",
|
|
346
|
+
description: "Description 1",
|
|
347
|
+
content: "Content 1",
|
|
348
|
+
best_chunk: "Chunk 1",
|
|
349
|
+
semantics: {
|
|
350
|
+
origin_shard: 1,
|
|
351
|
+
chunks_total: 10,
|
|
352
|
+
chunks_matched: 5,
|
|
353
|
+
chunks_kept: 3,
|
|
354
|
+
similarity: 0.85,
|
|
355
|
+
bitstring: "101010",
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
url_hash: "hash2",
|
|
360
|
+
url: "https://test.com/article2",
|
|
361
|
+
netloc: "test.com",
|
|
362
|
+
published: "2023-02-20T14:30:00Z",
|
|
363
|
+
visited: "2023-02-21T16:00:00Z",
|
|
364
|
+
language: "en",
|
|
365
|
+
author: "Jane Smith",
|
|
366
|
+
title: "Test Article 2",
|
|
367
|
+
description: "Description 2",
|
|
368
|
+
content: "Content 2",
|
|
369
|
+
best_chunk: "Chunk 2",
|
|
370
|
+
semantics: {
|
|
371
|
+
origin_shard: 2,
|
|
372
|
+
chunks_total: 8,
|
|
373
|
+
chunks_matched: 4,
|
|
374
|
+
chunks_kept: 2,
|
|
375
|
+
similarity: 0.92,
|
|
376
|
+
bitstring: "110011",
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
];
|
|
380
|
+
|
|
381
|
+
// Convert to Result instances
|
|
382
|
+
const resultInstances = resultsWithInvalidDates.map(
|
|
383
|
+
(searchResponse) => new Result({client: mockClient, result: searchResponse})
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
const result = analyzeResults(resultInstances, AnalyzeBy.published);
|
|
387
|
+
expect(result).toEqual({
|
|
388
|
+
"2023-02": 1,
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("handles default case", () => {
|
|
393
|
+
const result = analyzeResults(mockResults, 999 as AnalyzeBy);
|
|
394
|
+
expect(result).toEqual({});
|
|
395
|
+
});
|
|
396
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import {mean, standardDeviation, min, max, quantile} from "simple-statistics";
|
|
2
|
+
import type {Result} from "./result";
|
|
3
|
+
|
|
4
|
+
export enum AnalyzeBy {
|
|
5
|
+
netloc,
|
|
6
|
+
published,
|
|
7
|
+
visited,
|
|
8
|
+
author,
|
|
9
|
+
language,
|
|
10
|
+
similarity,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type AnalyzeResult =
|
|
14
|
+
| AnalyzeNumericResult
|
|
15
|
+
| AnalyzeCategoricalResult
|
|
16
|
+
| AnalyzeDateResult;
|
|
17
|
+
|
|
18
|
+
export const analyzeResults = (
|
|
19
|
+
results: Result[],
|
|
20
|
+
by: AnalyzeBy
|
|
21
|
+
): AnalyzeResult => {
|
|
22
|
+
switch (by) {
|
|
23
|
+
case AnalyzeBy.netloc:
|
|
24
|
+
const netlocValues = results.map((r) => r.netloc);
|
|
25
|
+
const netlocStats = analyzeCategorical(netlocValues);
|
|
26
|
+
return netlocStats;
|
|
27
|
+
case AnalyzeBy.published:
|
|
28
|
+
const publishedValues = results.map((r) => new Date(r.published));
|
|
29
|
+
const publishedStats = analyzeDateByMonth(publishedValues);
|
|
30
|
+
return publishedStats;
|
|
31
|
+
case AnalyzeBy.visited:
|
|
32
|
+
const visitedValues = results.map((r) => new Date(r.visited));
|
|
33
|
+
const visitedStats = analyzeDateByMonth(visitedValues);
|
|
34
|
+
return visitedStats;
|
|
35
|
+
case AnalyzeBy.author:
|
|
36
|
+
const authorValues = results.map((r) => {
|
|
37
|
+
if (r.author) {
|
|
38
|
+
return r.author;
|
|
39
|
+
} else {
|
|
40
|
+
return "Unknown Author";
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
const authorStats = analyzeCategorical(authorValues);
|
|
44
|
+
return authorStats;
|
|
45
|
+
case AnalyzeBy.language:
|
|
46
|
+
const languageValues = results.map((r) => r.language);
|
|
47
|
+
const languageStats = analyzeCategorical(languageValues);
|
|
48
|
+
return languageStats;
|
|
49
|
+
case AnalyzeBy.similarity:
|
|
50
|
+
const simValues = results.map((r) => r.semantics.similarity);
|
|
51
|
+
const simStats = analyzeNumeric(simValues);
|
|
52
|
+
return simStats;
|
|
53
|
+
default:
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type AnalyzeCategoricalResult = Record<string, number>;
|
|
59
|
+
|
|
60
|
+
const analyzeCategorical = (arr: string[]): AnalyzeCategoricalResult => {
|
|
61
|
+
const result: Record<string, number> = {};
|
|
62
|
+
for (const v of arr) {
|
|
63
|
+
if (result[v]) {
|
|
64
|
+
result[v] += 1;
|
|
65
|
+
} else {
|
|
66
|
+
result[v] = 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
type AnalyzeNumericResult = {
|
|
73
|
+
count: number;
|
|
74
|
+
null_count: number;
|
|
75
|
+
mean: number;
|
|
76
|
+
std: number;
|
|
77
|
+
min: number;
|
|
78
|
+
q25: number;
|
|
79
|
+
q50: number;
|
|
80
|
+
q75: number;
|
|
81
|
+
max: number;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const analyzeNumeric = (arr: number[]): AnalyzeNumericResult => {
|
|
85
|
+
const meanValue = mean(arr);
|
|
86
|
+
const stdValue = standardDeviation(arr);
|
|
87
|
+
const minValue = min(arr);
|
|
88
|
+
const maxValue = max(arr);
|
|
89
|
+
const q25 = quantile(arr, 0.25);
|
|
90
|
+
const q50 = quantile(arr, 0.5);
|
|
91
|
+
const q75 = quantile(arr, 0.75);
|
|
92
|
+
|
|
93
|
+
// TODO: confirm quantile to percentiles
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
count: arr.length,
|
|
97
|
+
null_count: arr.filter((v) => v === null).length,
|
|
98
|
+
mean: meanValue,
|
|
99
|
+
std: stdValue,
|
|
100
|
+
min: minValue,
|
|
101
|
+
q25: q25,
|
|
102
|
+
q50: q50,
|
|
103
|
+
q75: q75,
|
|
104
|
+
max: maxValue,
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
type AnalyzeDateResult = Record<string, number>;
|
|
109
|
+
|
|
110
|
+
const analyzeDateByMonth = (arr: Date[]): AnalyzeDateResult => {
|
|
111
|
+
if (!arr || arr.length === 0) return {};
|
|
112
|
+
|
|
113
|
+
let minDate: Date | null = null;
|
|
114
|
+
let maxDate: Date | null = null;
|
|
115
|
+
const counts: Record<string, number> = {};
|
|
116
|
+
|
|
117
|
+
for (const d of arr) {
|
|
118
|
+
if (!(d instanceof Date)) continue;
|
|
119
|
+
const time = d.getTime();
|
|
120
|
+
if (Number.isNaN(time)) continue;
|
|
121
|
+
|
|
122
|
+
if (!minDate || d < minDate) minDate = d;
|
|
123
|
+
if (!maxDate || d > maxDate) maxDate = d;
|
|
124
|
+
|
|
125
|
+
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(
|
|
126
|
+
2,
|
|
127
|
+
"0"
|
|
128
|
+
)}`;
|
|
129
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!minDate || !maxDate) return {};
|
|
133
|
+
|
|
134
|
+
const result: Record<string, number> = {};
|
|
135
|
+
let y = minDate.getFullYear();
|
|
136
|
+
let m = minDate.getMonth() + 1;
|
|
137
|
+
const endY = maxDate.getFullYear();
|
|
138
|
+
const endM = maxDate.getMonth() + 1;
|
|
139
|
+
|
|
140
|
+
while (y < endY || (y === endY && m <= endM)) {
|
|
141
|
+
const key = `${y}-${String(m).padStart(2, "0")}`;
|
|
142
|
+
result[key] = counts[key] ?? 0;
|
|
143
|
+
m += 1;
|
|
144
|
+
if (m > 12) {
|
|
145
|
+
m = 1;
|
|
146
|
+
y += 1;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return result;
|
|
151
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {gunzipSync} from "zlib";
|
|
2
|
+
import {fernetDecrypt} from "../utils/fernet";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Download and decrypt bulk search results
|
|
6
|
+
* @param data - Bulk search response containing download_from and decrypt_using
|
|
7
|
+
* @param filterResponses - Maximum number of results to return
|
|
8
|
+
* @returns ResultSet containing the search results
|
|
9
|
+
*/
|
|
10
|
+
export async function bulkSearchDownload({
|
|
11
|
+
downloadFrom,
|
|
12
|
+
decryptUsing,
|
|
13
|
+
}: {
|
|
14
|
+
downloadFrom: string;
|
|
15
|
+
decryptUsing: string;
|
|
16
|
+
}): Promise<any> {
|
|
17
|
+
// Replace .zstd. with .gzip. in download URL
|
|
18
|
+
if (downloadFrom.includes(".zstd.")) {
|
|
19
|
+
downloadFrom = downloadFrom.replace(".zstd.", ".gzip.");
|
|
20
|
+
} else {
|
|
21
|
+
throw new Error("Results were not retrieved from Nosible");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Retry up to 100 times with 10 second delays
|
|
25
|
+
try {
|
|
26
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
27
|
+
const response = await fetch(downloadFrom);
|
|
28
|
+
|
|
29
|
+
if (response.status === 200) {
|
|
30
|
+
try {
|
|
31
|
+
const responseData = await response.arrayBuffer();
|
|
32
|
+
const encryptedData = Buffer.from(responseData);
|
|
33
|
+
|
|
34
|
+
// Fernet tokens are base64-encoded, so decode first
|
|
35
|
+
const tokenBuffer = Buffer.from(
|
|
36
|
+
encryptedData.toString("utf-8"),
|
|
37
|
+
"base64"
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Decrypt using Fernet
|
|
41
|
+
const decrypted = fernetDecrypt(tokenBuffer, decryptUsing);
|
|
42
|
+
|
|
43
|
+
// Decompress using gzip
|
|
44
|
+
const decompressed = gunzipSync(decrypted);
|
|
45
|
+
|
|
46
|
+
// Parse JSON
|
|
47
|
+
const apiResp = JSON.parse(decompressed.toString("utf-8"));
|
|
48
|
+
|
|
49
|
+
return apiResp;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error("Error decrypting data:", error);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Wait 10 seconds before retrying
|
|
56
|
+
await new Promise((resolve) => setTimeout(resolve, 10000));
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error("Error downloading data:", error);
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|