CAP-2031 · stored file paths
Every file column now holds the file’s complete path instead of its bare name, and new uploads are filed under the account that owns them. This page lists what changed, column by column, and the path shape those columns now hold. Checked against clone1, with capClone as the comparison.
Everything stays in the bucket we already have. The path is what separates one account’s files from another’s, so the path shape is the access model — not a cosmetic detail. The first segment is always the tree:
| Tree | Holds |
|---|---|
| img | Pictures — asset photos, logos, tag sheets, QR codes |
| doc | Everything else — quotes, contracts, invoices, exports, PDFs |
The second segment says who the file belongs to, and that decides the rest of the shape:
account <tree>/<capexId>/<domain>/<file> account-user <tree>/<capexId>/<branch>/<userId>/<domain>/<file> user <tree>/<capexId>/user/<domain>/<file> library img/<branch>/<file> doc/<branch>/<domain>/<file> global <tree>/<branch>/<file> belongs to nobody
Worked through, with real values out of clone1:
img/CAP-0YNN74P/assets/2BQISjpe_1782915476233CTS5HSW.jpeg account img/CAP-BBA2A600/user/profile/avatar_1788.png user doc/CAP-NJQJGDJ/copilot-quote/OrderCAPSKOCI7V4DZZE9K.pdf account doc/CAP-NJQJGDJ/copilot/warranty/file_example_XLS_PRPTP48.xls account doc/CAP-048YNSJ/document-manager/purchase-orders/OrderCAPSK.pdf account doc/CAP-BBA2A600/export/1/assets/camp_export_1788.xlsx account-user — user 1 ran it img/model-library/pump_x200.jpg library doc/export/system/nightly_1788.xlsx global
A browser upload cannot be written to its final home, because the record referencing it does not exist yet. So it lands in temp, which takes the owner slot and leaves everything below it intact — a staged path already says what it will become:
img/temp/assets → img/<capexId>/assets doc/temp/export/1/assets → doc/<capexId>/export/1/assets
Filing copies; it does not move. A failed filing therefore still reads — the record keeps the staged path, and that path still resolves. Where a module has thumbnails, the small copy sits at <same folder>/thumb/<same file name>.
A complete path does not fit where a file name did. Widths were raised before any value was written, so no statement in the backfill could overflow.
| Table.column | Before | After | Why |
|---|---|---|---|
| users.profile | varchar(150) | varchar(512) | One path per row |
| equipment_categories.default_image | varchar(255) | varchar(512) | One path per row |
| equipment_models.files | varchar(255) | text | Several paths joined in one value — no fixed width bounds the total |
| warranties.file | varchar(255) | text | Several paths joined in one value |
| feedbacks.attachment | varchar(200) | varchar[] | One path per element |
| copilot_launch_proposals.warranty_document | varchar(255) | varchar[] | One path per element |
| copilot_launch_proposals.service_agreement | varchar(255) | varchar[] | One path per element |
| copilot_launch_proposals.equipment_condition_attachment | varchar(255) | varchar[] | One path per element |
| copilot_requests.attachments | text | varchar[] | One path per element |
The five varchar[] columns used to hold one comma-joined string; each file is now its own element, trimmed, with the empties dropped. A value that was nothing but separators became NULL rather than an array of blanks — an empty array reads as present in JS and would defeat every .length guard. Three further columns took paths without needing a widening, because they were already text: equipment_models.images, copilot_launch_proposals.quote and copilot_requests.quote.
Existing rows were filled in with where the file is now — the legacy flat folder — never where it is going. The copier moves the object and the save then records the new path. A value that already contained a slash was left alone: it is either a complete path already or an address somewhere else, and prefixing either would break it.
| Table.column | Shape | Folder it was resolved against |
|---|---|---|
| users.profile | one path | profile/ |
| equipment_categories.default_image | one path | categories/ |
| equipment_models.images | comma list | inventory/images/ |
| equipment_models.files | comma list | inventory-reference/files/ |
| inventory_sticker_details.sticker_name | one path | inventory/images/ |
| camp_inventory_items.images | varchar[] | rebuilt from sticker_name |
| warranties.file | comma list | inventory/warranty/ |
| bill_of_sale_requests.file_name | one path | bill-of-sales/ |
| feedbacks.attachment | varchar[] | feedback/ |
| shipping_estimates.bol_document | one path | document-manager/shipping-documents/ |
| docusign_requests.attachments | one path | cer-documents/ or pa-documents/, by request_type |
| copilot_launch_proposals.quote | comma list | ezrfp-quote/ |
| copilot_launch_proposals.warranty_document | varchar[] | ezrfp-warranty/ |
| copilot_launch_proposals.service_agreement | varchar[] | service-agreement/ |
| copilot_launch_proposals.equipment_condition_attachment | varchar[] | inventory/images/ |
| copilot_requests.quote | comma list | ezrfp-quote/ |
| copilot_requests.attachments | varchar[] | ezrfp/attachments/ |
Comma lists and arrays were rebuilt entry by entry in their original order — the first picture is the one shown as the preview, so a reshuffle would have changed what customers see on thousands of rows. docusign_requests.attachments also carries a fix: a CER row named pa-documents/… while the object sat in cer-documents/. Those rows are re-pointed at the folder the object is actually in.
Each row was read, rewritten in memory and written back only where a key actually changed. Order inside an array is preserved.
| Table.column | Keys that now hold a complete path |
|---|---|
| users.agreement_details | name |
| biomeds.extracted_data | biomedFile |
| copilot_launch_proposals.additional_details | parentQuote · specifications[].fileName |
| copilot_parent_requests.action_logs | equipments[].changes[].new · equipments[].changes[].old only where the sibling key is quote or attachments — without that test a status or a zip code would be prefixed as though it were a file |
| transactions.additional_documents | poDocument · poAttachments[].attachment · cerDocument · billOfSale · shippingQuote · bolDocument |
| transaction_equipment_details.equipment_details | images · referenceImages · userManualPdf · serviceContract · warrantyFile · equipmentCategoryData.img · inventoryStickerData.inventoryStickerDetail[] · …[].stickerName · …[].sticker · childInventories[].equipmentCategoryData.img · childInventories[]…stickerName · tradeInventoryDetail.images · tradeInventoryDetail.newImages[] · tradeInventoryDetail.equipmentCategoryData.img |
| po_requests.additional_details | comparisonView.fileName |
| shipping_estimates.request_details | items[].images[] |
| shipping_estimates.shipping_responses | [].fileName |
| shipping_estimates.quote_email_details | attachments[] |
Columns that already stored complete paths were left exactly as they were, as was email_logs.data — a frozen copy of what the mailer was sent, which rewriting would falsify.
Every value in every column and key above, counted by whether it contains a slash.
| Column or key | Values | Complete path | Bare name |
|---|---|---|---|
| inventory_sticker_details.sticker_name | 757,840 | 757,840 | 0 |
| camp_inventory_items.images | 678,643 | 678,605 | 38 |
| biomeds.extracted_data → biomedFile | 20,052 | 20,052 | 0 |
| copilot_launch_proposals.quote | 9,043 | 9,043 | 0 |
| equipment_models.images | 8,280 | 8,280 | 0 |
| equipment_models.files | 5,088 | 5,088 | 0 |
| copilot_launch_proposals.additional_details → parentQuote | 3,435 | 3,435 | 0 |
| users.agreement_details → name | 1,086 | 1,086 | 0 |
| transactions.additional_documents → sellerInvoice | 962 | 962 | 0 |
| transactions.additional_documents → buyerInvoice | 961 | 961 | 0 |
| transactions.additional_documents → poDocument | 754 | 754 | 0 |
| copilot_requests.attachments | 684 | 684 | 0 |
| copilot_launch_proposals.warranty_document | 680 | 680 | 0 |
| transactions.additional_documents → poAttachments[].attachment | 482 | 482 | 0 |
| transaction_equipment_details.equipment_details → images | 389 | 389 | 0 |
| copilot_requests.quote | 114 | 114 | 0 |
| equipment_categories.default_image | 105 | 105 | 0 |
| shipping_estimates.shipping_responses → [].fileName | 92 | 92 | 0 |
| copilot_launch_proposals.service_agreement | 74 | 74 | 0 |
| docusign_requests.attachments | 53 | 53 | 0 |
| po_requests.additional_details → comparisonView.fileName | 36 | 36 | 0 |
| transactions.additional_documents → shippingQuote | 35 | 35 | 0 |
| users.profile | 29 | 29 | 0 |
| transactions.additional_documents → cerDocument | 22 | 22 | 0 |
| warranties.file | 18 | 18 | 0 |
| transaction_equipment_details.equipment_details → userManualPdf | 15 | 15 | 0 |
| transactions.additional_documents → billOfSale | 12 | 12 | 0 |
| shipping_estimates.bol_document | 10 | 10 | 0 |
| bill_of_sale_requests.file_name | 8 | 8 | 0 |
| transactions.additional_documents → bolDocument | 7 | 7 | 0 |
| transaction_equipment_details.equipment_details → warrantyFile | 3 | 3 | 0 |
| feedbacks.attachment | 2 | 2 | 0 |
| copilot_launch_proposals.equipment_condition_attachment | 1 | 1 | 0 |
| Total | 1,489,015 | 1,488,977 | 38 |
capClone is not a clean “before” picture. It already carries the conversion — every value in it is a complete path too — but all of them are still in the legacy flat folders. The new img/ and doc/ layout appears only in clone1.
| Migration | capClone | clone1 |
|---|---|---|
| 20260902101441 … 101448 widen + backfill | applied | applied |
| 20260904102729 invoice paths | applied | applied |
| 20260902101440 PO paths on transactions | not applied | applied |
| 20260905110855 · 110946 file-migration tables | not applied | applied |
| 20260908064726 presigned-url refs to jsonb | not applied | applied |
The before-types in the type table were read from clone2 and capCloneDev, where the widening has not run — not inferred from the migration files.