Add ORDS v0.3 repair data export endpoint - #900
Conversation
The Open Repair Data Standard requires alpha-3 country codes, but groups.country_code is alpha-2. Checked in as a static map rather than adding a dependency, so the exporter stays self-contained for instances that pick it up. The only existing country helper returns translated display names, not codes.
Redacts emails, phone numbers, long digit runs and URL query strings from volunteer-written free text before it is published. Whitespace is normalised before the redaction passes rather than after, because an address broken by a newline or a non-breaking space is still reconstructible by a reader. Inline HTML tags are deleted rather than replaced with a space for the same reason, and the tag pattern tracks quoted attributes so a > inside one cannot end the tag early. Personal names are not pattern-detectable and are not removed.
Maps a device row onto the standard's fourteen columns in spec order. year_of_manufacture is not stored so it is derived from the event year minus the item's age, and product_category_id is a name lookup because our category ids do not match the standard's. The vocabulary follows the Open Repair Alliance's published data rather than its tableschema.json, which is stale in places. ORDS_ID_PREFIX and ORDS_DATA_PROVIDER have no defaults: the id is a stable key the consumer upserts on across releases, so a borrowed namespace would overwrite another provider's rows.
GET /api/v2/repairs, behind auth:api alongside the other external-consumer endpoints. Serves JSON or CSV with updated_since, event_start, event_end, powered, page and per_page filters. Only approved events on approved groups are visible and soft-deleted events are excluded. The per-page ceiling is 1000 rather than the interactive endpoints' 100 because this is a bulk export. Returns 503 until the id namespace and data provider are configured.
Covers the config guard, the fourteen columns and their mapping, the barrier and category vocabularies, visibility rules, filters, pagination, CSV output and problem-text redaction. Unauthenticated requests are asserted as refused rather than as 401, because this codebase renders any JSON exception without a status code as a 500 and that behaviour is not this endpoint's to change.
|
|
Thanks for this. I've reviewed it with AI assistance, so treat the findings below as a starting point rather than gospel — push back on anything that looks wrong. Fixes pushed to Authorization. I think the right model here is Administrator-only. As written the endpoint returns every device from every approved group to any authenticated caller, and as you noted, Redaction. Three gaps, all with tests:
Config.
The other two deferred items look right to me as they stand. One thing I want to think about rather than change: |



Description
Adds
GET /api/v2/repairs, which exports repair records in Open Repair Data Standard v0.3. The Open Repair Alliance asked for a codified export rather than a periodic database extract, and there's no endpoint for it in the codebase today.This started in our fork at iFixit, where it's merged and running: iFixit/restarters#58. It isn't a straight cherry-pick. That version sits on an API-client auth layer, a feature flag and Helm config that are specific to our deployment, none of which apply here, so this is rebuilt on
developagainst your conventions with a handful of fixes on top. Happy to adjust anything that doesn't fit.Changes
GET /api/v2/repairs, in the existingv2group behindauth:api, the same way the Zapier and Repair Together endpoints are already served. JSON or CSV, withupdated_since,event_start,event_end,powered,pageandper_page. Per-page tops out at 1000 rather than 100 because it's a bulk export. Only approved events on approved groups are visible, and soft-deleted events are excluded. Files:API/RepairController.php,routes/api.php.year_of_manufactureisn't stored, so it's derived from the event year minusdevices.age.countryconvertsgroups.country_codefrom alpha-2 to alpha-3, andproduct_category_idis a name lookup becauseidcategoriesdoesn't line up with the standard's ids. Files:Services/Ords/OrdsRecordMapper.php,Helpers/Iso3166.php.config/ords.phpfollows ORA's published data rather than theirtableschema.json, which is stale in places. The standard collapses the screen-size and laptop-size category splits, carriesUnknownas a realrepair_statusvalue, and drops the "the" from "No way to open the product" as seeded in2018_11_12_135805.partner_product_categoryuses the<category> ~ <item_type>shape from your own published rows.devices.problem,devices.item_typeanddevices.brandare all volunteer-written, anditem_typeis validated as a barestringinDeviceControllerwith no vocabulary behind it. All three go throughProblemTextScrubber, which strips HTML and redacts emails, phone numbers, digit runs of 8 or more, and URL query strings.ORDS_ID_PREFIXandORDS_DATA_PROVIDERhave no defaults, and the endpoint returns 503 while either is blank. The id is the stable key ORA upserts on across releases, so publishing under an unassigned namespace would overwrite another provider's rows. Nothing is exported until you set them.EnsureAPITokenmints a token for every logged-in user, so any account can reach this, and that log is the only trace afterwards if someone asks who pulled what.Output was validated against ORA's published
tableschema.json. Column names and order match exactly and every declared constraint passes, except theiridregex, which uses a hyphen while all of their own published rows use an underscore.On the free-text columns
The scrubber is best-effort. It takes out contact details and identifiers, but personal names aren't pattern-detectable and stay in. Two bypasses turned up during review, both fixed with regression tests: whitespace or inline markup inside an address (
owner@\nexample.com,owner@ex<b>ample</b>.com) defeated the email pattern, and a>inside a quoted HTML attribute ended the tag early and left the address whole.As written,
problemships on every export with no toggle. That's the call I'd flag hardest for you, since it's your community's data under your terms, not ours. If you'd rather it were opt-in, or dropped entirely for a structured-fields-only export, that's a small change and I'm happy to make it.Deferred
Three gaps, all of which I'm happy to split out or fold in, whichever you prefer.
updated_sincekeys offdevices.updated_at, so approving an old event leaves its devices invisible to an incremental consumer. Widening it toevents.updated_atandgroups.updated_atwould fix it.Pagination is offset-based, so a row leaving the set mid-crawl shifts later pages, and a deep crawl is quadratic. Keyset pagination on
devices.iddeviceswould fix both.The route inherits the app-wide 300/min bucket from
RouteServiceProvider. That's generous for a request that runs three full passes over thedevices/events/groups/categoriesjoin, but I didn't want to guess a limit that might throttle ORA's own crawl.QA Notes
Run
php artisan l5-swagger:generateafter pulling.TestCase::get()validates every/api/v2response against the generated schema, so the suite depends on it.To exercise it, set
ORDS_ID_PREFIXandORDS_DATA_PROVIDERand pull a full export against the spec. The prefix carries its own separator:ORDS_ID_PREFIX=restarters_producesrestarters_12345. Worth confirming with ORA which prefix is assigned to you before publishing under it, since it's the upsert key.Tests: 69 covering the config guard, column mapping, both vocabularies, visibility, filters, pagination, CSV output and redaction.