openvibers/openvibe.host Commits

Readiness waits for the offers, domain and benchmark tables again (0009-0012 applied 2026-10-19 00:47 UTC), and their 'before the migration applies' fallbacks are gone: offers, domain prices, the TLD search documents and benchmarks read their tables directly (#133)

OP OpenVibers committed 25 days ago · c015f48c6e · 7 files, +42 −114

Changes

ddiff --git a/server/catalog/benchmarks.js b/server/catalog/benchmarks.js
iindex 7946002..6c46737 100644
---- a/server/catalog/benchmarks.js
++++ b/server/catalog/benchmarks.js
@@@ -5,11 +5,10 @@
* JSON result to that link; Host keeps the measurements and shows them per provider and plan.
*
* parseYabs(body) YABS's JSON → the stored fields, or throws ApiError 400
-- * createBenchmarks({ store, baseUrl }) → { createLink, submit, list, get, summary, remove, ready }
++ * createBenchmarks({ store, baseUrl }) → { createLink, submit, list, get, summary, remove }
*
* A link works once, for 24 hours; a person gets 10 a day. The server's address is never stored: YABS's own lookup gives
-- * the city and network, which are kept. Until 0012 has applied (a held contract migration holds it), reads answer empty
-- * and `ready()` is false.
++ * the city and network, which are kept.
*/
const crypto = require('crypto');
const { newId } = require('../ids');
@@@ -22,7 +21,6 @@ const YABS_COMMAND = (url) => `curl -sL https://yabs.sh | bash -s -- -s "${url}"
const bad = (message) => { throw new ApiError(400, 'host.benchmark.invalid', message); };
const invalid = (message) => { throw new ApiError(422, 'host.catalog.invalid', message); };
--const missingTable = (err) => err && err.code === '42P01';
const sha = (s) => crypto.createHash('sha256').update(s).digest('hex');
const text = (v, max) => (typeof v === 'string' && v.trim() ? v.trim().replace(/[\u0000-\u001f\u007f]/g, ' ').slice(0, max) : null);
const numberIn = (v, max) => (typeof v === 'number' && Number.isFinite(v) && v >= 0 && v <= max ? v : null);
@@@ -77,21 +75,10 @@ const SORTS = {
function createBenchmarks({ store, baseUrl }) {
const db = store.db;
-- let isReady = null;
-- async function ready() {
-- if (isReady) return true;
-- const r = await db.prepare("SELECT to_regclass('catalog_benchmarks') AS t").get();
-- isReady = Boolean(r && r.t);
-- return isReady;
-- }
-- async function safe(fn, fallback) {
-- try { return await fn(); } catch (err) { if (missingTable(err)) { isReady = false; return fallback; } throw err; }
-- }
/** A one-time submit link for `person`, naming a catalog provider (by slug) or another provider by name, and a plan. */
async function createLink(person, input = {}) {
if (!person) throw new ApiError(401, 'host.benchmark.sign_in', 'Sign in to submit a benchmark');
-- if (!(await ready())) throw new ApiError(503, 'host.benchmark.unavailable', 'Benchmarks open after the next maintenance');
const now = store.now();
const recent = await db.prepare('SELECT COUNT(*) AS n FROM catalog_bench_links WHERE person = ? AND created_at > ?').get(person, now - 86400_000);
if (Number(recent.n) >= LINKS_PER_DAY) throw new ApiError(429, 'host.benchmark.too_many', `At most ${LINKS_PER_DAY} benchmark links a day`);
@@@ -128,7 +115,7 @@ function createBenchmarks({ store, baseUrl }) {
/** Before reading a posted body: the link exists, is unused and unexpired. */
async function checkLink(token) {
if (!tokenOk(token)) usable(null);
-- const link = await safe(() => db.prepare('SELECT used_at, expires_at FROM catalog_bench_links WHERE token_hash = ?').get(sha(token)), null);
++ const link = await db.prepare('SELECT used_at, expires_at FROM catalog_bench_links WHERE token_hash = ?').get(sha(token));
usable(link, store.now());
}
@@@ -170,14 +157,12 @@ function createBenchmarks({ store, baseUrl }) {
const args = [];
if (query.provider) { where += ' AND (v.slug = ? OR lower(b.provider_name) = lower(?))'; args.push(String(query.provider), String(query.provider)); }
if (query.plan) { where += ' AND b.plan_id = ?'; args.push(String(query.plan)); }
-- return await safe(async () => {
-- const rows = await db.prepare(`${SELECT} WHERE ${where} ORDER BY ${SORTS[sort]} LIMIT ${PAGE + 1} OFFSET ${offset}`).all(...args);
-- return { benchmarks: rows.slice(0, PAGE).map(out), next_cursor: rows.length > PAGE ? String(offset + PAGE) : null };
-- }, { benchmarks: [], next_cursor: null });
++ const rows = await db.prepare(`${SELECT} WHERE ${where} ORDER BY ${SORTS[sort]} LIMIT ${PAGE + 1} OFFSET ${offset}`).all(...args);
++ return { benchmarks: rows.slice(0, PAGE).map(out), next_cursor: rows.length > PAGE ? String(offset + PAGE) : null };
}
async function get(id, viewer = null) {
-- const row = await safe(() => db.prepare(`${SELECT} WHERE b.id = ? AND b.status = 'active'`).get(String(id)), null);
++ const row = await db.prepare(`${SELECT} WHERE b.id = ? AND b.status = 'active'`).get(String(id));
if (!row) throw new ApiError(404, 'host.catalog.not_found', 'No such benchmark');
return { ...out(row), mine: Boolean(viewer && viewer.subject && viewer.subject === row.submitted_by) };
}
@@@ -185,14 +170,12 @@ function createBenchmarks({ store, baseUrl }) {
/** The medians for a provider or a plan (a provider page's and a plan page's line). */
async function summary({ providerId, planId }) {
const [col, val] = planId ? ['b.plan_id', planId] : ['b.provider_id', providerId];
-- return await safe(async () => {
-- const r = await db.prepare(`SELECT COUNT(*) AS n, percentile_cont(0.5) WITHIN GROUP (ORDER BY b.gb_single) AS gb_single, percentile_cont(0.5) WITHIN GROUP (ORDER BY b.gb_multi) AS gb_multi,
-- percentile_cont(0.5) WITHIN GROUP (ORDER BY b.disk_4k_iops) AS disk_4k_iops, MAX(b.gb_version) AS gb_version, MAX(b.created_at) AS latest
-- FROM catalog_benchmarks b WHERE ${col} = ? AND b.status = 'active'`).get(val);
-- const n = Number(r.n) || 0;
-- return n ? { count: n, gb_single: r.gb_single == null ? null : Math.round(Number(r.gb_single)), gb_multi: r.gb_multi == null ? null : Math.round(Number(r.gb_multi)),
-- disk_4k_iops: r.disk_4k_iops == null ? null : Math.round(Number(r.disk_4k_iops)), gb_version: r.gb_version == null ? null : Number(r.gb_version), latest: new Date(Number(r.latest)).toISOString() } : { count: 0 };
-- }, { count: 0 });
++ const r = await db.prepare(`SELECT COUNT(*) AS n, percentile_cont(0.5) WITHIN GROUP (ORDER BY b.gb_single) AS gb_single, percentile_cont(0.5) WITHIN GROUP (ORDER BY b.gb_multi) AS gb_multi,
++ percentile_cont(0.5) WITHIN GROUP (ORDER BY b.disk_4k_iops) AS disk_4k_iops, MAX(b.gb_version) AS gb_version, MAX(b.created_at) AS latest
++ FROM catalog_benchmarks b WHERE ${col} = ? AND b.status = 'active'`).get(val);
++ const n = Number(r.n) || 0;
++ return n ? { count: n, gb_single: r.gb_single == null ? null : Math.round(Number(r.gb_single)), gb_multi: r.gb_multi == null ? null : Math.round(Number(r.gb_multi)),
++ disk_4k_iops: r.disk_4k_iops == null ? null : Math.round(Number(r.disk_4k_iops)), gb_version: r.gb_version == null ? null : Number(r.gb_version), latest: new Date(Number(r.latest)).toISOString() } : { count: 0 };
}
/** The person who submitted a result, or staff, takes it down. */
@@@ -203,7 +186,7 @@ function createBenchmarks({ store, baseUrl }) {
await db.prepare("UPDATE catalog_benchmarks SET status = 'hidden', updated_at = ? WHERE id = ?").run(store.now(), String(id));
}
-- return { createLink, checkLink, submit, list, get, summary, remove, ready, SORTS: Object.keys(SORTS) };
++ return { createLink, checkLink, submit, list, get, summary, remove, SORTS: Object.keys(SORTS) };
}
module.exports = { parseYabs, createBenchmarks, YABS_COMMAND, LINK_TTL_MS, LINKS_PER_DAY };
ddiff --git a/server/catalog/domain-store.js b/server/catalog/domain-store.js
iindex 46e4df4..7ae6dbb 100644
---- a/server/catalog/domain-store.js
++++ b/server/catalog/domain-store.js
@@@ -2,9 +2,6 @@
/**
* Domain prices in the catalog (migrations/0011_catalog_domains.sql, server/catalog/domains.js): one row per registrar
* and TLD, applied from Sources items; the TLD table, a TLD's page and the cheapest price for a list of endings.
-- *
-- * Until 0011 has applied (a held contract migration holds every later one) the tables do not exist: reads answer
-- * empty and `ready()` is false, so pages leave the price sections out rather than fail.
*/
const { newId } = require('../ids');
const { ApiError } = require('../http/errors');
@@@ -28,24 +25,12 @@ const KINDS = {
'second-level': "position('.' in p.tld) > 0",
internationalized: "position('.' in p.tld) = 0 AND p.tld LIKE 'xn--%'",
};
--const missingTable = (err) => err && err.code === '42P01';
const n = (v) => (v == null ? null : Number(v));
const iso = (ms) => (ms == null ? null : new Date(Number(ms)).toISOString());
function createDomainStore({ store, announce = () => {} }) {
const db = store.db;
let known = { at: 0, set: new Set() };
-- let isReady = null;
--
-- async function ready() {
-- if (isReady) return true;
-- const r = await db.prepare("SELECT to_regclass('catalog_tld_prices') AS t").get();
-- isReady = Boolean(r && r.t);
-- return isReady;
-- }
-- async function safe(fn, fallback) {
-- try { return await fn(); } catch (err) { if (missingTable(err)) { isReady = false; return fallback; } throw err; }
-- }
async function apply(item, t = db) {
const now = store.now();
@@@ -115,24 +100,20 @@ function createDomainStore({ store, announce = () => {} }) {
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(String(query.registrar))) invalid('Invalid registrar');
where += ' AND v.slug = ?'; args.push(query.registrar);
}
-- return await safe(async () => {
-- const rows = await db.prepare(`SELECT p.tld, 'USD' AS currency, MIN(p.register) AS register, MIN(p.renew) AS renew, MIN(p.transfer) AS transfer, MIN(${FIVE_YEAR}) AS five_year, COUNT(*) AS registrars, MAX(p.checked_at) AS checked_at
-- FROM catalog_tld_prices p JOIN catalog_providers v ON v.id = p.provider_id WHERE ${where} GROUP BY p.tld ORDER BY ${SORTS[sort]} LIMIT ${PAGE + 1} OFFSET ${offset}`).all(...args);
-- return { tlds: rows.slice(0, PAGE).map(tldRow), next_cursor: rows.length > PAGE ? String(offset + PAGE) : null };
-- }, { tlds: [], next_cursor: null });
++ const rows = await db.prepare(`SELECT p.tld, 'USD' AS currency, MIN(p.register) AS register, MIN(p.renew) AS renew, MIN(p.transfer) AS transfer, MIN(${FIVE_YEAR}) AS five_year, COUNT(*) AS registrars, MAX(p.checked_at) AS checked_at
++ FROM catalog_tld_prices p JOIN catalog_providers v ON v.id = p.provider_id WHERE ${where} GROUP BY p.tld ORDER BY ${SORTS[sort]} LIMIT ${PAGE + 1} OFFSET ${offset}`).all(...args);
++ return { tlds: rows.slice(0, PAGE).map(tldRow), next_cursor: rows.length > PAGE ? String(offset + PAGE) : null };
}
/** One TLD: every registrar's price (cheapest first year first) and the latest price changes. */
async function tld(input) {
const t = domains.normalizeTld(String(input || ''));
if (!t) throw new ApiError(404, 'host.catalog.not_found', 'Not a domain ending');
-- const data = await safe(async () => {
-- const rows = await db.prepare(`SELECT p.*, ${FIVE_YEAR} AS five_year, v.slug AS provider_slug, v.name AS provider_name, v.homepage_url, v.affiliate_url, v.affiliate_note
-- FROM catalog_tld_prices p JOIN catalog_providers v ON v.id = p.provider_id WHERE ${ACTIVE} AND p.tld = ? ORDER BY p.register ASC NULLS LAST, v.name, p.id`).all(t);
-- const history = rows.length ? await db.prepare(`SELECT h.*, v.name AS provider_name FROM catalog_tld_price_changes h JOIN catalog_tld_prices p ON p.id = h.price_id JOIN catalog_providers v ON v.id = p.provider_id
-- WHERE p.tld = ? AND ${ACTIVE} ORDER BY h.observed_at DESC LIMIT 20`).all(t) : [];
-- return { rows, history };
-- }, { rows: [], history: [] });
++ const rows = await db.prepare(`SELECT p.*, ${FIVE_YEAR} AS five_year, v.slug AS provider_slug, v.name AS provider_name, v.homepage_url, v.affiliate_url, v.affiliate_note
++ FROM catalog_tld_prices p JOIN catalog_providers v ON v.id = p.provider_id WHERE ${ACTIVE} AND p.tld = ? ORDER BY p.register ASC NULLS LAST, v.name, p.id`).all(t);
++ const history = rows.length ? await db.prepare(`SELECT h.*, v.name AS provider_name FROM catalog_tld_price_changes h JOIN catalog_tld_prices p ON p.id = h.price_id JOIN catalog_providers v ON v.id = p.provider_id
++ WHERE p.tld = ? AND ${ACTIVE} ORDER BY h.observed_at DESC LIMIT 20`).all(t) : [];
++ const data = { rows, history };
if (!data.rows.length) throw new ApiError(404, 'host.catalog.not_found', 'No registrar price is listed for this ending');
const prices = data.rows.map((r) => ({
registrar: r.provider_name, registrar_slug: r.provider_slug, homepage_url: r.homepage_url, affiliate_url: r.affiliate_url, affiliate_note: r.affiliate_note,
@@@ -154,8 +135,8 @@ function createDomainStore({ store, announce = () => {} }) {
async function cheapest(tlds) {
const list = [...new Set(tlds.map((x) => domains.normalizeTld(x)).filter(Boolean))].slice(0, 30);
if (!list.length) return [];
-- const rows = await safe(() => db.prepare(`SELECT p.tld, 'USD' AS currency, MIN(p.register) AS register, MIN(p.renew) AS renew, MIN(p.transfer) AS transfer, MIN(${FIVE_YEAR}) AS five_year, COUNT(*) AS registrars, MAX(p.checked_at) AS checked_at
-- FROM catalog_tld_prices p JOIN catalog_providers v ON v.id = p.provider_id WHERE ${ACTIVE} AND p.currency = 'USD' AND p.tld IN (${list.map(() => '?').join(', ')}) GROUP BY p.tld`).all(...list), []);
++ const rows = await db.prepare(`SELECT p.tld, 'USD' AS currency, MIN(p.register) AS register, MIN(p.renew) AS renew, MIN(p.transfer) AS transfer, MIN(${FIVE_YEAR}) AS five_year, COUNT(*) AS registrars, MAX(p.checked_at) AS checked_at
++ FROM catalog_tld_prices p JOIN catalog_providers v ON v.id = p.provider_id WHERE ${ACTIVE} AND p.currency = 'USD' AND p.tld IN (${list.map(() => '?').join(', ')}) GROUP BY p.tld`).all(...list);
const by = new Map(rows.map((r) => [r.tld, tldRow(r)]));
return list.filter((t) => by.has(t)).map((t) => by.get(t));
}
@@@ -163,29 +144,27 @@ function createDomainStore({ store, announce = () => {} }) {
/** Every TLD with a listed price (for reading what a person typed), refreshed every five minutes. */
async function knownTlds() {
if (store.now() - known.at < 5 * 60_000) return known.set;
-- const rows = await safe(() => db.prepare(`SELECT DISTINCT p.tld FROM catalog_tld_prices p JOIN catalog_providers v ON v.id = p.provider_id WHERE ${ACTIVE}`).all(), []);
++ const rows = await db.prepare(`SELECT DISTINCT p.tld FROM catalog_tld_prices p JOIN catalog_providers v ON v.id = p.provider_id WHERE ${ACTIVE}`).all();
known = { at: store.now(), set: new Set(rows.map((r) => r.tld)) };
return known.set;
}
async function stats() {
-- return await safe(async () => {
-- const r = await db.prepare(`SELECT COUNT(DISTINCT p.tld) AS tlds, MAX(p.checked_at) AS last FROM catalog_tld_prices p JOIN catalog_providers v ON v.id = p.provider_id WHERE ${ACTIVE}`).get();
-- const names = await db.prepare(`SELECT DISTINCT v.name FROM catalog_tld_prices p JOIN catalog_providers v ON v.id = p.provider_id WHERE ${ACTIVE} ORDER BY v.name`).all();
-- return { tlds: Number(r.tlds) || 0, registrars: names.map((x) => x.name), lastChecked: r.last == null ? null : Number(r.last) };
-- }, { tlds: 0, registrars: [], lastChecked: null });
++ const r = await db.prepare(`SELECT COUNT(DISTINCT p.tld) AS tlds, MAX(p.checked_at) AS last FROM catalog_tld_prices p JOIN catalog_providers v ON v.id = p.provider_id WHERE ${ACTIVE}`).get();
++ const names = await db.prepare(`SELECT DISTINCT v.name FROM catalog_tld_prices p JOIN catalog_providers v ON v.id = p.provider_id WHERE ${ACTIVE} ORDER BY v.name`).all();
++ return { tlds: Number(r.tlds) || 0, registrars: names.map((x) => x.name), lastChecked: r.last == null ? null : Number(r.last) };
}
async function sitemap() {
-- return await safe(() => db.prepare(`SELECT p.tld, MAX(p.checked_at) AS checked_at FROM catalog_tld_prices p JOIN catalog_providers v ON v.id = p.provider_id WHERE ${ACTIVE} GROUP BY p.tld ORDER BY p.tld`).all(), []);
++ return db.prepare(`SELECT p.tld, MAX(p.checked_at) AS checked_at FROM catalog_tld_prices p JOIN catalog_providers v ON v.id = p.provider_id WHERE ${ACTIVE} GROUP BY p.tld ORDER BY p.tld`).all();
}
/** How many TLDs each registrar lists (a provider page's domain line). */
async function countFor(providerId) {
-- return await safe(async () => Number((await db.prepare("SELECT COUNT(*) AS n FROM catalog_tld_prices WHERE provider_id = ? AND status = 'active'").get(providerId)).n) || 0, 0);
++ return Number((await db.prepare("SELECT COUNT(*) AS n FROM catalog_tld_prices WHERE provider_id = ? AND status = 'active'").get(providerId)).n) || 0;
}
-- return { apply, list, tld, cheapest, knownTlds, stats, sitemap, countFor, ready, SORTS: Object.keys(SORTS), KINDS: Object.keys(KINDS) };
++ return { apply, list, tld, cheapest, knownTlds, stats, sitemap, countFor, SORTS: Object.keys(SORTS), KINDS: Object.keys(KINDS) };
}
module.exports = { createDomainStore };
ddiff --git a/server/catalog/search-index.js b/server/catalog/search-index.js
iindex 5fe8e7c..a66ade3 100644
---- a/server/catalog/search-index.js
++++ b/server/catalog/search-index.js
@@@ -89,9 +89,6 @@ function tldDocument(row) {
};
}
--// Before migration 0011 applies (a held contract holds it) there is no price table: no ending to send yet.
--const orNone = (p) => p.catch((err) => { if (err && err.code === '42P01') return []; throw err; });
--
function createCatalogSearch({ config, store, outbox, log = console }) {
const db = store.db;
const feed = createSearchFeed({
@@@ -119,7 +116,7 @@ function createCatalogSearch({ config, store, outbox, log = console }) {
page: (row) => `/catalog/domains/${encodeURIComponent(row.tld)}`,
document: tldDocument,
// Every ending any registrar has listed: its active prices aggregated (none active: unlisted, a tombstone).
-- rows: (after, limit) => orNone(db.many(`SELECT 'tld-' || p.tld AS id, p.tld,
++ rows: (after, limit) => db.many(`SELECT 'tld-' || p.tld AS id, p.tld,
MIN(p.register) FILTER (WHERE p.status = 'active' AND v.status = 'active') AS register,
MIN(p.renew) FILTER (WHERE p.status = 'active' AND v.status = 'active') AS renew,
MIN(COALESCE(p.register, p.renew) + 4 * COALESCE(p.renew, p.register)) FILTER (WHERE p.status = 'active' AND v.status = 'active') AS five_year,
@@@ -128,8 +125,8 @@ function createCatalogSearch({ config, store, outbox, log = console }) {
(array_agg(p.source_url ORDER BY p.register NULLS LAST))[1] AS source_url,
(array_agg(v.name ORDER BY p.register NULLS LAST))[1] AS registrar
FROM catalog_tld_prices p JOIN catalog_providers v ON v.id = p.provider_id
-- GROUP BY p.tld HAVING 'tld-' || p.tld > $1 ORDER BY 1 LIMIT $2`, [after, limit])),
-- exists: async (ids) => (await orNone(db.many("SELECT DISTINCT 'tld-' || tld AS id FROM catalog_tld_prices WHERE 'tld-' || tld = ANY($1)", [ids]))).map((r) => r.id),
++ GROUP BY p.tld HAVING 'tld-' || p.tld > $1 ORDER BY 1 LIMIT $2`, [after, limit]),
++ exists: async (ids) => (await db.many("SELECT DISTINCT 'tld-' || tld AS id FROM catalog_tld_prices WHERE 'tld-' || tld = ANY($1)", [ids])).map((r) => r.id),
},
},
});
ddiff --git a/server/catalog/store.js b/server/catalog/store.js
iindex a527121..bb8fbe3 100644
---- a/server/catalog/store.js
++++ b/server/catalog/store.js
@@@ -193,14 +193,7 @@ function createCatalog({ store, community = null, log = console, indexnow = null
}
const offset = query.cursor == null ? 0 : Number(query.cursor);
if (!Number.isSafeInteger(offset) || offset < 0) invalid('Invalid cursor');
-- let rows;
-- try {
-- rows = await db.prepare(`SELECT * FROM catalog_offers WHERE ${where} ORDER BY ${OFFER_SORTS[sort]} LIMIT 51 OFFSET ${offset}`).all(...args);
-- } catch (err) {
-- // Before migration 0009 has applied (a held contract migration holds it), there are no offers yet.
-- if (err && err.code === '42P01') return { offers: [], next_cursor: null };
-- throw err;
-- }
++ const rows = await db.prepare(`SELECT * FROM catalog_offers WHERE ${where} ORDER BY ${OFFER_SORTS[sort]} LIMIT 51 OFFSET ${offset}`).all(...args);
return { offers: rows.slice(0, 50).map(offerOut), next_cursor: rows.length > 50 ? String(offset + 50) : null };
}
const planOut = (p) => ({ ...p, affiliate_rel: p.affiliate_url ? 'sponsored nofollow noopener' : null, affiliate_disclosure: p.affiliate_url ? 'OpenVibe may earn a commission from links marked as affiliate links; it never changes the order or the prices shown.' : null, price_per_vcpu: p.vcpu > 0 ? Number(p.price_monthly) / Number(p.vcpu) : null, price_per_gb_ram: p.ram_mb > 0 ? Number(p.price_monthly) * 1024 / Number(p.ram_mb) : null, price_per_gb_disk: p.disk_gb > 0 ? Number(p.price_monthly) / Number(p.disk_gb) : null, checked_at: new Date(Number(p.checked_at)).toISOString() });
ddiff --git a/server/db.js b/server/db.js
iindex 56dc10b..a38cc32 100644
---- a/server/db.js
++++ b/server/db.js
@@@ -9,9 +9,8 @@ const { createDiscussionRefs } = require('openvibe-publishing/discussion');
const MIGRATIONS = path.join(__dirname, '..', 'migrations');
const DEV_PGLITE = path.join(__dirname, '..', 'data', 'pglite');
--// catalog_offers (0009) is not required yet: the 0008 contract holds every later migration until 2026-10-19 00:37 UTC
--// (sdk/db keeps the order), and the offers page shows none until then. Required again once 0009 has applied.
--const CHARTER_TABLES = ['host_catalog_discussion_refs', 'catalog_providers', 'catalog_plans', 'catalog_price_changes', 'catalog_promos', 'catalog_ingest_cursor'];
++const CHARTER_TABLES = ['host_catalog_discussion_refs', 'catalog_providers', 'catalog_plans', 'catalog_price_changes', 'catalog_promos', 'catalog_ingest_cursor',
++ 'catalog_offers', 'catalog_tld_prices', 'catalog_tld_price_changes', 'catalog_bench_links', 'catalog_benchmarks'];
/**
* The serving handle (ADR-035): DATABASE_URL through PgBouncer; in development without it, an embedded PGlite database
ddiff --git a/test/catalog-domains.test.js b/test/catalog-domains.test.js
iindex be133fc..fb71373 100644
---- a/test/catalog-domains.test.js
++++ b/test/catalog-domains.test.js
@@@ -4,8 +4,7 @@
* price list from Sources becomes one price per TLD (Handshake names left out, "2,060.25" read, a change kept as history,
* a TLD no longer sold gone); the TLD table sorts popular first, by price or over five years; a TLD's page lists every
* registrar; a name check reads what was typed, asks the registry over RDAP (cached, refused redirects, unknown when the
-- * ending has no RDAP) and prices the same name at the popular endings; without the tables (a held migration) the pages
-- * still answer.
++ * ending has no RDAP) and prices the same name at the popular endings; readiness waits for the tables.
*/
const assert = require('assert');
const { boot, check, done } = require('./helpers/boot');
@@@ -175,23 +174,9 @@ function fakeRegistry() {
assert.deepStrictEqual([empty.status, empty.headers.location], [303, '/catalog/domains']);
});
-- await check('without the domain tables (a held migration) the pages still answer and a name can be checked', async () => {
-- // The app role cannot drop tables on PostgreSQL: every statement naming them answers as PostgreSQL does when they are missing.
-- const db = t.ctx.store.db;
-- const prepare = db.prepare;
-- const missing = () => Promise.reject(Object.assign(new Error('relation "catalog_tld_prices" does not exist'), { code: '42P01' }));
-- db.prepare = (sql) => (/catalog_tld_/.test(sql) ? { all: missing, get: missing, run: missing } : prepare.call(db, sql));
-- const index = await t.api('GET', '/catalog/domains');
-- assert.strictEqual(index.status, 200);
-- assert.match(index.text, /Is your name free\?/);
-- assert.doesNotMatch(index.text, /<table/, 'no empty price table');
-- assert.deepStrictEqual((await t.api('GET', '/api/v1/catalog/domains')).json(), { tlds: [], next_cursor: null });
-- assert.strictEqual((await t.api('GET', '/catalog/domains/dev')).status, 404);
-- assert.strictEqual((await t.api('GET', '/')).status, 200);
-- assert.strictEqual((await t.api('GET', '/sitemap.xml')).status, 200);
-- assert.strictEqual((await t.api('GET', '/catalog/providers/porkbun')).status, 200);
-- assert.strictEqual((await t.api('GET', '/api/v1/catalog/domains/check?name=taken.com')).json().check.status, 'registered');
-- db.prepare = prepare;
++ await check('readiness waits for the domain tables (a release never answers without them)', async () => {
++ const { CHARTER_TABLES } = require('../server/db');
++ for (const table of ['catalog_tld_prices', 'catalog_tld_price_changes']) assert.ok(CHARTER_TABLES.includes(table), table);
});
} finally { await t.close(); }
done();
ddiff --git a/test/catalog-offers.test.js b/test/catalog-offers.test.js
iindex 159dc79..d865e13 100644
---- a/test/catalog-offers.test.js
++++ b/test/catalog-offers.test.js
@@@ -94,16 +94,8 @@ const item = (key, title, { published = '2026-09-20T10:00:00Z', summary = null,
assert.match(front.text, /Latest hosting offers/);
assert.match(front.text, /href="\/catalog\/offers"/);
});
-- await check('before migration 0009 applies (a held contract holds it), offers answer empty and readiness does not need the table', async () => {
-- const { createCatalog } = require('../server/catalog/store');
-- const missing = Object.assign(new Error('relation "catalog_offers" does not exist'), { code: '42P01' });
-- const db = { prepare: () => ({ all: async () => { throw missing; }, get: async () => null, run: async () => ({ changes: 0 }) }) };
-- const early = createCatalog({ store: { db, now: () => NOW } });
-- assert.deepStrictEqual(await early.offers({}), { offers: [], next_cursor: null });
-- assert.ok(!require('../server/db').CHARTER_TABLES.includes('catalog_offers'), 'readiness does not wait for the table');
-- const other = Object.assign(new Error('boom'), { code: '57014' });
-- const broken = createCatalog({ store: { db: { prepare: () => ({ all: async () => { throw other; } }) }, now: () => NOW } });
-- await assert.rejects(broken.offers({}), /boom/, 'any other error still fails');
++ await check('readiness waits for the offers table (a release never answers without it)', async () => {
++ assert.ok(require('../server/db').CHARTER_TABLES.includes('catalog_offers'));
});
} finally { await t.close(); }
done();