openvibers/openvibe.host Commits
Host: the catalog serves (and is ready) before the offers migration applies; it waits behind the 0008 contract until 10-19 (#90)
Changes
@@@ -177,7 +177,14 @@ function createCatalog({ store, community = null, log = console }) {}const offset = query.cursor == null ? 0 : Number(query.cursor);if (!Number.isSafeInteger(offset) || offset < 0) invalid('Invalid cursor');-- const rows = await db.prepare(`SELECT * FROM catalog_offers WHERE ${where} ORDER BY ${OFFER_SORTS[sort]} LIMIT 51 OFFSET ${offset}`).all(...args);++ 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;++ }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() });@@@ -9,7 +9,9 @@ const { createDiscussionRefs } = require('openvibe-publishing/discussion');const MIGRATIONS = path.join(__dirname, '..', 'migrations');const DEV_PGLITE = path.join(__dirname, '..', 'data', 'pglite');--const CHARTER_TABLES = ['host_catalog_discussion_refs', 'catalog_providers', 'catalog_plans', 'catalog_price_changes', 'catalog_promos', 'catalog_offers', 'catalog_ingest_cursor'];++// 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'];/*** The serving handle (ADR-035): DATABASE_URL through PgBouncer; in development without it, an embedded PGlite database@@@ -91,6 +91,17 @@ 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');++ });} finally { await t.close(); }done();})().catch((e) => { console.error(e); process.exit(1); });
@@ -177,7 +177,14 @@ function createCatalog({ store, community = null, log = console }) {
@@ -177,7 +177,14 @@ function createCatalog({ store, community = null, log = console }) {
}
}
const offset = query.cursor == null ? 0 : Number(query.cursor);
const offset = query.cursor == null ? 0 : Number(query.cursor);
if (!Number.isSafeInteger(offset) || offset < 0) invalid('Invalid cursor');
if (!Number.isSafeInteger(offset) || offset < 0) invalid('Invalid cursor');
- const rows = await db.prepare(`SELECT * FROM catalog_offers WHERE ${where} ORDER BY ${OFFER_SORTS[sort]} LIMIT 51 OFFSET ${offset}`).all(...args);
+ 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;
+ }
return { offers: rows.slice(0, 50).map(offerOut), next_cursor: rows.length > 50 ? String(offset + 50) : null };
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() });
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() });
@@ -9,7 +9,9 @@ const { createDiscussionRefs } = require('openvibe-publishing/discussion');
@@ -9,7 +9,9 @@ const { createDiscussionRefs } = require('openvibe-publishing/discussion');
const MIGRATIONS = path.join(__dirname, '..', 'migrations');
const MIGRATIONS = path.join(__dirname, '..', 'migrations');
const DEV_PGLITE = path.join(__dirname, '..', 'data', 'pglite');
const DEV_PGLITE = path.join(__dirname, '..', 'data', 'pglite');
-const CHARTER_TABLES = ['host_catalog_discussion_refs', 'catalog_providers', 'catalog_plans', 'catalog_price_changes', 'catalog_promos', 'catalog_offers', 'catalog_ingest_cursor'];
+// 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'];
/**
/**
* The serving handle (ADR-035): DATABASE_URL through PgBouncer; in development without it, an embedded PGlite database
* The serving handle (ADR-035): DATABASE_URL through PgBouncer; in development without it, an embedded PGlite database
@@ -91,6 +91,17 @@ const item = (key, title, { published = '2026-09-20T10:00:00Z', summary = null,
@@ -91,6 +91,17 @@ const item = (key, title, { published = '2026-09-20T10:00:00Z', summary = null,
assert.match(front.text, /Latest hosting offers/);
assert.match(front.text, /Latest hosting offers/);
assert.match(front.text, /href="\/catalog\/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');
+ });
} finally { await t.close(); }
} finally { await t.close(); }
done();
done();
})().catch((e) => { console.error(e); process.exit(1); });
})().catch((e) => { console.error(e); process.exit(1); });