import fs from "node:fs"; interface NjsRequest { readonly args: Readonly>; readonly headersOut: Record; readonly uri: string; readonly variables: { readonly document_root: string; readonly host?: string }; return: (status: number, body: string) => void; } type Row = Record; interface Table { readonly columns: readonly string[]; readonly rows: readonly (readonly unknown[])[]; } interface IdRow extends Row { readonly json: string; readonly kind: string; readonly ref: string; readonly title: string; } interface MoveRow { readonly json: string; readonly to: string | null; } interface Address { readonly json: string; } interface IdsShard extends Address { readonly group: string; } interface IdsHead { readonly kinds: readonly string[]; readonly shards: readonly IdsShard[]; } interface FuzzyRules { readonly stemMinimum: number; readonly suffixes: readonly string[]; readonly suggestions: number; } interface NameMatch { readonly key: string; readonly match: string; } interface QueryLeaf { readonly endpoint: string; readonly fuzzy: FuzzyRules; readonly index: string; readonly limits: { readonly defaultLimit: number; readonly depth: number; readonly limit: number }; readonly messages: Readonly>; readonly relations: readonly string[]; readonly site: string; } interface SlugRules { readonly characters: string; readonly separator: string; } interface WordRules { readonly americanWords: Readonly>; readonly fuzzyMinimum: number; readonly izeStems: readonly string[]; readonly izeSuffixes: Readonly>; readonly pluralIes: readonly [string, string]; readonly pluralKeptAfter: readonly string[]; readonly pluralMinimum: number; readonly pluralSuffix: string; readonly spellingPrefixes: readonly string[]; readonly wordCharacters: string; } interface SearchLeaf { readonly kinds: Readonly>; readonly rules: WordRules; } type ShardMap = Readonly>; interface SearchIndex extends Table { readonly parts?: readonly string[]; readonly shards: ShardMap; } interface ResolveLeaf { readonly shards: ShardMap; readonly slugRules: SlugRules; } interface PostingShard { readonly postings: Readonly>; } interface SlugShard { readonly slugs: Readonly>; } interface EntryLeaf { readonly entries: readonly { readonly ref: string }[]; readonly parts?: readonly Address[]; } interface RecordLeaf { readonly relations?: readonly { readonly links: readonly { readonly json: string; readonly label: string; readonly ref: string | null }[]; readonly relation: string; }[]; } interface State { readonly groups: Map>; readonly ids: IdsHead; readonly mtime: number; readonly query: QueryLeaf; readonly root: string; readonly shards: Record; readonly site: string; } interface Ranked { readonly position: number | null; readonly row: Row; readonly score: number; readonly title: string; } interface Refusal { readonly code: string; readonly values: Readonly>; } const EXTENSION = ".json"; const QUERY_PATH = "/json/api/query"; const IDS_PATH = "/json/api/ids"; const MOVED_PATH = "/json/api/moved"; const SEARCH_PATH = "/json/api/search"; const RESOLVE_PATH = "/json/api/resolve"; const FACETS_PATH = "/json/api/facets/"; const RECORDS_PATH = "/json/api/records/"; const REF_SEPARATOR = ":"; const OPERATIONS: ReadonlySet = new Set(["ref", "name", "q", "collection", "id", "walk"]); const PAGING: ReadonlySet = new Set(["limit", "offset"]); const OPERATION_OPTIONS: Readonly | undefined>> = { id: new Set(["kind"]), name: new Set(["kind"]), q: new Set(["kind"]), walk: new Set(["depth", "relation"]), }; const KIND_OPTION = "kind"; const WILDCARD = "*"; const MISSING = "ENOENT"; const SPACE = " "; const DIGIT_CHARACTERS = "0123456789"; const PERMANENT_REDIRECT = 308; const MARKDOWN_EXTENSION = ".md"; const PAGE_EXTENSION = ".html"; const HOME_TWIN = "/index.md"; const HOME_ROUTE = "/"; const HOME_PAGE = "/index.html"; type Guard = (value: unknown) => value is T; const states: Record = {}; const isObject = function isObject(value: unknown): value is Row { return typeof value === "object" && value !== null; }; const isText = function isText(value: Row, key: string): boolean { return typeof value[key] === "string"; }; const isTable = function isTable(value: unknown): value is Table { return isObject(value) && Array.isArray(value["columns"]) && Array.isArray(value["rows"]); }; const hasTexts = function hasTexts(value: Row, keys: readonly string[]): boolean { return keys.every((key) => isText(value, key)); }; const hasObjects = function hasObjects(value: Row, keys: readonly string[]): boolean { return keys.every((key) => isObject(value[key])); }; const isQueryLeaf = function isQueryLeaf(value: unknown): value is QueryLeaf { return ( isObject(value) && hasTexts(value, ["site"]) && hasObjects(value, ["fuzzy", "limits", "messages"]) && Array.isArray(value["relations"]) ); }; const isIdsHead = function isIdsHead(value: unknown): value is IdsHead { return isObject(value) && Array.isArray(value["shards"]) && Array.isArray(value["kinds"]); }; const isSearchLeaf = function isSearchLeaf(value: unknown): value is SearchLeaf { return isObject(value) && isObject(value["kinds"]) && isObject(value["rules"]); }; const isResolveLeaf = function isResolveLeaf(value: unknown): value is ResolveLeaf { return isObject(value) && isObject(value["slugRules"]) && isObject(value["shards"]); }; const isSearchIndex = function isSearchIndex(value: unknown): value is SearchIndex { return isTable(value) && isObject(Reflect.get(value, "shards")); }; const isPostingShard = function isPostingShard(value: unknown): value is PostingShard { return isObject(value) && isObject(value["postings"]); }; const isSlugShard = function isSlugShard(value: unknown): value is SlugShard { return isObject(value) && isObject(value["slugs"]); }; const isEntryLeaf = function isEntryLeaf(value: unknown): value is EntryLeaf { return isObject(value) && Array.isArray(value["entries"]); }; const isRecordLeaf = function isRecordLeaf(value: unknown): value is RecordLeaf { return isObject(value); }; const isIdRow = function isIdRow(value: unknown): value is IdRow { return isObject(value) && hasTexts(value, ["json", "kind", "ref", "title"]); }; const isMoveRow = function isMoveRow(value: unknown): value is MoveRow { return isObject(value) && isText(value, "json") && (value["to"] === null || isText(value, "to")); }; const hasRef = function hasRef(value: Row): value is Row & { readonly ref: string } { return isText(value, "ref"); }; const read = function read(root: string, path: string, guard: Guard): T { const parsed: unknown = JSON.parse(fs.readFileSync(root + path + EXTENSION, "utf8")); if (!guard(parsed)) { throw new TypeError(root + path + EXTENSION); } return parsed; }; const isMissing = function isMissing(error: unknown): boolean { return typeof error === "object" && error !== null && "code" in error && error.code === MISSING; }; const readIfPresent = function readIfPresent(root: string, path: string, guard: Guard): T | null { try { return read(root, path, guard); } catch (error) { if (isMissing(error)) { return null; } throw error; } }; const localOf = function localOf(site: string, url: string): string { return url.startsWith(site) ? url.slice(site.length) : url; }; const rowsOf = function rowsOf(leaf: Table): Row[] { return leaf.rows.map((row) => { const record: Row = {}; leaf.columns.forEach((column, at) => { record[column] = row[at]; }); return record; }); }; const loadState = function loadState(root: string, mtime: number): State { const query = read(root, QUERY_PATH, isQueryLeaf); return { groups: new Map(), ids: read(root, IDS_PATH, isIdsHead), mtime, query, root, shards: {}, site: query.site, }; }; const stateOf = function stateOf(root: string): State { const held = states[root]; const mtime = fs.statSync(root + IDS_PATH + EXTENSION).mtimeMs; if (held?.mtime === mtime) { return held; } const loaded = loadState(root, mtime); states[root] = loaded; return loaded; }; const cached = function cached(state: State, path: string, guard: Guard): T | null { if (!(path in state.shards)) { state.shards[path] = readIfPresent(state.root, path, guard); } const held = state.shards[path]; return guard(held) ? held : null; }; const groupRows = function groupRows(state: State, group: string): ReadonlyMap { const held = state.groups.get(group); if (held !== undefined) { return held; } const rows = new Map(); state.ids.shards .filter((shard) => shard.group === group) .forEach((shard) => { rowsOf(read(state.root, localOf(state.site, shard.json), isTable)).forEach((row) => { if (isIdRow(row)) { rows.set(row.ref, row); } }); }); state.groups.set(group, rows); return rows; }; const rowOf = function rowOf(state: State, ref: string): IdRow | null { const at = ref.indexOf(REF_SEPARATOR); return at === -1 ? null : (groupRows(state, ref.slice(0, at)).get(ref) ?? null); }; const allRows = function allRows(state: State): IdRow[] { const groups = [...new Set(state.ids.shards.map((shard) => shard.group))]; return groups.flatMap((group) => [...groupRows(state, group).values()]); }; const knownRows = function knownRows(state: State, refs: readonly string[]): IdRow[] { return refs.flatMap((ref) => { const row = rowOf(state, ref); return row === null ? [] : [row]; }); }; const searchLeafOf = function searchLeafOf(state: State): SearchLeaf { const leaf = cached(state, SEARCH_PATH, isSearchLeaf); if (leaf === null) { throw new TypeError(state.root + SEARCH_PATH + EXTENSION); } return leaf; }; const resolveLeafOf = function resolveLeafOf(state: State): ResolveLeaf { const leaf = cached(state, RESOLVE_PATH, isResolveLeaf); if (leaf === null) { throw new TypeError(state.root + RESOLVE_PATH + EXTENSION); } return leaf; }; const slugRulesOf = function slugRulesOf(state: State): SlugRules { return resolveLeafOf(state).slugRules; }; const shardsUnder = function shardsUnder(shards: ShardMap, start: string): string[] { return Object.keys(shards) .filter((prefix) => prefix.startsWith(start) || start.startsWith(prefix)) .flatMap((prefix) => { const address = shards[prefix]; return address === undefined ? [] : [address]; }); }; const indexRows = function indexRows(state: State, index: SearchIndex): Row[] { if (index.parts === undefined) { return rowsOf(index); } return index.parts.flatMap((part) => rowsOf(read(state.root, localOf(state.site, part), isTable))); }; const entriesOf = function entriesOf(state: State, leaf: EntryLeaf): readonly { readonly ref: string }[] { if (leaf.parts === undefined) { return leaf.entries; } return leaf.parts.flatMap((part) => read(state.root, localOf(state.site, part.json), isEntryLeaf).entries); }; const filled = function filled(template: string, values: Readonly>): string { return Object.keys(values).reduce((text, key) => text.split(`{${key}}`).join(String(values[key])), template); }; const slugOf = function slugOf(text: string, rules: SlugRules): string { let out = ""; let joined = false; for (const char of text.toLowerCase()) { if (rules.characters.includes(char)) { out += char; joined = false; continue; } if (out.length > 0 && !joined) { out += rules.separator; joined = true; } } return joined ? out.slice(0, -rules.separator.length) : out; }; const isPrefixedStem = function isPrefixedStem(stem: string, rules: WordRules): boolean { return ( rules.izeStems.includes(stem) || rules.spellingPrefixes.some( (prefix) => stem.startsWith(prefix) && rules.izeStems.includes(stem.slice(prefix.length)), ) ); }; const listedOf = function listedOf(word: string, rules: WordRules): string | null { const listed = Object.keys(rules.americanWords); if (listed.includes(word)) { return rules.americanWords[word] ?? null; } const prefix = rules.spellingPrefixes.find( (candidate) => word.startsWith(candidate) && listed.includes(word.slice(candidate.length)), ); return prefix === undefined ? null : prefix + (rules.americanWords[word.slice(prefix.length)] ?? ""); }; const spelledOf = function spelledOf(word: string, rules: WordRules): string { const listed = listedOf(word, rules); if (listed !== null) { return listed; } const suffixes = [...Object.keys(rules.izeSuffixes)].sort((left, right) => right.length - left.length); for (const british of suffixes) { const stem = word.slice(0, -british.length); if (word.endsWith(british) && isPrefixedStem(stem, rules)) { return stem + (rules.izeSuffixes[british] ?? british); } } return word; }; const singularOf = function singularOf(word: string, rules: WordRules): string { const suffix = rules.pluralSuffix; if (word.length < rules.pluralMinimum || !word.endsWith(suffix)) { return word; } if (word.endsWith(rules.pluralIes[0])) { return word.slice(0, -rules.pluralIes[0].length) + rules.pluralIes[1]; } const before = word.charAt(word.length - suffix.length - 1); return rules.pluralKeptAfter.includes(before) ? word : word.slice(0, -suffix.length); }; const normalizeWord = function normalizeWord(word: string, rules: WordRules): string { const spelled = spelledOf(word.toLowerCase(), rules); return spelledOf(singularOf(spelled, rules), rules); }; const wordsOf = function wordsOf(text: string, rules: WordRules): string[] { const words: string[] = []; let current = ""; for (const char of text.toLowerCase() + SPACE) { if (rules.wordCharacters.includes(char)) { current += char; continue; } if (current.length > 0) { words.push(normalizeWord(current, rules)); current = ""; } } return words; }; const isSwap = function isSwap(left: string, right: string, at: number): boolean { return ( left.length === right.length && left.charAt(at) === right.charAt(at + 1) && left.charAt(at + 1) === right.charAt(at) && left.slice(at + 2) === right.slice(at + 2) ); }; const oneEditApart = function oneEditApart(left: string, right: string): boolean { if (Math.abs(left.length - right.length) > 1 || left === right) { return false; } let at = 0; while (at < left.length && at < right.length && left.charAt(at) === right.charAt(at)) { at += 1; } return ( left.slice(at + 1) === right.slice(at + 1) || left.slice(at + 1) === right.slice(at) || left.slice(at) === right.slice(at + 1) || isSwap(left, right, at) ); }; const matchedKeys = function matchedKeys( postings: PostingShard["postings"], word: string, minimum: number, ): readonly string[] { if (word in postings) { return [word]; } const keys = Object.keys(postings); const prefixed = keys.filter((key) => key.startsWith(word)); if (prefixed.length > 0 || word.length < minimum) { return prefixed; } return keys.filter((key) => oneEditApart(key, word)); }; const termScores = function termScores( state: State, rules: WordRules, index: SearchIndex, word: string, ): Map { const scores = new Map(); const postings: Record = {}; shardsUnder(index.shards, word.charAt(0)).forEach((address) => { const shard = cached(state, localOf(state.site, address), isPostingShard); Object.assign(postings, shard === null ? {} : shard.postings); }); matchedKeys(postings, word, rules.fuzzyMinimum).forEach((key) => { (postings[key] ?? []).forEach((posting) => { scores.set(posting[0], (scores.get(posting[0]) ?? 0) + posting[1]); }); }); return scores; }; const searchKind = function searchKind( state: State, leaf: SearchLeaf, kind: string, words: readonly string[], ): Ranked[] { const address = leaf.kinds[kind]; const index = address === undefined ? null : cached(state, localOf(state.site, address.json), isSearchIndex); if (index === null) { return []; } const rows = indexRows(state, index); const perWord = words.map((word) => termScores(state, leaf.rules, index, word)); const results: Ranked[] = []; (perWord[0] ?? new Map()).forEach((_score, at) => { const entry = rows[at]; const listed = entry !== undefined && hasRef(entry) ? rowOf(state, entry.ref) : null; if (listed === null || entry === undefined || !perWord.every((scores) => scores.has(at))) { return; } const score = perWord.reduce((acc, scores) => acc + (scores.get(at) ?? 0), 0); const position = typeof entry["position"] === "number" ? entry["position"] : null; results.push({ position, row: { ...listed, score }, score, title: listed.title }); }); return results; }; const textOrder = function textOrder(left: string, right: string): number { const length = Math.min(left.length, right.length); for (let at = 0; at < length; at += 1) { const difference = (left.codePointAt(at) ?? 0) - (right.codePointAt(at) ?? 0); if (difference !== 0) { return difference; } } return left.length - right.length; }; const rankOf = function rankOf(left: Ranked, right: Ranked): number { if (left.score !== right.score) { return right.score - left.score; } const leftAt = left.position ?? Number.MAX_VALUE; const rightAt = right.position ?? Number.MAX_VALUE; return leftAt === rightAt ? textOrder(left.title, right.title) : leftAt - rightAt; }; const byWords = function byWords(state: State, text: string): readonly Row[] { const leaf = searchLeafOf(state); const words = wordsOf(text, leaf.rules); if (words.length === 0) { return []; } return Object.keys(leaf.kinds) .flatMap((kind) => searchKind(state, leaf, kind, words)) .sort(rankOf) .map((ranked) => ranked.row); }; const caseSplit = function caseSplit(text: string): string { let out = ""; let previousLower = false; for (const char of text) { const upper = char !== char.toLowerCase(); if (upper && previousLower) { out += SPACE; } out += char; previousLower = !upper && !DIGIT_CHARACTERS.includes(char); } return out; }; const nameKeyOf = function nameKeyOf(text: string, slugRules: SlugRules, wordRules: WordRules): string { return slugOf(caseSplit(text), slugRules) .split(slugRules.separator) .filter((word) => word.length > 0) .map((word) => normalizeWord(word, wordRules)) .join(slugRules.separator); }; const slugsOf = function slugsOf(state: State, letter: string): SlugShard["slugs"] { if (letter.length === 0) { return {}; } const slugs: Record = {}; shardsUnder(resolveLeafOf(state).shards, letter).forEach((address) => { const shard = cached(state, localOf(state.site, address), isSlugShard); Object.assign(slugs, shard === null ? {} : shard.slugs); }); return slugs; }; const nearKeys = function nearKeys(state: State, key: string): string[] { const letters = [key.charAt(0), key.charAt(1)].filter((letter, at, all) => all.indexOf(letter) === at); return letters.flatMap((letter) => Object.keys(slugsOf(state, letter))); }; const stemOf = function stemOf(key: string, fuzzy: FuzzyRules): string { const fitting = fuzzy.suffixes.filter( (candidate) => key.endsWith(candidate) && key.length - candidate.length >= fuzzy.stemMinimum, ); const [suffix] = [...fitting].sort((left, right) => right.length - left.length); return suffix === undefined ? key : key.slice(0, -suffix.length); }; const fuzzyKeys = function fuzzyKeys(state: State, key: string, minimum: number): string[] { const near = nearKeys(state, key); const edits = key.length < minimum ? [] : near.filter((candidate) => oneEditApart(candidate, key)); if (edits.length > 0) { return edits; } const stem = stemOf(key, state.query.fuzzy); return stem.length < state.query.fuzzy.stemMinimum ? [] : near.filter((candidate) => candidate !== key && stemOf(candidate, state.query.fuzzy) === stem); }; const nameMatches = function nameMatches(state: State, text: string): NameMatch[] { const slugRules = slugRulesOf(state); const wordRules = searchLeafOf(state).rules; const key = nameKeyOf(text, slugRules, wordRules); if (key.length === 0) { return []; } if (slugsOf(state, key.charAt(0))[key] !== undefined) { return [{ key, match: slugOf(text, slugRules) === key ? "exact" : "normalized" }]; } return fuzzyKeys(state, key, wordRules.fuzzyMinimum).map((found) => ({ key: found, match: "fuzzy" })); }; const byName = function byName(state: State, text: string): Row[] { const rows = nameMatches(state, text).flatMap((found) => knownRows(state, slugsOf(state, found.key.charAt(0))[found.key] ?? []).map((row) => ({ ...row, match: found.match, })), ); return rows.filter((row, at) => rows.findIndex((other) => other.ref === row.ref) === at); }; const sharedPrefix = function sharedPrefix(left: string, right: string): number { let at = 0; while (at < left.length && at < right.length && left.charAt(at) === right.charAt(at)) { at += 1; } return at; }; const nameSuggestions = function nameSuggestions(state: State, text: string): string[] { const key = nameKeyOf(text, slugRulesOf(state), searchLeafOf(state).rules); return Object.keys(slugsOf(state, key.charAt(0))) .sort((left, right) => sharedPrefix(right, key) - sharedPrefix(left, key) || textOrder(left, right)) .slice(0, state.query.fuzzy.suggestions); }; const byId = function byId(state: State, pattern: string): IdRow[] { const prefix = pattern.endsWith(WILDCARD) ? pattern.slice(0, -WILDCARD.length) : null; if (prefix === null) { return knownRows(state, [pattern]); } return allRows(state).filter((row) => row.ref.startsWith(prefix)); }; const byFacets = function byFacets( state: State, collection: string, fields: readonly (readonly [string, string])[], ): IdRow[] { const rules = slugRulesOf(state); const lists = fields.length === 0 ? [cached(state, RECORDS_PATH + slugOf(collection, rules), isEntryLeaf)] : fields.map((field) => cached(state, FACETS_PATH + [collection, field[0], slugOf(field[1], rules)].join("/"), isEntryLeaf), ); const present = lists.flatMap((leaf) => (leaf === null ? [] : [leaf])); if (present.length !== lists.length) { return []; } const sets = present.map((leaf) => entriesOf(state, leaf).map((entry) => entry.ref)); const [first = []] = sets; return knownRows( state, first.filter((ref) => sets.every((set) => set.includes(ref))), ); }; const byWalk = function byWalk(state: State, start: string, relation: string | undefined, depth: number): Row[] { const edges: Row[] = []; const seen = new Set([start]); let frontier = [start]; for (let hop = 1; hop <= depth && frontier.length > 0; hop += 1) { const next: string[] = []; frontier.forEach((ref) => { const row = rowOf(state, ref); const leaf = row === null ? null : readIfPresent(state.root, localOf(state.site, row.json), isRecordLeaf); (leaf?.relations ?? []).forEach((group) => { if (relation !== undefined && group.relation !== relation) { return; } group.links.forEach((target) => { const listed = target.ref === null ? null : rowOf(state, target.ref); if (listed === null) { return; } edges.push({ ...listed, from: ref, hop, relation: group.relation }); if (!seen.has(listed.ref)) { seen.add(listed.ref); next.push(listed.ref); } }); }); }); frontier = next; } return edges; }; const numberOf = function numberOf(value: string | undefined, fallback: number): number | null { if (value === undefined) { return fallback; } const parsed = Number(value); return Number.isInteger(parsed) && parsed >= 0 ? parsed : null; }; const respond = function respond(r: NjsRequest, status: number, body: object): void { r.headersOut["Content-Type"] = "application/json"; r.headersOut["Access-Control-Allow-Origin"] = "*"; r.return(status, `${JSON.stringify(body)}\n`); }; const messageOf = function messageOf(state: State, code: string): string { return state.query.messages[code] ?? ""; }; const refuse = function refuse(r: NjsRequest, state: State, refusal: Refusal): void { const values = { ids: state.site + IDS_PATH, index: state.query.index, ...refusal.values }; respond(r, 400, { code: refusal.code, error: filled(messageOf(state, refusal.code), values), index: state.query.index, }); }; const argOf = function argOf(args: NjsRequest["args"], key: string): string { return args[key] ?? ""; }; type Check = (state: State, args: NjsRequest["args"], operation: string) => Refusal | null; const unknownParameter: Check = (_state, args, operation) => { if (operation === "collection") { return null; } const options = OPERATION_OPTIONS[operation] ?? new Set(); const unknown = Object.keys(args).find((key) => !OPERATIONS.has(key) && !PAGING.has(key) && !options.has(key)); return unknown === undefined ? null : { code: "unknownParameter", values: { name: unknown } }; }; const emptyValue: Check = (_state, args, operation) => argOf(args, operation).length === 0 ? { code: "emptyValue", values: { name: operation } } : null; const badLimit: Check = (state, args) => { const { limits } = state.query; const limit = numberOf(args["limit"], limits.defaultLimit); return limit === null || limit > limits.limit ? { code: "badNumber", values: { max: limits.limit, name: "limit" } } : null; }; const badOffset: Check = (state, args) => numberOf(args["offset"], 0) === null ? { code: "badNumber", values: { max: state.query.limits.limit, name: "offset" } } : null; const badDepth: Check = (state, args) => { const depth = numberOf(args["depth"], 1); const inRange = depth !== null && depth >= 1 && depth <= state.query.limits.depth; return inRange ? null : { code: "badDepth", values: { max: state.query.limits.depth } }; }; const unknownRelation: Check = (state, args) => { const { relation } = args; return relation === undefined || state.query.relations.includes(relation) ? null : { code: "unknownRelation", values: { name: relation } }; }; const unknownKind: Check = (state, args, operation) => { const kind = operation === "collection" ? undefined : args[KIND_OPTION]; return kind === undefined || state.ids.kinds.includes(kind) ? null : { code: "unknownKind", values: { name: kind } }; }; const CHECKS: readonly Check[] = [ unknownParameter, emptyValue, badLimit, badOffset, badDepth, unknownRelation, unknownKind, ]; const refusalOf = function refusalOf( state: State, args: NjsRequest["args"], operations: readonly string[], ): Refusal | null { const [operation] = operations; if (operation === undefined) { return { code: "noOperation", values: {} }; } if (operations.length > 1) { return { code: "manyOperations", values: {} }; } for (const check of CHECKS) { const refusal = check(state, args, operation); if (refusal !== null) { return refusal; } } return null; }; const run = function run(state: State, args: NjsRequest["args"], operation: string): readonly Row[] { if (operation === "ref") { return knownRows(state, [argOf(args, "ref")]); } if (operation === "name") { return byName(state, argOf(args, "name")); } if (operation === "q") { return byWords(state, argOf(args, "q")); } if (operation === "id") { return byId(state, argOf(args, "id")); } if (operation === "walk") { return byWalk(state, argOf(args, "walk"), args["relation"], numberOf(args["depth"], 1) ?? 1); } const fields = Object.keys(args) .filter((key) => !OPERATIONS.has(key) && !PAGING.has(key)) .map((key): readonly [string, string] => [key, argOf(args, key)]); return byFacets(state, argOf(args, "collection"), fields); }; const q = function q(r: NjsRequest): void { const state = stateOf(r.variables.document_root); const { args } = r; const operations = Object.keys(args).filter((key) => OPERATIONS.has(key)); const refusal = refusalOf(state, args, operations); const [operation] = operations; if (refusal !== null || operation === undefined) { refuse(r, state, refusal ?? { code: "noOperation", values: {} }); return; } const kind = operation === "collection" ? undefined : args[KIND_OPTION]; const results = run(state, args, operation).filter((row) => kind === undefined || row["kind"] === kind); if (results.length === 0) { const suggestions = operation === "name" ? { suggestions: nameSuggestions(state, argOf(args, "name")) } : {}; respond(r, 404, { code: "notFound", error: messageOf(state, "notFound"), index: state.query.index, ...suggestions, }); return; } const limit = numberOf(args["limit"], state.query.limits.defaultLimit) ?? state.query.limits.defaultLimit; const offset = numberOf(args["offset"], 0) ?? 0; respond(r, 200, { count: results.length, limit, offset, operation, results: results.slice(offset, offset + limit), }); }; const movesOf = function movesOf(state: State): Map { const moves = new Map(); const moved = cached(state, MOVED_PATH, isTable); rowsOf(moved ?? { columns: [], rows: [] }).forEach((row) => { if (isMoveRow(row)) { moves.set(localOf(state.site, row.json), row.to); } }); return moves; }; const missing = function missing(r: NjsRequest): void { const state = stateOf(r.variables.document_root); const index = `${state.site}/json/api`; const moves = movesOf(state); if (!moves.has(r.uri)) { respond(r, 404, { code: "missing", error: messageOf(state, "missing"), index }); return; } const to = moves.get(r.uri) ?? null; if (to === null) { respond(r, 410, { code: "gone", error: messageOf(state, "gone"), index }); return; } r.headersOut["Access-Control-Allow-Origin"] = "*"; r.return(PERMANENT_REDIRECT, to); }; const staticQuery = function staticQuery(r: NjsRequest): void { const state = stateOf(r.variables.document_root); respond(r, 400, { code: "staticQuery", error: messageOf(state, "staticQuery"), query: state.query.endpoint }); }; const pageExists = function pageExists(root: string, page: string): boolean { try { fs.statSync(root + page); return true; } catch (error) { if (isMissing(error)) { return false; } throw error; } }; const markdownCanonical = function markdownCanonical(r: NjsRequest): string { if (!r.uri.endsWith(MARKDOWN_EXTENSION)) { return ""; } const home = r.uri === HOME_TWIN; const route = home ? HOME_ROUTE : r.uri.slice(0, -MARKDOWN_EXTENSION.length); const page = home ? HOME_PAGE : route + PAGE_EXTENSION; if (!pageExists(r.variables.document_root, page)) { return ""; } return `; rel="canonical"`; }; export default { markdownCanonical, missing, nameKeyOf, normalizeWord, q, staticQuery };