diff --git a/src/index.js b/src/index.js index 581c9c5..2273408 100644 --- a/src/index.js +++ b/src/index.js @@ -57,9 +57,6 @@ const nameSelector = (segs) => { return literals.join('-'); }; -/** - * Routing table. - */ export const router = new Router(nameSelector) .add('/auth/*', auth) .add('/discover', discover) @@ -105,7 +102,7 @@ async function run(request, context) { if (!handler) { return new Response('', { status: 404 }); } - const info = RequestInfo.create(request, variables); + const info = RequestInfo.create(request, router, variables); if (info.method === 'OPTIONS') { return new Response('', { status: 204, diff --git a/src/live/status.js b/src/live/status.js index 5a3cab7..c9eaa01 100644 --- a/src/live/status.js +++ b/src/live/status.js @@ -36,7 +36,7 @@ export default async function liveStatus(context, info) { webPath: info.webPath, resourcePath: info.resourcePath, live, - // TODO links: getAPIUrls(ctx, info, 'status', 'preview', 'live', 'code'), + links: info.getAPIUrls('status', 'preview', 'live', 'code'), }; return new Response(JSON.stringify(resp, null, 2), { diff --git a/src/preview/status.js b/src/preview/status.js index d0c335c..62b5085 100644 --- a/src/preview/status.js +++ b/src/preview/status.js @@ -36,7 +36,7 @@ export default async function previewStatus(context, info) { webPath: info.webPath, resourcePath: info.resourcePath, preview, - // TODO links: getAPIUrls(context, info, 'status', 'preview', 'live', 'code'), + links: info.getAPIUrls('status', 'preview', 'live', 'code'), }; return new Response(JSON.stringify(resp, null, 2), { diff --git a/src/router/node.js b/src/router/node.js index 607af43..fdf1871 100644 --- a/src/router/node.js +++ b/src/router/node.js @@ -9,6 +9,11 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ +const NodeType = { + LITERAL: 1, + VARIABLE: 2, + PATH: 3, +}; /** * Node in the router tree, either intermediate or leaf. @@ -19,6 +24,16 @@ export class Node { */ #label; + /** + * Type of node. + */ + #type; + + /** + * Parent for this node. + */ + #parent; + /** * Literal children of this node. */ @@ -39,27 +54,29 @@ export class Node { */ #route; - constructor(label) { + constructor(label, type = NodeType.LITERAL, parent = undefined) { this.#label = label; + this.#type = type; + this.#parent = parent; this.#children = []; } #getOrCreateChild(seg) { if (seg === '*') { if (!this.#star) { - this.#star = new Node(seg); + this.#star = new Node(seg, NodeType.PATH, this); } return this.#star; } if (seg.startsWith(':')) { if (!this.#variable) { - this.#variable = new Node(seg.substring(1)); + this.#variable = new Node(seg.substring(1), NodeType.VARIABLE, this); } return this.#variable; } let ret = this.#children.find((child) => child.#label === seg); if (!ret) { - ret = new Node(seg); + ret = new Node(seg, NodeType.LITERAL, this); this.#children.push(ret); } return ret; @@ -113,4 +130,31 @@ export class Node { } return null; } + + /** + * Returns the external path by traversing from a leaf back + * to the root. + * + * @param {string[]} segs path segments to collect + * @param {Map} variables variables + * @returns {void} + */ + external(segs, variables) { + const label = this.#label; + + switch (this.#type) { + case NodeType.LITERAL: + segs.unshift(label); + break; + case NodeType.VARIABLE: + segs.unshift(variables[label]); + break; + case NodeType.PATH: + segs.unshift(variables.path); + break; + default: + break; + } + this.#parent?.external(segs, variables); + } } diff --git a/src/router/router.js b/src/router/router.js index 94d9f4c..f5bec3e 100644 --- a/src/router/router.js +++ b/src/router/router.js @@ -31,7 +31,7 @@ export default class Router { #routes; constructor(nameSelector) { - this.#root = new Node('', (info, segs) => segs.push('')); + this.#root = new Node(''); this.#nameSelector = nameSelector; this.#routes = new Map(); } @@ -73,4 +73,23 @@ export default class Router { } return null; } + + /** + * Returns the external path for a route with some variables + * to fill in the variable segments traversing. + * + * @param {string} name route name + * @param {Map} variables variables + * @returns {string} external path + */ + external(name, variables) { + /** @type {Node} */ + const route = this.#routes.get(name); + if (!route) { + throw new Error(`route not found: ${name}`); + } + const segs = []; + route.external(segs, variables); + return segs.join('/'); + } } diff --git a/src/status/status.js b/src/status/status.js index b7cbff9..5695273 100644 --- a/src/status/status.js +++ b/src/status/status.js @@ -112,7 +112,7 @@ export default async function status(context, info) { live: await getLiveInfo(context, info), preview: await getPreviewInfo(context, info), edit, - // TODO links: getAPIUrls(context, info, 'status', 'preview', 'live', 'code'), + links: info.getAPIUrls('status', 'preview', 'live', 'code'), }; if (authInfo.profile) { diff --git a/src/support/RequestInfo.js b/src/support/RequestInfo.js index 0e0df21..ba3272a 100644 --- a/src/support/RequestInfo.js +++ b/src/support/RequestInfo.js @@ -207,6 +207,8 @@ class PathInfo { export class RequestInfo { #request; + #router; + #pathInfo; #owner; @@ -215,8 +217,9 @@ export class RequestInfo { #ref; - constructor(request, pathInfo) { + constructor(request, router, pathInfo) { this.#request = request; + this.#router = router; this.#pathInfo = pathInfo; } @@ -310,6 +313,7 @@ export class RequestInfo { * Create a new request info. * * @param {import('@adobe/fetch').Request} request request + * @param {import('../router/router.js').default} router router * @param {object} param0 params * @param {string} [param0.org] org, optional * @param {string} [param0.site] site, optional @@ -318,13 +322,13 @@ export class RequestInfo { * @param {string} [param0.route] route, optional * @returns {RequestInfo} */ - static create(request, { + static create(request, router, { org, site, path, ref, route, } = {}) { const httpRequest = new HttpRequest(request); const pathInfo = new PathInfo(route, org, site, path); - return Object.freeze(new RequestInfo(httpRequest, pathInfo).withRef(ref)); + return Object.freeze(new RequestInfo(httpRequest, router, pathInfo).withRef(ref)); } /** @@ -343,6 +347,7 @@ export class RequestInfo { }) { const info = new RequestInfo( other.#request, + other.#router, PathInfo.clone(other.#pathInfo, { org, site, path, route, }), @@ -375,6 +380,21 @@ export class RequestInfo { return url.href; } + getAPIUrls(...routes) { + const links = {}; + const variables = { + org: this.org, + site: this.site, + path: this.webPath.slice(1), + ref: this.ref, + }; + routes.forEach((name) => { + const path = this.#router.external(name, variables); + links[name] = this.getLinkUrl(path); + }); + return links; + } + toResourcePath() { return toResourcePath(this.webPath); } diff --git a/test/index.test.js b/test/index.test.js index 0c3e358..a040905 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -141,6 +141,12 @@ describe('Index Tests', () => { assert.strictEqual(result.status, 200); assert.deepStrictEqual(await result.json(), { edit: {}, + links: { + code: 'https://api.aem.live/org/sites/site/code/main/document', + live: 'https://api.aem.live/org/sites/site/live/document', + preview: 'https://api.aem.live/org/sites/site/preview/document', + status: 'https://api.aem.live/org/sites/site/status/document', + }, live: { contentBusId: `helix-content-bus/${SITE_CONFIG.content.contentBusId}/live/document.md`, contentType: 'text/plain; charset=utf-8', diff --git a/test/live/info.test.js b/test/live/info.test.js index 44a8cae..54f62aa 100644 --- a/test/live/info.test.js +++ b/test/live/info.test.js @@ -65,6 +65,12 @@ describe('Live Info Tests', () => { assert.strictEqual(response.status, 200); assert.deepStrictEqual(await response.json(), { + links: { + code: 'https://api.aem.live/org/sites/site/code/main/document', + live: 'https://api.aem.live/org/sites/site/live/document', + preview: 'https://api.aem.live/org/sites/site/preview/document', + status: 'https://api.aem.live/org/sites/site/status/document', + }, live: { contentBusId: `helix-content-bus/${SITE_CONFIG.content.contentBusId}/live/document.md`, contentType: 'text/plain; charset=utf-8', diff --git a/test/live/publish.test.js b/test/live/publish.test.js index 4372cb2..e73262c 100644 --- a/test/live/publish.test.js +++ b/test/live/publish.test.js @@ -257,6 +257,12 @@ describe('Publish Action Tests', () => { assert.strictEqual(response.status, 200); assert.deepStrictEqual(await response.json(), { + links: { + code: 'https://api.aem.live/org/sites/site/code/main/', + live: 'https://api.aem.live/org/sites/site/live/', + preview: 'https://api.aem.live/org/sites/site/preview/', + status: 'https://api.aem.live/org/sites/site/status/', + }, live: { configRedirectLocation: '/target', contentBusId: `helix-content-bus/${SITE_CONFIG.content.contentBusId}/live/index.md`, diff --git a/test/preview/info.test.js b/test/preview/info.test.js index e5de9a6..8de8271 100644 --- a/test/preview/info.test.js +++ b/test/preview/info.test.js @@ -65,6 +65,12 @@ describe('Preview Info Tests', () => { assert.strictEqual(response.status, 200); assert.deepStrictEqual(await response.json(), { + links: { + code: 'https://api.aem.live/org/sites/site/code/main/document', + live: 'https://api.aem.live/org/sites/site/live/document', + preview: 'https://api.aem.live/org/sites/site/preview/document', + status: 'https://api.aem.live/org/sites/site/status/document', + }, preview: { contentBusId: `helix-content-bus/${SITE_CONFIG.content.contentBusId}/preview/document.md`, contentType: 'text/plain; charset=utf-8', diff --git a/test/preview/preview.test.js b/test/preview/preview.test.js index f8d3b6e..a29dbfd 100644 --- a/test/preview/preview.test.js +++ b/test/preview/preview.test.js @@ -149,6 +149,12 @@ describe('Preview Action Tests', () => { assert.strictEqual(response.status, 200); assert.deepStrictEqual(await response.json(), { + links: { + code: 'https://api.aem.live/org/sites/site/code/main/', + live: 'https://api.aem.live/org/sites/site/live/', + preview: 'https://api.aem.live/org/sites/site/preview/', + status: 'https://api.aem.live/org/sites/site/status/', + }, preview: { configRedirectLocation: '/target', contentBusId: `helix-content-bus/${SITE_CONFIG.content.contentBusId}/preview/index.md`, diff --git a/test/status/handler.test.js b/test/status/handler.test.js index 13526de..c261bdc 100644 --- a/test/status/handler.test.js +++ b/test/status/handler.test.js @@ -101,6 +101,12 @@ describe('Status Handler Tests', () => { edit: { status: 403, }, + links: { + code: 'https://api.aem.live/org/sites/site/code/main/', + live: 'https://api.aem.live/org/sites/site/live/', + preview: 'https://api.aem.live/org/sites/site/preview/', + status: 'https://api.aem.live/org/sites/site/status/', + }, live: { error: 'forbidden', status: 403, @@ -153,8 +159,32 @@ describe('Status Handler Tests', () => { assert.strictEqual(result.status, 200); assert.deepStrictEqual(await result.json(), { - webPath: '/folder/page', - resourcePath: '/folder/page.md', + edit: { + url: 'https://docs.google.com/document/d/1LSIpJMKoYeVn8-o4c2okZ6x0EwdGKtgOEkaxbnM8nZ4/edit', + name: 'page', + contentType: 'application/vnd.google-apps.document', + folders: [ + { + name: 'folder', + url: 'https://drive.google.com/drive/u/0/folders/1BHM3lyqi0bEeaBZho8UD328oFsmsisyJ', + path: '/folder', + }, + { + name: '', + url: 'https://drive.google.com/drive/u/0/folders/18G2V_SZflhaBrSo_0fMYqhGaEF9Vetky', + path: '/', + }, + ], + lastModified: 'Tue, 15 Jun 2021 03:54:28 GMT', + sourceLocation: 'gdrive:1LSIpJMKoYeVn8-o4c2okZ6x0EwdGKtgOEkaxbnM8nZ4', + status: 200, + }, + links: { + code: 'https://api.aem.live/org/sites/site/code/main/folder/page', + live: 'https://api.aem.live/org/sites/site/live/folder/page', + preview: 'https://api.aem.live/org/sites/site/preview/folder/page', + status: 'https://api.aem.live/org/sites/site/status/folder/page', + }, live: { url: 'https://main--site--org.aem.live/folder/page', status: 200, @@ -179,26 +209,8 @@ describe('Status Handler Tests', () => { 'write', ], }, - edit: { - url: 'https://docs.google.com/document/d/1LSIpJMKoYeVn8-o4c2okZ6x0EwdGKtgOEkaxbnM8nZ4/edit', - name: 'page', - contentType: 'application/vnd.google-apps.document', - folders: [ - { - name: 'folder', - url: 'https://drive.google.com/drive/u/0/folders/1BHM3lyqi0bEeaBZho8UD328oFsmsisyJ', - path: '/folder', - }, - { - name: '', - url: 'https://drive.google.com/drive/u/0/folders/18G2V_SZflhaBrSo_0fMYqhGaEF9Vetky', - path: '/', - }, - ], - lastModified: 'Tue, 15 Jun 2021 03:54:28 GMT', - sourceLocation: 'gdrive:1LSIpJMKoYeVn8-o4c2okZ6x0EwdGKtgOEkaxbnM8nZ4', - status: 200, - }, + resourcePath: '/folder/page.md', + webPath: '/folder/page', }); }); @@ -252,6 +264,12 @@ describe('Status Handler Tests', () => { status: 200, url: 'https://docs.google.com/document/d/1ZJWJwL9szyTq6B-W0_Y7bFL1Tk1vyym4RyQ7AKXS7Ys/edit', }, + links: { + code: 'https://api.aem.live/org/sites/site/code/main/', + live: 'https://api.aem.live/org/sites/site/live/', + preview: 'https://api.aem.live/org/sites/site/preview/', + status: 'https://api.aem.live/org/sites/site/status/', + }, live: { contentBusId: `helix-content-bus/${SITE_CONFIG.content.contentBusId}/live/index.md`, contentType: 'text/plain; charset=utf-8', diff --git a/test/support/RequestInfo.test.js b/test/support/RequestInfo.test.js index a9a6cee..8fe7967 100644 --- a/test/support/RequestInfo.test.js +++ b/test/support/RequestInfo.test.js @@ -96,7 +96,11 @@ describe('RequestInfo Tests', () => { it('check RequestInfo creation', () => { // deny .aspx assert.throws( - () => RequestInfo.create(new Request('http:/api.aem.live'), { org: 'org', path: '/test.aspx' }), + () => RequestInfo.create( + new Request('http:/api.aem.live'), + undefined, + { org: 'org', path: '/test.aspx' }, + ), new StatusCodeError('', 404), ); }); diff --git a/test/utils.js b/test/utils.js index 95a3ae4..7ee6b1d 100644 --- a/test/utils.js +++ b/test/utils.js @@ -249,7 +249,8 @@ export function createContext(suffix, { * @returns {RequestInfo} info */ export function createInfo(suffix, headers = {}) { + const { variables } = router.match(suffix); return RequestInfo.create(new Request('http://api.aem.live/', { headers, - }), router.match(suffix).variables); + }), router, variables); }