diff --git a/src/html-pipe.js b/src/html-pipe.js index 0e6c7e80..cbae6eab 100644 --- a/src/html-pipe.js +++ b/src/html-pipe.js @@ -37,6 +37,7 @@ import { PipelineStatusError } from './PipelineStatusError.js'; import { PipelineResponse } from './PipelineResponse.js'; import { validatePathInfo } from './utils/path.js'; import { initAuthRoute } from './utils/auth.js'; +import fetchMappedMetadata from './steps/fetch-mapped-metadata.js'; /** * Runs the default pipeline and returns the response. @@ -88,6 +89,7 @@ export async function htmlPipe(state, req) { await Promise.all([ fetchConfigAll(state, req, res), fetchContent(state, req, res), + fetchMappedMetadata(state), ]); await requireProject(state, req, res); diff --git a/src/steps/extract-metadata.js b/src/steps/extract-metadata.js index e3e42f12..f2ba512c 100644 --- a/src/steps/extract-metadata.js +++ b/src/steps/extract-metadata.js @@ -158,6 +158,7 @@ export default function extractMetaData(state, req) { // with local metadata from document const metaConfig = Object.assign( state.metadata.getModifiers(state.info.unmappedPath || state.info.path), + state.mappedMetadata.getModifiers(state.info.path), getLocalMetadata(hast), ); diff --git a/src/steps/fetch-config-all.js b/src/steps/fetch-config-all.js index 3131b8b1..4056bcef 100644 --- a/src/steps/fetch-config-all.js +++ b/src/steps/fetch-config-all.js @@ -35,12 +35,12 @@ async function fetchMetadata(state, req, res) { try { json = JSON.parse(ret.body); } catch (e) { - throw new PipelineStatusError(400, `failed parsing of /metadata.json: ${e.message}`); + throw new PipelineStatusError(500, `failed parsing of /metadata.json: ${e.message}`); } const { data } = json.default ?? json; if (!Array.isArray(data)) { - throw new PipelineStatusError(400, 'failed loading of /metadata.json: data must be an array'); + throw new PipelineStatusError(500, 'failed loading of /metadata.json: data must be an array'); } state.metadata = Modifiers.fromModifierSheet( diff --git a/src/steps/fetch-mapped-metadata.js b/src/steps/fetch-mapped-metadata.js new file mode 100644 index 00000000..21053fdc --- /dev/null +++ b/src/steps/fetch-mapped-metadata.js @@ -0,0 +1,55 @@ +/* + * Copyright 2021 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { PipelineStatusError } from '../PipelineStatusError.js'; +import { Modifiers } from '../utils/modifiers.js'; + +/** + * Loads metadata for a mapped path and puts it into `state.mappedMetadata`. If path is not + * mapped or there is no `metadata.json` at that path, puts an `EMPTY` Modifiers object into + * `state.mappedMetadata`. + * + * @type PipelineStep + * @param {PipelineState} state + * @returns {Promise} + */ +export default async function fetchMappedMetadata(state) { + state.mappedMetadata = Modifiers.EMPTY; + if (!state.mapped) { + return; + } + const { contentBusId, partition } = state; + const metadataPath = `${state.info.path}/metadata.json`; + const key = `${contentBusId}/${partition}${metadataPath}`; + const ret = await state.s3Loader.getObject('helix-content-bus', key); + if (ret.status === 200) { + let json; + try { + json = JSON.parse(ret.body); + } catch (e) { + throw new PipelineStatusError(500, `failed parsing of ${metadataPath}: ${e.message}`); + } + + const { data } = json.default ?? json; + if (!Array.isArray(data)) { + throw new PipelineStatusError(500, `failed loading of ${metadataPath}: data must be an array`); + } + + state.mappedMetadata = Modifiers.fromModifierSheet( + data, + ); + return; + } + if (ret.status !== 404) { + throw new PipelineStatusError(502, `failed to load ${metadataPath}: ${ret.status}`); + } +} diff --git a/src/steps/folder-mapping.js b/src/steps/folder-mapping.js index e6d4b75d..d91262a8 100644 --- a/src/steps/folder-mapping.js +++ b/src/steps/folder-mapping.js @@ -55,6 +55,7 @@ export default function folderMapping(state) { state.info.resourcePath = mapped; state.log.info(`mapped ${path} to ${state.info.resourcePath} (code-bus)`); } else { + state.mapped = true; state.log.info(`mapped ${path} to ${state.info.path} (content-bus)`); } } diff --git a/src/steps/set-x-surrogate-key-header.js b/src/steps/set-x-surrogate-key-header.js index 196243c7..979deac6 100644 --- a/src/steps/set-x-surrogate-key-header.js +++ b/src/steps/set-x-surrogate-key-header.js @@ -37,10 +37,15 @@ export default async function setXSurrogateKeyHeader(state, req, res) { } else if (path.endsWith('.plain.html')) { path = path.substring(0, path.length - '.plain.html'.length); } - keys.push(await computeSurrogateKey(`${contentBusId}${path}`)); + const hash = await computeSurrogateKey(`${contentBusId}${path}`); + keys.push(hash); keys.push(`${contentBusId}_metadata`); keys.push(`${ref}--${repo}--${owner}_head`); + if (state.mapped) { + keys.push(`${hash}_metadata`); + } + res.headers.set('x-surrogate-key', keys.join(' ')); } diff --git a/test/steps/fetch-config-all.test.js b/test/steps/fetch-config-all.test.js index 33de077e..de992e79 100644 --- a/test/steps/fetch-config-all.test.js +++ b/test/steps/fetch-config-all.test.js @@ -210,7 +210,7 @@ describe('Fetch Metadata fallback', () => { headers: new Map(), }), }); - await assert.rejects(promise, new PipelineStatusError(400, 'failed parsing of /metadata.json: Unexpected token h in JSON at position 1')); + await assert.rejects(promise, new PipelineStatusError(500, 'failed parsing of /metadata.json: Unexpected token h in JSON at position 1')); }); it('throws error on metadata with no data array', async () => { @@ -225,7 +225,7 @@ describe('Fetch Metadata fallback', () => { headers: new Map(), }), }); - await assert.rejects(promise, new PipelineStatusError(400, 'failed loading of /metadata.json: data must be an array')); + await assert.rejects(promise, new PipelineStatusError(500, 'failed loading of /metadata.json: data must be an array')); }); it('throws error on generic error', async () => { diff --git a/test/steps/fetch-mapped-metadata.test.js b/test/steps/fetch-mapped-metadata.test.js new file mode 100644 index 00000000..b3cad718 --- /dev/null +++ b/test/steps/fetch-mapped-metadata.test.js @@ -0,0 +1,75 @@ +/* + * Copyright 2018 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +/* eslint-env mocha */ +import assert from 'assert'; +import { PipelineStatusError } from '../../src/index.js'; +import { StaticS3Loader } from '../StaticS3Loader.js'; +import fetchMappedMetadata from '../../src/steps/fetch-mapped-metadata.js'; + +describe('Fetch Mapped Metadata', () => { + it('throws error on invalid json', async () => { + const promise = fetchMappedMetadata({ + log: console, + contentBusId: 'foo-id', + partition: 'live', + mapped: true, + info: { + path: '/mapped', + }, + s3Loader: new StaticS3Loader() + .reply('helix-content-bus', 'foo-id/live/mapped/metadata.json', { + status: 200, + body: 'this is no json!', + headers: new Map(), + }), + }); + await assert.rejects(promise, new PipelineStatusError(500, 'failed parsing of /mapped/metadata.json: Unexpected token h in JSON at position 1')); + }); + + it('throws error on metadata with no data array', async () => { + const promise = fetchMappedMetadata({ + log: console, + contentBusId: 'foo-id', + partition: 'live', + mapped: true, + info: { + path: '/mapped', + }, + s3Loader: new StaticS3Loader() + .reply('helix-content-bus', 'foo-id/live/mapped/metadata.json', { + status: 200, + body: '{}', + headers: new Map(), + }), + }); + await assert.rejects(promise, new PipelineStatusError(500, 'failed loading of /mapped/metadata.json: data must be an array')); + }); + + it('throws error on generic error', async () => { + const promise = fetchMappedMetadata({ + log: console, + contentBusId: 'foo-id', + partition: 'live', + mapped: true, + info: { + path: '/mapped', + }, + s3Loader: new StaticS3Loader() + .reply('helix-content-bus', 'foo-id/live/mapped/metadata.json', { + status: 500, + body: '', + headers: new Map(), + }), + }); + await assert.rejects(promise, new PipelineStatusError(502, 'failed to load /mapped/metadata.json: 500')); + }); +});