From 0aa69ea559d528f2d37d6f67ea075c981af1c436 Mon Sep 17 00:00:00 2001 From: Dominique Pfister Date: Mon, 17 Nov 2025 12:59:39 +0100 Subject: [PATCH 1/3] fix: implement `getAPIUrls` --- src/index.js | 74 +------------------------------ src/live/status.js | 2 +- src/preview/status.js | 2 +- src/router/node.js | 44 +++++++++++++++++-- src/router/router.js | 13 +++++- src/router/table.js | 84 ++++++++++++++++++++++++++++++++++++ src/status/status.js | 2 +- src/support/RequestInfo.js | 15 +++++++ test/index.test.js | 11 ++++- test/live/info.test.js | 6 +++ test/live/publish.test.js | 6 +++ test/preview/info.test.js | 6 +++ test/preview/preview.test.js | 6 +++ test/status/handler.test.js | 62 ++++++++++++++++---------- test/utils.js | 4 +- 15 files changed, 231 insertions(+), 106 deletions(-) create mode 100644 src/router/table.js diff --git a/src/index.js b/src/index.js index 581c9c5..75e4fba 100644 --- a/src/index.js +++ b/src/index.js @@ -15,21 +15,7 @@ import bodyData from '@adobe/helix-shared-body-data'; import secrets from '@adobe/helix-shared-secrets'; import { helixStatus } from '@adobe/helix-status'; -import cache from './cache/handler.js'; -import code from './code/handler.js'; -import contentproxy from './contentproxy/handler.js'; -import discover from './discover/handler.js'; -import index from './index/handler.js'; -import live from './live/handler.js'; -import log from './log/handler.js'; -import { auth, login, logout } from './login/handler.js'; -import media from './media/handler.js'; -import preview from './preview/handler.js'; -import profile from './profile/handler.js'; -import sitemap from './sitemap/handler.js'; -import status from './status/handler.js'; - -import Router from './router/router.js'; +import { table } from './router/table.js'; import { adminContext } from './support/AdminContext.js'; import { RequestInfo } from './support/RequestInfo.js'; import { logRequest } from './support/utils.js'; @@ -37,62 +23,6 @@ import catchAll from './wrappers/catch-all.js'; import { contentEncodeWrapper } from './wrappers/content-encode.js'; import commonResponseHeaders from './wrappers/response-headers.js'; -/** - * Dummy NYI handler - * @returns {Response} response - */ -const notImplemented = () => new Response('', { status: 405 }); - -/** - * Name selector for routes. - */ -const nameSelector = (segs) => { - const literals = segs.filter((seg) => seg !== '*' && !seg.startsWith(':')); - if (literals.length === 0) { - return 'org'; - } - if (literals.at(0) === 'sites' && literals.length > 1) { - literals.shift(); - } - return literals.join('-'); -}; - -/** - * Routing table. - */ -export const router = new Router(nameSelector) - .add('/auth/*', auth) - .add('/discover', discover) - .add('/login', login) - .add('/logout', logout) - .add('/profile', profile) - .add('/:org', notImplemented) - .add('/:org/config', notImplemented) - .add('/:org/config/access', notImplemented) - .add('/:org/config/versions', notImplemented) - .add('/:org/profiles', notImplemented) - .add('/:org/profiles/:profile/versions', notImplemented) - .add('/:org/sites', notImplemented) - .add('/:org/sites/:site/status/*', status) - .add('/:org/sites/:site/config', notImplemented) - .add('/:org/sites/:site/config/da', notImplemented) - .add('/:org/sites/:site/config/sidekick', notImplemented) - .add('/:org/sites/:site/config/access', notImplemented) - .add('/:org/sites/:site/config/versions', notImplemented) - .add('/:org/sites/:site/contentproxy/*', contentproxy) - .add('/:org/sites/:site/preview/*', preview) - .add('/:org/sites/:site/live/*', live) - .add('/:org/sites/:site/log', log) - .add('/:org/sites/:site/login', login) - .add('/:org/sites/:site/media/*', media) - .add('/:org/sites/:site/code/:ref/*', code) - .add('/:org/sites/:site/cache/*', cache) - .add('/:org/sites/:site/index/*', index) - .add('/:org/sites/:site/sitemap/*', sitemap) - .add('/:org/sites/:site/snapshots/*', notImplemented) - .add('/:org/sites/:site/source/*', notImplemented) - .add('/:org/sites/:site/jobs', notImplemented); - /** * Main entry point. * @@ -101,7 +31,7 @@ export const router = new Router(nameSelector) * @returns {import('@adobe/fetch').Response} response */ async function run(request, context) { - const { handler, variables } = router.match(context.suffix) ?? {}; + const { handler, variables } = table.match(context.suffix) ?? {}; if (!handler) { return new Response('', { status: 404 }); } 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..e6c126e 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,23 @@ export class Node { } return null; } + + fill(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?.fill(segs, variables); + } } diff --git a/src/router/router.js b/src/router/router.js index 94d9f4c..b377182 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,15 @@ export default class Router { } return null; } + + fill(name, variables) { + /** @type {Node} */ + const route = this.#routes.get(name); + if (!route) { + throw new Error(`route not found: ${name}`); + } + const segs = []; + route.fill(segs, variables); + return segs.join('/'); + } } diff --git a/src/router/table.js b/src/router/table.js new file mode 100644 index 0000000..fe513e9 --- /dev/null +++ b/src/router/table.js @@ -0,0 +1,84 @@ +/* + * Copyright 2025 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 { Response } from '@adobe/fetch'; + +import cache from '../cache/handler.js'; +import code from '../code/handler.js'; +import contentproxy from '../contentproxy/handler.js'; +import discover from '../discover/handler.js'; +import index from '../index/handler.js'; +import live from '../live/handler.js'; +import log from '../log/handler.js'; +import { auth, login, logout } from '../login/handler.js'; +import media from '../media/handler.js'; +import preview from '../preview/handler.js'; +import profile from '../profile/handler.js'; +import sitemap from '../sitemap/handler.js'; +import status from '../status/handler.js'; + +import Router from './router.js'; + +/** + * Dummy NYI handler + * @returns {Response} response + */ +const notImplemented = () => new Response('', { status: 405 }); + +/** + * Name selector for routes. + */ +const nameSelector = (segs) => { + const literals = segs.filter((seg) => seg !== '*' && !seg.startsWith(':')); + if (literals.length === 0) { + return 'org'; + } + if (literals.at(0) === 'sites' && literals.length > 1) { + literals.shift(); + } + return literals.join('-'); +}; + +/** + * Routing table. + */ +export const table = new Router(nameSelector) + .add('/auth/*', auth) + .add('/discover', discover) + .add('/login', login) + .add('/logout', logout) + .add('/profile', profile) + .add('/:org', notImplemented) + .add('/:org/config', notImplemented) + .add('/:org/config/access', notImplemented) + .add('/:org/config/versions', notImplemented) + .add('/:org/profiles', notImplemented) + .add('/:org/profiles/:profile/versions', notImplemented) + .add('/:org/sites', notImplemented) + .add('/:org/sites/:site/status/*', status) + .add('/:org/sites/:site/config', notImplemented) + .add('/:org/sites/:site/config/da', notImplemented) + .add('/:org/sites/:site/config/sidekick', notImplemented) + .add('/:org/sites/:site/config/access', notImplemented) + .add('/:org/sites/:site/config/versions', notImplemented) + .add('/:org/sites/:site/contentproxy/*', contentproxy) + .add('/:org/sites/:site/preview/*', preview) + .add('/:org/sites/:site/live/*', live) + .add('/:org/sites/:site/log', log) + .add('/:org/sites/:site/login', login) + .add('/:org/sites/:site/media/*', media) + .add('/:org/sites/:site/code/:ref/*', code) + .add('/:org/sites/:site/cache/*', cache) + .add('/:org/sites/:site/index/*', index) + .add('/:org/sites/:site/sitemap/*', sitemap) + .add('/:org/sites/:site/snapshots/*', notImplemented) + .add('/:org/sites/:site/source/*', notImplemented) + .add('/:org/sites/:site/jobs', notImplemented); 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..a470cfa 100644 --- a/src/support/RequestInfo.js +++ b/src/support/RequestInfo.js @@ -13,6 +13,7 @@ import { parse } from 'cookie'; import { sanitizeName } from '@adobe/helix-shared-string'; +import { table } from '../router/table.js'; import { StatusCodeError } from './StatusCodeError.js'; /** @@ -375,6 +376,20 @@ 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) => { + links[name] = table.fill(name, variables); + }); + return links; + } + toResourcePath() { return toResourcePath(this.webPath); } diff --git a/test/index.test.js b/test/index.test.js index 0c3e358..e58092c 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -13,7 +13,8 @@ /* eslint-env mocha */ import assert from 'assert'; import { Request } from '@adobe/fetch'; -import { main, router } from '../src/index.js'; +import { main } from '../src/index.js'; +import { table } from '../src/router/table.js'; import { Nock, ORG_CONFIG, SITE_CONFIG } from './utils.js'; import { AuthInfo } from '../src/auth/auth-info.js'; @@ -141,6 +142,12 @@ describe('Index Tests', () => { assert.strictEqual(result.status, 200); assert.deepStrictEqual(await result.json(), { edit: {}, + links: { + code: '/org/sites/site/code/main/document', + live: '/org/sites/site/live/document', + preview: '/org/sites/site/preview/document', + status: '/org/sites/site/status/document', + }, live: { contentBusId: `helix-content-bus/${SITE_CONFIG.content.contentBusId}/live/document.md`, contentType: 'text/plain; charset=utf-8', @@ -289,7 +296,7 @@ describe('Index Tests', () => { }]; entries.forEach((entry) => { - const { variables } = router.match(entry.suffix) ?? {}; + const { variables } = table.match(entry.suffix) ?? {}; assert.deepStrictEqual(variables, entry.variables); }); }); diff --git a/test/live/info.test.js b/test/live/info.test.js index 44a8cae..7b65311 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: '/org/sites/site/code/main/document', + live: '/org/sites/site/live/document', + preview: '/org/sites/site/preview/document', + status: '/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..bc91657 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: '/org/sites/site/code/main/', + live: '/org/sites/site/live/', + preview: '/org/sites/site/preview/', + status: '/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..635796f 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: '/org/sites/site/code/main/document', + live: '/org/sites/site/live/document', + preview: '/org/sites/site/preview/document', + status: '/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..536e4b8 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: '/org/sites/site/code/main/', + live: '/org/sites/site/live/', + preview: '/org/sites/site/preview/', + status: '/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..997c537 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: '/org/sites/site/code/main/', + live: '/org/sites/site/live/', + preview: '/org/sites/site/preview/', + status: '/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: '/org/sites/site/code/main/folder/page', + live: '/org/sites/site/live/folder/page', + preview: '/org/sites/site/preview/folder/page', + status: '/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: '/org/sites/site/code/main/', + live: '/org/sites/site/live/', + preview: '/org/sites/site/preview/', + status: '/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/utils.js b/test/utils.js index 95a3ae4..0b35352 100644 --- a/test/utils.js +++ b/test/utils.js @@ -16,7 +16,7 @@ import xml2js from 'xml2js'; import { Headers, Request } from '@adobe/fetch'; import { AuthInfo } from '../src/auth/auth-info.js'; -import { router } from '../src/index.js'; +import { table } from '../src/router/table.js'; import { AdminContext } from '../src/support/AdminContext.js'; import { RequestInfo } from '../src/support/RequestInfo.js'; import { GoogleNock } from './nocks/google.js'; @@ -251,5 +251,5 @@ export function createContext(suffix, { export function createInfo(suffix, headers = {}) { return RequestInfo.create(new Request('http://api.aem.live/', { headers, - }), router.match(suffix).variables); + }), table.match(suffix).variables); } From 4d5ff8600e975cea4557304f7a9284b0e8e214e9 Mon Sep 17 00:00:00 2001 From: Dominique Pfister Date: Mon, 17 Nov 2025 14:28:36 +0100 Subject: [PATCH 2/3] fix: move handler back where they belong --- src/index.js | 73 +++++++++++++++++++++++++-- src/router/table.js | 84 -------------------------------- src/support/RequestInfo.js | 15 ++++-- test/index.test.js | 13 +++-- test/live/info.test.js | 8 +-- test/live/publish.test.js | 8 +-- test/preview/info.test.js | 8 +-- test/preview/preview.test.js | 8 +-- test/status/handler.test.js | 24 ++++----- test/support/RequestInfo.test.js | 6 ++- test/utils.js | 5 +- 11 files changed, 122 insertions(+), 130 deletions(-) delete mode 100644 src/router/table.js diff --git a/src/index.js b/src/index.js index 75e4fba..2273408 100644 --- a/src/index.js +++ b/src/index.js @@ -15,7 +15,21 @@ import bodyData from '@adobe/helix-shared-body-data'; import secrets from '@adobe/helix-shared-secrets'; import { helixStatus } from '@adobe/helix-status'; -import { table } from './router/table.js'; +import cache from './cache/handler.js'; +import code from './code/handler.js'; +import contentproxy from './contentproxy/handler.js'; +import discover from './discover/handler.js'; +import index from './index/handler.js'; +import live from './live/handler.js'; +import log from './log/handler.js'; +import { auth, login, logout } from './login/handler.js'; +import media from './media/handler.js'; +import preview from './preview/handler.js'; +import profile from './profile/handler.js'; +import sitemap from './sitemap/handler.js'; +import status from './status/handler.js'; + +import Router from './router/router.js'; import { adminContext } from './support/AdminContext.js'; import { RequestInfo } from './support/RequestInfo.js'; import { logRequest } from './support/utils.js'; @@ -23,6 +37,59 @@ import catchAll from './wrappers/catch-all.js'; import { contentEncodeWrapper } from './wrappers/content-encode.js'; import commonResponseHeaders from './wrappers/response-headers.js'; +/** + * Dummy NYI handler + * @returns {Response} response + */ +const notImplemented = () => new Response('', { status: 405 }); + +/** + * Name selector for routes. + */ +const nameSelector = (segs) => { + const literals = segs.filter((seg) => seg !== '*' && !seg.startsWith(':')); + if (literals.length === 0) { + return 'org'; + } + if (literals.at(0) === 'sites' && literals.length > 1) { + literals.shift(); + } + return literals.join('-'); +}; + +export const router = new Router(nameSelector) + .add('/auth/*', auth) + .add('/discover', discover) + .add('/login', login) + .add('/logout', logout) + .add('/profile', profile) + .add('/:org', notImplemented) + .add('/:org/config', notImplemented) + .add('/:org/config/access', notImplemented) + .add('/:org/config/versions', notImplemented) + .add('/:org/profiles', notImplemented) + .add('/:org/profiles/:profile/versions', notImplemented) + .add('/:org/sites', notImplemented) + .add('/:org/sites/:site/status/*', status) + .add('/:org/sites/:site/config', notImplemented) + .add('/:org/sites/:site/config/da', notImplemented) + .add('/:org/sites/:site/config/sidekick', notImplemented) + .add('/:org/sites/:site/config/access', notImplemented) + .add('/:org/sites/:site/config/versions', notImplemented) + .add('/:org/sites/:site/contentproxy/*', contentproxy) + .add('/:org/sites/:site/preview/*', preview) + .add('/:org/sites/:site/live/*', live) + .add('/:org/sites/:site/log', log) + .add('/:org/sites/:site/login', login) + .add('/:org/sites/:site/media/*', media) + .add('/:org/sites/:site/code/:ref/*', code) + .add('/:org/sites/:site/cache/*', cache) + .add('/:org/sites/:site/index/*', index) + .add('/:org/sites/:site/sitemap/*', sitemap) + .add('/:org/sites/:site/snapshots/*', notImplemented) + .add('/:org/sites/:site/source/*', notImplemented) + .add('/:org/sites/:site/jobs', notImplemented); + /** * Main entry point. * @@ -31,11 +98,11 @@ import commonResponseHeaders from './wrappers/response-headers.js'; * @returns {import('@adobe/fetch').Response} response */ async function run(request, context) { - const { handler, variables } = table.match(context.suffix) ?? {}; + const { handler, variables } = router.match(context.suffix) ?? {}; 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/router/table.js b/src/router/table.js deleted file mode 100644 index fe513e9..0000000 --- a/src/router/table.js +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2025 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 { Response } from '@adobe/fetch'; - -import cache from '../cache/handler.js'; -import code from '../code/handler.js'; -import contentproxy from '../contentproxy/handler.js'; -import discover from '../discover/handler.js'; -import index from '../index/handler.js'; -import live from '../live/handler.js'; -import log from '../log/handler.js'; -import { auth, login, logout } from '../login/handler.js'; -import media from '../media/handler.js'; -import preview from '../preview/handler.js'; -import profile from '../profile/handler.js'; -import sitemap from '../sitemap/handler.js'; -import status from '../status/handler.js'; - -import Router from './router.js'; - -/** - * Dummy NYI handler - * @returns {Response} response - */ -const notImplemented = () => new Response('', { status: 405 }); - -/** - * Name selector for routes. - */ -const nameSelector = (segs) => { - const literals = segs.filter((seg) => seg !== '*' && !seg.startsWith(':')); - if (literals.length === 0) { - return 'org'; - } - if (literals.at(0) === 'sites' && literals.length > 1) { - literals.shift(); - } - return literals.join('-'); -}; - -/** - * Routing table. - */ -export const table = new Router(nameSelector) - .add('/auth/*', auth) - .add('/discover', discover) - .add('/login', login) - .add('/logout', logout) - .add('/profile', profile) - .add('/:org', notImplemented) - .add('/:org/config', notImplemented) - .add('/:org/config/access', notImplemented) - .add('/:org/config/versions', notImplemented) - .add('/:org/profiles', notImplemented) - .add('/:org/profiles/:profile/versions', notImplemented) - .add('/:org/sites', notImplemented) - .add('/:org/sites/:site/status/*', status) - .add('/:org/sites/:site/config', notImplemented) - .add('/:org/sites/:site/config/da', notImplemented) - .add('/:org/sites/:site/config/sidekick', notImplemented) - .add('/:org/sites/:site/config/access', notImplemented) - .add('/:org/sites/:site/config/versions', notImplemented) - .add('/:org/sites/:site/contentproxy/*', contentproxy) - .add('/:org/sites/:site/preview/*', preview) - .add('/:org/sites/:site/live/*', live) - .add('/:org/sites/:site/log', log) - .add('/:org/sites/:site/login', login) - .add('/:org/sites/:site/media/*', media) - .add('/:org/sites/:site/code/:ref/*', code) - .add('/:org/sites/:site/cache/*', cache) - .add('/:org/sites/:site/index/*', index) - .add('/:org/sites/:site/sitemap/*', sitemap) - .add('/:org/sites/:site/snapshots/*', notImplemented) - .add('/:org/sites/:site/source/*', notImplemented) - .add('/:org/sites/:site/jobs', notImplemented); diff --git a/src/support/RequestInfo.js b/src/support/RequestInfo.js index a470cfa..aea8a9d 100644 --- a/src/support/RequestInfo.js +++ b/src/support/RequestInfo.js @@ -13,7 +13,6 @@ import { parse } from 'cookie'; import { sanitizeName } from '@adobe/helix-shared-string'; -import { table } from '../router/table.js'; import { StatusCodeError } from './StatusCodeError.js'; /** @@ -208,6 +207,8 @@ class PathInfo { export class RequestInfo { #request; + #router; + #pathInfo; #owner; @@ -216,8 +217,9 @@ export class RequestInfo { #ref; - constructor(request, pathInfo) { + constructor(request, router, pathInfo) { this.#request = request; + this.#router = router; this.#pathInfo = pathInfo; } @@ -311,6 +313,8 @@ export class RequestInfo { * Create a new request info. * * @param {import('@adobe/fetch').Request} request request + * @param {import('../router/router.js')} router router + * @poram * @param {object} param0 params * @param {string} [param0.org] org, optional * @param {string} [param0.site] site, optional @@ -319,13 +323,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)); } /** @@ -344,6 +348,7 @@ export class RequestInfo { }) { const info = new RequestInfo( other.#request, + other.#router, PathInfo.clone(other.#pathInfo, { org, site, path, route, }), @@ -385,7 +390,7 @@ export class RequestInfo { ref: this.ref, }; routes.forEach((name) => { - links[name] = table.fill(name, variables); + links[name] = this.getLinkUrl(this.#router.fill(name, variables)); }); return links; } diff --git a/test/index.test.js b/test/index.test.js index e58092c..a040905 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -13,8 +13,7 @@ /* eslint-env mocha */ import assert from 'assert'; import { Request } from '@adobe/fetch'; -import { main } from '../src/index.js'; -import { table } from '../src/router/table.js'; +import { main, router } from '../src/index.js'; import { Nock, ORG_CONFIG, SITE_CONFIG } from './utils.js'; import { AuthInfo } from '../src/auth/auth-info.js'; @@ -143,10 +142,10 @@ describe('Index Tests', () => { assert.deepStrictEqual(await result.json(), { edit: {}, links: { - code: '/org/sites/site/code/main/document', - live: '/org/sites/site/live/document', - preview: '/org/sites/site/preview/document', - status: '/org/sites/site/status/document', + 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`, @@ -296,7 +295,7 @@ describe('Index Tests', () => { }]; entries.forEach((entry) => { - const { variables } = table.match(entry.suffix) ?? {}; + const { variables } = router.match(entry.suffix) ?? {}; assert.deepStrictEqual(variables, entry.variables); }); }); diff --git a/test/live/info.test.js b/test/live/info.test.js index 7b65311..54f62aa 100644 --- a/test/live/info.test.js +++ b/test/live/info.test.js @@ -66,10 +66,10 @@ describe('Live Info Tests', () => { assert.strictEqual(response.status, 200); assert.deepStrictEqual(await response.json(), { links: { - code: '/org/sites/site/code/main/document', - live: '/org/sites/site/live/document', - preview: '/org/sites/site/preview/document', - status: '/org/sites/site/status/document', + 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`, diff --git a/test/live/publish.test.js b/test/live/publish.test.js index bc91657..e73262c 100644 --- a/test/live/publish.test.js +++ b/test/live/publish.test.js @@ -258,10 +258,10 @@ describe('Publish Action Tests', () => { assert.strictEqual(response.status, 200); assert.deepStrictEqual(await response.json(), { links: { - code: '/org/sites/site/code/main/', - live: '/org/sites/site/live/', - preview: '/org/sites/site/preview/', - status: '/org/sites/site/status/', + 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', diff --git a/test/preview/info.test.js b/test/preview/info.test.js index 635796f..8de8271 100644 --- a/test/preview/info.test.js +++ b/test/preview/info.test.js @@ -66,10 +66,10 @@ describe('Preview Info Tests', () => { assert.strictEqual(response.status, 200); assert.deepStrictEqual(await response.json(), { links: { - code: '/org/sites/site/code/main/document', - live: '/org/sites/site/live/document', - preview: '/org/sites/site/preview/document', - status: '/org/sites/site/status/document', + 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`, diff --git a/test/preview/preview.test.js b/test/preview/preview.test.js index 536e4b8..a29dbfd 100644 --- a/test/preview/preview.test.js +++ b/test/preview/preview.test.js @@ -150,10 +150,10 @@ describe('Preview Action Tests', () => { assert.strictEqual(response.status, 200); assert.deepStrictEqual(await response.json(), { links: { - code: '/org/sites/site/code/main/', - live: '/org/sites/site/live/', - preview: '/org/sites/site/preview/', - status: '/org/sites/site/status/', + 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', diff --git a/test/status/handler.test.js b/test/status/handler.test.js index 997c537..c261bdc 100644 --- a/test/status/handler.test.js +++ b/test/status/handler.test.js @@ -102,10 +102,10 @@ describe('Status Handler Tests', () => { status: 403, }, links: { - code: '/org/sites/site/code/main/', - live: '/org/sites/site/live/', - preview: '/org/sites/site/preview/', - status: '/org/sites/site/status/', + 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', @@ -180,10 +180,10 @@ describe('Status Handler Tests', () => { status: 200, }, links: { - code: '/org/sites/site/code/main/folder/page', - live: '/org/sites/site/live/folder/page', - preview: '/org/sites/site/preview/folder/page', - status: '/org/sites/site/status/folder/page', + 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', @@ -265,10 +265,10 @@ describe('Status Handler Tests', () => { url: 'https://docs.google.com/document/d/1ZJWJwL9szyTq6B-W0_Y7bFL1Tk1vyym4RyQ7AKXS7Ys/edit', }, links: { - code: '/org/sites/site/code/main/', - live: '/org/sites/site/live/', - preview: '/org/sites/site/preview/', - status: '/org/sites/site/status/', + 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`, 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 0b35352..7ee6b1d 100644 --- a/test/utils.js +++ b/test/utils.js @@ -16,7 +16,7 @@ import xml2js from 'xml2js'; import { Headers, Request } from '@adobe/fetch'; import { AuthInfo } from '../src/auth/auth-info.js'; -import { table } from '../src/router/table.js'; +import { router } from '../src/index.js'; import { AdminContext } from '../src/support/AdminContext.js'; import { RequestInfo } from '../src/support/RequestInfo.js'; import { GoogleNock } from './nocks/google.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, - }), table.match(suffix).variables); + }), router, variables); } From ae36e9ee76ef4c55469ecf3d2ba7f088acd47d28 Mon Sep 17 00:00:00 2001 From: Dominique Pfister Date: Mon, 17 Nov 2025 17:21:41 +0100 Subject: [PATCH 3/3] chore(jsdoc): use better naming and document methods --- src/router/node.js | 12 ++++++++++-- src/router/router.js | 12 ++++++++++-- src/support/RequestInfo.js | 6 +++--- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/router/node.js b/src/router/node.js index e6c126e..fdf1871 100644 --- a/src/router/node.js +++ b/src/router/node.js @@ -131,7 +131,15 @@ export class Node { return null; } - fill(segs, variables) { + /** + * 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) { @@ -147,6 +155,6 @@ export class Node { default: break; } - this.#parent?.fill(segs, variables); + this.#parent?.external(segs, variables); } } diff --git a/src/router/router.js b/src/router/router.js index b377182..f5bec3e 100644 --- a/src/router/router.js +++ b/src/router/router.js @@ -74,14 +74,22 @@ export default class Router { return null; } - fill(name, variables) { + /** + * 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.fill(segs, variables); + route.external(segs, variables); return segs.join('/'); } } diff --git a/src/support/RequestInfo.js b/src/support/RequestInfo.js index aea8a9d..ba3272a 100644 --- a/src/support/RequestInfo.js +++ b/src/support/RequestInfo.js @@ -313,8 +313,7 @@ export class RequestInfo { * Create a new request info. * * @param {import('@adobe/fetch').Request} request request - * @param {import('../router/router.js')} router router - * @poram + * @param {import('../router/router.js').default} router router * @param {object} param0 params * @param {string} [param0.org] org, optional * @param {string} [param0.site] site, optional @@ -390,7 +389,8 @@ export class RequestInfo { ref: this.ref, }; routes.forEach((name) => { - links[name] = this.getLinkUrl(this.#router.fill(name, variables)); + const path = this.#router.external(name, variables); + links[name] = this.getLinkUrl(path); }); return links; }