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'));
+ });
+});