diff --git a/README.md b/README.md index 6eef72f..1f79267 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ DWithEase Discover is the DWithEase news engine for ecommerce and Salesforce B2C ## Public links - [Discover](https://discover.dwithease.com/) -- [Live v2 feed](https://discover.dwithease.com/feed-live.json) -- [Development v2 feed](https://discover.dwithease.com/feed-dev.json) +- [Live feed](https://discover.dwithease.com/feed-live.json) +- [Development feed](https://discover.dwithease.com/feed-dev.json) - [Interactive API reference](https://discover.dwithease.com/schema.html) - [JSON Schema](https://discover.dwithease.com/feed.schema.json) - [OpenAPI document](https://discover.dwithease.com/openapi.json) @@ -17,13 +17,12 @@ DWithEase Discover is the DWithEase news engine for ecommerce and Salesforce B2C See [SOURCES.md](SOURCES.md) for the full source list and curation rules. -## Feed v2 +## Feed format -Both public endpoints use the v2 JSON format: +Both public endpoints use the current JSON format: ```json { - "version": 2, "locale": "en", "updatedAt": "2026-09-01T12:00:00Z", "items": [] diff --git a/SOURCES.md b/SOURCES.md index 3b15668..aa55cbc 100644 --- a/SOURCES.md +++ b/SOURCES.md @@ -45,4 +45,4 @@ This file records the inputs for `feed-dev.json`. - 4 promotion items. - 42 items in total. -`feed-live.json` remains a valid empty v2 feed during dev curation. +`feed-live.json` remains a valid empty feed during dev curation. diff --git a/assets/feed-model.js b/assets/feed-model.js index 6552f76..14446eb 100644 --- a/assets/feed-model.js +++ b/assets/feed-model.js @@ -75,7 +75,7 @@ function validImage(image) { || (typeof image.src === 'string' && image.src.startsWith('assets/')); } -function validV2Item(item) { +function validFeedItem(item) { if (!item || !['article', 'news', 'promotion'].includes(item.type)) return false; const requiredKeys = [ 'id', @@ -111,13 +111,12 @@ function validV2Item(item) { return campaign.placements.every((placement) => PRODUCT_PLACEMENTS.has(placement)); } -function validV2Feed(raw) { - if (!exactObject(raw, ['version', 'locale', 'updatedAt', 'items'])) return false; - if (raw.version !== 2 - || typeof raw.locale !== 'string' +function validFeed(raw) { + if (!exactObject(raw, ['locale', 'updatedAt', 'items'])) return false; + if (typeof raw.locale !== 'string' || !LOCALE_PATTERN.test(raw.locale) || !dateTime(raw.updatedAt)) return false; - if (!Array.isArray(raw.items) || !raw.items.every((item) => validV2Item(item))) return false; + if (!Array.isArray(raw.items) || !raw.items.every((item) => validFeedItem(item))) return false; return new Set(raw.items.map(({ id }) => id)).size === raw.items.length; } @@ -159,7 +158,7 @@ export function selectFeedName(search = '') { } export function buildCatalog(raw, now = Date.now()) { - const items = validV2Feed(raw) ? raw.items : []; + const items = validFeed(raw) ? raw.items : []; const promotions = items.filter((item) => item.type === 'promotion' && item.campaign.placements.includes('discover') && Date.parse(item.campaign.startsAt) <= now diff --git a/feed-dev.json b/feed-dev.json index a8a18f7..bb6bb70 100644 --- a/feed-dev.json +++ b/feed-dev.json @@ -1,5 +1,4 @@ { - "version": 2, "locale": "en", "updatedAt": "2026-09-01T12:50:31.306Z", "items": [ diff --git a/feed-live.json b/feed-live.json index 7a9b6ca..3142ddd 100644 --- a/feed-live.json +++ b/feed-live.json @@ -1,5 +1,4 @@ { - "version": 2, "locale": "en", "updatedAt": "2026-09-01T12:50:31.306Z", "items": [] diff --git a/feed.schema.json b/feed.schema.json index 2d54a8d..a5a519a 100644 --- a/feed.schema.json +++ b/feed.schema.json @@ -2,10 +2,6 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { - "version": { - "type": "number", - "const": 2 - }, "locale": { "type": "string", "pattern": "^[a-z]{2}(?:-[A-Z]{2})?(?![\\s\\S])" @@ -317,7 +313,6 @@ } }, "required": [ - "version", "locale", "updatedAt", "items" diff --git a/openapi.json b/openapi.json index 0b646c2..4fd1bbb 100644 --- a/openapi.json +++ b/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.1.1", "info": { "title": "DWithEase Discover Feed API", - "version": "2.0.0", + "version": "1.0.0", "description": "Public commerce articles, news, and DWithEase product promotions. The Zod runtime also rejects duplicate item IDs and campaign end dates that do not follow their start dates." }, "tags": [ @@ -61,13 +61,9 @@ "schemas": { "DiscoverFeed": { "title": "Discover Feed", - "description": "The complete version 2 Discover document.", + "description": "The complete Discover feed document.", "type": "object", "properties": { - "version": { - "type": "number", - "const": 2 - }, "locale": { "type": "string", "pattern": "^[a-z]{2}(?:-[A-Z]{2})?(?![\\s\\S])" @@ -379,7 +375,6 @@ } }, "required": [ - "version", "locale", "updatedAt", "items" diff --git a/scripts/feed-schema.ts b/scripts/feed-schema.ts index 8b4249d..ed271ab 100644 --- a/scripts/feed-schema.ts +++ b/scripts/feed-schema.ts @@ -115,7 +115,6 @@ export const FeedItemSchema = z.discriminatedUnion('type', [ ]); export const FeedSchema = z.strictObject({ - version: z.literal(2), locale: z.string().regex(new RegExp(`^[a-z]{2}(?:-[A-Z]{2})?${STRICT_END}`)), updatedAt: DateTimeSchema, items: z.array(FeedItemSchema), diff --git a/scripts/write-openapi.ts b/scripts/write-openapi.ts index efda425..ad989b1 100644 --- a/scripts/write-openapi.ts +++ b/scripts/write-openapi.ts @@ -24,7 +24,7 @@ const document = { openapi: '3.1.1', info: { title: 'DWithEase Discover Feed API', - version: '2.0.0', + version: '1.0.0', description: 'Public commerce articles, news, and DWithEase product promotions. The Zod runtime also rejects duplicate item IDs and campaign end dates that do not follow their start dates.', }, tags: [ @@ -61,7 +61,7 @@ const document = { schemas: { DiscoverFeed: { title: 'Discover Feed', - description: 'The complete version 2 Discover document.', + description: 'The complete Discover feed document.', ...discoverFeedSchema, }, }, diff --git a/test/browser-page.test.js b/test/browser-page.test.js index 2fcedf7..ce88957 100644 --- a/test/browser-page.test.js +++ b/test/browser-page.test.js @@ -29,7 +29,6 @@ const COMMON_ITEM = { }, }; const TEST_FEED = { - version: 2, locale: 'en', updatedAt: '2026-09-01T00:00:00Z', items: [ diff --git a/test/discover-page.test.js b/test/discover-page.test.js index 2b91efd..5f9570b 100644 --- a/test/discover-page.test.js +++ b/test/discover-page.test.js @@ -16,7 +16,7 @@ function createRoot() { return document.getElementById('discover-content'); } -const V2_PROMOTION = { +const PROMOTION = { id: 'catalogspark-2026', type: 'promotion', title: 'Make product data ready', @@ -39,7 +39,7 @@ const V2_PROMOTION = { }, }; -test('renders v2 promotions and editorial items with useful metadata', () => { +test('renders promotions and editorial items with useful metadata', () => { const root = createRoot(); const unsafeTitle = ''; const article = { @@ -64,7 +64,7 @@ test('renders v2 promotions and editorial items with useful metadata', () => { { icon: 'https://discover.example/assets/sources/example.svg' }, ]]); renderCatalog(root, { - promotions: [V2_PROMOTION], + promotions: [PROMOTION], editorial: [article], }, sources); @@ -118,7 +118,7 @@ test('does not render actions for raw non-RFC URLs', () => { 'https://catalogspark.com:65536/audit', ]) { renderCatalog(root, { - promotions: [{ ...V2_PROMOTION, url }], + promotions: [{ ...PROMOTION, url }], editorial: [], }); @@ -138,7 +138,7 @@ test('renders a working retry action after a feed error', () => { assert.equal(retries, 1); }); -test('loads only the selected v2 feed', async () => { +test('loads only the selected feed', async () => { const root = createRoot(); const requests = []; const fetchImpl = async (url) => { @@ -159,10 +159,9 @@ test('loads only the selected v2 feed', async () => { return { ok: true, json: async () => ({ - version: 2, locale: 'en', updatedAt: '2026-09-01T00:00:00Z', - items: [V2_PROMOTION], + items: [PROMOTION], }), }; }; @@ -191,10 +190,9 @@ test('keeps feed text when the source registry fails', async () => { return { ok: true, json: async () => ({ - version: 2, locale: 'en', updatedAt: '2026-09-01T00:00:00Z', - items: [V2_PROMOTION], + items: [PROMOTION], }), }; }; diff --git a/test/feed-model.test.js b/test/feed-model.test.js index ad396cb..4ed375a 100644 --- a/test/feed-model.test.js +++ b/test/feed-model.test.js @@ -11,7 +11,7 @@ import { validateFeed } from '../scripts/feed-schema.ts'; const NOW = Date.parse('2026-09-10T12:00:00Z'); -const V2_PROMOTION = { +const PROMOTION = { id: 'catalogspark-2026', type: 'promotion', title: 'Make product data ready', @@ -40,7 +40,7 @@ test('selects the live feed unless the query asks for the dev feed', () => { assert.equal(selectFeedName('?feed=other'), 'live'); }); -test('builds the v2 catalog with schema-valid offsets and sorted editorial items', () => { +test('builds the catalog with schema-valid offsets and sorted editorial items', () => { const olderArticle = { id: 'older-article', type: 'article', @@ -67,30 +67,29 @@ test('builds the v2 catalog with schema-valid offsets and sorted editorial items }; const catalog = buildCatalog({ - version: 2, locale: 'en', updatedAt: '2026-09-01T00:00:00Z', - items: [olderArticle, V2_PROMOTION, newerNews], + items: [olderArticle, PROMOTION, newerNews], }, NOW); assert.deepEqual(catalog.promotions.map(({ id }) => id), ['catalogspark-2026']); assert.deepEqual(catalog.editorial.map(({ id }) => id), ['newer-news', 'older-article']); }); -test('requires the complete v2 document before building a catalog', () => { +test('requires the complete feed document before building a catalog', () => { const validFeed = { - version: 2, locale: 'en', updatedAt: '2026-09-01T00:00:00Z', - items: [V2_PROMOTION], + items: [PROMOTION], }; const { updatedAt, ...missingUpdatedAt } = validFeed; const invalidFeeds = [ missingUpdatedAt, { ...validFeed, updatedAt: 'not-a-date' }, { ...validFeed, extra: true }, - { ...validFeed, items: [{ ...V2_PROMOTION, extra: true }] }, - { ...validFeed, items: [V2_PROMOTION, { ...V2_PROMOTION }] }, + { ...validFeed, version: 2 }, + { ...validFeed, items: [{ ...PROMOTION, extra: true }] }, + { ...validFeed, items: [PROMOTION, { ...PROMOTION }] }, ]; for (const feed of invalidFeeds) { @@ -99,7 +98,7 @@ test('requires the complete v2 document before building a catalog', () => { } }); -test('enforces raw v2 text limits in Zod and the browser catalog', () => { +test('enforces raw text limits in Zod and the browser catalog', () => { const article = { id: 'raw-text-limits', type: 'article', @@ -121,7 +120,6 @@ test('enforces raw v2 text limits in Zod and the browser catalog', () => { }, }; const validFeed = { - version: 2, locale: 'en', updatedAt: '2026-09-01T00:00:00Z', items: [article], @@ -170,7 +168,6 @@ test('counts Unicode code points for browser text limits', () => { }, }; const feed = { - version: 2, locale: 'en', updatedAt: '2026-09-01T00:00:00Z', items: [article], @@ -205,7 +202,6 @@ test('requires raw RFC 3986 lowercase HTTPS URLs across feed validators', () => }, }; const validFeed = { - version: 2, locale: 'en', updatedAt: '2026-09-01T00:00:00Z', items: [article], @@ -299,7 +295,6 @@ test('keeps web host validation aligned across feed contracts', () => { for (const [url, accepted] of cases) { const feed = { - version: 2, locale: 'en', updatedAt: '2026-09-01T00:00:00Z', items: [{ ...article, url }], @@ -330,7 +325,6 @@ test('rejects final line breaks across regex-backed public fields', () => { }, }; const validFeed = { - version: 2, locale: 'en', updatedAt: '2026-09-01T00:00:00Z', items: [article], @@ -398,16 +392,15 @@ test('renders schema-valid reserved-word slugs', () => { }, }; const promotion = { - ...V2_PROMOTION, + ...PROMOTION, id: 'prototype', tags: ['constructor'], campaign: { - ...V2_PROMOTION.campaign, + ...PROMOTION.campaign, id: 'constructor', }, }; const feed = { - version: 2, locale: 'en', updatedAt: '2026-09-01T00:00:00Z', items: [article, promotion], @@ -419,7 +412,7 @@ test('renders schema-valid reserved-word slugs', () => { assert.deepEqual(catalog.promotions.map(({ id }) => id), ['prototype']); }); -test('returns an empty v2 catalog for invalid payloads', () => { +test('returns an empty catalog for invalid payloads', () => { assert.deepEqual(buildCatalog({ items: [] }, NOW), { promotions: [], editorial: [], diff --git a/test/feed-schema.test.ts b/test/feed-schema.test.ts index aedfea8..414229c 100644 --- a/test/feed-schema.test.ts +++ b/test/feed-schema.test.ts @@ -52,13 +52,12 @@ const PROMOTION = { }; const VALID_FEED = { - version: 2, locale: 'en', updatedAt: '2026-09-01T12:00:00Z', items: [ARTICLE, PROMOTION], }; -test('accepts the version 2 article and promotion contract', () => { +test('accepts the article and promotion contract', () => { const result = validateFeed(VALID_FEED); assert.equal(result.success, true); @@ -66,7 +65,6 @@ test('accepts the version 2 article and promotion contract', () => { test('rejects unsafe URLs, duplicate IDs, and invalid campaign dates', () => { const result = validateFeed({ - version: 2, locale: 'en', updatedAt: '2026-09-01T12:00:00Z', items: [ @@ -90,6 +88,14 @@ test('rejects unsafe URLs, duplicate IDs, and invalid campaign dates', () => { ); }); +test('rejects a version field because the feed format is unversioned', () => { + const result = validateFeed({ ...VALID_FEED, version: 2 }); + + assert.equal(result.success, false); + assert.deepEqual(result.errors.map(({ path }) => path), ['']); + assert.match(result.errors[0]?.message ?? '', /Unrecognized key/); +}); + test('checks a valid feed file from the command line', async (context) => { const directory = await mkdtemp(join(tmpdir(), 'discover-feed-')); context.after(() => rm(directory, { recursive: true, force: true })); @@ -135,7 +141,7 @@ test('writes the public JSON Schema from the Zod contract', async (context) => { assert.equal(schema.$schema, 'https://json-schema.org/draft/2020-12/schema'); assert.equal(schema.$id, 'https://discover.dwithease.com/feed.schema.json'); assert.equal(schema.title, 'DWithEase Discover Feed'); - assert.equal(schema.properties.version.const, 2); + assert.equal(schema.properties.version, undefined); const article = schema.properties.items.items.oneOf[0]; const urlPattern = new RegExp(article.properties.url.pattern); assert.equal(article.properties.url.format, 'uri'); diff --git a/test/schema-docs.test.js b/test/schema-docs.test.js index b3eb276..456570f 100644 --- a/test/schema-docs.test.js +++ b/test/schema-docs.test.js @@ -23,6 +23,7 @@ test('writes an OpenAPI 3.1 contract for both Discover feeds', async (context) = const document = JSON.parse(await readFile(outputPath, 'utf8')); assert.equal(document.openapi, '3.1.1'); assert.equal(document.info.title, 'DWithEase Discover Feed API'); + assert.equal(document.info.version, '1.0.0'); assert.equal(document.servers, undefined); assert.equal( document.paths['/feed-live.json'].get.responses['200'].content['application/json'].schema.$ref, @@ -32,7 +33,7 @@ test('writes an OpenAPI 3.1 contract for both Discover feeds', async (context) = document.paths['/feed-dev.json'].get.responses['200'].content['application/json'].schema.$ref, '#/components/schemas/DiscoverFeed', ); - assert.equal(document.components.schemas.DiscoverFeed.properties.version.const, 2); + assert.equal(document.components.schemas.DiscoverFeed.properties.version, undefined); const article = document.components.schemas.DiscoverFeed.properties.items.items.oneOf[0]; const urlPattern = new RegExp(article.properties.url.pattern); assert.equal(article.properties.url.format, 'uri'); @@ -62,7 +63,7 @@ test('writes an OpenAPI 3.1 contract for both Discover feeds', async (context) = ); }); -test('publishes both documented endpoints as v2 feeds', async () => { +test('publishes both documented feed endpoints', async () => { const live = JSON.parse(await readFile('feed-live.json', 'utf8')); const development = JSON.parse(await readFile('feed-dev.json', 'utf8'));