Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions packages/core/src/seed.js
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,132 @@ export const DEFAULT_FEEDS = [
name: 'Makes, models and years',
query: { kinds: ['model'] },
},
/*
* The same automotive data indexed by the PART that failed rather than
* by the vehicle it was bolted to.
*
* There is no free car-parts catalogue with year/make/model fitment --
* ACES/VCdb and TecDoc are both subscription, and eBay's Browse API is
* the only free source of real fitment, which this deployment cannot
* use. What IS free and already flowing is NHTSA's component taxonomy,
* which goes down to the individual fastener ('SUSPENSION:FRONT:
* SPRINGS:LEAF SPRING ASSEMBLY:U-BOLT, LEAF SPRING TO AXLE').
*
* So these are feeds, not a new source: the adapter already slugifies
* Component into tags, and nothing needs to be fetched again. Every tag
* below was checked against the live API before being written down.
*
* Two of them look misspelt and are not. `slugify` drops "/" instead of
* turning it into a hyphen, so a component containing one collapses into a
* single run-on word: LATCHES/LOCKS/LINKAGES is `latcheslockslinkages` and
* VISIBILITY/WIPER is `visibilitywiper`. The hyphenated spellings match
* nothing. Changing slugify would be the real fix, but it would silently
* re-tag every adapter in the deployment and orphan the tags already
* written, so the feeds use what is actually in the data.
*/
{
collection: 'automotive',
slug: 'parts-brakes',
name: 'Brakes',
description:
'Braking recalls and complaints across every manufacturer: hydraulic, air and parking brakes.',
/* Recalls and complaints together: a recall is the manufacturer
admitting it, a complaint is an owner noticing first. */
query: {
kinds: ['recall', 'complaint'],
tags: ['service-brakes-hydraulic', 'service-brakes-air', 'parking-brake'],
},
},
{
collection: 'automotive',
slug: 'parts-airbags',
name: 'Airbags and restraints',
description: 'The restraint system: airbags, inflators, seat belts and child seats.',
/* Recalls and complaints together: a recall is the manufacturer
admitting it, a complaint is an owner noticing first. */
query: { kinds: ['recall', 'complaint'], tags: ['air-bags', 'seat-belts', 'child-seat'] },
},
{
collection: 'automotive',
slug: 'parts-tires-and-wheels',
name: 'Tires and wheels',
description:
'Tire and wheel recalls, including the tire-only campaigns that never name a vehicle.',
/* Recalls and complaints together: a recall is the manufacturer
admitting it, a complaint is an owner noticing first. */
query: { kinds: ['recall', 'complaint'], tags: ['tires', 'wheels'] },
},
{
collection: 'automotive',
slug: 'parts-electrical',
name: 'Electrical system',
description:
'Wiring, charging, control modules and lighting — the largest single component category.',
/* Recalls and complaints together: a recall is the manufacturer
admitting it, a complaint is an owner noticing first. */
query: { kinds: ['recall', 'complaint'], tags: ['electrical-system', 'exterior-lighting'] },
},
{
collection: 'automotive',
slug: 'parts-fuel-system',
name: 'Fuel system',
description: 'Pumps, lines, tanks and injection, where a defect usually means a fire risk.',
/* Recalls and complaints together: a recall is the manufacturer
admitting it, a complaint is an owner noticing first. */
query: {
kinds: ['recall', 'complaint'],
tags: ['fuel-system-gasoline', 'fuel-system-diesel', 'fuel-system-other'],
},
},
{
collection: 'automotive',
slug: 'parts-engine-and-powertrain',
name: 'Engine and powertrain',
description: 'Engine, cooling, transmission, driveline and axles.',
/* Recalls and complaints together: a recall is the manufacturer
admitting it, a complaint is an owner noticing first. */
query: {
kinds: ['recall', 'complaint'],
tags: ['engine', 'power-train', 'engine-and-engine-cooling'],
},
},
{
collection: 'automotive',
slug: 'parts-steering-and-suspension',
name: 'Steering and suspension',
description: 'Steering gear, columns, springs, control arms and the fasteners holding them on.',
/* Recalls and complaints together: a recall is the manufacturer
admitting it, a complaint is an owner noticing first. */
query: { kinds: ['recall', 'complaint'], tags: ['steering', 'suspension'] },
},
{
collection: 'automotive',
slug: 'parts-structure-and-body',
name: 'Structure and body',
description: 'Frame, body, latches and locks.',
/* Recalls and complaints together: a recall is the manufacturer
admitting it, a complaint is an owner noticing first. */
query: { kinds: ['recall', 'complaint'], tags: ['structure', 'latcheslockslinkages'] },
},
{
collection: 'automotive',
slug: 'parts-visibility',
name: 'Glass, mirrors and wipers',
description: 'Windshields, mirrors, defrosters and wiper systems.',
/* Recalls and complaints together: a recall is the manufacturer
admitting it, a complaint is an owner noticing first. */
query: { kinds: ['recall', 'complaint'], tags: ['visibilitywiper', 'visibility'] },
},
{
collection: 'automotive',
slug: 'parts-equipment',
name: 'Equipment and aftermarket parts',
description:
'Recalls filed against a part rather than a vehicle: aftermarket equipment, cameras and accessories.',
/* Recalls and complaints together: a recall is the manufacturer
admitting it, a complaint is an owner noticing first. */
query: { kinds: ['recall', 'complaint'], tags: ['equipment', 'back-over-prevention'] },
},
/* Markets. The first two are the worldwide half and the rest are US, which
is the shape of what is actually given away: the register of the world's
venues is public, and their prices are not. */
Expand Down
92 changes: 92 additions & 0 deletions test/automotive-part-feeds.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { describe, expect, test } from 'bun:test';
import { slugify } from '../packages/core/src/adapter.js';

process.env.DATABASE_URL ??= 'postgres://test:test@localhost:5432/test';
process.env.SITE_URL ??= 'https://nichedb.test';
const { DEFAULT_FEEDS } = await import('../packages/core/src/seed.js');

const partFeeds = DEFAULT_FEEDS.filter(
(f) => f.collection === 'automotive' && f.slug.startsWith('parts-'),
);

describe('the automotive collection, indexed by the part that failed', () => {
test('there is a feed per major component group', () => {
expect(partFeeds.length).toBeGreaterThanOrEqual(10);
const slugs = partFeeds.map((f) => f.slug);
for (const expected of [
'parts-brakes',
'parts-airbags',
'parts-tires-and-wheels',
'parts-electrical',
'parts-fuel-system',
'parts-engine-and-powertrain',
'parts-steering-and-suspension',
]) {
expect(slugs).toContain(expected);
}
});

test('every part feed carries both recalls and complaints', () => {
// A recall is the manufacturer admitting it; a complaint is an owner
// noticing first. A feed with only one of them is half the story.
for (const feed of partFeeds) {
expect(feed.query.kinds).toEqual(['recall', 'complaint']);
}
});

test('every part feed filters on at least one component tag', () => {
for (const feed of partFeeds) {
expect(Array.isArray(feed.query.tags)).toBe(true);
expect(feed.query.tags.length).toBeGreaterThan(0);
}
});

test('slugs and names are unique', () => {
const slugs = partFeeds.map((f) => f.slug);
const names = partFeeds.map((f) => f.name);
expect(new Set(slugs).size).toBe(slugs.length);
expect(new Set(names).size).toBe(names.length);
});

test('every part feed explains itself', () => {
for (const feed of partFeeds) {
expect(typeof feed.description).toBe('string');
expect(feed.description.length).toBeGreaterThan(20);
}
});
});

describe('the tags that look misspelt and are not', () => {
/*
* `slugify` drops "/" rather than turning it into a hyphen, so an NHTSA
* component containing one collapses into a single run-on word. Both of
* these were checked against the live API: the run-on form returns items and
* the hyphenated form returns nothing. These tests exist so nobody
* "corrects" the spelling and silently empties two feeds.
*/
test('slugify drops a slash instead of hyphenating it', () => {
expect(slugify('LATCHES/LOCKS/LINKAGES')).toBe('latcheslockslinkages');
expect(slugify('VISIBILITY/WIPER')).toBe('visibilitywiper');
});

test('it does hyphenate spaces and commas, which is why most tags look normal', () => {
expect(slugify('SERVICE BRAKES, AIR')).toBe('service-brakes-air');
expect(slugify('FUEL SYSTEM, GASOLINE')).toBe('fuel-system-gasoline');
expect(slugify('BACK OVER PREVENTION')).toBe('back-over-prevention');
});

test('the feeds use the run-on spellings that are actually in the data', () => {
const tags = partFeeds.flatMap((f) => f.query.tags);
expect(tags).toContain('latcheslockslinkages');
expect(tags).toContain('visibilitywiper');
expect(tags).not.toContain('latches-locks-linkages');
expect(tags).not.toContain('visibility-wiper');
});

test('every tag is what slugify would produce for some component', () => {
// Guards against a hand-written tag that no adapter could ever emit.
for (const tag of partFeeds.flatMap((f) => f.query.tags)) {
expect(slugify(tag)).toBe(tag);
}
});
});
Loading