@nitpicker/crawler 0.13.0 → 0.14.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.
Files changed (71) hide show
  1. package/lib/archive/archive-accessor.d.ts +56 -0
  2. package/lib/archive/archive-accessor.js +72 -0
  3. package/lib/archive/archive.d.ts +11 -0
  4. package/lib/archive/archive.js +13 -0
  5. package/lib/archive/create-adjunct-tables.d.ts +7 -0
  6. package/lib/archive/create-adjunct-tables.js +150 -0
  7. package/lib/archive/create-entity-tables.d.ts +14 -0
  8. package/lib/archive/create-entity-tables.js +32 -1
  9. package/lib/archive/database.d.ts +55 -1
  10. package/lib/archive/database.js +81 -0
  11. package/lib/archive/db-ops/analysis/replace-page-templates.d.ts +20 -0
  12. package/lib/archive/db-ops/analysis/replace-page-templates.js +52 -0
  13. package/lib/archive/db-ops/config/info-column-allowlist.js +1 -0
  14. package/lib/archive/db-ops/lifecycle/init.d.ts +16 -10
  15. package/lib/archive/db-ops/lifecycle/init.js +20 -10
  16. package/lib/archive/db-ops/meta/get-audios-of-page.d.ts +11 -0
  17. package/lib/archive/db-ops/meta/get-audios-of-page.js +15 -0
  18. package/lib/archive/db-ops/meta/get-buttons-of-page.d.ts +11 -0
  19. package/lib/archive/db-ops/meta/get-buttons-of-page.js +15 -0
  20. package/lib/archive/db-ops/meta/get-canvases-of-page.d.ts +11 -0
  21. package/lib/archive/db-ops/meta/get-canvases-of-page.js +15 -0
  22. package/lib/archive/db-ops/meta/get-headings-of-page.d.ts +11 -0
  23. package/lib/archive/db-ops/meta/get-headings-of-page.js +15 -0
  24. package/lib/archive/db-ops/meta/get-iframes-of-page.d.ts +11 -0
  25. package/lib/archive/db-ops/meta/get-iframes-of-page.js +15 -0
  26. package/lib/archive/db-ops/meta/get-main-content-images-of-page.d.ts +11 -0
  27. package/lib/archive/db-ops/meta/get-main-content-images-of-page.js +15 -0
  28. package/lib/archive/db-ops/meta/get-main-content-tables-of-page.d.ts +11 -0
  29. package/lib/archive/db-ops/meta/get-main-content-tables-of-page.js +15 -0
  30. package/lib/archive/db-ops/meta/get-videos-of-page.d.ts +11 -0
  31. package/lib/archive/db-ops/meta/get-videos-of-page.js +15 -0
  32. package/lib/archive/db-ops/pages/read/build-page-query.js +1 -1
  33. package/lib/archive/db-ops/pages/reset/repromote-external-pages.js +19 -10
  34. package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +5 -3
  35. package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +13 -3
  36. package/lib/archive/db-ops/pages/write/insert-audios.d.ts +14 -0
  37. package/lib/archive/db-ops/pages/write/insert-audios.js +25 -0
  38. package/lib/archive/db-ops/pages/write/insert-buttons.d.ts +14 -0
  39. package/lib/archive/db-ops/pages/write/insert-buttons.js +29 -0
  40. package/lib/archive/db-ops/pages/write/insert-canvases.d.ts +14 -0
  41. package/lib/archive/db-ops/pages/write/insert-canvases.js +26 -0
  42. package/lib/archive/db-ops/pages/write/insert-headings.d.ts +15 -0
  43. package/lib/archive/db-ops/pages/write/insert-headings.js +27 -0
  44. package/lib/archive/db-ops/pages/write/insert-iframes.d.ts +14 -0
  45. package/lib/archive/db-ops/pages/write/insert-iframes.js +28 -0
  46. package/lib/archive/db-ops/pages/write/insert-main-content-images.d.ts +20 -0
  47. package/lib/archive/db-ops/pages/write/insert-main-content-images.js +32 -0
  48. package/lib/archive/db-ops/pages/write/insert-main-content-tables.d.ts +14 -0
  49. package/lib/archive/db-ops/pages/write/insert-main-content-tables.js +29 -0
  50. package/lib/archive/db-ops/pages/write/insert-page.js +15 -2
  51. package/lib/archive/db-ops/pages/write/insert-videos.d.ts +14 -0
  52. package/lib/archive/db-ops/pages/write/insert-videos.js +28 -0
  53. package/lib/archive/db-ops/pages/write/update-page.js +21 -0
  54. package/lib/archive/init-schema.js +2 -1
  55. package/lib/archive/meta/compute-main-contents-denormalized.d.ts +22 -0
  56. package/lib/archive/meta/compute-main-contents-denormalized.js +63 -0
  57. package/lib/archive/meta/types.d.ts +263 -0
  58. package/lib/archive/migrate-info-main-content-selector.d.ts +11 -0
  59. package/lib/archive/migrate-info-main-content-selector.js +24 -0
  60. package/lib/archive/migrate-main-contents-columns.d.ts +21 -0
  61. package/lib/archive/migrate-main-contents-columns.js +50 -0
  62. package/lib/archive/page.d.ts +144 -2
  63. package/lib/archive/page.js +195 -0
  64. package/lib/archive/types.d.ts +39 -0
  65. package/lib/crawler/crawler.js +4 -2
  66. package/lib/crawler/fetch-destination.js +2 -0
  67. package/lib/crawler/link-to-page-data.js +2 -0
  68. package/lib/crawler/resource-to-page-data.js +2 -0
  69. package/lib/crawler/types.d.ts +5 -0
  70. package/lib/crawler-orchestrator.js +2 -0
  71. package/package.json +3 -3
@@ -8,6 +8,7 @@ import { emitError } from '../utils/error/emit-error.js';
8
8
  import { createWriteRefCaches } from './db-ops/_shared/create-write-ref-caches.js';
9
9
  import { retrySetting } from './db-ops/_shared/retry-setting.js';
10
10
  import { replaceAnalysisViolations as replaceAnalysisViolationsOp } from './db-ops/analysis/replace-analysis-violations.js';
11
+ import { replacePageTemplates as replacePageTemplatesOp } from './db-ops/analysis/replace-page-templates.js';
11
12
  import { getAnchorsOnPage as getAnchorsOnPageOp } from './db-ops/anchors/get-anchors-on-page.js';
12
13
  import { getBaseUrl as getBaseUrlOp } from './db-ops/config/get-base-url.js';
13
14
  import { getConfig as getConfigOp } from './db-ops/config/get-config.js';
@@ -22,8 +23,16 @@ import { recordInventoryRun as recordInventoryRunOp } from './db-ops/inventory/r
22
23
  import { checkpoint as checkpointOp } from './db-ops/lifecycle/checkpoint.js';
23
24
  import { destroy as destroyOp } from './db-ops/lifecycle/destroy.js';
24
25
  import { init as initOp } from './db-ops/lifecycle/init.js';
26
+ import { getAudiosOfPage as getAudiosOfPageOp } from './db-ops/meta/get-audios-of-page.js';
27
+ import { getButtonsOfPage as getButtonsOfPageOp } from './db-ops/meta/get-buttons-of-page.js';
28
+ import { getCanvasesOfPage as getCanvasesOfPageOp } from './db-ops/meta/get-canvases-of-page.js';
29
+ import { getHeadingsOfPage as getHeadingsOfPageOp } from './db-ops/meta/get-headings-of-page.js';
30
+ import { getIframesOfPage as getIframesOfPageOp } from './db-ops/meta/get-iframes-of-page.js';
25
31
  import { getJsonLdOfPage as getJsonLdOfPageOp } from './db-ops/meta/get-jsonld-of-page.js';
32
+ import { getMainContentImagesOfPage as getMainContentImagesOfPageOp } from './db-ops/meta/get-main-content-images-of-page.js';
33
+ import { getMainContentTablesOfPage as getMainContentTablesOfPageOp } from './db-ops/meta/get-main-content-tables-of-page.js';
26
34
  import { getTagsOfPage as getTagsOfPageOp } from './db-ops/meta/get-tags-of-page.js';
35
+ import { getVideosOfPage as getVideosOfPageOp } from './db-ops/meta/get-videos-of-page.js';
27
36
  import { setUrlOrder as setUrlOrderOp } from './db-ops/pages/order/set-url-order.js';
28
37
  import { getCrawlingState as getCrawlingStateOp } from './db-ops/pages/read/get-crawling-state.js';
29
38
  import { getExistingPageUrls as getExistingPageUrlsOp } from './db-ops/pages/read/get-existing-page-urls.js';
@@ -128,6 +137,14 @@ export class Database extends EventEmitter {
128
137
  async getAnchorsOnPage(pageId) {
129
138
  return emitErrorAndRetry(this, 'Database.getAnchorsOnPage', async () => await getAnchorsOnPageOp(this.#instance, pageId), retrySetting);
130
139
  }
140
+ /**
141
+ * Retrieves all `page_main_content_audios` rows for the given page id.
142
+ * Delegates to {@link getAudiosOfPageOp}.
143
+ * @param pageId
144
+ */
145
+ async getAudiosOfPage(pageId) {
146
+ return emitErrorAndRetry(this, 'Database.getAudiosOfPage', async () => await getAudiosOfPageOp(this.#instance, pageId), retrySetting);
147
+ }
131
148
  /**
132
149
  * Retrieves the base URL of the crawl session from the `info` table.
133
150
  * Delegates to {@link getBaseUrlOp}.
@@ -137,6 +154,22 @@ export class Database extends EventEmitter {
137
154
  async getBaseUrl() {
138
155
  return emitErrorAndRetry(this, 'Database.getBaseUrl', async () => await getBaseUrlOp(this.#instance), retrySetting);
139
156
  }
157
+ /**
158
+ * Retrieves all `page_main_content_buttons` rows for the given page id.
159
+ * Delegates to {@link getButtonsOfPageOp}.
160
+ * @param pageId
161
+ */
162
+ async getButtonsOfPage(pageId) {
163
+ return emitErrorAndRetry(this, 'Database.getButtonsOfPage', async () => await getButtonsOfPageOp(this.#instance, pageId), retrySetting);
164
+ }
165
+ /**
166
+ * Retrieves all `page_main_content_canvases` rows for the given page id.
167
+ * Delegates to {@link getCanvasesOfPageOp}.
168
+ * @param pageId
169
+ */
170
+ async getCanvasesOfPage(pageId) {
171
+ return emitErrorAndRetry(this, 'Database.getCanvasesOfPage', async () => await getCanvasesOfPageOp(this.#instance, pageId), retrySetting);
172
+ }
140
173
  /**
141
174
  * Retrieves the full crawl configuration from the `info` table.
142
175
  * Delegates to {@link getConfigOp}.
@@ -174,6 +207,14 @@ export class Database extends EventEmitter {
174
207
  async getExistingResourceUrls(urls) {
175
208
  return emitError(this, 'Database.getExistingResourceUrls', async () => await getExistingResourceUrlsOp(this.#instance, urls));
176
209
  }
210
+ /**
211
+ * Retrieves all `page_main_content_headings` rows for the given page id.
212
+ * Delegates to {@link getHeadingsOfPageOp}.
213
+ * @param pageId
214
+ */
215
+ async getHeadingsOfPage(pageId) {
216
+ return emitErrorAndRetry(this, 'Database.getHeadingsOfPage', async () => await getHeadingsOfPageOp(this.#instance, pageId), retrySetting);
217
+ }
177
218
  /**
178
219
  * Reads the HTML snapshot stored as a zstd-compressed BLOB for the given page.
179
220
  * Delegates to {@link getHtmlOfPageByIdOp}.
@@ -183,6 +224,14 @@ export class Database extends EventEmitter {
183
224
  async getHtmlOfPageById(pageId) {
184
225
  return emitErrorAndRetry(this, 'Database.getHtmlOfPageById', async () => await getHtmlOfPageByIdOp(this.#instance, pageId), retrySetting);
185
226
  }
227
+ /**
228
+ * Retrieves all `page_main_content_iframes` rows for the given page id.
229
+ * Delegates to {@link getIframesOfPageOp}.
230
+ * @param pageId
231
+ */
232
+ async getIframesOfPage(pageId) {
233
+ return emitErrorAndRetry(this, 'Database.getIframesOfPage', async () => await getIframesOfPageOp(this.#instance, pageId), retrySetting);
234
+ }
186
235
  /**
187
236
  * Retrieves all `page_jsonld` rows for the given page id, parsed back into
188
237
  * {@link JsonLdRow} shape. Delegates to {@link getJsonLdOfPageOp}.
@@ -200,6 +249,22 @@ export class Database extends EventEmitter {
200
249
  getKnex() {
201
250
  return this.#instance;
202
251
  }
252
+ /**
253
+ * Retrieves all `page_main_content_images` rows for the given page id.
254
+ * Delegates to {@link getMainContentImagesOfPageOp}.
255
+ * @param pageId
256
+ */
257
+ async getMainContentImagesOfPage(pageId) {
258
+ return emitErrorAndRetry(this, 'Database.getMainContentImagesOfPage', async () => await getMainContentImagesOfPageOp(this.#instance, pageId), retrySetting);
259
+ }
260
+ /**
261
+ * Retrieves all `page_main_content_tables` rows for the given page id.
262
+ * Delegates to {@link getMainContentTablesOfPageOp}.
263
+ * @param pageId
264
+ */
265
+ async getMainContentTablesOfPage(pageId) {
266
+ return emitErrorAndRetry(this, 'Database.getMainContentTablesOfPage', async () => await getMainContentTablesOfPageOp(this.#instance, pageId), retrySetting);
267
+ }
203
268
  /**
204
269
  * Retrieves the crawl session name from the `info` table.
205
270
  * Delegates to {@link getNameOp}.
@@ -326,6 +391,14 @@ export class Database extends EventEmitter {
326
391
  async getTagsOfPage(pageId) {
327
392
  return emitErrorAndRetry(this, 'Database.getTagsOfPage', async () => await getTagsOfPageOp(this.#instance, pageId), retrySetting);
328
393
  }
394
+ /**
395
+ * Retrieves all `page_main_content_videos` rows for the given page id.
396
+ * Delegates to {@link getVideosOfPageOp}.
397
+ * @param pageId
398
+ */
399
+ async getVideosOfPage(pageId) {
400
+ return emitErrorAndRetry(this, 'Database.getVideosOfPage', async () => await getVideosOfPageOp(this.#instance, pageId), retrySetting);
401
+ }
329
402
  /**
330
403
  * Records a crawler-level (`error` channel) failure into `crawl_errors`.
331
404
  * Delegates to {@link insertCrawlErrorOp}.
@@ -421,6 +494,14 @@ export class Database extends EventEmitter {
421
494
  async replaceAnalysisViolations(violations) {
422
495
  return emitErrorAndRetry(this, 'Database.replaceAnalysisViolations', async () => await replaceAnalysisViolationsOp(this.#instance, violations), retrySetting);
423
496
  }
497
+ /**
498
+ * Replaces the stored DOM-structure template classification with a
499
+ * freshly generated set. Delegates to {@link replacePageTemplatesOp}.
500
+ * @param templateKeysByUrl - Page URL → template key.
501
+ */
502
+ async replacePageTemplates(templateKeysByUrl) {
503
+ return emitErrorAndRetry(this, 'Database.replacePageTemplates', async () => await replacePageTemplatesOp(this.#instance, templateKeysByUrl), retrySetting);
504
+ }
424
505
  /**
425
506
  * Promote previously-external pages whose URL falls under any of the new
426
507
  * scope entries back to a "needs scraping" state.
@@ -0,0 +1,20 @@
1
+ import type { Knex } from 'knex';
2
+ /**
3
+ * Replaces the stored DOM-structure template classification (`--templates`)
4
+ * with a freshly generated set.
5
+ *
6
+ * Every classified page is a full-archive, all-or-nothing recomputation
7
+ * (see `@nitpicker/core`'s `classifyPageTemplates`), so this always deletes
8
+ * every existing row before inserting the new set — there is no per-page
9
+ * incremental update path, matching `replaceAnalysisViolations`'s
10
+ * whole-table replace shape. Unlike violations, a page whose URL can't be
11
+ * resolved back to a `content_items` row is silently skipped rather than
12
+ * treated as a hard failure: losing one page's template classification
13
+ * (e.g. a URL-normalization mismatch between the in-memory `Page.url.href`
14
+ * and the stored `url_refs.url`) should not discard the rest of a
15
+ * potentially multi-thousand-page classification run.
16
+ * @param knex - Knex query builder connected to the archive DB.
17
+ * @param templateKeysByUrl - Page URL → template key, as produced by
18
+ * `classifyPageTemplates`.
19
+ */
20
+ export declare function replacePageTemplates(knex: Knex, templateKeysByUrl: ReadonlyMap<string, string>): Promise<void>;
@@ -0,0 +1,52 @@
1
+ import { eachSplitted } from '../../../utils/array/each-splitted.js';
2
+ /**
3
+ * Replaces the stored DOM-structure template classification (`--templates`)
4
+ * with a freshly generated set.
5
+ *
6
+ * Every classified page is a full-archive, all-or-nothing recomputation
7
+ * (see `@nitpicker/core`'s `classifyPageTemplates`), so this always deletes
8
+ * every existing row before inserting the new set — there is no per-page
9
+ * incremental update path, matching `replaceAnalysisViolations`'s
10
+ * whole-table replace shape. Unlike violations, a page whose URL can't be
11
+ * resolved back to a `content_items` row is silently skipped rather than
12
+ * treated as a hard failure: losing one page's template classification
13
+ * (e.g. a URL-normalization mismatch between the in-memory `Page.url.href`
14
+ * and the stored `url_refs.url`) should not discard the rest of a
15
+ * potentially multi-thousand-page classification run.
16
+ * @param knex - Knex query builder connected to the archive DB.
17
+ * @param templateKeysByUrl - Page URL → template key, as produced by
18
+ * `classifyPageTemplates`.
19
+ */
20
+ export async function replacePageTemplates(knex, templateKeysByUrl) {
21
+ await knex.transaction(async (trx) => {
22
+ await trx('page_templates').delete();
23
+ if (templateKeysByUrl.size === 0) {
24
+ return;
25
+ }
26
+ const urls = [...templateKeysByUrl.keys()];
27
+ const pageIdByUrl = new Map();
28
+ await eachSplitted(urls, 500, async (chunk) => {
29
+ const pageRows = await trx('content_items')
30
+ .join('url_refs', 'url_refs.id', 'content_items.url_id')
31
+ .select('content_items.id as id', 'url_refs.url as url')
32
+ .whereIn('url_refs.url', chunk);
33
+ for (const row of pageRows) {
34
+ pageIdByUrl.set(row.url, row.id);
35
+ }
36
+ });
37
+ const rows = [];
38
+ for (const [url, templateKey] of templateKeysByUrl) {
39
+ const pageId = pageIdByUrl.get(url);
40
+ if (pageId == null) {
41
+ continue;
42
+ }
43
+ rows.push({ page_id: pageId, template_key: templateKey });
44
+ }
45
+ if (rows.length === 0) {
46
+ return;
47
+ }
48
+ await eachSplitted(rows, 500, async (chunk) => {
49
+ await trx('page_templates').insert(chunk);
50
+ });
51
+ });
52
+ }
@@ -23,4 +23,5 @@ export const INFO_COLUMN_ALLOWLIST = new Set([
23
23
  'disableQueries',
24
24
  'userAgent',
25
25
  'ignoreRobots',
26
+ 'mainContentSelector',
26
27
  ]);
@@ -1,17 +1,23 @@
1
1
  import type { Knex } from 'knex';
2
2
  /**
3
3
  * Initializes the database schema if tables do not exist, then runs the
4
- * one remaining lightweight migration (`info.roots`).
4
+ * remaining lightweight migrations (`info.roots`, `info.mainContentSelector`,
5
+ * `page_meta.main_content_*`).
5
6
  *
6
- * There is deliberately no per-table lazy-migration chain here:
7
- * `assertCompatibleVersion` (called below, before any schema work)
8
- * rejects every archive older than the current format, so a connection
9
- * that reaches `initSchema` is either brand new (initSchema provisions
10
- * the full schema) or was produced by `scripts/migrate-to-0.13.mjs`
11
- * (which guarantees the full table set before it repacks). A
12
- * `hasTable`-guarded catch-up migration could therefore never fire —
13
- * schema catch-up for old archives is the migration script's job, not
14
- * the open path's.
7
+ * There is deliberately no per-table *table-creation* migration chain here:
8
+ * `assertCompatibleVersion` (called below, before any schema work) rejects
9
+ * every archive older than the current format, so a connection that reaches
10
+ * `initSchema` is either brand new (`initSchema` provisions the full schema)
11
+ * or was produced by `scripts/migrate-to-0.13.mjs` (which guarantees the
12
+ * full table set before it repacks) — and `initSchema` itself re-runs
13
+ * `createEntityTables` / `createAdjunctTables` unconditionally on every
14
+ * open, self-healing any *missing table* via their internal
15
+ * `IF NOT EXISTS` / `hasTable` guards. What that self-healing cannot do is
16
+ * retrofit a *new column* onto an entity table that already exists —
17
+ * `CREATE TABLE IF NOT EXISTS` is a no-op once the table is present. Column
18
+ * additions to an existing 0.13 table are therefore the one case that still
19
+ * needs an explicit `hasColumn`-guarded `ALTER TABLE` here (`migrateInfoRoots`,
20
+ * `migrateMainContentsColumns`) rather than a DDL-string change alone.
15
21
  *
16
22
  * In read-only mode schema init + migration are SKIPPED so the same DB
17
23
  * can be opened safely by a viewer attached to a live (or interrupted)
@@ -1,19 +1,27 @@
1
1
  import { applyConnectionPragmas, initSchema } from '../../init-schema.js';
2
2
  import { assertCompatibleVersion } from '../../meta/assert-compatible-version.js';
3
+ import { migrateInfoMainContentSelector } from '../../migrate-info-main-content-selector.js';
3
4
  import { migrateInfoRoots } from '../../migrate-info-roots.js';
5
+ import { migrateMainContentsColumns } from '../../migrate-main-contents-columns.js';
4
6
  /**
5
7
  * Initializes the database schema if tables do not exist, then runs the
6
- * one remaining lightweight migration (`info.roots`).
8
+ * remaining lightweight migrations (`info.roots`, `info.mainContentSelector`,
9
+ * `page_meta.main_content_*`).
7
10
  *
8
- * There is deliberately no per-table lazy-migration chain here:
9
- * `assertCompatibleVersion` (called below, before any schema work)
10
- * rejects every archive older than the current format, so a connection
11
- * that reaches `initSchema` is either brand new (initSchema provisions
12
- * the full schema) or was produced by `scripts/migrate-to-0.13.mjs`
13
- * (which guarantees the full table set before it repacks). A
14
- * `hasTable`-guarded catch-up migration could therefore never fire —
15
- * schema catch-up for old archives is the migration script's job, not
16
- * the open path's.
11
+ * There is deliberately no per-table *table-creation* migration chain here:
12
+ * `assertCompatibleVersion` (called below, before any schema work) rejects
13
+ * every archive older than the current format, so a connection that reaches
14
+ * `initSchema` is either brand new (`initSchema` provisions the full schema)
15
+ * or was produced by `scripts/migrate-to-0.13.mjs` (which guarantees the
16
+ * full table set before it repacks) — and `initSchema` itself re-runs
17
+ * `createEntityTables` / `createAdjunctTables` unconditionally on every
18
+ * open, self-healing any *missing table* via their internal
19
+ * `IF NOT EXISTS` / `hasTable` guards. What that self-healing cannot do is
20
+ * retrofit a *new column* onto an entity table that already exists —
21
+ * `CREATE TABLE IF NOT EXISTS` is a no-op once the table is present. Column
22
+ * additions to an existing 0.13 table are therefore the one case that still
23
+ * needs an explicit `hasColumn`-guarded `ALTER TABLE` here (`migrateInfoRoots`,
24
+ * `migrateMainContentsColumns`) rather than a DDL-string change alone.
17
25
  *
18
26
  * In read-only mode schema init + migration are SKIPPED so the same DB
19
27
  * can be opened safely by a viewer attached to a live (or interrupted)
@@ -39,4 +47,6 @@ export async function init(knex, readOnly) {
39
47
  }
40
48
  await initSchema(knex);
41
49
  await migrateInfoRoots(knex);
50
+ await migrateInfoMainContentSelector(knex);
51
+ await migrateMainContentsColumns(knex);
42
52
  }
@@ -0,0 +1,11 @@
1
+ import type { MainContentAudioRow } from '../../meta/types.js';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Retrieves all `page_main_content_audios` rows for the given page id, in
5
+ * DOM traversal order.
6
+ *
7
+ * Read-side counterpart to `insertAudios`.
8
+ * @param knex - Knex query builder connected to the archive DB.
9
+ * @param pageId
10
+ */
11
+ export declare function getAudiosOfPage(knex: Knex, pageId: number): Promise<MainContentAudioRow[]>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Retrieves all `page_main_content_audios` rows for the given page id, in
3
+ * DOM traversal order.
4
+ *
5
+ * Read-side counterpart to `insertAudios`.
6
+ * @param knex - Knex query builder connected to the archive DB.
7
+ * @param pageId
8
+ */
9
+ export async function getAudiosOfPage(knex, pageId) {
10
+ return knex
11
+ .select('id', 'pageId', 'order', 'src')
12
+ .from('page_main_content_audios')
13
+ .where('pageId', pageId)
14
+ .orderBy('order', 'asc');
15
+ }
@@ -0,0 +1,11 @@
1
+ import type { MainContentButtonRow } from '../../meta/types.js';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Retrieves all `page_main_content_buttons` rows for the given page id, in
5
+ * DOM traversal order.
6
+ *
7
+ * Read-side counterpart to `insertButtons`.
8
+ * @param knex - Knex query builder connected to the archive DB.
9
+ * @param pageId
10
+ */
11
+ export declare function getButtonsOfPage(knex: Knex, pageId: number): Promise<MainContentButtonRow[]>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Retrieves all `page_main_content_buttons` rows for the given page id, in
3
+ * DOM traversal order.
4
+ *
5
+ * Read-side counterpart to `insertButtons`.
6
+ * @param knex - Knex query builder connected to the archive DB.
7
+ * @param pageId
8
+ */
9
+ export async function getButtonsOfPage(knex, pageId) {
10
+ return knex
11
+ .select('id', 'pageId', 'order', 'nodeName', 'role', 'type', 'text', 'disabled')
12
+ .from('page_main_content_buttons')
13
+ .where('pageId', pageId)
14
+ .orderBy('order', 'asc');
15
+ }
@@ -0,0 +1,11 @@
1
+ import type { MainContentCanvasRow } from '../../meta/types.js';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Retrieves all `page_main_content_canvases` rows for the given page id, in
5
+ * DOM traversal order.
6
+ *
7
+ * Read-side counterpart to `insertCanvases`.
8
+ * @param knex - Knex query builder connected to the archive DB.
9
+ * @param pageId
10
+ */
11
+ export declare function getCanvasesOfPage(knex: Knex, pageId: number): Promise<MainContentCanvasRow[]>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Retrieves all `page_main_content_canvases` rows for the given page id, in
3
+ * DOM traversal order.
4
+ *
5
+ * Read-side counterpart to `insertCanvases`.
6
+ * @param knex - Knex query builder connected to the archive DB.
7
+ * @param pageId
8
+ */
9
+ export async function getCanvasesOfPage(knex, pageId) {
10
+ return knex
11
+ .select('id', 'pageId', 'order', 'width', 'height')
12
+ .from('page_main_content_canvases')
13
+ .where('pageId', pageId)
14
+ .orderBy('order', 'asc');
15
+ }
@@ -0,0 +1,11 @@
1
+ import type { MainContentHeadingRow } from '../../meta/types.js';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Retrieves all `page_main_content_headings` rows for the given page id, in
5
+ * DOM traversal order.
6
+ *
7
+ * Read-side counterpart to `insertHeadings`.
8
+ * @param knex - Knex query builder connected to the archive DB.
9
+ * @param pageId
10
+ */
11
+ export declare function getHeadingsOfPage(knex: Knex, pageId: number): Promise<MainContentHeadingRow[]>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Retrieves all `page_main_content_headings` rows for the given page id, in
3
+ * DOM traversal order.
4
+ *
5
+ * Read-side counterpart to `insertHeadings`.
6
+ * @param knex - Knex query builder connected to the archive DB.
7
+ * @param pageId
8
+ */
9
+ export async function getHeadingsOfPage(knex, pageId) {
10
+ return knex
11
+ .select('id', 'pageId', 'order', 'text', 'level')
12
+ .from('page_main_content_headings')
13
+ .where('pageId', pageId)
14
+ .orderBy('order', 'asc');
15
+ }
@@ -0,0 +1,11 @@
1
+ import type { MainContentIframeRow } from '../../meta/types.js';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Retrieves all `page_main_content_iframes` rows for the given page id, in
5
+ * DOM traversal order.
6
+ *
7
+ * Read-side counterpart to `insertIframes`.
8
+ * @param knex - Knex query builder connected to the archive DB.
9
+ * @param pageId
10
+ */
11
+ export declare function getIframesOfPage(knex: Knex, pageId: number): Promise<MainContentIframeRow[]>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Retrieves all `page_main_content_iframes` rows for the given page id, in
3
+ * DOM traversal order.
4
+ *
5
+ * Read-side counterpart to `insertIframes`.
6
+ * @param knex - Knex query builder connected to the archive DB.
7
+ * @param pageId
8
+ */
9
+ export async function getIframesOfPage(knex, pageId) {
10
+ return knex
11
+ .select('id', 'pageId', 'order', 'src', 'title', 'width', 'height')
12
+ .from('page_main_content_iframes')
13
+ .where('pageId', pageId)
14
+ .orderBy('order', 'asc');
15
+ }
@@ -0,0 +1,11 @@
1
+ import type { MainContentImageRow } from '../../meta/types.js';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Retrieves all `page_main_content_images` rows for the given page id, in
5
+ * DOM traversal order.
6
+ *
7
+ * Read-side counterpart to `insertMainContentImages`.
8
+ * @param knex - Knex query builder connected to the archive DB.
9
+ * @param pageId
10
+ */
11
+ export declare function getMainContentImagesOfPage(knex: Knex, pageId: number): Promise<MainContentImageRow[]>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Retrieves all `page_main_content_images` rows for the given page id, in
3
+ * DOM traversal order.
4
+ *
5
+ * Read-side counterpart to `insertMainContentImages`.
6
+ * @param knex - Knex query builder connected to the archive DB.
7
+ * @param pageId
8
+ */
9
+ export async function getMainContentImagesOfPage(knex, pageId) {
10
+ return knex
11
+ .select('id', 'pageId', 'order', 'src', 'alt')
12
+ .from('page_main_content_images')
13
+ .where('pageId', pageId)
14
+ .orderBy('order', 'asc');
15
+ }
@@ -0,0 +1,11 @@
1
+ import type { MainContentTableRow } from '../../meta/types.js';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Retrieves all `page_main_content_tables` rows for the given page id, in
5
+ * DOM traversal order.
6
+ *
7
+ * Read-side counterpart to `insertMainContentTables`.
8
+ * @param knex - Knex query builder connected to the archive DB.
9
+ * @param pageId
10
+ */
11
+ export declare function getMainContentTablesOfPage(knex: Knex, pageId: number): Promise<MainContentTableRow[]>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Retrieves all `page_main_content_tables` rows for the given page id, in
3
+ * DOM traversal order.
4
+ *
5
+ * Read-side counterpart to `insertMainContentTables`.
6
+ * @param knex - Knex query builder connected to the archive DB.
7
+ * @param pageId
8
+ */
9
+ export async function getMainContentTablesOfPage(knex, pageId) {
10
+ return knex
11
+ .select('id', 'pageId', 'order', 'rows', 'cols', 'hasHeader', 'hasFooter', 'hasMergedCell')
12
+ .from('page_main_content_tables')
13
+ .where('pageId', pageId)
14
+ .orderBy('order', 'asc');
15
+ }
@@ -0,0 +1,11 @@
1
+ import type { MainContentVideoRow } from '../../meta/types.js';
2
+ import type { Knex } from 'knex';
3
+ /**
4
+ * Retrieves all `page_main_content_videos` rows for the given page id, in
5
+ * DOM traversal order.
6
+ *
7
+ * Read-side counterpart to `insertVideos`.
8
+ * @param knex - Knex query builder connected to the archive DB.
9
+ * @param pageId
10
+ */
11
+ export declare function getVideosOfPage(knex: Knex, pageId: number): Promise<MainContentVideoRow[]>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Retrieves all `page_main_content_videos` rows for the given page id, in
3
+ * DOM traversal order.
4
+ *
5
+ * Read-side counterpart to `insertVideos`.
6
+ * @param knex - Knex query builder connected to the archive DB.
7
+ * @param pageId
8
+ */
9
+ export async function getVideosOfPage(knex, pageId) {
10
+ return knex
11
+ .select('id', 'pageId', 'order', 'src', 'poster', 'width', 'height')
12
+ .from('page_main_content_videos')
13
+ .where('pageId', pageId)
14
+ .orderBy('order', 'asc');
15
+ }
@@ -36,5 +36,5 @@ export function buildPageQuery(knex) {
36
36
  .leftJoin('url_refs as og_image_ur', 'og_image_ur.id', 'pm.og_image_url_id')
37
37
  .leftJoin('url_refs as twitter_image_ur', 'twitter_image_ur.id', 'pm.twitter_image_url_id')
38
38
  .leftJoin('json_refs as extras_ref', 'extras_ref.id', 'pm.meta_extras_json_id')
39
- .select('ci.id as id', 'ur.url as url', 'ci.redirect_dest_id as redirectDestId', 'ci.scraped as scraped', 'ci.is_target as isTarget', 'ci.is_external as isExternal', 'ci.status as status', 'ci.status_text as statusText', 'ctr.raw as contentType', 'ci.content_length as contentLength', 'ci.header_set_id as headerSetId', 'pm.lang as lang', 'pm.dir as dir', 'pm.charset as charset', 'pm.base_href as baseHref', 'pm.viewport_raw as viewport_raw', 'pm.theme_color as themeColor', 'pm.application_name as applicationName', 'pm.author as author', 'pm.generator as generator', 'pm.publisher as publisher', 'title_ref.text as title', 'description_ref.text as description', 'keywords_ref.text as keywords', 'robots_raw_ref.text as robots_raw', 'pm.robots_noindex as robots_noindex', 'pm.robots_nofollow as robots_nofollow', 'pm.robots_noarchive as robots_noarchive', 'pm.robots_noimageindex as robots_noimageindex', 'pm.googlebot as googlebot', 'canonical_ur.url as canonical', 'amphtml_ur.url as amphtml', 'manifest_ur.url as manifest', 'icon_ur.url as icon_href', 'apple_ur.url as appleTouchIcon_href', 'pm.og_type as og_type', 'og_title_ref.text as og_title', 'og_url_ur.url as og_url', 'pm.og_site_name as og_site_name', 'og_description_ref.text as og_description', 'og_image_ur.url as og_image', 'pm.og_image_alt as og_image_alt', 'pm.og_image_width as og_image_width', 'pm.og_image_height as og_image_height', 'pm.og_locale as og_locale', 'pm.og_article_published_time as og_article_published_time', 'pm.og_article_modified_time as og_article_modified_time', 'pm.twitter_card as twitter_card', 'pm.twitter_site as twitter_site', 'pm.twitter_creator as twitter_creator', 'twitter_title_ref.text as twitter_title', 'twitter_description_ref.text as twitter_description', 'twitter_image_ur.url as twitter_image', 'pm.fb_app_id as fb_app_id', 'pm.verification_google as verification_google', 'pm.format_detection_telephone as formatDetection_telephone', 'ci.first_crawled_at as firstCrawledAt', 'ci.last_crawled_at as lastCrawledAt', 'pm.tag_count as tag_count', 'pm.jsonld_count as jsonld_count', 'pm.tags_providers_csv as tags_providers_csv', 'extras_ref.json_text as extras_body', 'extras_ref.codec as extras_codec', 'ci.is_skipped as isSkipped', 'ci.skip_reason as skipReason', 'ci.crawl_order as order', 'ci.source as source');
39
+ .select('ci.id as id', 'ur.url as url', 'ci.redirect_dest_id as redirectDestId', 'ci.scraped as scraped', 'ci.is_target as isTarget', 'ci.is_external as isExternal', 'ci.status as status', 'ci.status_text as statusText', 'ctr.raw as contentType', 'ci.content_length as contentLength', 'ci.header_set_id as headerSetId', 'pm.lang as lang', 'pm.dir as dir', 'pm.charset as charset', 'pm.base_href as baseHref', 'pm.viewport_raw as viewport_raw', 'pm.theme_color as themeColor', 'pm.application_name as applicationName', 'pm.author as author', 'pm.generator as generator', 'pm.publisher as publisher', 'title_ref.text as title', 'description_ref.text as description', 'keywords_ref.text as keywords', 'robots_raw_ref.text as robots_raw', 'pm.robots_noindex as robots_noindex', 'pm.robots_nofollow as robots_nofollow', 'pm.robots_noarchive as robots_noarchive', 'pm.robots_noimageindex as robots_noimageindex', 'pm.googlebot as googlebot', 'canonical_ur.url as canonical', 'amphtml_ur.url as amphtml', 'manifest_ur.url as manifest', 'icon_ur.url as icon_href', 'apple_ur.url as appleTouchIcon_href', 'pm.og_type as og_type', 'og_title_ref.text as og_title', 'og_url_ur.url as og_url', 'pm.og_site_name as og_site_name', 'og_description_ref.text as og_description', 'og_image_ur.url as og_image', 'pm.og_image_alt as og_image_alt', 'pm.og_image_width as og_image_width', 'pm.og_image_height as og_image_height', 'pm.og_locale as og_locale', 'pm.og_article_published_time as og_article_published_time', 'pm.og_article_modified_time as og_article_modified_time', 'pm.twitter_card as twitter_card', 'pm.twitter_site as twitter_site', 'pm.twitter_creator as twitter_creator', 'twitter_title_ref.text as twitter_title', 'twitter_description_ref.text as twitter_description', 'twitter_image_ur.url as twitter_image', 'pm.fb_app_id as fb_app_id', 'pm.verification_google as verification_google', 'pm.format_detection_telephone as formatDetection_telephone', 'ci.first_crawled_at as firstCrawledAt', 'ci.last_crawled_at as lastCrawledAt', 'pm.tag_count as tag_count', 'pm.jsonld_count as jsonld_count', 'pm.tags_providers_csv as tags_providers_csv', 'pm.main_content_node_name as main_content_node_name', 'pm.main_content_id as main_content_id', 'pm.main_content_role as main_content_role', 'pm.main_content_selector as main_content_selector', 'pm.main_content_class_list as main_content_class_list', 'pm.main_content_word_count as main_content_word_count', 'pm.main_content_body_word_count as main_content_body_word_count', 'pm.main_content_heading_count as main_content_heading_count', 'pm.main_content_image_count as main_content_image_count', 'pm.main_content_table_count as main_content_table_count', 'pm.main_content_button_count as main_content_button_count', 'pm.main_content_iframe_count as main_content_iframe_count', 'pm.main_content_video_count as main_content_video_count', 'pm.main_content_audio_count as main_content_audio_count', 'pm.main_content_canvas_count as main_content_canvas_count', 'pm.scroll_height_desktop as scroll_height_desktop', 'pm.scroll_height_mobile as scroll_height_mobile', 'extras_ref.json_text as extras_body', 'extras_ref.codec as extras_codec', 'ci.is_skipped as isSkipped', 'ci.skip_reason as skipReason', 'ci.crawl_order as order', 'ci.source as source');
40
40
  }
@@ -65,19 +65,20 @@ export async function repromoteExternalPages(knex, scopes, options) {
65
65
  // demotion.
66
66
  });
67
67
  // Clear the prior crawl's data for the repromoted pages. `updatePage`
68
- // also replaces anchor_edges/image_items/tags/jsonld when it
69
- // re-scrapes them, but only when the new scrape is non-empty — so
70
- // this pre-clear is still load-bearing for pages that get
68
+ // also replaces anchor_edges/image_items/tags/jsonld/page_main_content_*
69
+ // when it re-scrapes them, but only when the new scrape is non-empty —
70
+ // so this pre-clear is still load-bearing for pages that get
71
71
  // repromoted but then re-scrape to nothing (or are never reached
72
72
  // again), and it is the only place `resource_ref_edges` is cleared.
73
73
  // Deleting the `page_meta` row (rather than nulling every column)
74
- // clears title / description / og:* / twitter:* / meta_extras in
75
- // one statement; a re-scrape re-inserts it via
76
- // `ON CONFLICT(page_id) DO UPDATE`. `page_tags` / `page_jsonld` are
77
- // cleared explicitly even though both tables also carry ON DELETE
78
- // CASCADE — we keep the existing pattern of explicit chunked
79
- // DELETEs rather than relying on CASCADE indirectly (and would not
80
- // cascade anyway: the parent `content_items` row is updated, not
74
+ // clears title / description / og:* / twitter:* / meta_extras /
75
+ // main_content_* in one statement; a re-scrape re-inserts it via
76
+ // `ON CONFLICT(page_id) DO UPDATE`. `page_tags` / `page_jsonld` /
77
+ // `page_main_content_*` are cleared explicitly even though all of
78
+ // them also carry ON DELETE CASCADE — we keep the existing pattern
79
+ // of explicit chunked DELETEs rather than relying on CASCADE
80
+ // indirectly (and would not cascade anyway: the parent
81
+ // `content_items` row is updated, not
81
82
  // deleted). Orphan blobs in `page_html_blobs` are left behind; #23
82
83
  // will add GC.
83
84
  await knex('page_meta').whereIn('page_id', chunk).delete();
@@ -87,6 +88,14 @@ export async function repromoteExternalPages(knex, scopes, options) {
87
88
  await knex('page_html_ref').whereIn('page_id', chunk).delete();
88
89
  await knex('page_tags').whereIn('pageId', chunk).delete();
89
90
  await knex('page_jsonld').whereIn('pageId', chunk).delete();
91
+ await knex('page_main_content_headings').whereIn('pageId', chunk).delete();
92
+ await knex('page_main_content_images').whereIn('pageId', chunk).delete();
93
+ await knex('page_main_content_tables').whereIn('pageId', chunk).delete();
94
+ await knex('page_main_content_buttons').whereIn('pageId', chunk).delete();
95
+ await knex('page_main_content_iframes').whereIn('pageId', chunk).delete();
96
+ await knex('page_main_content_videos').whereIn('pageId', chunk).delete();
97
+ await knex('page_main_content_audios').whereIn('pageId', chunk).delete();
98
+ await knex('page_main_content_canvases').whereIn('pageId', chunk).delete();
90
99
  }
91
100
  dbLog('Repromoted %d external pages back to pending', promotedUrls.length);
92
101
  return promotedUrls;
@@ -35,9 +35,11 @@ import type { Knex } from 'knex';
35
35
  * The page row itself is kept (id preserved) so existing
36
36
  * `anchor_edges.href_page_id` referrers stay valid, and `is_external` is
37
37
  * left untouched so the next pass re-classifies each page from the crawl
38
- * scope. Related `anchor_edges`, `image_items`, `resource_ref_edges`, and
39
- * `page_errors` rows are deleted so the re-scrape can re-insert fresh data
40
- * without duplicates.
38
+ * scope. Related `anchor_edges`, `image_items`, `resource_ref_edges`,
39
+ * `page_errors`, and the `page_main_content_*` child tables are deleted so
40
+ * the re-scrape can re-insert fresh data without duplicates — kept in sync
41
+ * with the `page_meta` row deletion above so a reset page's `main_content_*`
42
+ * counts and its child-table detail never disagree.
41
43
  *
42
44
  * SELECT and UPDATE/DELETE statements are chunked to stay below SQLite's
43
45
  * `SQLITE_LIMIT_VARIABLE_NUMBER`.