From 40eceddea52afc51f2af3aabe4ec60705e3f869c Mon Sep 17 00:00:00 2001 From: Austin Akers Date: Thu, 7 May 2026 15:48:11 -0400 Subject: [PATCH 1/2] initial commit for v5 harper upgrade --- package.json | 4 ++-- resources/PageContent.js | 2 +- resources/Worker.js | 2 +- resources/index.js | 2 +- utils/converter.js | 2 +- utils/filter.js | 2 +- utils/handleOnDemand.js | 2 +- utils/pageSource.js | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 3b66ba6..4c9bbe5 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,8 @@ }, "prettier": "@harperdb/code-guidelines/prettier", "scripts": { - "dev": "harperdb dev .", - "start": "harperdb run .", + "dev": "harper dev .", + "start": "harper run .", "lint": "eslint . --config eslint.config.mjs", "test": "npx vitest run --config vitest.config.js" }, diff --git a/resources/PageContent.js b/resources/PageContent.js index 49d9663..070b64a 100644 --- a/resources/PageContent.js +++ b/resources/PageContent.js @@ -35,7 +35,7 @@ * - All write methods (POST, PUT, PATCH, DELETE) are intentionally disabled to preserve read-only semantics. */ -import { server } from 'harperdb'; +import { server } from 'harper'; import Job from './JobQueue.js'; import { allowedReadRoles, allowStaleContent } from '../utils/constants.js'; import { parseQuery } from '../utils/parse.js'; diff --git a/resources/Worker.js b/resources/Worker.js index eba9456..7b1c3dd 100644 --- a/resources/Worker.js +++ b/resources/Worker.js @@ -32,7 +32,7 @@ */ import { performance } from 'perf_hooks'; -import { server } from 'harperdb'; +import { server } from 'harper'; import Job from './JobQueue.js'; import { pageSource } from '../utils/pageSource.js'; diff --git a/resources/index.js b/resources/index.js index 74da41c..cc8fbc9 100644 --- a/resources/index.js +++ b/resources/index.js @@ -31,7 +31,7 @@ */ import '../utils/cachedDNS.js'; // Import ensures DNS cache and global agent are set up -import { server } from 'harperdb'; +import { server } from 'harper'; import { schedulerNode, nodeName, deliveryNodes, workerNodes } from '../utils/constants.js'; import Content from './PageContent.js'; import Filter from './PageFilter.js'; diff --git a/utils/converter.js b/utils/converter.js index 0b7f7da..8c9a4e9 100644 --- a/utils/converter.js +++ b/utils/converter.js @@ -25,7 +25,7 @@ import { performance } from 'node:perf_hooks'; import { NodeHtmlMarkdown } from 'node-html-markdown'; -import { server } from 'harperdb'; +import { server } from 'harper'; /** * Converts cleaned HTML content into markdown format using node-html-markdown diff --git a/utils/filter.js b/utils/filter.js index 37c09f0..b8f7882 100644 --- a/utils/filter.js +++ b/utils/filter.js @@ -33,7 +33,7 @@ import { performance } from 'node:perf_hooks'; import { parse } from 'node-html-parser'; -import { server } from 'harperdb'; +import { server } from 'harper'; import Filter from '../resources/PageFilter.js'; /** diff --git a/utils/handleOnDemand.js b/utils/handleOnDemand.js index 9ffc636..98bc54d 100644 --- a/utils/handleOnDemand.js +++ b/utils/handleOnDemand.js @@ -26,7 +26,7 @@ * - Ensures database consistency by persisting only successfully rendered content. */ -import { databases } from 'harperdb'; +import { databases } from 'harper'; import { pageSource } from './pageSource.js'; import { defaultRefreshInterval } from '../utils/constants.js'; diff --git a/utils/pageSource.js b/utils/pageSource.js index 1b33e80..86db030 100644 --- a/utils/pageSource.js +++ b/utils/pageSource.js @@ -35,7 +35,7 @@ import { performance } from 'node:perf_hooks'; import { createRequire } from 'node:module'; import { gzip, constants } from 'node:zlib'; import { promisify } from 'node:util'; -import { server } from 'harperdb'; +import { server } from 'harper'; import { filterContent } from './filter.js'; import { convertContent } from './converter.js'; import { createErrorPage, createMarkdownHeader } from './errorPage.js'; From a8a4f737fe2cd1c9f300951d486a635acc711015 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Sun, 24 May 2026 22:33:44 -0600 Subject: [PATCH 2/2] refactor(resources): convert v4 instance methods to v5 static REST methods Convert all REST handler methods (get, post, put, patch, delete) from v4 instance style to v5 static style with (target, context) or (target, data, context) signatures. Replace this.getId() with target.id, this.getContext() with context, and await data before use in write handlers. Extract getHeaders() and methodNotAllowed() in PageContent to module-level functions; convert BulkUpload helper methods to static. --- resources/BulkUpload.js | 27 ++++--- resources/CacheMetrics.js | 4 +- resources/PageContent.js | 156 +++++++++++++++++++------------------- resources/PageFilter.js | 25 +++--- 4 files changed, 111 insertions(+), 101 deletions(-) diff --git a/resources/BulkUpload.js b/resources/BulkUpload.js index 56d5256..4ed9ea2 100644 --- a/resources/BulkUpload.js +++ b/resources/BulkUpload.js @@ -55,10 +55,13 @@ export default class BulkUpload extends Resource { * { urlList: Array, refreshInterval: number } * { sitemap: string, isIndex: boolean, pageRefreshInterval: number, sitemapRefreshInterval: number } * - * @param {object} data - Bulk upload request payload. + * @param {object} target - Request target. + * @param {Promise} data - Bulk upload request payload (must be awaited). + * @param {object} context - Request context. * @returns {Promise} - Status code and message. */ - async post(data) { + static async post(target, data, context) { + data = await data; if (!data) { return { status: 400, @@ -68,13 +71,13 @@ export default class BulkUpload extends Resource { } // Add single URL to job queue - if (data.url) return await this.handleSingleUrl(data); + if (data.url) return await BulkUpload.handleSingleUrl(data); // Add list of URLs to job queue - if (data.urlList) return await this.handleUrlList(data); + if (data.urlList) return await BulkUpload.handleUrlList(data); // Parse sitemap and add URLs to job queue - if (data.sitemap) return await this.handleSitemap(data); + if (data.sitemap) return await BulkUpload.handleSitemap(data); return { status: 400, @@ -87,7 +90,7 @@ export default class BulkUpload extends Resource { * Handle PATCH requests. * @returns {object} - 405 error for PATCH method */ - async patch() { + static async patch(target, data, context) { return { status: 405, headers: { 'Content-Type': 'application/json' }, @@ -101,7 +104,7 @@ export default class BulkUpload extends Resource { /** Handle PUT requests. * @returns {object} - 405 error for PUT method */ - async put() { + static async put(target, data, context) { return { status: 405, headers: { 'Content-Type': 'application/json' }, @@ -115,7 +118,7 @@ export default class BulkUpload extends Resource { /** Handle DELETE requests. * @returns {object} - 405 error for DELETE method */ - async delete() { + static async delete(target, context) { return { status: 405, headers: { 'Content-Type': 'application/json' }, @@ -129,7 +132,7 @@ export default class BulkUpload extends Resource { /** Handle GET requests. * @returns {object} - 405 error for GET method */ - async get() { + static async get(target, context) { return { status: 405, headers: { 'Content-Type': 'application/json' }, @@ -147,7 +150,7 @@ export default class BulkUpload extends Resource { * @param {number} data.refreshInterval - Refresh interval in milliseconds. * @returns {Promise} - Status code and message. */ - async handleSingleUrl({ url, refreshInterval }) { + static async handleSingleUrl({ url, refreshInterval }) { try { if (!url || typeof url !== 'string') { return { @@ -181,7 +184,7 @@ export default class BulkUpload extends Resource { * @returns {Promise} - Status code and message. * @throws {object} - If the entire batch fails, throws an object with the URLs and error message. */ - async handleUrlList({ urlList, refreshInterval }) { + static async handleUrlList({ urlList, refreshInterval }) { try { if (!urlList || !Array.isArray(urlList) || urlList.length === 0) { return { @@ -237,7 +240,7 @@ export default class BulkUpload extends Resource { * @param {number} data.sitemapRefreshInterval - Sitemap refresh interval in milliseconds. * @returns {Promise} - Status code and message. */ - async handleSitemap({ sitemap, isIndex, pageRefreshInterval, sitemapRefreshInterval }) { + static async handleSitemap({ sitemap, isIndex, pageRefreshInterval, sitemapRefreshInterval }) { try { if (!sitemap || typeof sitemap !== 'string') { return { diff --git a/resources/CacheMetrics.js b/resources/CacheMetrics.js index d1e81ac..5d8983f 100644 --- a/resources/CacheMetrics.js +++ b/resources/CacheMetrics.js @@ -39,9 +39,11 @@ const { hdb_analytics } = databases.system; export default class CacheMetrics extends Resource { /** * Retrieves page content cache metrics from the last 60 seconds. + * @param {object} target - Request target. + * @param {object} context - Request context. * @returns {Promise>} - An array of metric objects matching the query. */ - async get() { + static async get(target, context) { logger.info('Retrieving page content cache metrics from the last 60 seconds'); // Compute rolling time window: [now - 60s, now] diff --git a/resources/PageContent.js b/resources/PageContent.js index 070b64a..2195d39 100644 --- a/resources/PageContent.js +++ b/resources/PageContent.js @@ -56,6 +56,70 @@ let usageCache = new Map(); * - Normalizes headers, injects error handling, and generates error pages on failures. * - Responds with 405 "Method Not Allowed" for unsupported HTTP methods. */ +/** + * Merge and normalize headers from page + context. + * Applies overrides for content errors and caching rules. + * + * @param {object} pageHeaders - Headers returned by source/page. + * @param {Headers} responseHeaders - Existing response headers. + * @param {boolean} contentError - Whether content retrieval failed. + * @param {number} contentLength - Final length of the content. + * @returns {Headers} - Normalized headers object. + */ +function getHeaders(pageHeaders, responseHeaders, contentError, contentLength) { + const pgHeaders = JSON.parse(pageHeaders || {}); + const headers = responseHeaders || new Headers(); + for (const [key, value] of Object.entries(pgHeaders)) { + switch (key) { + case 'server-timing': { + headers.append(key, value); + break; + } + + case 'content-type': { + headers.set(key, contentError ? 'text/markdown; charset=utf-8' : value); + break; + } + + case 'content-length': { + headers.set(key, contentLength); + break; + } + + default: { + headers.set(key, value); + break; + } + } + } + + if (contentError) { + headers.set('retry-after', 300); // 5 minute retry for errors + headers.delete('cache-control'); + headers.delete('content-encoding'); // Blob error page is not gzipped to save time + } + + return headers; +} + +/** + * Returns 405 Method Not Allowed response + * for unsupported HTTP methods (POST, PUT, PATCH, DELETE) + * with a markdown message indicating the allowed method (GET) + * @returns {object} - object with status, headers, and markdown content + */ +function methodNotAllowed() { + const content = '# Method Not Allowed \n\nPlease use GET to retrieve the page content.'; + const contentType = 'text/markdown; charset=utf-8'; + const contentLength = Buffer.byteLength(content, 'utf8'); + + return { + status: 405, + data: { data: content, contentType }, + headers: new Headers({ 'content-type': contentType, 'content-length': contentLength }), + }; +} + export default class Content extends Resource { static directURLMapping = true; @@ -75,15 +139,15 @@ export default class Content extends Resource { * and scheduling a background refresh job. * - Merges response headers with overrides based on error conditions. * - * @param {object} query - Request query. + * @param {object} target - Request target (extends URLSearchParams). + * @param {object} context - Request context. * @returns {Promise} - Response object with status, headers, and content. */ - async get(query) { - const queryPath = this.getId(); - const context = this.getContext(); + static async get(target, context) { + const queryPath = target.id; // Parse the request into host, path, and query string - const urlCacheKey = parseQuery(queryPath, query, context); + const urlCacheKey = parseQuery(queryPath, target, context); logger.info(`Fetching content for: ${urlCacheKey}`); // Check cache first, update lastAccessed if more than 1 minute old @@ -134,7 +198,7 @@ export default class Content extends Resource { } const contentLength = contentError ? Buffer.byteLength(blob, 'utf8') : page.contentLength; - const respHeaders = this.getHeaders(page.headers, context.responseHeaders, contentError, contentLength); + const respHeaders = getHeaders(page.headers, context.responseHeaders, contentError, contentLength); return { headers: respHeaders, @@ -144,88 +208,24 @@ export default class Content extends Resource { }; } - /** - * Merge and normalize headers from page + context. - * Applies overrides for content errors and caching rules. - * - * @param {object} pageHeaders - Headers returned by source/page. - * @param {Headers} responseHeaders - Existing response headers. - * @param {boolean} contentError - Whether content retrieval failed. - * @param {number} contentLength - Final length of the content. - * @returns {Headers} - Normalized headers object. - */ - getHeaders(pageHeaders, responseHeaders, contentError, contentLength) { - const pgHeaders = JSON.parse(pageHeaders || {}); - const headers = responseHeaders || new Headers(); - for (const [key, value] of Object.entries(pgHeaders)) { - switch (key) { - case 'server-timing': { - headers.append(key, value); - break; - } - - case 'content-type': { - headers.set(key, contentError ? 'text/markdown; charset=utf-8' : value); - break; - } - - case 'content-length': { - headers.set(key, contentLength); - break; - } - - default: { - headers.set(key, value); - break; - } - } - } - - if (contentError) { - headers.set('retry-after', 300); // 5 minute retry for errors - headers.delete('cache-control'); - headers.delete('content-encoding'); // Blob error page is not gzipped to save time - } - - return headers; - } - - /** - * Returns 405 Method Not Allowed response - * for unsupported HTTP methods (POST, PUT, PATCH, DELETE) - * with a markdown message indicating the allowed method (GET) - * @returns {object} - object with status, headers, and markdown content - */ - methodNotAllowed() { - const content = '# Method Not Allowed \n\nPlease use GET to retrieve the page content.'; - const contentType = 'text/markdown; charset=utf-8'; - const contentLength = Buffer.byteLength(content, 'utf8'); - - return { - status: 405, - data: { data: content, contentType }, - headers: new Headers({ 'content-type': contentType, 'content-length': contentLength }), - }; - } - /** @returns {object} - 405 error for POST method */ - post() { - return this.methodNotAllowed(); + static post() { + return methodNotAllowed(); } /** @returns {object} - 405 error for PUT method */ - put() { - return this.methodNotAllowed(); + static put() { + return methodNotAllowed(); } /** @returns {object} - 405 error for PATCH method */ - patch() { - return this.methodNotAllowed(); + static patch() { + return methodNotAllowed(); } /** @returns {object} - 405 error for DELETE method */ - delete() { - return this.methodNotAllowed(); + static delete() { + return methodNotAllowed(); } } diff --git a/resources/PageFilter.js b/resources/PageFilter.js index 6b5e1b8..c9ac4a1 100644 --- a/resources/PageFilter.js +++ b/resources/PageFilter.js @@ -39,11 +39,12 @@ export default class Filter extends Resource { /** * Get filters for a given path. * - * @param {string} pathname - Path identifier for the page. + * @param {object} target - Request target (extends URLSearchParams); target.id is the path. + * @param {object} context - Request context. * @returns {Promise} Result of the get operation. */ - async get() { - const pathname = this.getId(); + static async get(target, context) { + const pathname = target.id; if (!pathname || typeof pathname !== 'string') { return { status: 400, @@ -68,13 +69,16 @@ export default class Filter extends Resource { /** * Create or update a filter record for a path. * - * @param {object} data - The filter data. + * @param {object} target - Request target. + * @param {Promise} data - The filter data (must be awaited). * @param {string} data.path - Path identifier for the page (gets normalized internally). * @param {string} data.filters - Comma-separated CSS selectors string. * Example: `"header, #id1, .class2"`. + * @param {object} context - Request context. * @returns {Promise} Result of the post operation, or an error response if validation fails. */ - async post(data) { + static async post(target, data, context) { + data = await data; if (!data.path || typeof data.path !== 'string') { return { status: 400, @@ -113,11 +117,12 @@ export default class Filter extends Resource { /** * Delete filters for a given path. * - * @param {string} pathname - Path identifier for the page. + * @param {object} target - Request target; target.id is the path identifier. + * @param {object} context - Request context. * @returns {Promise} Result of the delete operation. */ - async delete() { - const pathname = this.getId(); + static async delete(target, context) { + const pathname = target.id; if (!pathname || typeof pathname !== 'string') { return { status: 400, @@ -145,7 +150,7 @@ export default class Filter extends Resource { * Method not allowed, must use PUT to create/update filters. * @returns {object} - 405 error for PATCH method */ - async patch() { + static async patch(target, data, context) { return { status: 405, headers: { 'Content-Type': 'application/json' }, @@ -160,7 +165,7 @@ export default class Filter extends Resource { * Method not allowed, must use POST to create/update filters. * @returns {object} - 405 error for PUT method */ - async put() { + static async put(target, data, context) { return { status: 405, headers: { 'Content-Type': 'application/json' },