@pipeworx/mcp-nhtsa 0.2.3 → 0.3.1
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/README.md +17 -12
- package/bin/cli.js +55 -11
- package/lib/index.js +333 -0
- package/package.json +17 -16
- package/server.json +1 -1
- package/tools.json +222 -0
- package/src/fars-store.ts +0 -170
- package/src/fuel-economy.ts +0 -1057
- package/src/index.ts +0 -1526
- package/src/server.ts +0 -48
- package/tsconfig.json +0 -18
package/tools.json
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "decode_vin",
|
|
4
|
+
"description": "Decode a VIN to get vehicle details. Returns make, model, year, body style, engine type, drivetrain, and plant. E.g., '1HGBH41JXMN109186'. (For recalls/ratings use get_recalls / get_safety_ratings.)",
|
|
5
|
+
"inputSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"vin": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"description": "17-character VIN (e.g., \"1HGBH41JXMN109186\")"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"required": [
|
|
14
|
+
"vin"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "get_makes",
|
|
20
|
+
"description": "Get all vehicle brands for a model year. Returns make names and IDs. E.g., year '2023'.",
|
|
21
|
+
"inputSchema": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"properties": {}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "get_models",
|
|
28
|
+
"description": "Get all vehicle models for a make and year. Returns model names and IDs. E.g., make 'Toyota', year '2023'.",
|
|
29
|
+
"inputSchema": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"properties": {
|
|
32
|
+
"make": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"description": "Vehicle make name (e.g., \"Toyota\", \"Ford\", \"BMW\")"
|
|
35
|
+
},
|
|
36
|
+
"year": {
|
|
37
|
+
"type": "number",
|
|
38
|
+
"description": "Model year (e.g., 2022)"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"required": [
|
|
42
|
+
"make",
|
|
43
|
+
"year"
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "get_recalls",
|
|
49
|
+
"description": "Get official NHTSA safety RECALLS for a vehicle. PREFER OVER WEB SEARCH for \"is my car recalled\", \"recalls on a 2021 Honda Civic\", \"open recalls for make/model/year\". Returns each recall: component, summary, safety consequence, remedy, NHTSA campaign number, and report date. Pass make + model + model_year.",
|
|
50
|
+
"inputSchema": {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"properties": {
|
|
53
|
+
"make": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"description": "Vehicle make (e.g., \"Honda\")"
|
|
56
|
+
},
|
|
57
|
+
"model": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"description": "Vehicle model (e.g., \"Civic\")"
|
|
60
|
+
},
|
|
61
|
+
"model_year": {
|
|
62
|
+
"type": "number",
|
|
63
|
+
"description": "Model year (e.g., 2021)"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"required": [
|
|
67
|
+
"make",
|
|
68
|
+
"model",
|
|
69
|
+
"model_year"
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "get_complaints",
|
|
75
|
+
"description": "Get owner-filed NHTSA complaints for a vehicle — real-world problems drivers reported (vs official recalls). Use for \"common problems with a 2020 Ford Explorer\", \"what are owners complaining about\". Returns components, summary, crash/fire flags, injuries/deaths, and filing date. Pass make + model + model_year.",
|
|
76
|
+
"inputSchema": {
|
|
77
|
+
"type": "object",
|
|
78
|
+
"properties": {
|
|
79
|
+
"make": {
|
|
80
|
+
"type": "string",
|
|
81
|
+
"description": "Vehicle make (e.g., \"Ford\")"
|
|
82
|
+
},
|
|
83
|
+
"model": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"description": "Vehicle model (e.g., \"Explorer\")"
|
|
86
|
+
},
|
|
87
|
+
"model_year": {
|
|
88
|
+
"type": "number",
|
|
89
|
+
"description": "Model year (e.g., 2020)"
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"required": [
|
|
93
|
+
"make",
|
|
94
|
+
"model",
|
|
95
|
+
"model_year"
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"name": "get_safety_ratings",
|
|
101
|
+
"description": "Get NHTSA 5-Star Safety Ratings (NCAP crash-test results) for a vehicle — overall, frontal, side, and rollover star ratings. Use for \"crash test rating for a 2021 Honda Civic\", \"how safe is X\". Pass make + model + model_year.",
|
|
102
|
+
"inputSchema": {
|
|
103
|
+
"type": "object",
|
|
104
|
+
"properties": {
|
|
105
|
+
"make": {
|
|
106
|
+
"type": "string",
|
|
107
|
+
"description": "Vehicle make (e.g., \"Honda\")"
|
|
108
|
+
},
|
|
109
|
+
"model": {
|
|
110
|
+
"type": "string",
|
|
111
|
+
"description": "Vehicle model (e.g., \"Civic\")"
|
|
112
|
+
},
|
|
113
|
+
"model_year": {
|
|
114
|
+
"type": "number",
|
|
115
|
+
"description": "Model year (e.g., 2021)"
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"required": [
|
|
119
|
+
"make",
|
|
120
|
+
"model",
|
|
121
|
+
"model_year"
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"name": "fars_state_fatalities",
|
|
127
|
+
"description": "Fatal motor-vehicle crash counts and fatalities for a US state, from NHTSA's Fatality Analysis Reporting System (FARS). Pass a state (full name like \"Texas\", 2-letter postal code like \"TX\", or FIPS code) and a year. Returns monthly rows: crashes, fatalities, persons involved, vehicles involved, pedestrians involved. Omit year to get every loaded year/month for that state. Covers a recent multi-year window (see data_as_of and call fars_coverage for the exact range), not full FARS history. Unknown states are refused with a clear error, never an empty success.",
|
|
128
|
+
"inputSchema": {
|
|
129
|
+
"type": "object",
|
|
130
|
+
"properties": {
|
|
131
|
+
"state": {
|
|
132
|
+
"type": "string",
|
|
133
|
+
"description": "State name (\"Texas\"), 2-letter postal code (\"TX\"), or FIPS code (\"48\")."
|
|
134
|
+
},
|
|
135
|
+
"year": {
|
|
136
|
+
"type": "number",
|
|
137
|
+
"description": "Optional: calendar year, e.g. 2023. Omit to return every loaded year for this state."
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
"required": [
|
|
141
|
+
"state"
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"name": "fars_vehicle_makes",
|
|
147
|
+
"description": "Vehicle makes involved in fatal crashes nationwide for a given year, from NHTSA FARS. Pass year alone to get the top makes ranked by vehicle-involvement count (e.g. \"which vehicle makes show up most in fatal crashes in 2023\"); pass make as well to get that one make's involvement count across every loaded year. This is an INVOLVEMENT count, not a claim that the make caused the crash. Unknown makes are refused with a clear error naming a few valid makes, never an empty success.",
|
|
148
|
+
"inputSchema": {
|
|
149
|
+
"type": "object",
|
|
150
|
+
"properties": {
|
|
151
|
+
"year": {
|
|
152
|
+
"type": "number",
|
|
153
|
+
"description": "Calendar year, e.g. 2023. Required unless make is given (then optional, scoping to one year)."
|
|
154
|
+
},
|
|
155
|
+
"make": {
|
|
156
|
+
"type": "string",
|
|
157
|
+
"description": "Optional: vehicle make, e.g. \"Toyota\", \"Ford\", \"Harley-Davidson\"."
|
|
158
|
+
},
|
|
159
|
+
"limit": {
|
|
160
|
+
"type": "number",
|
|
161
|
+
"description": "Max makes to return when ranking by year. Default 20, max 50."
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
"required": []
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"name": "fars_coverage",
|
|
169
|
+
"description": "What FARS fatal-crash data this pack actually has loaded right now: the earliest and latest year covered and when it was last loaded. Call this before fars_state_fatalities/fars_vehicle_makes if unsure a given year is covered.",
|
|
170
|
+
"inputSchema": {
|
|
171
|
+
"type": "object",
|
|
172
|
+
"properties": {}
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"name": "fuel_economy_mpg",
|
|
177
|
+
"description": "EPA fuel-economy ratings (city/highway/combined MPG, CO2 g/mile, fuel type) for a specific model year + make + model, from fueleconomy.gov (DOE/EPA), proxied live. Pass the model string exactly as fueleconomy.gov lists it (e.g. \"Camry\", \"F-150 2WD\", \"Model 3\") -- if unsure, call fuel_economy_models first to see valid model strings for that year+make. A model year/make/model can have several engine/transmission/drivetrain VARIANTS; this returns up to 5, each with its own MPG. Unknown make/model/year combos are refused with a clear error (never an empty success) and, when the make or model itself is not recognized for that year, list a few valid options.",
|
|
178
|
+
"inputSchema": {
|
|
179
|
+
"type": "object",
|
|
180
|
+
"properties": {
|
|
181
|
+
"year": {
|
|
182
|
+
"type": "number",
|
|
183
|
+
"description": "Model year, e.g. 2024."
|
|
184
|
+
},
|
|
185
|
+
"make": {
|
|
186
|
+
"type": "string",
|
|
187
|
+
"description": "Vehicle make, e.g. \"Toyota\"."
|
|
188
|
+
},
|
|
189
|
+
"model": {
|
|
190
|
+
"type": "string",
|
|
191
|
+
"description": "Vehicle model exactly as fueleconomy.gov lists it, e.g. \"Camry\". Call fuel_economy_models to see valid strings."
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
"required": [
|
|
195
|
+
"year",
|
|
196
|
+
"make",
|
|
197
|
+
"model"
|
|
198
|
+
]
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"name": "fuel_economy_models",
|
|
203
|
+
"description": "List the exact model strings fueleconomy.gov recognizes for a given model year + make (e.g. year 2024, make \"Toyota\" -> \"Camry\", \"Camry Hybrid LE\", \"RAV4 AWD\", ...). Call this before fuel_economy_mpg when you are not sure of the exact model string, or when fuel_economy_mpg refuses a model as unrecognized.",
|
|
204
|
+
"inputSchema": {
|
|
205
|
+
"type": "object",
|
|
206
|
+
"properties": {
|
|
207
|
+
"year": {
|
|
208
|
+
"type": "number",
|
|
209
|
+
"description": "Model year, e.g. 2024."
|
|
210
|
+
},
|
|
211
|
+
"make": {
|
|
212
|
+
"type": "string",
|
|
213
|
+
"description": "Vehicle make, e.g. \"Toyota\"."
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
"required": [
|
|
217
|
+
"year",
|
|
218
|
+
"make"
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
]
|
package/src/fars-store.ts
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* D1 reads for the FARS fatal-crash tables added to the nhtsa pack (fleet #2972).
|
|
3
|
-
*
|
|
4
|
-
* Data: NHTSA Fatality Analysis Reporting System (FARS), annual National CSV
|
|
5
|
-
* bulk files (static.nhtsa.gov/nhtsa/downloads/FARS/<YYYY>/National/). US
|
|
6
|
-
* federal government work -- public domain (17 U.S.C. §105). FARS's live
|
|
7
|
-
* CrashAPI (crashviewer.nhtsa.dot.gov) returned an Akamai 403 to every probe
|
|
8
|
-
* on 2026-10-12, so this pack loads the annual bulk files ahead of time (CLAUDE.md "BUILD THE COPY")
|
|
9
|
-
* into D1 by scripts/nhtsa-fars/load-data.mjs -- never by a deploy. See
|
|
10
|
-
* mcps/nhtsa/README.md "FARS data".
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
export interface D1Like {
|
|
14
|
-
prepare(sql: string): D1StmtLike;
|
|
15
|
-
}
|
|
16
|
-
export interface D1StmtLike {
|
|
17
|
-
bind(...values: unknown[]): D1StmtLike;
|
|
18
|
-
all<T = Record<string, unknown>>(): Promise<{ results: T[] }>;
|
|
19
|
-
first<T = Record<string, unknown>>(): Promise<T | null>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export class FarsStoreUnavailableError extends Error {
|
|
23
|
-
constructor() {
|
|
24
|
-
super(
|
|
25
|
-
'nhtsa: the FARS fatal-crash store is not bound in this runtime (no _d1). This tool only answers through the Pipeworx gateway; a standalone install has no data to read.',
|
|
26
|
-
);
|
|
27
|
-
this.name = 'FarsStoreUnavailableError';
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
export function requireFarsDb(d1: unknown): D1Like {
|
|
31
|
-
if (!d1 || typeof (d1 as D1Like).prepare !== 'function') throw new FarsStoreUnavailableError();
|
|
32
|
-
return d1 as D1Like;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export const FARS_SOURCE = {
|
|
36
|
-
source: 'NHTSA Fatality Analysis Reporting System (FARS), annual National CSV files',
|
|
37
|
-
source_url: 'https://www.nhtsa.gov/research-data/fatality-analysis-reporting-system-fars',
|
|
38
|
-
license: 'US federal government work -- public domain (17 U.S.C. §105)',
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
// 2-letter postal code -> full state name used by this pack's loaded data
|
|
42
|
-
// (FARS's own STATENAME values, e.g. "Texas", "District of Columbia").
|
|
43
|
-
const STATE_ABBR: Record<string, string> = {
|
|
44
|
-
AL: 'Alabama', AK: 'Alaska', AZ: 'Arizona', AR: 'Arkansas', CA: 'California',
|
|
45
|
-
CO: 'Colorado', CT: 'Connecticut', DE: 'Delaware', DC: 'District of Columbia',
|
|
46
|
-
FL: 'Florida', GA: 'Georgia', HI: 'Hawaii', ID: 'Idaho', IL: 'Illinois',
|
|
47
|
-
IN: 'Indiana', IA: 'Iowa', KS: 'Kansas', KY: 'Kentucky', LA: 'Louisiana',
|
|
48
|
-
ME: 'Maine', MD: 'Maryland', MA: 'Massachusetts', MI: 'Michigan', MN: 'Minnesota',
|
|
49
|
-
MS: 'Mississippi', MO: 'Missouri', MT: 'Montana', NE: 'Nebraska', NV: 'Nevada',
|
|
50
|
-
NH: 'New Hampshire', NJ: 'New Jersey', NM: 'New Mexico', NY: 'New York',
|
|
51
|
-
NC: 'North Carolina', ND: 'North Dakota', OH: 'Ohio', OK: 'Oklahoma', OR: 'Oregon',
|
|
52
|
-
PA: 'Pennsylvania', RI: 'Rhode Island', SC: 'South Carolina', SD: 'South Dakota',
|
|
53
|
-
TN: 'Tennessee', TX: 'Texas', UT: 'Utah', VT: 'Vermont', VA: 'Virginia',
|
|
54
|
-
WA: 'Washington', WV: 'West Virginia', WI: 'Wisconsin', WY: 'Wyoming',
|
|
55
|
-
PR: 'Puerto Rico',
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
/** Resolve caller input (full name, 2-letter abbreviation, or FIPS code) to a
|
|
59
|
-
* candidate state name/FIPS pair. Does NOT guarantee the state is actually
|
|
60
|
-
* loaded -- call validState() for that. Returns null only on unparseable input. */
|
|
61
|
-
export function resolveState(input: unknown): { byName: string | null; byFips: number | null } | null {
|
|
62
|
-
if (input == null) return null;
|
|
63
|
-
const raw = String(input).trim();
|
|
64
|
-
if (!raw) return null;
|
|
65
|
-
if (/^\d{1,2}$/.test(raw)) return { byName: null, byFips: Number(raw) };
|
|
66
|
-
const upper = raw.toUpperCase();
|
|
67
|
-
if (STATE_ABBR[upper]) return { byName: STATE_ABBR[upper], byFips: null };
|
|
68
|
-
return { byName: raw, byFips: null };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export async function lookupState(db: D1Like, resolved: { byName: string | null; byFips: number | null }): Promise<{ state_fips: number; state_name: string } | null> {
|
|
72
|
-
if (resolved.byFips != null) {
|
|
73
|
-
const r = await db.prepare('SELECT state_fips, state_name FROM fars_state_month WHERE state_fips = ? LIMIT 1').bind(resolved.byFips).first<{ state_fips: number; state_name: string }>();
|
|
74
|
-
return r ?? null;
|
|
75
|
-
}
|
|
76
|
-
if (resolved.byName) {
|
|
77
|
-
const r = await db
|
|
78
|
-
.prepare('SELECT state_fips, state_name FROM fars_state_month WHERE state_name = ? COLLATE NOCASE LIMIT 1')
|
|
79
|
-
.bind(resolved.byName)
|
|
80
|
-
.first<{ state_fips: number; state_name: string }>();
|
|
81
|
-
return r ?? null;
|
|
82
|
-
}
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export interface FarsMeta {
|
|
87
|
-
loaded_at: string;
|
|
88
|
-
earliest_year: number;
|
|
89
|
-
latest_year: number;
|
|
90
|
-
years_loaded: number[];
|
|
91
|
-
}
|
|
92
|
-
let cachedMeta: FarsMeta | null = null;
|
|
93
|
-
export async function getFarsMeta(db: D1Like): Promise<FarsMeta> {
|
|
94
|
-
if (cachedMeta) return cachedMeta;
|
|
95
|
-
const r = await db.prepare('SELECT key, value FROM fars_meta').all<{ key: string; value: string }>();
|
|
96
|
-
const m = Object.fromEntries(r.results.map((row) => [row.key, row.value]));
|
|
97
|
-
cachedMeta = {
|
|
98
|
-
loaded_at: m.loaded_at ?? '',
|
|
99
|
-
earliest_year: Number(m.earliest_year ?? 0),
|
|
100
|
-
latest_year: Number(m.latest_year ?? 0),
|
|
101
|
-
years_loaded: (m.years_loaded ?? '').split(',').filter(Boolean).map(Number),
|
|
102
|
-
};
|
|
103
|
-
return cachedMeta;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export interface StateMonthRow {
|
|
107
|
-
year: number;
|
|
108
|
-
month: number;
|
|
109
|
-
state_fips: number;
|
|
110
|
-
state_name: string;
|
|
111
|
-
crashes: number;
|
|
112
|
-
fatalities: number;
|
|
113
|
-
persons_involved: number;
|
|
114
|
-
vehicles_involved: number;
|
|
115
|
-
pedestrians_involved: number;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Monthly rows for one state and (optionally) one year. Omitting year
|
|
119
|
-
* returns every loaded year/month for that state, most recent first. */
|
|
120
|
-
export async function stateMonths(db: D1Like, stateFips: number, year: number | null): Promise<StateMonthRow[]> {
|
|
121
|
-
const where = ['state_fips = ?'];
|
|
122
|
-
const params: unknown[] = [stateFips];
|
|
123
|
-
if (year != null) {
|
|
124
|
-
where.push('year = ?');
|
|
125
|
-
params.push(year);
|
|
126
|
-
}
|
|
127
|
-
const sql = `SELECT year, month, state_fips, state_name, crashes, fatalities, persons_involved, vehicles_involved, pedestrians_involved
|
|
128
|
-
FROM fars_state_month WHERE ${where.join(' AND ')}
|
|
129
|
-
ORDER BY year DESC, month DESC`;
|
|
130
|
-
const r = await db.prepare(sql).bind(...params).all<StateMonthRow>();
|
|
131
|
-
return r.results;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** All states for one year, summed across months -- for cross-state comparison. */
|
|
135
|
-
export async function allStatesForYear(db: D1Like, year: number, limit: number): Promise<Array<Omit<StateMonthRow, 'month'>>> {
|
|
136
|
-
const sql = `SELECT year, state_fips, state_name,
|
|
137
|
-
SUM(crashes) AS crashes, SUM(fatalities) AS fatalities,
|
|
138
|
-
SUM(persons_involved) AS persons_involved, SUM(vehicles_involved) AS vehicles_involved,
|
|
139
|
-
SUM(pedestrians_involved) AS pedestrians_involved
|
|
140
|
-
FROM fars_state_month WHERE year = ?
|
|
141
|
-
GROUP BY state_fips, state_name
|
|
142
|
-
ORDER BY fatalities DESC
|
|
143
|
-
LIMIT ?`;
|
|
144
|
-
const r = await db.prepare(sql).bind(year, limit).all<Omit<StateMonthRow, 'month'>>();
|
|
145
|
-
return r.results;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export interface MakeYearRow {
|
|
149
|
-
year: number;
|
|
150
|
-
make_name: string;
|
|
151
|
-
vehicles_involved: number;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export async function vehicleMakesForYear(db: D1Like, year: number, limit: number): Promise<MakeYearRow[]> {
|
|
155
|
-
const sql = `SELECT year, make_name, vehicles_involved FROM fars_vehicle_make_year WHERE year = ? ORDER BY vehicles_involved DESC LIMIT ?`;
|
|
156
|
-
const r = await db.prepare(sql).bind(year, limit).all<MakeYearRow>();
|
|
157
|
-
return r.results;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export async function vehicleMakeYears(db: D1Like, makeName: string): Promise<MakeYearRow[]> {
|
|
161
|
-
const sql = `SELECT year, make_name, vehicles_involved FROM fars_vehicle_make_year WHERE make_name = ? COLLATE NOCASE ORDER BY year DESC`;
|
|
162
|
-
const r = await db.prepare(sql).bind(makeName).all<MakeYearRow>();
|
|
163
|
-
return r.results;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Distinct make names actually loaded (for a "did you mean" / discovery hint). */
|
|
167
|
-
export async function listMakes(db: D1Like, limit = 80): Promise<string[]> {
|
|
168
|
-
const r = await db.prepare('SELECT DISTINCT make_name FROM fars_vehicle_make_year ORDER BY make_name LIMIT ?').bind(limit).all<{ make_name: string }>();
|
|
169
|
-
return r.results.map((row) => row.make_name);
|
|
170
|
-
}
|