@entifix/next-shell 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ export declare const SEARCH_LIMIT_PARAM = "limit";
5
5
  /**
6
6
  * Below this, nothing is queried at all.
7
7
  *
8
- * A `like` becomes an unanchored, case-insensitive `$regex`, which no index can
8
+ * A `like` becomes an unanchored `$regex` that ignores accents and case, which no index can
9
9
  * serve — so this endpoint is a collection scan by construction. One character
10
10
  * matches most of every collection and is worth nothing to the person typing, so
11
11
  * the floor costs them nothing and saves four scans per keystroke.
@@ -7,7 +7,7 @@ export const SEARCH_LIMIT_PARAM = 'limit';
7
7
  /**
8
8
  * Below this, nothing is queried at all.
9
9
  *
10
- * A `like` becomes an unanchored, case-insensitive `$regex`, which no index can
10
+ * A `like` becomes an unanchored `$regex` that ignores accents and case, which no index can
11
11
  * serve — so this endpoint is a collection scan by construction. One character
12
12
  * matches most of every collection and is worth nothing to the person typing, so
13
13
  * the floor costs them nothing and saves four scans per keystroke.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/lib/search/record-search-route.ts"],"sourcesContent":["import { NextResponse } from 'next/server';\n\nimport { bearerHeader, sessionToken } from '../session/bearer.js';\nimport type {\n RecordSearchGroup,\n RecordSearchResponse,\n RecordSearchUnavailable,\n RecordSearchUnavailableReason,\n} from './record-search.types.js';\nimport type { RecordSearchSource } from './record-search-source.js';\n\nexport const SEARCH_TERM_PARAM = 'q';\nexport const SEARCH_SOURCES_PARAM = 'sources';\nexport const SEARCH_LIMIT_PARAM = 'limit';\n\n/**\n * Below this, nothing is queried at all.\n *\n * A `like` becomes an unanchored, case-insensitive `$regex`, which no index can\n * serve — so this endpoint is a collection scan by construction. One character\n * matches most of every collection and is worth nothing to the person typing, so\n * the floor costs them nothing and saves four scans per keystroke.\n */\nexport const MIN_TERM_LENGTH = 2;\nexport const DEFAULT_GROUP_LIMIT = 5;\nexport const MAX_GROUP_LIMIT = 20;\nexport const DEFAULT_SOURCE_TIMEOUT_MS = 1_500;\n\n/** Upstream statuses that mean something other than \"we could not reach it\". */\nconst REASON_FOR_STATUS: Readonly<\n Record<number, RecordSearchUnavailableReason>\n> = {\n 400: 'invalidQuery',\n 401: 'unauthenticated',\n 403: 'forbidden',\n 404: 'notFound',\n 409: 'noActiveOrganization',\n};\n\nexport interface RecordSearchRouteOptions {\n /**\n * The sources to fan out over, **in the order results are presented**. That\n * order is the ranking: an operator learns where a kind of record lands, which\n * a relevance score across four unrelated entities cannot offer.\n */\n readonly sources: readonly RecordSearchSource[];\n /** Per-source budget. Concurrent, so this also bounds the whole request. */\n readonly timeoutMs?: number;\n /** Records per group. */\n readonly limit?: number;\n}\n\ntype SourceOutcome =\n | { readonly ok: true; readonly group: RecordSearchGroup }\n | { readonly ok: false; readonly unavailable: RecordSearchUnavailable };\n\nconst unavailable = (\n source: RecordSearchSource,\n reason: RecordSearchUnavailableReason,\n status?: number,\n): SourceOutcome => ({\n ok: false,\n unavailable: {\n source: source.key,\n entity: source.entity,\n reason,\n ...(status === undefined ? {} : { status }),\n },\n});\n\n/**\n * Ask one source, and **never reject**.\n *\n * Returning a typed outcome rather than throwing is what makes the fan-out a\n * plain `Promise.all`: isolation comes from this catch, not from\n * `allSettled` — whose rejected arm would then be unreachable and untestable.\n */\nconst runSource = async (\n source: RecordSearchSource,\n term: string,\n limit: number,\n token: string | undefined,\n timeoutMs: number,\n): Promise<SourceOutcome> => {\n let upstream: Response;\n try {\n upstream = await fetch(source.url(term, limit), {\n headers: bearerHeader(token),\n cache: 'no-store',\n signal: AbortSignal.timeout(timeoutMs),\n });\n } catch (error) {\n // `AbortSignal.timeout` rejects with a `DOMException` named `TimeoutError`;\n // branching on the name rather than the class keeps this reachable from a\n // test without fabricating a platform error.\n return unavailable(\n source,\n (error as { name?: string }).name === 'TimeoutError'\n ? 'timeout'\n : 'network',\n );\n }\n\n if (!upstream.ok) {\n return unavailable(\n source,\n REASON_FOR_STATUS[upstream.status] ?? 'unexpected',\n upstream.status,\n );\n }\n\n const body = await upstream.json().catch(() => undefined);\n const page = source.read(body);\n return page === undefined\n ? unavailable(source, 'unexpected', upstream.status)\n : {\n ok: true,\n group: {\n source: source.key,\n entity: source.entity,\n labelKey: source.labelKey,\n items: page.items,\n total: page.total,\n },\n };\n};\n\nconst clampLimit = (raw: string | null, fallback: number): number => {\n const asked = Number(raw);\n if (!Number.isInteger(asked) || asked < 1) return fallback;\n return Math.min(asked, MAX_GROUP_LIMIT);\n};\n\n/**\n * `GET /api/search` — records matching a term, across every source this host\n * mounts.\n *\n * The caller's own session is the only credential that crosses: each source is\n * asked over the same guarded route its screens use, so what comes back is\n * already authorized for this principal and this organization. There is no\n * index, and there must never be one — a prefetched index is a tenant leak, and\n * it is the reason this is a fan-out rather than a search service.\n *\n * A source that fails is **named** in `unavailable` rather than dropped. An\n * absent group renders as \"no matches\", which is a confident claim the server is\n * in no position to make about a service it could not reach.\n */\nexport const createRecordSearchRoute = ({\n sources,\n timeoutMs = DEFAULT_SOURCE_TIMEOUT_MS,\n limit: defaultLimit = DEFAULT_GROUP_LIMIT,\n}: RecordSearchRouteOptions) => {\n const byKey = new Map(sources.map(source => [source.key, source]));\n\n return async function GET(request: Request): Promise<Response> {\n // The services decide what this caller may read; this only carries the\n // token. But the gate is not redundant: `catalog-reference` reads are\n // unauthenticated at the service by design, so without it a signed-out\n // caller gets brand matches back and the answer reads as a real search —\n // and the endpoint becomes an anonymous amplifier against that service.\n const token = await sessionToken();\n if (token === undefined) {\n return NextResponse.json(\n { error: 'unauthenticated', code: 'unauthenticated' },\n { status: 401 },\n );\n }\n\n const params = new URL(request.url).searchParams;\n\n const asked = (params.get(SEARCH_SOURCES_PARAM) ?? '')\n .split(',')\n .filter(name => name !== '');\n const unknown = asked.find(name => !byKey.has(name));\n if (unknown !== undefined) {\n // Loud rather than ignored: no keystroke can produce this, only a caller\n // naming a source that does not exist — and silently searching everything\n // instead would answer a question nobody asked.\n return NextResponse.json(\n {\n error: 'invalid query',\n code: 'invalidQuery',\n detail: `unknown search source \"${unknown}\"`,\n },\n { status: 400 },\n );\n }\n const selected =\n asked.length === 0\n ? sources\n : sources.filter(source => asked.includes(source.key));\n\n const term = (params.get(SEARCH_TERM_PARAM) ?? '').trim();\n const answer = (body: RecordSearchResponse) =>\n // The body is scoped to one principal and one organization. `no-store` is\n // correctness here, not tuning.\n NextResponse.json(body, { headers: { 'cache-control': 'no-store' } });\n\n if (term.length < MIN_TERM_LENGTH) {\n return answer({ term, groups: [], unavailable: [] });\n }\n\n const limit = clampLimit(params.get(SEARCH_LIMIT_PARAM), defaultLimit);\n const outcomes = await Promise.all(\n selected.map(source => runSource(source, term, limit, token, timeoutMs)),\n );\n\n return answer({\n term,\n // Declared order, never completion order — otherwise the list reshuffles\n // with the network and nobody can learn where anything lands.\n groups: outcomes.flatMap(outcome => (outcome.ok ? [outcome.group] : [])),\n unavailable: outcomes.flatMap(outcome =>\n outcome.ok ? [] : [outcome.unavailable],\n ),\n });\n };\n};\n"],"names":["NextResponse","bearerHeader","sessionToken","SEARCH_TERM_PARAM","SEARCH_SOURCES_PARAM","SEARCH_LIMIT_PARAM","MIN_TERM_LENGTH","DEFAULT_GROUP_LIMIT","MAX_GROUP_LIMIT","DEFAULT_SOURCE_TIMEOUT_MS","REASON_FOR_STATUS","unavailable","source","reason","status","ok","key","entity","undefined","runSource","term","limit","token","timeoutMs","upstream","fetch","url","headers","cache","signal","AbortSignal","timeout","error","name","body","json","catch","page","read","group","labelKey","items","total","clampLimit","raw","fallback","asked","Number","isInteger","Math","min","createRecordSearchRoute","sources","defaultLimit","byKey","Map","map","GET","request","params","code","URL","searchParams","get","split","filter","unknown","find","has","detail","selected","length","includes","trim","answer","groups","outcomes","Promise","all","flatMap","outcome"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/lib/search/record-search-route.ts"],"sourcesContent":["import { NextResponse } from 'next/server';\n\nimport { bearerHeader, sessionToken } from '../session/bearer.js';\nimport type {\n RecordSearchGroup,\n RecordSearchResponse,\n RecordSearchUnavailable,\n RecordSearchUnavailableReason,\n} from './record-search.types.js';\nimport type { RecordSearchSource } from './record-search-source.js';\n\nexport const SEARCH_TERM_PARAM = 'q';\nexport const SEARCH_SOURCES_PARAM = 'sources';\nexport const SEARCH_LIMIT_PARAM = 'limit';\n\n/**\n * Below this, nothing is queried at all.\n *\n * A `like` becomes an unanchored `$regex` that ignores accents and case, which no index can\n * serve — so this endpoint is a collection scan by construction. One character\n * matches most of every collection and is worth nothing to the person typing, so\n * the floor costs them nothing and saves four scans per keystroke.\n */\nexport const MIN_TERM_LENGTH = 2;\nexport const DEFAULT_GROUP_LIMIT = 5;\nexport const MAX_GROUP_LIMIT = 20;\nexport const DEFAULT_SOURCE_TIMEOUT_MS = 1_500;\n\n/** Upstream statuses that mean something other than \"we could not reach it\". */\nconst REASON_FOR_STATUS: Readonly<\n Record<number, RecordSearchUnavailableReason>\n> = {\n 400: 'invalidQuery',\n 401: 'unauthenticated',\n 403: 'forbidden',\n 404: 'notFound',\n 409: 'noActiveOrganization',\n};\n\nexport interface RecordSearchRouteOptions {\n /**\n * The sources to fan out over, **in the order results are presented**. That\n * order is the ranking: an operator learns where a kind of record lands, which\n * a relevance score across four unrelated entities cannot offer.\n */\n readonly sources: readonly RecordSearchSource[];\n /** Per-source budget. Concurrent, so this also bounds the whole request. */\n readonly timeoutMs?: number;\n /** Records per group. */\n readonly limit?: number;\n}\n\ntype SourceOutcome =\n | { readonly ok: true; readonly group: RecordSearchGroup }\n | { readonly ok: false; readonly unavailable: RecordSearchUnavailable };\n\nconst unavailable = (\n source: RecordSearchSource,\n reason: RecordSearchUnavailableReason,\n status?: number,\n): SourceOutcome => ({\n ok: false,\n unavailable: {\n source: source.key,\n entity: source.entity,\n reason,\n ...(status === undefined ? {} : { status }),\n },\n});\n\n/**\n * Ask one source, and **never reject**.\n *\n * Returning a typed outcome rather than throwing is what makes the fan-out a\n * plain `Promise.all`: isolation comes from this catch, not from\n * `allSettled` — whose rejected arm would then be unreachable and untestable.\n */\nconst runSource = async (\n source: RecordSearchSource,\n term: string,\n limit: number,\n token: string | undefined,\n timeoutMs: number,\n): Promise<SourceOutcome> => {\n let upstream: Response;\n try {\n upstream = await fetch(source.url(term, limit), {\n headers: bearerHeader(token),\n cache: 'no-store',\n signal: AbortSignal.timeout(timeoutMs),\n });\n } catch (error) {\n // `AbortSignal.timeout` rejects with a `DOMException` named `TimeoutError`;\n // branching on the name rather than the class keeps this reachable from a\n // test without fabricating a platform error.\n return unavailable(\n source,\n (error as { name?: string }).name === 'TimeoutError'\n ? 'timeout'\n : 'network',\n );\n }\n\n if (!upstream.ok) {\n return unavailable(\n source,\n REASON_FOR_STATUS[upstream.status] ?? 'unexpected',\n upstream.status,\n );\n }\n\n const body = await upstream.json().catch(() => undefined);\n const page = source.read(body);\n return page === undefined\n ? unavailable(source, 'unexpected', upstream.status)\n : {\n ok: true,\n group: {\n source: source.key,\n entity: source.entity,\n labelKey: source.labelKey,\n items: page.items,\n total: page.total,\n },\n };\n};\n\nconst clampLimit = (raw: string | null, fallback: number): number => {\n const asked = Number(raw);\n if (!Number.isInteger(asked) || asked < 1) return fallback;\n return Math.min(asked, MAX_GROUP_LIMIT);\n};\n\n/**\n * `GET /api/search` — records matching a term, across every source this host\n * mounts.\n *\n * The caller's own session is the only credential that crosses: each source is\n * asked over the same guarded route its screens use, so what comes back is\n * already authorized for this principal and this organization. There is no\n * index, and there must never be one — a prefetched index is a tenant leak, and\n * it is the reason this is a fan-out rather than a search service.\n *\n * A source that fails is **named** in `unavailable` rather than dropped. An\n * absent group renders as \"no matches\", which is a confident claim the server is\n * in no position to make about a service it could not reach.\n */\nexport const createRecordSearchRoute = ({\n sources,\n timeoutMs = DEFAULT_SOURCE_TIMEOUT_MS,\n limit: defaultLimit = DEFAULT_GROUP_LIMIT,\n}: RecordSearchRouteOptions) => {\n const byKey = new Map(sources.map(source => [source.key, source]));\n\n return async function GET(request: Request): Promise<Response> {\n // The services decide what this caller may read; this only carries the\n // token. But the gate is not redundant: `catalog-reference` reads are\n // unauthenticated at the service by design, so without it a signed-out\n // caller gets brand matches back and the answer reads as a real search —\n // and the endpoint becomes an anonymous amplifier against that service.\n const token = await sessionToken();\n if (token === undefined) {\n return NextResponse.json(\n { error: 'unauthenticated', code: 'unauthenticated' },\n { status: 401 },\n );\n }\n\n const params = new URL(request.url).searchParams;\n\n const asked = (params.get(SEARCH_SOURCES_PARAM) ?? '')\n .split(',')\n .filter(name => name !== '');\n const unknown = asked.find(name => !byKey.has(name));\n if (unknown !== undefined) {\n // Loud rather than ignored: no keystroke can produce this, only a caller\n // naming a source that does not exist — and silently searching everything\n // instead would answer a question nobody asked.\n return NextResponse.json(\n {\n error: 'invalid query',\n code: 'invalidQuery',\n detail: `unknown search source \"${unknown}\"`,\n },\n { status: 400 },\n );\n }\n const selected =\n asked.length === 0\n ? sources\n : sources.filter(source => asked.includes(source.key));\n\n const term = (params.get(SEARCH_TERM_PARAM) ?? '').trim();\n const answer = (body: RecordSearchResponse) =>\n // The body is scoped to one principal and one organization. `no-store` is\n // correctness here, not tuning.\n NextResponse.json(body, { headers: { 'cache-control': 'no-store' } });\n\n if (term.length < MIN_TERM_LENGTH) {\n return answer({ term, groups: [], unavailable: [] });\n }\n\n const limit = clampLimit(params.get(SEARCH_LIMIT_PARAM), defaultLimit);\n const outcomes = await Promise.all(\n selected.map(source => runSource(source, term, limit, token, timeoutMs)),\n );\n\n return answer({\n term,\n // Declared order, never completion order — otherwise the list reshuffles\n // with the network and nobody can learn where anything lands.\n groups: outcomes.flatMap(outcome => (outcome.ok ? [outcome.group] : [])),\n unavailable: outcomes.flatMap(outcome =>\n outcome.ok ? [] : [outcome.unavailable],\n ),\n });\n };\n};\n"],"names":["NextResponse","bearerHeader","sessionToken","SEARCH_TERM_PARAM","SEARCH_SOURCES_PARAM","SEARCH_LIMIT_PARAM","MIN_TERM_LENGTH","DEFAULT_GROUP_LIMIT","MAX_GROUP_LIMIT","DEFAULT_SOURCE_TIMEOUT_MS","REASON_FOR_STATUS","unavailable","source","reason","status","ok","key","entity","undefined","runSource","term","limit","token","timeoutMs","upstream","fetch","url","headers","cache","signal","AbortSignal","timeout","error","name","body","json","catch","page","read","group","labelKey","items","total","clampLimit","raw","fallback","asked","Number","isInteger","Math","min","createRecordSearchRoute","sources","defaultLimit","byKey","Map","map","GET","request","params","code","URL","searchParams","get","split","filter","unknown","find","has","detail","selected","length","includes","trim","answer","groups","outcomes","Promise","all","flatMap","outcome"],"mappings":"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"}
package/package.json CHANGED
@@ -21,7 +21,7 @@
21
21
  }
22
22
  }
23
23
  },
24
- "version": "0.8.0",
24
+ "version": "0.9.0",
25
25
  "description": "Next.js application shell: back-office chrome, workspace tabs, CRUD screens derived from entity metadata, and session and proxy routes.",
26
26
  "license": "MIT",
27
27
  "repository": {
@@ -57,17 +57,17 @@
57
57
  "react": "19.3.1",
58
58
  "react-dom": "19.3.1",
59
59
  "zustand": "^5.0.15",
60
- "@entifix/testing-unit": "0.8.0"
60
+ "@entifix/testing-unit": "0.9.0"
61
61
  },
62
62
  "dependencies": {
63
63
  "idb": "^8.0.4",
64
- "@entifix/authz": "0.8.0",
65
- "@entifix/i18n": "0.8.0",
66
- "@entifix/business": "0.8.0",
67
- "@entifix/core": "0.8.0",
68
- "@entifix/react-controls": "0.8.0",
69
- "@entifix/react-integration": "0.8.0",
70
- "@entifix/transactions": "0.8.0"
64
+ "@entifix/authz": "0.9.0",
65
+ "@entifix/i18n": "0.9.0",
66
+ "@entifix/business": "0.9.0",
67
+ "@entifix/react-controls": "0.9.0",
68
+ "@entifix/core": "0.9.0",
69
+ "@entifix/react-integration": "0.9.0",
70
+ "@entifix/transactions": "0.9.0"
71
71
  },
72
72
  "files": [
73
73
  "dist",