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
package/src/index.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {z} from "zod";
|
|
2
|
+
|
|
3
|
+
const snippetStatisticsSchema = z.object({
|
|
4
|
+
sentences: z.number(),
|
|
5
|
+
words: z.number(),
|
|
6
|
+
characters: z.number(),
|
|
7
|
+
links: z.number().optional(),
|
|
8
|
+
images: z.number().optional(),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const snippetSchema = z.object({
|
|
12
|
+
url_hash: z.string(),
|
|
13
|
+
snippet_hash: z.string(),
|
|
14
|
+
prev_snippet_hash: z.string().nullable(),
|
|
15
|
+
next_snippet_hash: z.string().nullable(),
|
|
16
|
+
content: z.string(),
|
|
17
|
+
words: z.string().optional(),
|
|
18
|
+
language: z.string(),
|
|
19
|
+
statistics: snippetStatisticsSchema.optional(),
|
|
20
|
+
links: z.record(z.string(), z.string()).optional(),
|
|
21
|
+
images: z.array(z.string()).optional(),
|
|
22
|
+
});
|
|
23
|
+
export interface Snippet extends z.infer<typeof snippetSchema> {}
|
|
24
|
+
|
|
25
|
+
const scrapeStatisticsSchema = z.object({
|
|
26
|
+
snippets: z.number(),
|
|
27
|
+
sentences: z.number(),
|
|
28
|
+
words: z.number(),
|
|
29
|
+
characters: z.number(),
|
|
30
|
+
images: z.number(),
|
|
31
|
+
videos: z.number(),
|
|
32
|
+
audio: z.number(),
|
|
33
|
+
tables: z.number(),
|
|
34
|
+
lists: z.number(),
|
|
35
|
+
blocks: z.number(),
|
|
36
|
+
links: z.number(),
|
|
37
|
+
files: z.number(),
|
|
38
|
+
});
|
|
39
|
+
export type ScrapeStatistics = z.infer<typeof scrapeStatisticsSchema>;
|
|
40
|
+
|
|
41
|
+
const scrapePageSchema = z.object({
|
|
42
|
+
title: z.string(),
|
|
43
|
+
description: z.string(),
|
|
44
|
+
author: z.string(),
|
|
45
|
+
published: z.coerce.date(),
|
|
46
|
+
visited: z.coerce.date(),
|
|
47
|
+
certain: z.boolean().optional(),
|
|
48
|
+
});
|
|
49
|
+
export type ScrapePage = z.infer<typeof scrapePageSchema>;
|
|
50
|
+
|
|
51
|
+
const querySchema = z.object({});
|
|
52
|
+
|
|
53
|
+
const scrapeRequestSchema = z.object({
|
|
54
|
+
raw_url: z.string(),
|
|
55
|
+
url: z.string(),
|
|
56
|
+
hash: z.string(),
|
|
57
|
+
geo: z.string(),
|
|
58
|
+
proxy: z.string(),
|
|
59
|
+
scheme: z.string(),
|
|
60
|
+
netloc: z.string(),
|
|
61
|
+
prefix: z.string(),
|
|
62
|
+
domain: z.string(),
|
|
63
|
+
suffix: z.string(),
|
|
64
|
+
path: z.string(),
|
|
65
|
+
query: z.string(),
|
|
66
|
+
fragment: z.string(),
|
|
67
|
+
query_allowed: querySchema,
|
|
68
|
+
query_blocked: querySchema,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const metadataSchema = z.record(z.string(), z.any());
|
|
72
|
+
|
|
73
|
+
const logoSchema = z.object({
|
|
74
|
+
"@type": z.string().optional(),
|
|
75
|
+
"@id": z.string().optional(),
|
|
76
|
+
caption: z.string().optional(),
|
|
77
|
+
inLanguage: z.string().optional(),
|
|
78
|
+
url: z.string(),
|
|
79
|
+
width: z.union([z.string(), z.number()]).optional(),
|
|
80
|
+
height: z.union([z.string(), z.number()]).optional(),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const publisherSchema = z.object({
|
|
84
|
+
"@type": z.string(),
|
|
85
|
+
name: z.string().optional(),
|
|
86
|
+
url: z.string().optional(),
|
|
87
|
+
logo: z.union([logoSchema, z.string()]).optional(),
|
|
88
|
+
sameAs: z.union([z.array(z.string()), z.string()]).optional(),
|
|
89
|
+
});
|
|
90
|
+
const structuredSchema = z.object({
|
|
91
|
+
"@context": z.string().optional(),
|
|
92
|
+
"@type": z.string().optional(),
|
|
93
|
+
headline: z.string().optional(),
|
|
94
|
+
publisher: publisherSchema.optional(),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const urlTreeSchema = z.record(z.string(), z.any());
|
|
98
|
+
|
|
99
|
+
export const scrapeResponseSchema = z.object({
|
|
100
|
+
request: scrapeRequestSchema,
|
|
101
|
+
page: scrapePageSchema,
|
|
102
|
+
statistics: scrapeStatisticsSchema,
|
|
103
|
+
languages: z.record(z.string(), z.number()),
|
|
104
|
+
snippets: z.record(z.string(), snippetSchema),
|
|
105
|
+
full_text: z.string(),
|
|
106
|
+
metadata: metadataSchema,
|
|
107
|
+
structured: z.array(structuredSchema),
|
|
108
|
+
url_tree: urlTreeSchema,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export const scrapeFullResSchema = z.object({
|
|
112
|
+
message: z.string(),
|
|
113
|
+
added_to_batch: z.boolean(),
|
|
114
|
+
response: scrapeResponseSchema,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
export type ScrapeFullRes = z.infer<typeof scrapeFullResSchema>;
|
|
118
|
+
|
|
119
|
+
export type ScrapeResponse = z.infer<typeof scrapeResponseSchema>;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import {describe, test, expect, afterEach} from "bun:test";
|
|
2
|
+
import {mapSnippetToArray, WebPageData} from "./webPageData";
|
|
3
|
+
import type {ScrapeResponse, Snippet} from "./types";
|
|
4
|
+
import type {NosibleClient} from "../client";
|
|
5
|
+
import {readFile, unlink} from "node:fs/promises";
|
|
6
|
+
|
|
7
|
+
// Helper function to create a mock snippet
|
|
8
|
+
export const createMockSnippet = (
|
|
9
|
+
snippet_hash: string,
|
|
10
|
+
prev_snippet_hash: string | null,
|
|
11
|
+
next_snippet_hash: string | null,
|
|
12
|
+
content: string = "Test content"
|
|
13
|
+
): Snippet => ({
|
|
14
|
+
url_hash: "test_url_hash",
|
|
15
|
+
snippet_hash,
|
|
16
|
+
prev_snippet_hash,
|
|
17
|
+
next_snippet_hash,
|
|
18
|
+
content,
|
|
19
|
+
language: "en",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Helper function to create a minimal mock ScrapeResponse
|
|
23
|
+
export const createMockScrapeResponse = (
|
|
24
|
+
snippets: Record<string, Snippet>
|
|
25
|
+
): ScrapeResponse => ({
|
|
26
|
+
request: {
|
|
27
|
+
raw_url: "https://example.com",
|
|
28
|
+
url: "https://example.com",
|
|
29
|
+
hash: "test_hash",
|
|
30
|
+
geo: "US",
|
|
31
|
+
proxy: "none",
|
|
32
|
+
scheme: "https",
|
|
33
|
+
netloc: "example.com",
|
|
34
|
+
prefix: "www",
|
|
35
|
+
domain: "example",
|
|
36
|
+
suffix: "com",
|
|
37
|
+
path: "/",
|
|
38
|
+
query: "",
|
|
39
|
+
fragment: "",
|
|
40
|
+
query_allowed: {},
|
|
41
|
+
query_blocked: {},
|
|
42
|
+
},
|
|
43
|
+
page: {
|
|
44
|
+
title: "Test Page",
|
|
45
|
+
description: "Test Description",
|
|
46
|
+
author: "Test Author",
|
|
47
|
+
published: new Date("2024-01-01"),
|
|
48
|
+
visited: new Date("2024-01-02"),
|
|
49
|
+
},
|
|
50
|
+
statistics: {
|
|
51
|
+
snippets: Object.keys(snippets).length,
|
|
52
|
+
sentences: 10,
|
|
53
|
+
words: 100,
|
|
54
|
+
characters: 500,
|
|
55
|
+
images: 0,
|
|
56
|
+
videos: 0,
|
|
57
|
+
audio: 0,
|
|
58
|
+
tables: 0,
|
|
59
|
+
lists: 0,
|
|
60
|
+
blocks: 5,
|
|
61
|
+
links: 3,
|
|
62
|
+
files: 0,
|
|
63
|
+
},
|
|
64
|
+
languages: {en: 1.0},
|
|
65
|
+
snippets,
|
|
66
|
+
full_text: "Test full text",
|
|
67
|
+
metadata: {},
|
|
68
|
+
structured: [],
|
|
69
|
+
url_tree: {},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("mapSnippetToArray", () => {
|
|
73
|
+
test("should correctly order a simple chain of 3 snippets", () => {
|
|
74
|
+
const snippet1 = createMockSnippet("hash1", null, "hash2", "First snippet");
|
|
75
|
+
const snippet2 = createMockSnippet(
|
|
76
|
+
"hash2",
|
|
77
|
+
"hash1",
|
|
78
|
+
"hash3",
|
|
79
|
+
"Second snippet"
|
|
80
|
+
);
|
|
81
|
+
const snippet3 = createMockSnippet("hash3", "hash2", null, "Third snippet");
|
|
82
|
+
|
|
83
|
+
const mockData = createMockScrapeResponse({
|
|
84
|
+
hash1: snippet1,
|
|
85
|
+
hash2: snippet2,
|
|
86
|
+
hash3: snippet3,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const result = mapSnippetToArray(mockData);
|
|
90
|
+
|
|
91
|
+
expect(result).toHaveLength(3);
|
|
92
|
+
expect(result[0]!.snippet_hash).toBe("hash1");
|
|
93
|
+
expect(result[1]!.snippet_hash).toBe("hash2");
|
|
94
|
+
expect(result[2]!.snippet_hash).toBe("hash3");
|
|
95
|
+
expect(result[0]!.content).toBe("First snippet");
|
|
96
|
+
expect(result[1]!.content).toBe("Second snippet");
|
|
97
|
+
expect(result[2]!.content).toBe("Third snippet");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("should handle a single snippet", () => {
|
|
101
|
+
const snippet1 = createMockSnippet("hash1", null, null, "Only snippet");
|
|
102
|
+
|
|
103
|
+
const mockData = createMockScrapeResponse({
|
|
104
|
+
hash1: snippet1,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const result = mapSnippetToArray(mockData);
|
|
108
|
+
|
|
109
|
+
expect(result).toHaveLength(1);
|
|
110
|
+
expect(result[0]!.snippet_hash).toBe("hash1");
|
|
111
|
+
expect(result[0]!.content).toBe("Only snippet");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("should correctly order snippets even when provided in random order", () => {
|
|
115
|
+
const snippet1 = createMockSnippet("hash1", null, "hash2");
|
|
116
|
+
const snippet2 = createMockSnippet("hash2", "hash1", "hash3");
|
|
117
|
+
const snippet3 = createMockSnippet("hash3", "hash2", "hash4");
|
|
118
|
+
const snippet4 = createMockSnippet("hash4", "hash3", null);
|
|
119
|
+
|
|
120
|
+
// Provide snippets in non-sequential order
|
|
121
|
+
const mockData = createMockScrapeResponse({
|
|
122
|
+
hash3: snippet3,
|
|
123
|
+
hash1: snippet1,
|
|
124
|
+
hash4: snippet4,
|
|
125
|
+
hash2: snippet2,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const result = mapSnippetToArray(mockData);
|
|
129
|
+
|
|
130
|
+
expect(result).toHaveLength(4);
|
|
131
|
+
expect(result[0]!.snippet_hash).toBe("hash1");
|
|
132
|
+
expect(result[1]!.snippet_hash).toBe("hash2");
|
|
133
|
+
expect(result[2]!.snippet_hash).toBe("hash3");
|
|
134
|
+
expect(result[3]!.snippet_hash).toBe("hash4");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("should throw error when no first snippet is found", () => {
|
|
138
|
+
// All snippets have a prev_snippet_hash, so none is first
|
|
139
|
+
const snippet1 = createMockSnippet("hash1", "hash0", "hash2");
|
|
140
|
+
const snippet2 = createMockSnippet("hash2", "hash1", null);
|
|
141
|
+
|
|
142
|
+
const mockData = createMockScrapeResponse({
|
|
143
|
+
hash1: snippet1,
|
|
144
|
+
hash2: snippet2,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(() => mapSnippetToArray(mockData)).toThrow("No first snippet found");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("should throw error when chain is broken (missing next snippet)", () => {
|
|
151
|
+
const snippet1 = createMockSnippet("hash1", null, "hash2");
|
|
152
|
+
const snippet3 = createMockSnippet("hash3", "hash2", null);
|
|
153
|
+
// hash2 is missing, but hash1 points to it
|
|
154
|
+
|
|
155
|
+
const mockData = createMockScrapeResponse({
|
|
156
|
+
hash1: snippet1,
|
|
157
|
+
hash3: snippet3,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(() => mapSnippetToArray(mockData)).toThrow("No next snippet found");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("should handle long chain of snippets", () => {
|
|
164
|
+
const snippets: Record<string, Snippet> = {};
|
|
165
|
+
const count = 100;
|
|
166
|
+
|
|
167
|
+
for (let i = 0; i < count; i++) {
|
|
168
|
+
const hash = `hash${i}`;
|
|
169
|
+
const prevHash = i === 0 ? null : `hash${i - 1}`;
|
|
170
|
+
const nextHash = i === count - 1 ? null : `hash${i + 1}`;
|
|
171
|
+
snippets[hash] = createMockSnippet(
|
|
172
|
+
hash,
|
|
173
|
+
prevHash,
|
|
174
|
+
nextHash,
|
|
175
|
+
`Snippet ${i}`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const mockData = createMockScrapeResponse(snippets);
|
|
180
|
+
const result = mapSnippetToArray(mockData);
|
|
181
|
+
|
|
182
|
+
expect(result).toHaveLength(count);
|
|
183
|
+
for (let i = 0; i < count; i++) {
|
|
184
|
+
expect(result[i]!.snippet_hash).toBe(`hash${i}`);
|
|
185
|
+
expect(result[i]!.content).toBe(`Snippet ${i}`);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("should maintain snippet properties during ordering", () => {
|
|
190
|
+
const snippet1 = createMockSnippet("hash1", null, "hash2");
|
|
191
|
+
snippet1.words = "test words";
|
|
192
|
+
snippet1.statistics = {
|
|
193
|
+
sentences: 5,
|
|
194
|
+
words: 50,
|
|
195
|
+
characters: 250,
|
|
196
|
+
links: 2,
|
|
197
|
+
images: 1,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const snippet2 = createMockSnippet("hash2", "hash1", null);
|
|
201
|
+
|
|
202
|
+
const mockData = createMockScrapeResponse({
|
|
203
|
+
hash1: snippet1,
|
|
204
|
+
hash2: snippet2,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const result = mapSnippetToArray(mockData);
|
|
208
|
+
|
|
209
|
+
expect(result[0]!.words).toBe("test words");
|
|
210
|
+
expect(result[0]!.statistics).toEqual({
|
|
211
|
+
sentences: 5,
|
|
212
|
+
words: 50,
|
|
213
|
+
characters: 250,
|
|
214
|
+
links: 2,
|
|
215
|
+
images: 1,
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("should return empty array when no snippets are provided", () => {
|
|
220
|
+
const mockData = createMockScrapeResponse({});
|
|
221
|
+
|
|
222
|
+
const result = mapSnippetToArray(mockData);
|
|
223
|
+
|
|
224
|
+
expect(result).toEqual([]);
|
|
225
|
+
expect(result).toHaveLength(0);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("WebPageData round trip test", () => {
|
|
230
|
+
const testFiles: string[] = [];
|
|
231
|
+
|
|
232
|
+
afterEach(async () => {
|
|
233
|
+
// Clean up test files
|
|
234
|
+
for (const file of testFiles) {
|
|
235
|
+
try {
|
|
236
|
+
await unlink(file);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
// Ignore errors if file doesn't exist
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
testFiles.length = 0; // Clear the array
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("should serialize and deserialize WebPageData without data loss", async () => {
|
|
245
|
+
// Create a mock client
|
|
246
|
+
const mockClient = {} as NosibleClient;
|
|
247
|
+
|
|
248
|
+
// Create mock snippets
|
|
249
|
+
const snippet1 = createMockSnippet("hash1", null, "hash2", "First snippet");
|
|
250
|
+
const snippet2 = createMockSnippet(
|
|
251
|
+
"hash2",
|
|
252
|
+
"hash1",
|
|
253
|
+
"hash3",
|
|
254
|
+
"Second snippet"
|
|
255
|
+
);
|
|
256
|
+
const snippet3 = createMockSnippet("hash3", "hash2", null, "Third snippet");
|
|
257
|
+
|
|
258
|
+
// Create a comprehensive mock ScrapeResponse
|
|
259
|
+
const mockResponse = createMockScrapeResponse({
|
|
260
|
+
hash1: snippet1,
|
|
261
|
+
hash2: snippet2,
|
|
262
|
+
hash3: snippet3,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Step 1: Create WebPageData from scrape response
|
|
266
|
+
const webPageData1 = new WebPageData(mockClient, mockResponse);
|
|
267
|
+
|
|
268
|
+
// Step 2: Write to JSON file (first time)
|
|
269
|
+
const jsonPath1 = "/tmp/test-webpage-data-1.json";
|
|
270
|
+
testFiles.push(jsonPath1);
|
|
271
|
+
await webPageData1.writeJson(jsonPath1);
|
|
272
|
+
|
|
273
|
+
// Step 3: Read JSON file and create new WebPageData
|
|
274
|
+
const webPageData2 = await WebPageData.fromJson(mockClient, jsonPath1);
|
|
275
|
+
|
|
276
|
+
// Step 4: Write to JSON file again (second time)
|
|
277
|
+
const jsonPath2 = "/tmp/test-webpage-data-2.json";
|
|
278
|
+
testFiles.push(jsonPath2);
|
|
279
|
+
await webPageData2.writeJson(jsonPath2);
|
|
280
|
+
|
|
281
|
+
// Step 5: Compare both JSON files
|
|
282
|
+
const json1Content = await readFile(jsonPath1, "utf-8");
|
|
283
|
+
const json2Content = await readFile(jsonPath2, "utf-8");
|
|
284
|
+
|
|
285
|
+
// Parse both JSON files to compare them properly
|
|
286
|
+
const json1Data = JSON.parse(json1Content);
|
|
287
|
+
const json2Data = JSON.parse(json2Content);
|
|
288
|
+
|
|
289
|
+
// The two JSON objects should be identical
|
|
290
|
+
expect(json2Data).toEqual(json1Data);
|
|
291
|
+
|
|
292
|
+
// Also verify that the WebPageData instances have the same data
|
|
293
|
+
expect(webPageData2.data).toEqual(webPageData1.data);
|
|
294
|
+
expect(webPageData2.page).toEqual(webPageData1.page);
|
|
295
|
+
expect(webPageData2.fullText).toEqual(webPageData1.fullText);
|
|
296
|
+
expect(webPageData2.metadata).toEqual(webPageData1.metadata);
|
|
297
|
+
expect(webPageData2.structured).toEqual(webPageData1.structured);
|
|
298
|
+
expect(webPageData2.urlTree).toEqual(webPageData1.urlTree);
|
|
299
|
+
expect(webPageData2.statistics).toEqual(webPageData1.statistics);
|
|
300
|
+
expect(webPageData2.getSnippets()).toEqual(webPageData1.getSnippets());
|
|
301
|
+
});
|
|
302
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import type {
|
|
3
|
+
ScrapePage,
|
|
4
|
+
ScrapeResponse,
|
|
5
|
+
ScrapeStatistics,
|
|
6
|
+
Snippet,
|
|
7
|
+
} from "./types";
|
|
8
|
+
import {scrapeResponseSchema} from "./types";
|
|
9
|
+
import type {NosibleClient} from "../client";
|
|
10
|
+
import {exportJson, importJson} from "../utils/file";
|
|
11
|
+
|
|
12
|
+
export class WebPageData {
|
|
13
|
+
private client: NosibleClient;
|
|
14
|
+
public data: ScrapeResponse;
|
|
15
|
+
private snippets: Snippet[];
|
|
16
|
+
public page: ScrapePage;
|
|
17
|
+
public fullText: string;
|
|
18
|
+
public metadata: Record<string, any>;
|
|
19
|
+
public structured: any[];
|
|
20
|
+
public urlTree: Record<string, any>;
|
|
21
|
+
public statistics: ScrapeStatistics;
|
|
22
|
+
constructor(client: NosibleClient, res: ScrapeResponse) {
|
|
23
|
+
this.client = client;
|
|
24
|
+
this.data = res;
|
|
25
|
+
this.snippets = mapSnippetToArray(res);
|
|
26
|
+
this.page = this.data.page;
|
|
27
|
+
this.fullText = this.data.full_text;
|
|
28
|
+
this.metadata = this.data.metadata;
|
|
29
|
+
this.structured = this.data.structured;
|
|
30
|
+
this.urlTree = this.data.url_tree;
|
|
31
|
+
this.statistics = this.data.statistics;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static async fromJson(
|
|
35
|
+
client: NosibleClient,
|
|
36
|
+
inputPath: string
|
|
37
|
+
): Promise<WebPageData> {
|
|
38
|
+
const data = await importJson({filePath: inputPath});
|
|
39
|
+
const validData = scrapeResponseSchema.parse(data) as ScrapeResponse;
|
|
40
|
+
return new WebPageData(client, validData);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public async writeJson(outputPath: string): Promise<void> {
|
|
44
|
+
return await exportJson(this.data, outputPath);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/*
|
|
48
|
+
* Returns an ordered array of snippets
|
|
49
|
+
* @returns {Snippet[]} An ordered array of snippets
|
|
50
|
+
*/
|
|
51
|
+
public getSnippets(): Snippet[] {
|
|
52
|
+
return this.snippets;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/*
|
|
56
|
+
* Returns a snippet by hash key
|
|
57
|
+
* @param {string} hashKey - The hash key of the snippet
|
|
58
|
+
* @returns {Snippet | undefined} The snippet with the specified hash key
|
|
59
|
+
*/
|
|
60
|
+
public getSnippet(hashKey: string): Snippet | undefined {
|
|
61
|
+
return this.snippets.find((snippet) => snippet.url_hash === hashKey);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const mapSnippetToArray = (data: ScrapeResponse) => {
|
|
66
|
+
const snippets: Snippet[] = [];
|
|
67
|
+
const snippetKeys = Object.keys(data.snippets);
|
|
68
|
+
|
|
69
|
+
if (snippetKeys.length === 0) {
|
|
70
|
+
return snippets; // Return empty array if no snippets
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Unsorted snippets
|
|
74
|
+
const unsortedSnippets: Snippet[] = [];
|
|
75
|
+
snippetKeys.forEach((key) => {
|
|
76
|
+
unsortedSnippets.push(data.snippets[key] as Snippet);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Find first snippet
|
|
80
|
+
const first = unsortedSnippets.find(
|
|
81
|
+
(snippet) => snippet.prev_snippet_hash === null
|
|
82
|
+
);
|
|
83
|
+
if (!first) {
|
|
84
|
+
throw new Error("No first snippet found");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Add first snippet to array
|
|
88
|
+
snippets.push(first);
|
|
89
|
+
|
|
90
|
+
// Add remaining snippets to array
|
|
91
|
+
let current = first;
|
|
92
|
+
while (current.next_snippet_hash) {
|
|
93
|
+
const next = unsortedSnippets.find(
|
|
94
|
+
(snippet) => snippet.snippet_hash === current.next_snippet_hash
|
|
95
|
+
);
|
|
96
|
+
if (!next) {
|
|
97
|
+
throw new Error("No next snippet found");
|
|
98
|
+
}
|
|
99
|
+
snippets.push(next);
|
|
100
|
+
current = next;
|
|
101
|
+
}
|
|
102
|
+
return snippets;
|
|
103
|
+
};
|