@stapel/search-react 0.1.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.
- package/CHANGELOG.md +1 -0
- package/MODULE.md +203 -0
- package/README.md +175 -0
- package/dist/api/generated/schema.d.ts +375 -0
- package/dist/api/generated/schema.d.ts.map +1 -0
- package/dist/api/generated/schema.js +2 -0
- package/dist/api/generated/schema.js.map +1 -0
- package/dist/api/searchApi.d.ts +77 -0
- package/dist/api/searchApi.d.ts.map +1 -0
- package/dist/api/searchApi.js +79 -0
- package/dist/api/searchApi.js.map +1 -0
- package/dist/api/types.d.ts +131 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +20 -0
- package/dist/api/types.js.map +1 -0
- package/dist/default/DegradationNotice.d.ts +19 -0
- package/dist/default/DegradationNotice.d.ts.map +1 -0
- package/dist/default/DegradationNotice.js +14 -0
- package/dist/default/DegradationNotice.js.map +1 -0
- package/dist/default/ErrorAlert.d.ts +9 -0
- package/dist/default/ErrorAlert.d.ts.map +1 -0
- package/dist/default/ErrorAlert.js +27 -0
- package/dist/default/ErrorAlert.js.map +1 -0
- package/dist/default/FacetPanelPane.d.ts +26 -0
- package/dist/default/FacetPanelPane.d.ts.map +1 -0
- package/dist/default/FacetPanelPane.js +32 -0
- package/dist/default/FacetPanelPane.js.map +1 -0
- package/dist/default/RankingDisclosurePane.d.ts +17 -0
- package/dist/default/RankingDisclosurePane.d.ts.map +1 -0
- package/dist/default/RankingDisclosurePane.js +23 -0
- package/dist/default/RankingDisclosurePane.js.map +1 -0
- package/dist/default/SearchPage.d.ts +34 -0
- package/dist/default/SearchPage.d.ts.map +1 -0
- package/dist/default/SearchPage.js +16 -0
- package/dist/default/SearchPage.js.map +1 -0
- package/dist/default/SearchResultCard.d.ts +34 -0
- package/dist/default/SearchResultCard.d.ts.map +1 -0
- package/dist/default/SearchResultCard.js +38 -0
- package/dist/default/SearchResultCard.js.map +1 -0
- package/dist/default/SearchResultsPane.d.ts +32 -0
- package/dist/default/SearchResultsPane.d.ts.map +1 -0
- package/dist/default/SearchResultsPane.js +46 -0
- package/dist/default/SearchResultsPane.js.map +1 -0
- package/dist/default/SortSelect.d.ts +22 -0
- package/dist/default/SortSelect.d.ts.map +1 -0
- package/dist/default/SortSelect.js +39 -0
- package/dist/default/SortSelect.js.map +1 -0
- package/dist/default/UrlIssueNotice.d.ts +12 -0
- package/dist/default/UrlIssueNotice.d.ts.map +1 -0
- package/dist/default/UrlIssueNotice.js +13 -0
- package/dist/default/UrlIssueNotice.js.map +1 -0
- package/dist/default/index.d.ts +40 -0
- package/dist/default/index.d.ts.map +1 -0
- package/dist/default/index.js +34 -0
- package/dist/default/index.js.map +1 -0
- package/dist/default/theme.d.ts +24 -0
- package/dist/default/theme.d.ts.map +1 -0
- package/dist/default/theme.js +39 -0
- package/dist/default/theme.js.map +1 -0
- package/dist/default/types.d.ts +12 -0
- package/dist/default/types.d.ts.map +1 -0
- package/dist/default/types.js +2 -0
- package/dist/default/types.js.map +1 -0
- package/dist/headless/FacetPanel.d.ts +61 -0
- package/dist/headless/FacetPanel.d.ts.map +1 -0
- package/dist/headless/FacetPanel.js +57 -0
- package/dist/headless/FacetPanel.js.map +1 -0
- package/dist/headless/RankingDisclosure.d.ts +39 -0
- package/dist/headless/RankingDisclosure.d.ts.map +1 -0
- package/dist/headless/RankingDisclosure.js +28 -0
- package/dist/headless/RankingDisclosure.js.map +1 -0
- package/dist/headless/SearchProvider.d.ts +19 -0
- package/dist/headless/SearchProvider.d.ts.map +1 -0
- package/dist/headless/SearchProvider.js +15 -0
- package/dist/headless/SearchProvider.js.map +1 -0
- package/dist/headless/SearchResults.d.ts +68 -0
- package/dist/headless/SearchResults.d.ts.map +1 -0
- package/dist/headless/SearchResults.js +70 -0
- package/dist/headless/SearchResults.js.map +1 -0
- package/dist/headless/SearchStateProvider.d.ts +69 -0
- package/dist/headless/SearchStateProvider.d.ts.map +1 -0
- package/dist/headless/SearchStateProvider.js +71 -0
- package/dist/headless/SearchStateProvider.js.map +1 -0
- package/dist/i18n/errorsMap.d.ts +27 -0
- package/dist/i18n/errorsMap.d.ts.map +1 -0
- package/dist/i18n/errorsMap.js +37 -0
- package/dist/i18n/errorsMap.js.map +1 -0
- package/dist/i18n/es.d.ts +13 -0
- package/dist/i18n/es.d.ts.map +1 -0
- package/dist/i18n/es.js +85 -0
- package/dist/i18n/es.js.map +1 -0
- package/dist/i18n/generated/errors.es.gen.d.ts +9 -0
- package/dist/i18n/generated/errors.es.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.es.gen.js +63 -0
- package/dist/i18n/generated/errors.es.gen.js.map +1 -0
- package/dist/i18n/generated/errors.gen.d.ts +359 -0
- package/dist/i18n/generated/errors.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.gen.js +183 -0
- package/dist/i18n/generated/errors.gen.js.map +1 -0
- package/dist/i18n/generated/errors.ru.gen.d.ts +9 -0
- package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.ru.gen.js +63 -0
- package/dist/i18n/generated/errors.ru.gen.js.map +1 -0
- package/dist/i18n/keys.d.ts +107 -0
- package/dist/i18n/keys.d.ts.map +1 -0
- package/dist/i18n/keys.js +185 -0
- package/dist/i18n/keys.js.map +1 -0
- package/dist/i18n/ru.d.ts +29 -0
- package/dist/i18n/ru.d.ts.map +1 -0
- package/dist/i18n/ru.js +101 -0
- package/dist/i18n/ru.js.map +1 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +66 -0
- package/dist/index.js.map +1 -0
- package/dist/model/context.d.ts +11 -0
- package/dist/model/context.d.ts.map +1 -0
- package/dist/model/context.js +16 -0
- package/dist/model/context.js.map +1 -0
- package/dist/model/queries.d.ts +48 -0
- package/dist/model/queries.d.ts.map +1 -0
- package/dist/model/queries.js +65 -0
- package/dist/model/queries.js.map +1 -0
- package/dist/model/queryKeys.d.ts +25 -0
- package/dist/model/queryKeys.d.ts.map +1 -0
- package/dist/model/queryKeys.js +22 -0
- package/dist/model/queryKeys.js.map +1 -0
- package/dist/model/runtime.d.ts +25 -0
- package/dist/model/runtime.d.ts.map +1 -0
- package/dist/model/runtime.js +6 -0
- package/dist/model/runtime.js.map +1 -0
- package/dist/nav/manifest.d.ts +21 -0
- package/dist/nav/manifest.d.ts.map +1 -0
- package/dist/nav/manifest.js +34 -0
- package/dist/nav/manifest.js.map +1 -0
- package/dist/router/index.d.ts +10 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +34 -0
- package/dist/router/index.js.map +1 -0
- package/dist/state/degradations.d.ts +30 -0
- package/dist/state/degradations.d.ts.map +1 -0
- package/dist/state/degradations.js +52 -0
- package/dist/state/degradations.js.map +1 -0
- package/dist/state/facets.d.ts +68 -0
- package/dist/state/facets.d.ts.map +1 -0
- package/dist/state/facets.js +171 -0
- package/dist/state/facets.js.map +1 -0
- package/dist/state/urlState.d.ts +143 -0
- package/dist/state/urlState.d.ts.map +1 -0
- package/dist/state/urlState.js +319 -0
- package/dist/state/urlState.js.map +1 -0
- package/llms.txt +87 -0
- package/manifest.json +652 -0
- package/nav-manifest.json +44 -0
- package/package.json +133 -0
- package/src/analytics/generated/events.json +7 -0
- package/src/api/generated/schema.ts +377 -0
- package/src/api/searchApi.ts +169 -0
- package/src/api/types.ts +160 -0
- package/src/default/DegradationNotice.tsx +50 -0
- package/src/default/ErrorAlert.tsx +46 -0
- package/src/default/FacetPanelPane.tsx +174 -0
- package/src/default/RankingDisclosurePane.tsx +116 -0
- package/src/default/SearchPage.tsx +87 -0
- package/src/default/SearchResultCard.tsx +96 -0
- package/src/default/SearchResultsPane.tsx +173 -0
- package/src/default/SortSelect.tsx +77 -0
- package/src/default/UrlIssueNotice.tsx +37 -0
- package/src/default/index.ts +48 -0
- package/src/default/theme.tsx +66 -0
- package/src/default/types.ts +12 -0
- package/src/headless/FacetPanel.tsx +113 -0
- package/src/headless/RankingDisclosure.tsx +58 -0
- package/src/headless/SearchProvider.tsx +20 -0
- package/src/headless/SearchResults.tsx +149 -0
- package/src/headless/SearchStateProvider.tsx +175 -0
- package/src/i18n/errorsMap.ts +50 -0
- package/src/i18n/es.ts +110 -0
- package/src/i18n/generated/errors.es.gen.ts +69 -0
- package/src/i18n/generated/errors.gen.ts +214 -0
- package/src/i18n/generated/errors.json +423 -0
- package/src/i18n/generated/errors.ru.gen.ts +69 -0
- package/src/i18n/keys.ts +220 -0
- package/src/i18n/ru.ts +126 -0
- package/src/index.ts +154 -0
- package/src/model/context.tsx +28 -0
- package/src/model/queries.ts +79 -0
- package/src/model/queryKeys.ts +48 -0
- package/src/model/runtime.ts +33 -0
- package/src/nav/manifest.ts +53 -0
- package/src/router/index.ts +38 -0
- package/src/state/degradations.ts +75 -0
- package/src/state/facets.ts +219 -0
- package/src/state/urlState.ts +458 -0
- package/tsconfig.json +26 -0
package/package.json
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stapel/search-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Headless React pair for stapel-search: a typed query client, TanStack Query hooks, and a URL-first state codec that makes a search shareable by construction (filters, ranges, geo, sort and the keyset cursor all live in the query string). Drill-down facets rendered with their remaining counts and with the server's own honesty flags — approximate, skipped, degraded — never swallowed; keyset pagination with the window refusal named; DSA Art. 26 `promoted` marking carried into every card slot and the P2B Art. 5 ranking disclosure exposed as data. Zero visual opinion in the main entry; an opt-in /default subpath ships the antd skin, and /router binds the codec to react-router's useSearchParams.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/usestapel/stapel-react.git",
|
|
9
|
+
"directory": "packages/search-react"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./default": {
|
|
21
|
+
"types": "./dist/default/index.d.ts",
|
|
22
|
+
"default": "./dist/default/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./router": {
|
|
25
|
+
"types": "./dist/router/index.d.ts",
|
|
26
|
+
"default": "./dist/router/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./i18n/ru": {
|
|
29
|
+
"types": "./dist/i18n/ru.d.ts",
|
|
30
|
+
"default": "./dist/i18n/ru.js"
|
|
31
|
+
},
|
|
32
|
+
"./i18n/es": {
|
|
33
|
+
"types": "./dist/i18n/es.d.ts",
|
|
34
|
+
"default": "./dist/i18n/es.js"
|
|
35
|
+
},
|
|
36
|
+
"./manifest": "./manifest.json",
|
|
37
|
+
"./manifest.json": "./manifest.json",
|
|
38
|
+
"./nav-manifest.json": "./nav-manifest.json",
|
|
39
|
+
"./llms.txt": "./llms.txt",
|
|
40
|
+
"./package.json": "./package.json"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist",
|
|
44
|
+
"src",
|
|
45
|
+
"tsconfig.json",
|
|
46
|
+
"README.md",
|
|
47
|
+
"MODULE.md",
|
|
48
|
+
"CHANGELOG.md",
|
|
49
|
+
"manifest.json",
|
|
50
|
+
"nav-manifest.json",
|
|
51
|
+
"llms.txt"
|
|
52
|
+
],
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsc -p tsconfig.json",
|
|
55
|
+
"test": "tsc -p tsconfig.test.json && vitest run --exclude test/prodBundlePurity.test.ts",
|
|
56
|
+
"lint": "eslint .",
|
|
57
|
+
"size": "size-limit",
|
|
58
|
+
"test:pack": "vitest run test/prodBundlePurity.test.ts"
|
|
59
|
+
},
|
|
60
|
+
"size-limit": [
|
|
61
|
+
{
|
|
62
|
+
"name": "index — the headless pair (client + hooks + URL codec + facet model): no antd, no react-router",
|
|
63
|
+
"path": "dist/index.js",
|
|
64
|
+
"limit": "10 KB"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"name": "default — the antd skin (results, facet panel, degradation notice, ranking disclosure) must stay out of the main bundle",
|
|
68
|
+
"path": "dist/default/index.js",
|
|
69
|
+
"limit": "13 KB"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"name": "router — the react-router binding is opt-in; the main entry must never pull a router",
|
|
73
|
+
"path": "dist/router/index.js",
|
|
74
|
+
"limit": "1 KB"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "i18n/ru — the Russian bundle is opt-in, not carried by the main entry",
|
|
78
|
+
"path": "dist/i18n/ru.js",
|
|
79
|
+
"limit": "4.5 KB"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"name": "i18n/es — the Spanish bundle is opt-in, not carried by the main entry",
|
|
83
|
+
"path": "dist/i18n/es.js",
|
|
84
|
+
"limit": "3.5 KB"
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
"peerDependencies": {
|
|
88
|
+
"@stapel/attributes-react": ">=0.1.0",
|
|
89
|
+
"@stapel/core": ">=0.15.0 <1.0.0",
|
|
90
|
+
"@stapel/tokens-antd": ">=0.5.0",
|
|
91
|
+
"@tanstack/react-query": "^5.0.0",
|
|
92
|
+
"antd": ">=5.20.0 <7",
|
|
93
|
+
"react": ">=19",
|
|
94
|
+
"react-router": ">=7.0.0 <8"
|
|
95
|
+
},
|
|
96
|
+
"peerDependenciesMeta": {
|
|
97
|
+
"@stapel/tokens-antd": {
|
|
98
|
+
"optional": true
|
|
99
|
+
},
|
|
100
|
+
"antd": {
|
|
101
|
+
"optional": true
|
|
102
|
+
},
|
|
103
|
+
"react-router": {
|
|
104
|
+
"optional": true
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"devDependencies": {
|
|
108
|
+
"@size-limit/preset-small-lib": "^11.2.0",
|
|
109
|
+
"@stapel/attributes-react": "workspace:^",
|
|
110
|
+
"@stapel/core": "workspace:^",
|
|
111
|
+
"@stapel/showcase": "workspace:^",
|
|
112
|
+
"@stapel/tokens": "workspace:^",
|
|
113
|
+
"@stapel/tokens-antd": "workspace:^",
|
|
114
|
+
"@tanstack/react-query": "^5.81.0",
|
|
115
|
+
"@testing-library/react": "^16.3.0",
|
|
116
|
+
"@types/react": "^19.1.0",
|
|
117
|
+
"@types/react-dom": "^19.1.0",
|
|
118
|
+
"antd": "^6.5.0",
|
|
119
|
+
"jsdom": "^26.1.0",
|
|
120
|
+
"react": "^19.1.0",
|
|
121
|
+
"react-dom": "^19.1.0",
|
|
122
|
+
"react-router": "^7.18.1",
|
|
123
|
+
"size-limit": "^11.2.0",
|
|
124
|
+
"typescript": "^5.8.3",
|
|
125
|
+
"vitest": "^3.2.4"
|
|
126
|
+
},
|
|
127
|
+
"engines": {
|
|
128
|
+
"node": ">=22"
|
|
129
|
+
},
|
|
130
|
+
"publishConfig": {
|
|
131
|
+
"access": "public"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// AUTO-GENERATED by scripts/gen-api.mjs — do not edit by hand.
|
|
2
|
+
// Source: the backend module's own docs/schema.json (§17-native per-module contract).
|
|
3
|
+
// Regenerate: pnpm gen:api · Drift gate: pnpm gen:api:check
|
|
4
|
+
export interface paths {
|
|
5
|
+
"/search/api/v1/health": {
|
|
6
|
+
parameters: {
|
|
7
|
+
query?: never;
|
|
8
|
+
header?: never;
|
|
9
|
+
path?: never;
|
|
10
|
+
cookie?: never;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Engine reachability, capabilities and index lag
|
|
14
|
+
* @description ``GET /search/api/v1/health`` — backend, capabilities, index lag.
|
|
15
|
+
*
|
|
16
|
+
* **Permissions:** `IsNotAnonymousUser`
|
|
17
|
+
*/
|
|
18
|
+
get: operations["search_api_v1_health_retrieve"];
|
|
19
|
+
put?: never;
|
|
20
|
+
post?: never;
|
|
21
|
+
delete?: never;
|
|
22
|
+
options?: never;
|
|
23
|
+
head?: never;
|
|
24
|
+
patch?: never;
|
|
25
|
+
trace?: never;
|
|
26
|
+
};
|
|
27
|
+
"/search/api/v1/query": {
|
|
28
|
+
parameters: {
|
|
29
|
+
query?: never;
|
|
30
|
+
header?: never;
|
|
31
|
+
path?: never;
|
|
32
|
+
cookie?: never;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Search one registered document type
|
|
36
|
+
* @description ``GET /search/api/v1/query`` — the whole read contract.
|
|
37
|
+
*
|
|
38
|
+
* **Permissions:** `AllowAny`
|
|
39
|
+
*/
|
|
40
|
+
get: operations["search_api_v1_query_retrieve"];
|
|
41
|
+
put?: never;
|
|
42
|
+
post?: never;
|
|
43
|
+
delete?: never;
|
|
44
|
+
options?: never;
|
|
45
|
+
head?: never;
|
|
46
|
+
patch?: never;
|
|
47
|
+
trace?: never;
|
|
48
|
+
};
|
|
49
|
+
"/search/api/v1/ranking": {
|
|
50
|
+
parameters: {
|
|
51
|
+
query?: never;
|
|
52
|
+
header?: never;
|
|
53
|
+
path?: never;
|
|
54
|
+
cookie?: never;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Ranking disclosure (P2B Art. 5), generated from the scorer registry
|
|
58
|
+
* @description ``GET /search/api/v1/ranking`` — the P2B Art. 5 disclosure.
|
|
59
|
+
*
|
|
60
|
+
* Generated from the scorer registry and annotated with what the
|
|
61
|
+
* configured engine can actually evaluate, so it cannot claim a parameter
|
|
62
|
+
* is in effect when the backend cannot compute it.
|
|
63
|
+
*
|
|
64
|
+
* **Permissions:** `AllowAny`
|
|
65
|
+
*/
|
|
66
|
+
get: operations["search_api_v1_ranking_retrieve"];
|
|
67
|
+
put?: never;
|
|
68
|
+
post?: never;
|
|
69
|
+
delete?: never;
|
|
70
|
+
options?: never;
|
|
71
|
+
head?: never;
|
|
72
|
+
patch?: never;
|
|
73
|
+
trace?: never;
|
|
74
|
+
};
|
|
75
|
+
"/search/api/v1/reindex": {
|
|
76
|
+
parameters: {
|
|
77
|
+
query?: never;
|
|
78
|
+
header?: never;
|
|
79
|
+
path?: never;
|
|
80
|
+
cookie?: never;
|
|
81
|
+
};
|
|
82
|
+
get?: never;
|
|
83
|
+
put?: never;
|
|
84
|
+
/**
|
|
85
|
+
* Re-pull specific keys, or rebuild a whole document type
|
|
86
|
+
* @description ``POST /search/api/v1/reindex`` — targeted re-pull or a full rebuild.
|
|
87
|
+
*
|
|
88
|
+
* **Permissions:** `IsNotAnonymousUser`
|
|
89
|
+
*/
|
|
90
|
+
post: operations["search_api_v1_reindex_create"];
|
|
91
|
+
delete?: never;
|
|
92
|
+
options?: never;
|
|
93
|
+
head?: never;
|
|
94
|
+
patch?: never;
|
|
95
|
+
trace?: never;
|
|
96
|
+
};
|
|
97
|
+
"/search/api/v1/suggest": {
|
|
98
|
+
parameters: {
|
|
99
|
+
query?: never;
|
|
100
|
+
header?: never;
|
|
101
|
+
path?: never;
|
|
102
|
+
cookie?: never;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Title prefixes from the index
|
|
106
|
+
* @description ``GET /search/api/v1/suggest`` — title prefixes out of the index.
|
|
107
|
+
*
|
|
108
|
+
* Not out of a query log: no query log is kept, which is a privacy
|
|
109
|
+
* decision before it is a product one, and on day one there would be
|
|
110
|
+
* nothing in it anyway.
|
|
111
|
+
*
|
|
112
|
+
* **Permissions:** `AllowAny`
|
|
113
|
+
*/
|
|
114
|
+
get: operations["search_api_v1_suggest_retrieve"];
|
|
115
|
+
put?: never;
|
|
116
|
+
post?: never;
|
|
117
|
+
delete?: never;
|
|
118
|
+
options?: never;
|
|
119
|
+
head?: never;
|
|
120
|
+
patch?: never;
|
|
121
|
+
trace?: never;
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
export type webhooks = Record<string, never>;
|
|
125
|
+
export interface components {
|
|
126
|
+
schemas: {
|
|
127
|
+
FacetMeta: {
|
|
128
|
+
/** @description True when counts came from a sample because the candidate set exceeded FACET_CANDIDATE_CAP. */
|
|
129
|
+
approximate: boolean;
|
|
130
|
+
/** @description Size of the largest counted set. */
|
|
131
|
+
candidates: number;
|
|
132
|
+
counted: string[];
|
|
133
|
+
/** @description Plan slugs dropped at MAX_FACET_FIELDS — reported, not vanished. */
|
|
134
|
+
skipped: string[];
|
|
135
|
+
};
|
|
136
|
+
HealthResponse: {
|
|
137
|
+
backend: string;
|
|
138
|
+
reachable: boolean;
|
|
139
|
+
detail: string;
|
|
140
|
+
documents?: number | null;
|
|
141
|
+
capabilities?: {
|
|
142
|
+
[key: string]: unknown;
|
|
143
|
+
};
|
|
144
|
+
types: string[];
|
|
145
|
+
lag_seconds?: number | null;
|
|
146
|
+
stale_reason?: string;
|
|
147
|
+
};
|
|
148
|
+
/** @description The P2B Art. 5 disclosure, generated from the scorer registry. */
|
|
149
|
+
RankingResponse: {
|
|
150
|
+
doc_type: string;
|
|
151
|
+
backend: string;
|
|
152
|
+
scorers: components["schemas"]["Scorer"][];
|
|
153
|
+
notes: string[];
|
|
154
|
+
};
|
|
155
|
+
ReindexRequest: {
|
|
156
|
+
doc_type: string;
|
|
157
|
+
/** @description Re-pull exactly these keys; omit to rebuild the whole type. */
|
|
158
|
+
keys?: string[];
|
|
159
|
+
};
|
|
160
|
+
ReindexResponse: {
|
|
161
|
+
doc_type: string;
|
|
162
|
+
indexed: number;
|
|
163
|
+
removed: number;
|
|
164
|
+
skipped_stale: number;
|
|
165
|
+
skipped_duplicate: number;
|
|
166
|
+
};
|
|
167
|
+
Scorer: {
|
|
168
|
+
slug: string;
|
|
169
|
+
/** Format: double */
|
|
170
|
+
weight: number;
|
|
171
|
+
description_key: string;
|
|
172
|
+
description: string;
|
|
173
|
+
params: {
|
|
174
|
+
[key: string]: unknown;
|
|
175
|
+
};
|
|
176
|
+
applies_to_sorts: string[];
|
|
177
|
+
/** @description False when the configured engine cannot evaluate this parameter. */
|
|
178
|
+
active: boolean;
|
|
179
|
+
inactive_reason: string;
|
|
180
|
+
};
|
|
181
|
+
/** @description One result row. */
|
|
182
|
+
SearchItem: {
|
|
183
|
+
/** @description The source's own document key. */
|
|
184
|
+
key: string;
|
|
185
|
+
/**
|
|
186
|
+
* Format: double
|
|
187
|
+
* @description Ranking score; 0 under an explicit sort.
|
|
188
|
+
*/
|
|
189
|
+
score: number;
|
|
190
|
+
/** @description Whether this result is promoted. Present on EVERY item under EVERY sort, including when false — a mandatory marking (DSA Art. 26), not an optional field. */
|
|
191
|
+
promoted: boolean;
|
|
192
|
+
/**
|
|
193
|
+
* Format: double
|
|
194
|
+
* @description Great-circle distance from the searched centre.
|
|
195
|
+
*/
|
|
196
|
+
distance_km: number | null;
|
|
197
|
+
/** @description Stored row fields, so a result page costs one query. */
|
|
198
|
+
card: {
|
|
199
|
+
[key: string]: unknown;
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
/** @description The query envelope: AnchorPagination's keys, plus what search owes. */
|
|
203
|
+
SearchResponse: {
|
|
204
|
+
items: components["schemas"]["SearchItem"][];
|
|
205
|
+
/** @description {slug: {value: count}}, counted with the slug's own filter removed. */
|
|
206
|
+
facets: {
|
|
207
|
+
[key: string]: {
|
|
208
|
+
[key: string]: number;
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
facet_meta: components["schemas"]["FacetMeta"];
|
|
212
|
+
next_anchor: string | null;
|
|
213
|
+
prev_anchor: string | null;
|
|
214
|
+
has_next: boolean;
|
|
215
|
+
has_prev: boolean;
|
|
216
|
+
count: number;
|
|
217
|
+
exact_total: boolean;
|
|
218
|
+
/** @description What the configured engine could not do for this query. */
|
|
219
|
+
degraded: string[];
|
|
220
|
+
backend: string;
|
|
221
|
+
sort: string;
|
|
222
|
+
took_ms: number;
|
|
223
|
+
};
|
|
224
|
+
SuggestResponse: {
|
|
225
|
+
items: string[];
|
|
226
|
+
backend: string;
|
|
227
|
+
};
|
|
228
|
+
};
|
|
229
|
+
responses: never;
|
|
230
|
+
parameters: never;
|
|
231
|
+
requestBodies: never;
|
|
232
|
+
headers: never;
|
|
233
|
+
pathItems: never;
|
|
234
|
+
}
|
|
235
|
+
export type $defs = Record<string, never>;
|
|
236
|
+
export interface operations {
|
|
237
|
+
search_api_v1_health_retrieve: {
|
|
238
|
+
parameters: {
|
|
239
|
+
query?: never;
|
|
240
|
+
header?: never;
|
|
241
|
+
path?: never;
|
|
242
|
+
cookie?: never;
|
|
243
|
+
};
|
|
244
|
+
requestBody?: never;
|
|
245
|
+
responses: {
|
|
246
|
+
200: {
|
|
247
|
+
headers: {
|
|
248
|
+
[name: string]: unknown;
|
|
249
|
+
};
|
|
250
|
+
content: {
|
|
251
|
+
"application/json": components["schemas"]["HealthResponse"];
|
|
252
|
+
};
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
search_api_v1_query_retrieve: {
|
|
257
|
+
parameters: {
|
|
258
|
+
query: {
|
|
259
|
+
/** @description Opaque keyset cursor from a previous answer. */
|
|
260
|
+
anchor?: string;
|
|
261
|
+
/** @description minLat,minLon,maxLat,maxLon. minLon > maxLon means the box crosses +/-180. */
|
|
262
|
+
bbox?: string;
|
|
263
|
+
/** @description root/leaf path; a prefix filter, so a parent finds its descendants. */
|
|
264
|
+
category?: string;
|
|
265
|
+
/** @description next | prev. */
|
|
266
|
+
direction?: string;
|
|
267
|
+
/** @description Facet filter. Repeat for OR within a slug; different slugs AND together. */
|
|
268
|
+
"f.<slug>"?: string;
|
|
269
|
+
/** @description on | off | comma-separated slugs. Default is the category's plan. */
|
|
270
|
+
facets?: string;
|
|
271
|
+
/** @description Language of the query: selects the analyzer AND narrows the corpus. Omit it and only the analyzer is chosen (from Accept-Language) — a header must not hide a catalogue. */
|
|
272
|
+
lang?: string;
|
|
273
|
+
/** @description Centre latitude (with lon). */
|
|
274
|
+
lat?: number;
|
|
275
|
+
limit?: number;
|
|
276
|
+
/** @description Centre longitude (with lat). */
|
|
277
|
+
lon?: number;
|
|
278
|
+
/** @description Opaque owner key — the seller's own listings. */
|
|
279
|
+
owner?: string;
|
|
280
|
+
/** @description Free text. Dictionary-normalized here; morphology belongs to the engine. */
|
|
281
|
+
q?: string;
|
|
282
|
+
/** @description Range filter, `from..to`; either end may be omitted. */
|
|
283
|
+
"r.<slug>"?: string;
|
|
284
|
+
/** @description Radius around the centre. */
|
|
285
|
+
radius_km?: number;
|
|
286
|
+
/** @description relevance | newest | price_asc | price_desc | distance. An explicit sort never receives a promotional boost. */
|
|
287
|
+
sort?: string;
|
|
288
|
+
/** @description Registered doc_type. One type per query — federated search across types is not in v1. */
|
|
289
|
+
type: string;
|
|
290
|
+
};
|
|
291
|
+
header?: never;
|
|
292
|
+
path?: never;
|
|
293
|
+
cookie?: never;
|
|
294
|
+
};
|
|
295
|
+
requestBody?: never;
|
|
296
|
+
responses: {
|
|
297
|
+
200: {
|
|
298
|
+
headers: {
|
|
299
|
+
[name: string]: unknown;
|
|
300
|
+
};
|
|
301
|
+
content: {
|
|
302
|
+
"application/json": components["schemas"]["SearchResponse"];
|
|
303
|
+
};
|
|
304
|
+
};
|
|
305
|
+
};
|
|
306
|
+
};
|
|
307
|
+
search_api_v1_ranking_retrieve: {
|
|
308
|
+
parameters: {
|
|
309
|
+
query?: {
|
|
310
|
+
type?: string;
|
|
311
|
+
};
|
|
312
|
+
header?: never;
|
|
313
|
+
path?: never;
|
|
314
|
+
cookie?: never;
|
|
315
|
+
};
|
|
316
|
+
requestBody?: never;
|
|
317
|
+
responses: {
|
|
318
|
+
200: {
|
|
319
|
+
headers: {
|
|
320
|
+
[name: string]: unknown;
|
|
321
|
+
};
|
|
322
|
+
content: {
|
|
323
|
+
"application/json": components["schemas"]["RankingResponse"];
|
|
324
|
+
};
|
|
325
|
+
};
|
|
326
|
+
};
|
|
327
|
+
};
|
|
328
|
+
search_api_v1_reindex_create: {
|
|
329
|
+
parameters: {
|
|
330
|
+
query?: never;
|
|
331
|
+
header?: never;
|
|
332
|
+
path?: never;
|
|
333
|
+
cookie?: never;
|
|
334
|
+
};
|
|
335
|
+
requestBody: {
|
|
336
|
+
content: {
|
|
337
|
+
"application/json": components["schemas"]["ReindexRequest"];
|
|
338
|
+
"application/x-www-form-urlencoded": components["schemas"]["ReindexRequest"];
|
|
339
|
+
"multipart/form-data": components["schemas"]["ReindexRequest"];
|
|
340
|
+
};
|
|
341
|
+
};
|
|
342
|
+
responses: {
|
|
343
|
+
200: {
|
|
344
|
+
headers: {
|
|
345
|
+
[name: string]: unknown;
|
|
346
|
+
};
|
|
347
|
+
content: {
|
|
348
|
+
"application/json": components["schemas"]["ReindexResponse"];
|
|
349
|
+
};
|
|
350
|
+
};
|
|
351
|
+
};
|
|
352
|
+
};
|
|
353
|
+
search_api_v1_suggest_retrieve: {
|
|
354
|
+
parameters: {
|
|
355
|
+
query: {
|
|
356
|
+
limit?: number;
|
|
357
|
+
/** @description Title prefix. */
|
|
358
|
+
q?: string;
|
|
359
|
+
type: string;
|
|
360
|
+
};
|
|
361
|
+
header?: never;
|
|
362
|
+
path?: never;
|
|
363
|
+
cookie?: never;
|
|
364
|
+
};
|
|
365
|
+
requestBody?: never;
|
|
366
|
+
responses: {
|
|
367
|
+
200: {
|
|
368
|
+
headers: {
|
|
369
|
+
[name: string]: unknown;
|
|
370
|
+
};
|
|
371
|
+
content: {
|
|
372
|
+
"application/json": components["schemas"]["SuggestResponse"];
|
|
373
|
+
};
|
|
374
|
+
};
|
|
375
|
+
};
|
|
376
|
+
};
|
|
377
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { StapelClient } from "@stapel/core";
|
|
2
|
+
import type {
|
|
3
|
+
RankingResponse,
|
|
4
|
+
SearchQueryState,
|
|
5
|
+
SearchResponse,
|
|
6
|
+
SuggestParams,
|
|
7
|
+
SuggestResponse,
|
|
8
|
+
} from "./types.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The pair's typed operation surface — one method per stapel-search endpoint a
|
|
12
|
+
* storefront may call, bound to the injected {@link StapelClient} (the
|
|
13
|
+
* per-module override seam of frontend-standard §7.2). Paths are relative to
|
|
14
|
+
* the runtime's `baseUrl` (`/search/api/v1/`).
|
|
15
|
+
*
|
|
16
|
+
* ── The three PUBLIC reads, and the two that are not here ─────────────────
|
|
17
|
+
*
|
|
18
|
+
* `query`, `suggest` and `ranking` are `AllowAny` (`stapel_anonymous_access =
|
|
19
|
+
* ANONYMOUS_ALLOWED`): a storefront calls them with no session at all, which
|
|
20
|
+
* is what lets a catalogue render for a visitor who will never sign in.
|
|
21
|
+
*
|
|
22
|
+
* `GET /health` and `POST /reindex` are NOT on this interface. Both are
|
|
23
|
+
* `IsNotAnonymousUser` + an in-view `can_manage` check that answers
|
|
24
|
+
* `error.403.search_forbidden` — they are index-operator endpoints, not
|
|
25
|
+
* storefront ones, and a pair that exposed them would invite a public screen
|
|
26
|
+
* to call something only an operator may. They stay in the generated schema
|
|
27
|
+
* (and therefore in `manifest.json`, which lists the whole contract), so
|
|
28
|
+
* nothing is hidden; they are simply not this pair's surface.
|
|
29
|
+
*
|
|
30
|
+
* These operations will be GENERATED from schema.json operationIds by gen-api
|
|
31
|
+
* v2 (task `core-typed-ops`); until then they are hand-authored here (the ONE
|
|
32
|
+
* legal home of path strings — `stapel/no-string-paths` §2.3 carve-out).
|
|
33
|
+
*/
|
|
34
|
+
export interface SearchApi {
|
|
35
|
+
readonly client: StapelClient;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* One keyset page of results, with the facet counts for the current
|
|
39
|
+
* candidate set.
|
|
40
|
+
*
|
|
41
|
+
* Refusals a skin must tell apart, all 400 with the offending value in
|
|
42
|
+
* `params`: `error.400.search_unknown_doc_type`, `…_unknown_sort`,
|
|
43
|
+
* `…_sort_needs_center`, `…_bad_geo`, `…_bad_range`, `…_bad_cursor`,
|
|
44
|
+
* `…_query_too_long`, `…_too_many_facets`, `…_too_many_ranges`, and
|
|
45
|
+
* `…_window_exceeded` — which is "narrow the search", NOT "nothing found".
|
|
46
|
+
* A dead engine answers 503 `error.503.search_backend_unavailable`; the
|
|
47
|
+
* views turn EVERY backend exception into that, so a 500 never reaches a
|
|
48
|
+
* client and "we could not ask" is always distinguishable from "no hits".
|
|
49
|
+
*/
|
|
50
|
+
query(
|
|
51
|
+
state: SearchQueryState,
|
|
52
|
+
options?: { readonly signal?: AbortSignal }
|
|
53
|
+
): Promise<SearchResponse>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Title-prefix suggestions. Typed and callable, but this pair ships NO hook
|
|
57
|
+
* and no autocomplete widget over it (spec §4.2 defers the debounce +
|
|
58
|
+
* keyboard work). A host that wants one today calls this directly rather
|
|
59
|
+
* than waiting for a version of the pair that has it.
|
|
60
|
+
*/
|
|
61
|
+
suggest(
|
|
62
|
+
params: SuggestParams,
|
|
63
|
+
options?: { readonly signal?: AbortSignal }
|
|
64
|
+
): Promise<SuggestResponse>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The P2B Art. 5 ranking disclosure for a doc type: which parameters rank
|
|
68
|
+
* results, their weights, and — per parameter — whether the configured
|
|
69
|
+
* engine can actually evaluate it (`active` / `inactive_reason`). A
|
|
70
|
+
* disclosure that listed a parameter the engine ignores would be a
|
|
71
|
+
* disclosure of the wrong ranking.
|
|
72
|
+
*/
|
|
73
|
+
ranking(
|
|
74
|
+
type?: string,
|
|
75
|
+
options?: { readonly signal?: AbortSignal }
|
|
76
|
+
): Promise<RankingResponse>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The wire query object for one search state.
|
|
81
|
+
*
|
|
82
|
+
* Exported because it is also what the query KEY is built from: the key and
|
|
83
|
+
* the request are then the same value by construction, and a filter that
|
|
84
|
+
* changes the URL but not the key (the classic stale-page bug) cannot happen.
|
|
85
|
+
*
|
|
86
|
+
* `f.<slug>` is an ARRAY — repeated keys are the OR, and `@stapel/core`'s
|
|
87
|
+
* client repeats an array-valued key rather than collapsing it (core 0.15.0).
|
|
88
|
+
*/
|
|
89
|
+
export function searchQueryParams(
|
|
90
|
+
state: SearchQueryState
|
|
91
|
+
): Record<
|
|
92
|
+
string,
|
|
93
|
+
string | number | boolean | undefined | readonly (string | number | boolean)[]
|
|
94
|
+
> {
|
|
95
|
+
const query: Record<
|
|
96
|
+
string,
|
|
97
|
+
| string
|
|
98
|
+
| number
|
|
99
|
+
| boolean
|
|
100
|
+
| undefined
|
|
101
|
+
| readonly (string | number | boolean)[]
|
|
102
|
+
> = { type: state.type };
|
|
103
|
+
|
|
104
|
+
if (state.q.length > 0) query["q"] = state.q;
|
|
105
|
+
if (state.lang !== undefined) query["lang"] = state.lang;
|
|
106
|
+
if (state.category !== undefined) query["category"] = state.category;
|
|
107
|
+
if (state.owner !== undefined) query["owner"] = state.owner;
|
|
108
|
+
|
|
109
|
+
for (const [slug, values] of Object.entries(state.filters)) {
|
|
110
|
+
if (values.length > 0) query[`f.${slug}`] = values;
|
|
111
|
+
}
|
|
112
|
+
for (const [slug, range] of Object.entries(state.ranges)) {
|
|
113
|
+
if (range.from === undefined && range.to === undefined) continue;
|
|
114
|
+
query[`r.${slug}`] = `${range.from ?? ""}..${range.to ?? ""}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (state.geo !== undefined) {
|
|
118
|
+
if (state.geo.kind === "bbox") {
|
|
119
|
+
const { minLat, minLon, maxLat, maxLon } = state.geo;
|
|
120
|
+
query["bbox"] = `${minLat},${minLon},${maxLat},${maxLon}`;
|
|
121
|
+
} else {
|
|
122
|
+
query["lat"] = state.geo.lat;
|
|
123
|
+
query["lon"] = state.geo.lon;
|
|
124
|
+
if (state.geo.radiusKm !== undefined) {
|
|
125
|
+
query["radius_km"] = state.geo.radiusKm;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (state.sort !== undefined) query["sort"] = state.sort;
|
|
131
|
+
if (state.facets !== undefined) {
|
|
132
|
+
query["facets"] = Array.isArray(state.facets)
|
|
133
|
+
? state.facets.join(",")
|
|
134
|
+
: (state.facets as string);
|
|
135
|
+
}
|
|
136
|
+
if (state.anchor !== undefined) query["anchor"] = state.anchor;
|
|
137
|
+
if (state.direction !== undefined) query["direction"] = state.direction;
|
|
138
|
+
if (state.limit !== undefined) query["limit"] = state.limit;
|
|
139
|
+
|
|
140
|
+
return query;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function createSearchApi(client: StapelClient): SearchApi {
|
|
144
|
+
return {
|
|
145
|
+
client,
|
|
146
|
+
|
|
147
|
+
query: (state, options) =>
|
|
148
|
+
client.get("/query", {
|
|
149
|
+
query: searchQueryParams(state),
|
|
150
|
+
...(options?.signal !== undefined ? { signal: options.signal } : {}),
|
|
151
|
+
}),
|
|
152
|
+
|
|
153
|
+
suggest: (params, options) =>
|
|
154
|
+
client.get("/suggest", {
|
|
155
|
+
query: {
|
|
156
|
+
type: params.type,
|
|
157
|
+
...(params.q !== undefined ? { q: params.q } : {}),
|
|
158
|
+
...(params.limit !== undefined ? { limit: params.limit } : {}),
|
|
159
|
+
},
|
|
160
|
+
...(options?.signal !== undefined ? { signal: options.signal } : {}),
|
|
161
|
+
}),
|
|
162
|
+
|
|
163
|
+
ranking: (type, options) =>
|
|
164
|
+
client.get("/ranking", {
|
|
165
|
+
query: type !== undefined ? { type } : {},
|
|
166
|
+
...(options?.signal !== undefined ? { signal: options.signal } : {}),
|
|
167
|
+
}),
|
|
168
|
+
};
|
|
169
|
+
}
|