Same-bucket running order · validation against clone1
Every file-bearing column in the clone1 database, classified as full path or bare filename, with the JSON keys that hold them and the code that has to change. Checked against the published running order.
The structural claims hold. The counting claims are low in three places, and one of them — the mobile service versions — is low enough to change the shape of Step 2.
Record type does fail the build.| Claim in the running order | Checked against | Result |
|---|---|---|
Bucket is capexpert-prod-bucket, single bucket | spreadsheet_runs.input_gcs_uri | holds Live rows carry gs://capexpert-prod-bucket/… |
| 70 flat prefixes across 43 distinct top-level names | BucketFolder enums, all repos | low 70/43 is backendApi only. Union is 73/46. |
| Migration table has 17 columns | 20260901134627-create-presigned-url-links.js | holds Exactly 17, names and meanings match. |
| Two unique guards, not on account + filename | same migration | holds …_destination_uq on source_path; …_staged_uq on temp file_path. |
| 45 upload kinds, each naming exactly one destination | enum/index.ts · upload-targets.ts | holds 45 enum members, 45 target entries. |
| ~170 website locations | capExpertApp | holds 171 GCS_BUCKET_URL sites in 67 files. |
| ~40 API hand-built locations | backendApi | low 57 concatenation sites in 31 files; 137 BucketFolder references in 56 files. |
| ~70 mobile app locations | mobileApp | high 47 sites (38 URL + 29 enum, overlapping) across 17 files. |
| 43 hand-built public URLs across 21 files | backendApi | close 47 across 22 files, excluding 3 config files that define the base. |
| Apply to all three mobile service versions | mobileApi | wrong Seven version trees touch BucketFolder. Inventory alone has six. |
| Records must store complete file paths | clone1 | 24 of 41 file columns hold bare names only. See §03. |
| Open question: longest filename, longest path | clone1 | answered 217 chars (documents.file_name) and 191 chars (documents.file_path). |
1 · Three legacy prefixes live only in mobileApi. shipping-estimate-documents, site-images and support/diagnostics are in mobileApi/src/shared/utils/enum/index.ts and absent from backendApi's BucketFolder. The first two are in production data — 101 objects under shipping-estimate-documents/, and site-images/logo.png is hardcoded in four backendApi files including every notification and email footer. Neither has an UploadModule destination, so nothing in the 45-item list files them.
2 · mobileApi has seven versioned trees, not three. inventory.service.ts exists at v9, v10, v11, v12, v13 and v14, each with ~19 BucketFolder references; audit-view spans v10–v13; market-place v10–v11; misc v6. The shared read rule has to land in 26 files, and old app builds pin the old versions, so none can be skipped.
3 · {SCOPE} can collide on the destination guard. presigned_url_links_destination_uq is unique on source_path for every non-deleted row, including rows still holding img/{SCOPE}/assets/<name>. Before the shape flip the staged guard does not apply, so two accounts uploading the same filename unsaved both write img/{SCOPE}/assets/<name> and the second insert fails. Most names carry a millisecond suffix, but category images (Autoclaves.jpg) and QR names (000123.png) do not.
4 · documents.file_name will not fit a path. It is varchar(256) and already holds a 217-character name. The longest account-shaped prefix is 54 characters (doc/CAP-12345678/document-manager/pre-approved-pricing/), so converting that column overflows by 16. Its sibling documents.file_path already holds paths and has 28 characters of headroom — tight, but it fits. See §07.
5 · clone1 already contains new-shape rows. documents.file_path has 79, document_manager.file_path 9, warranties.file 4, all under img/ or doc/, and the presigned_url_links table is already created. Any backfill written against this clone must treat "already contains a slash" and "already starts with img/ or doc/" as two separate skips, or it will re-prefix them.
These are the Step-1 targets: the record holds only the name, and the reader has to know the folder. Counts are full-table over non-null values that look like a file. Position is ordinal_position.
| Table | Column | # | Type | File values | Multi-value | Real value from clone1 |
|---|---|---|---|---|---|---|
| High volume — these three are the migration | ||||||
| inventory_sticker_details | sticker_name | 3 | varchar(256) | 709,806 | single | 00022jpe_1778195211181.jpeg |
| camp_inventory_items | images | 51 | varchar[] | 627,785 | array · 576,654 rows have >1 | {0009Xjpe_1761753757126.jpeg,YDBV0jpe_1761753757125.jpeg,ZO7C1jpe_1761753757126.jpeg,XACQPjpe_1761753757126.jpeg,8VTKYjpe_1761753757126.jpeg} |
| documents | file_name | 2 | varchar(256) | 20,369 | single | 000004.png |
| Comma-separated lists in a single text column | ||||||
| copilot_launch_proposals | quote | 26 | text | 7,951 | comma · 982 rows | 20109874293_New_York_Sporpd_1717614868585.pdf,20109874292_New_York_Sporpd_1717614870782.pdf,20109874291_New_York_Sporpd_1717614872335.pdf |
| equipment_models | files | 8 | varchar(255) | 5,031 | comma · 37 rows | 1670236294016_07003MD88Z9HII22.pdf,1670236294477_P4IG17B3NPYGM6G5.pdf |
| equipment_models | images | 7 | text | 4,137 | comma · 3,308 rows | 00OPPjpe_1759228802460.jpeg,S6H4Njpe_1759228802460.jpeg,MVBVLjpe_1759228802460.jpeg |
| copilot_launch_proposals | warranty_document | 16 | varchar(255) | 738 | comma · 9 rows | CADTest1pd_1788264925410.pdf,BOL_Documentpd_1788264928689.pdf |
| copilot_requests | attachments | 24 | text | 470 | comma · 92 rows | 0001433807_Power_ease_pd_1767797175378.pdf,Quote2_SOMA_AD01062608__Mdo_1767797182282.doc |
| copilot_requests | quote | 27 | text | 109 | comma · 1 row | CPASC_W9_2pd_1773087466801.pdf,Ortho_NY_ASC_RFP_Instructipd_1773087606222.pdf |
| copilot_launch_proposals | service_agreement | 37 | varchar(255) | 73 | comma · 1 row | OrderCAPVEDOB96pd_1788264959381.pdf,CADTest1pd_1788264963012.pdf,BOL_Documentpd_1788264965073.pdf |
| budget_planning | attachments | 11 | text | 72 | comma · 40 rows | 1670235719451_NB536REFJ7.jpg,IMG_7178scaledjp_1679498094263.jpg,IMG_7176scaledjp_1679498105238.jpg |
| Single-value bare names | ||||||
| document_manager | name | 3 | varchar(256) | 8,639 | single | 0000126993_400839528_Casspd_1770830529623.pdf |
| transactions | invoice_name | 18 | varchar(255) | 1,026 | single | 1680301533907.pdf |
| equipment_categories | default_image | 5 | varchar(255) | 107 | single | default_Anesthesia_Equipment.jpg |
| docusign_requests | attachments | 12 | text | 47 | single | cer_1750418847569MAP6U.pdf |
| bill_of_sale_requests | file_name | 5 | varchar(255) | 8 | single | Bill_of_sale_1719219486320Z5RTY.pdf |
| feedbacks | attachment | 6 | varchar(200) | 2 | single | Cooper_Surgical_Cleartone_pd_1717612100050.pdf |
| copilot_launch_proposals | equipment_condition_attachment | 13 | varchar(255) | 1 | single | 2jp_1713209028801.jpg |
On equipment_models.images. 4,862 rows carry a file value: 4,137 are the comma-separated CapExpert names above, and 725 are a JSON blob of scraped vendor URLs stuffed into the same text column. A backfill that assumes one shape will corrupt the other. Split on the leading { before touching it.
These are the ones the plan's "don't modify columns already containing slashes" warning is really about. Each already carries a mixture, so the backfill has to be per-row, not per-column.
| Table | Column | # | Type | Bare | Path | Bare example | Path example |
|---|---|---|---|---|---|---|---|
| email_logs | attachments | 10 | text | 3,808 | 3,187 | 419972pd_1762787793393.pdf | cer-documents/1jpe_1700159245303jpe_1726809815901.jpeg |
| document_manager | file_path | 6 | varchar(256) | 79 | 8,623 | 90210_Surgery_Medical_Centpd_1731344159705.pdf | bill-of-sales/Bill_of_sale_1719219486320Z5RTY.pdf |
| warranties | file | 35 | varchar(255) | 14 | 7 | CDL_USHV_Lafayette_Horizonpd_1774621762664.pdf | doc/temp/assets/warranty/4889pd_1788258781662.pdf |
| shipping_estimates | bol_document | 21 | text | 7 | 3 | Bill of Lading 1723044241771_1723044241771.pdf | document-manager/shipping-documents/Bill of Lading 1765229943240_1765229943241.pdf |
Note the whitespace. shipping_estimates.bol_document stores names containing spaces. Any backfill regex or path splitter that assumes no spaces in a key will drop these four rows.
Step 1 must leave these alone. They are already the shape the plan asks for; the work here is Step 5's rewrite to the account layout, not a Step-1 backfill.
| Table | Column | # | Type | Rows | Real value from clone1 |
|---|---|---|---|---|---|
| documents | file_path | 3 | varchar(256) | 20,377 | attachments/onboarding/CapExpert_SCA_Facility_Maxls_1718895984902.xlsx |
| po_requests | attachment | 3 | varchar(255) | 144 | cer-documents/2341727jpe_1726568391940jpe_1728417342272.jpeg |
| shipping_estimates | file_name | 6 | text | 70 | document-manager/shipping-documents/1742317022537_1742317022538.pdf |
| chat_messages | text | 2 | text | 51 | chat-message-documents/1010SalesSheetpd_1771260090875.pdf |
| users | profile | 8 | varchar(150) | 29 | profile/1A4A1364jpe_1772729243492.jpeg |
| file_import_logs | file_name | 3 | varchar(255) | 13 | onboard-file/CAHFxls_1754039288235.xlsx |
| shipping_estimates | original_filename | 19 | text | 8 | document-manager/shipping-documents/1745251206770_1745251206770.pdf |
| profiles | hospital_logo | 27 | varchar(255) | 7 | profile/CAPSlogopn_1720757692960.png |
| admin_queue | file_name | 4 | text | 5 | inventory/service-contract/Inventory_07-23-2025_01-25-49.xlsx |
| settings | attribute_value | 3 | text | 1 | site-images/logo.png |
| spreadsheet_runs | input_gcs_uri | 4 | varchar(500) | 40 | gs://capexpert-prod-bucket/denovo-projects/Cedarwood_FHC_Listxls_1777064533843.xlsx |
| spreadsheet_runs | output_gcs_uri | 5 | varchar(500) | 29 | gs://capexpert-prod-bucket/spreadsheets/output/34195eb6-6af3-4c00-b913-a8ad38ba1e0f/Cedarwood_FHC_Listxls_1777064533843_standardized.xlsx |
Two of these are not in the 45-item destination list. settings.attribute_value points at site-images/, and spreadsheet_runs writes absolute gs:// URIs under spreadsheets/output/ — a prefix that appears in no BucketFolder enum in any repo. Neither has a home in the account layout yet.
Every key path below resolves to a string holding a filename or a path. Notation: .key descends into an object, [] into an array. Counts are from a 3,000-row sample per column, so read them as proportions, not totals.
| Table | Column | # | Type | Key holding the file | n | Real value |
|---|---|---|---|---|---|---|
| biomeds | extracted_data | 13 | jsonb | .biomedFile | 2,305 | 012020260844530001pd_1768919771163.pdf |
| copilot_launch_proposals | additional_details | 49 | jsonb | .parentQuote | 2,748 | 1205TG_2436068_SPRING_pd_1pd_1779887679183.pdf |
| .specifications[].fileName | 164 | 0031053500pd_1779825946369.pdf | ||||
| users | agreement_details | 28 | jsonb | .name | 1,064 | eula_aaron_hayes1780079378131.pdf |
| transaction_equipment_details | equipment_details | 3 | jsonb | .inventoryStickerData.inventoryStickerDetail[].stickerName | 965 | 00OFGjpe_1781788180450.jpeg |
| .images[] | 826 | 00OFGjpe_1781788180450.jpeg | ||||
| .equipmentCategoryData.img | 215 | Autoclaves.jpg | ||||
| .inventoryStickerData.inventoryStickerDetail[].sticker | 125 | 1jpe_1701347472926.jpeg | ||||
| .images | 66 | 1jpe_1701478449843.jpeg | ||||
| .referenceImages | 52 | 0_12jp_1709196811360.jpg | ||||
| .inventoryStickerData.inventoryStickerDetail[] | 43 | 1jpe_1699746515688.jpeg | ||||
| .userManualPdf | 15 | 1670236966908_FCF1K3IWFOQF8UYU.pdf | ||||
| .serviceContract | 5 | DEMO_SERVICE_CONTRACT_1pd_1710185201917.pdf | ||||
| .childInventories[].inventoryStickerData.inventoryStickerDetail[].stickerName | 4 | 1jpe_1739390044463.jpeg | ||||
| .warrantyFile | 3 | CSA_GE_Vivid_T9_R6__620219pd_1744212068328.pdf | ||||
| .childInventories[].equipmentCategoryData.img | 2 | Stirrups.jpg | ||||
| .tradeInventoryDetail.newImages[] | 2 | Electrosurgeryjp_1723044572403.jpg | ||||
| .tradeInventoryDetail.images | 1 | 9900001jp_1694199213778.jpg | ||||
| .tradeInventoryDetail.equipmentCategoryData.img | 1 | C_Arms.jpg | ||||
| shipping_estimates | request_details | 2 | jsonb | .items[].images[] | 280 | 0730Xjpe_1752603773879.jpeg |
| po_requests | additional_details | 15 | jsonb | .comparisonView.fileName | 36 | comparison_view_1749043309118.pdf |
| copilot_parent_requests | action_logs | 20 | jsonb[] | .equipments[].changes[].new | 27 | allenfootpn_1759337538557.png |
| .equipments[].changes[].old | 11 | 111924_Star_Med_Ctr_OQpd_1733508636666.pdf,Star_Med_Ctr_Opts_GAdams_1pd_1733508676261.pdf | ||||
| camp_inventory_items | additional_details | 23 | json | .quote | 18 | 420176pd_1754346820897.pdf |
| .copilotReq.attachments | 1 | Screenshot_20251210_1010pn_1765383080588.png |
These already follow the pattern the plan wants — a fileName for display and a filePath for the location. They need no Step-1 change; they are the model for the rest.
| Table | Column | # | Type | Name key | Path key | Real path value |
|---|---|---|---|---|---|---|
| denovo_projects | additional_details | 15 | jsonb | .formData.cadDocuments[].fileName | .formData.cadDocuments[].filePath | denovo-projects/23009_A151A151pd_1777565439300.pdf |
| .formData.w9Attachment[].fileName | .formData.w9Attachment[].filePath | denovo-projects/CPASC_W9_2pd_1773161844217.pdf | ||||
| .formData.optionalDocuments[].fileName | .formData.optionalDocuments[].filePath | denovo-projects/Ortho_NY_ASC_RFP_Instructipd_1773161881404.pdf | ||||
| .formData.purchasingFormulary[].fileName | .formData.purchasingFormulary[].filePath | purchasing-formulary/Cedarwood_FHC_List_Sheetpd_1777064710141.pdf | ||||
| — | .denovoTemplate.filePath | document-manager/DNP-018F6BTE_denovo_template1777318758294.xlsx |
| Table | Column | # | Type | Key | n | Real value |
|---|---|---|---|---|---|---|
| camp_inventory_items | additional_details | 23 | json | .cad.cadUrl | 2,679 | document-manager/cad/extraction/A161_Northpoint_ASCA161pd_1776372936871_enhanced.xlsx |
| .transferDetail.fileName | 1 | inventory/images/8_Lafayette_Surgery_Centerpd_1738797079136.pdf | ||||
| copilot_requests | proposal_summary | 61 | jsonb | .comparisonViewAttachment | 712 | ezrfp/attachments/comparison_view_1761676961963.pdf |
| transactions | additional_documents | 24 | jsonb | .poDocument mixed | 727 path / 9 bare | cer-documents/1015_Stretcher_Equi_Demojpe_1724265729018.jpeg |
| .poAttachments[].attachment | 460 | cer-documents/396891798735333pd_1734545674644.pdf | ||||
| .shippingQuote | 34 | document-manager/shipping-documents/1743547691705_1743547691705.pdf | ||||
| .cerDocument | 22 | cer-documents/Bill_of_Salepd_1723044118308.pdf | ||||
| .billOfSale | 12 | bill-of-sales/Bill_of_sale_1719219486320Z5RTY.pdf | ||||
| .bolDocument | 7 | document-manager/shipping-documents/Bill of Lading 1765229943240_1765229943241.pdf | ||||
| transactions | seller_details | 6 | jsonb | .primaryUser.profile | 142 | profile/Didage_White_on_Blue_NO_TAGjp_1708444032630.jpg |
| inventory_transfers | transfer_details | 11 | json | .fileName | 115 | document-manager/purchase-orders/1212_6th_copyjpe_1743678572968.jpeg |
| copilot_pre_orders | additional_details | 11 | json | .poAttachments[].attachment | 98 | document-manager/purchase-orders/041526Neptune_1776266774975.pdf |
| copilot_launch_proposals | additional_details | 49 | jsonb | .poAttachment | 82 | document-manager/purchase-orders/02242025_Quotepd_1742594309796.pdf |
| file_imports | additional_details | 11 | jsonb | .derivedExcelPdf.filePath | 38 | inventory/asset-registry/203_UNV_AssetRegisterRexls_1773359218125_JSON_derived.pdf |
| accounts | images_attachment | 10 | jsonb | .assetTag comma-sep | 17 | account-tags/asset-tags/IMG_3633jpe_1763315944357.jpeg,account-tags/asset-tags/IMG_3632jpe_1763315945588.jpeg |
| .biomedTag comma-sep | 14 | account-tags/biomed-tags/Screenshot_20251114_at_1pn_1763144878456.png | ||||
| .logo | 11 | profile/CSA_Mainjp_1738712930274.jpg | ||||
| .smallLogo | 11 | profile/CSA_Microjpe_1738712931081.jpeg | ||||
| make_offers | additional_details | 16 | json | .poAttachment | 15 | document-manager/purchase-orders/Boco_Med_Tech_Signed_523pd_1748027216213.pdf |
| buy_orders | buyer_details | 3 | jsonb | .primaryUser.profile | 5 | profile/Didage_White_on_Blue_NO_TAGjp_1708444032630.jpg |
| shipping_estimates | shipping_responses | 12 | jsonb | [].fileName mixed | 61 path / 25 bare | document-manager/shipping-documents/1742317022537_1742317022538.pdf |
| shipping_estimates | quote_email_details | 13 | jsonb | .attachments[] mixed | 50 path / 25 bare | document-manager/shipping-documents/1742317022537_1742317022538.pdf |
| transaction_equipment_details | equipment_details | 3 | jsonb | .additionalDetails.transferDetail.fileName | 1 | transfer-documents/Bill_of_Salepd_1723043698956.pdf |
email_logs.data decideemail_logs.data (position 9, jsonb, 90,732 rows, 2.8 GB) is a frozen copy of whatever the mailer was handed. It carries at least 39 distinct file-bearing key paths, mirroring almost every column above — .copilotDetail.equipments[].attachments, .shortlistedProposals[].quote, .documentManager.filePath, .reqUser.logo, .transaction.poAttachment, .requestDetails.<id>.<id>[].text, and so on. Roughly half already hold paths because they snapshot a path column.
Recommendation: exclude it from Step 1. It is a log of what was sent, not a live reference; rewriting it would falsify the record, and its per-row key shape varies by email type. Read it through the migration-table fallback (bare name → real path) if a historical email ever has to be re-rendered.
These columns contain file-looking strings and will show up in any naive scan. None of them reference an object in capexpert-prod-bucket.
| Table | Column | # | Type | Why it is excluded |
|---|---|---|---|---|
| External vendor URLs from scraping and enrichment | ||||
| equipment_models | candidate_image_links | 25 | jsonb | Scraped vendor image URLs. [].filename is the vendor's own name, not ours. |
| equipment_models | candidate_file_links | 26 | jsonb | Scraped document URLs, including ftp://. |
| equipment_models | candidate_links | 16 | jsonb | Scraped source URLs. |
| equipment_models | image_candidate_links | 21 | jsonb | Scraped source URLs. |
| equipment_models | file_candidate_links | 22 | jsonb | Scraped source URLs. |
| equipment_models | selected_image | 40 | jsonb | .metadata.filename, .filename, .title — the chosen scrape's original name, not a stored object. |
| equipment_models | eol_data · recalls · secondhand_market_pricing · additional_identifiers · review_data · alt_specification | 49 33 32 47 55 15 | jsonb | Enrichment payloads carrying source links and titles. |
| pdf_extraction_cache | url | — | text | 5,848 third-party PDF URLs. No entity; not in the app's model layer. |
| shipping_companies | logo_url · public_url | 12 · 7 | varchar | Carrier brand assets on carrier domains. |
| Extracted document text — the match is inside prose | ||||
| pdf_extraction_cache | extracted_text · first_500_words | — | text | OCR output. The "path" is a sentence containing a slash. |
| equipment_models | scraped_data | 27 | text | Page text. |
| inventory_stickers | extracted_data | 5 | jsonb | Patent notices under .additionalDetails.stickers.* containing URLs. |
| email_logs | content | 4 | text | Rendered HTML email bodies. |
| email_templates · static_pages · prompts | content · content · prompt | 5 · 4 · 2 | text | Templates whose markup references site-images/; handled by the code change, not a backfill. |
| copilot_parent_requests | comment | 9 | text | Free-text notes containing vendor links. |
| service_contracts · file_imports · file_import_queues · inventory_sticker_details | extracted_data (+ variants) | 41 · 9 · 12 · 6 | jsonb / jsonb[] | 1–10 incidental matches each in extracted document text. |
| Regex false positives | ||||
| users | 4 | varchar | Two addresses whose local part ends in a file-like token. | |
| old_passwords | password | 3 | varchar | Hashes containing a matching substring. |
| security_tokens | value | 5 | varchar | Token body. |
| docusign_requests | webhook_response | 11 | jsonb[] | ISO timestamps ending .7Z matched the extension pattern. |
| account_module_permissions | key | 1 | varchar | Permission keys. |
| service_contracts | quote_number | 8 | varchar | Quote numbers with a dot. |
| Backup, snapshot and scratch tables — no Sequelize entity, no code reference | ||||
| equipment_models_bak_pre_p1_20260809 equipment_models_snapshot_20260717 equipment_models_bak_pre_batch_20260624 equipment_models_bak_acct1216_20260625 | files · images · selected_image · candidate_* · eol_data · scraped_data | — | mixed | Four frozen copies of equipment_models. ~20,000 bare filenames each. Copying them would multiply the Step-5 object count fourfold for no benefit. |
| camp_inventory_items_bak_pre_finalize_20260626 | images · additional_details · warranty_file | — | varchar[] · json · text | 583,120 bare image names — a frozen copy of the live array. |
| transactions_old · transaction_equipment_details_old | invoice_name · po_attachment · additional_documents · seller_details · buyer_details · equipment_details | — | mixed | Superseded tables. |
| family_barrier | parent_final | — | jsonb | 24,434 bare names under .images_array[].filename plus 33,512 external URLs. No entity, no code reference — a matching experiment. |
| temp_ai_image_camp · temp_tommy_data | HTTPS/GS link columns · images_url · image_name | — | varchar | Point at capexpert-prod-tmp-20250808151049-65b6, a different bucket entirely. |
| import_mh_staging · plano_rerun · updated_extraction_records · _bak_unified_is_audited_20260804 | steps · steps_c · extracted_data (+ variants) | — | mixed | Import staging and one-off remediation tables. |
Decide on the backups before Step 5, not during it. The four equipment_models copies and the camp_inventory_items copy together reference roughly 660,000 filenames. If the copier is driven from the migration table and the migration table is seeded from a column list, they are excluded automatically. If it is driven by scanning the bucket, they are irrelevant. If anyone seeds by scanning all columns, the object count roughly doubles.
The plan lists this as an open question before Step 1. Measured against the longest account-shaped prefix each column can receive. capexpert_id is 11–12 characters across all 3,104 accounts, so CAP-12345678 in the plan is the correct worst case.
doc/CAP-12345678/document-manager/pre-approved-pricing/ (worst case for any document)img/CAP-12345678/user/profile/img/CAP-12345678/assets/, and 30 with thumb/img/category/| Table | Column | Width | Longest today | + prefix | Total | Verdict |
|---|---|---|---|---|---|---|
| documents | file_name | 256 | 217 | 54 | 272 | overflow by 16 — leave it bare; the sibling file_path is the path column. |
| documents | file_path | 256 | 173 | 54 | 228 | fits 28 to spare — the tightest column that must hold a path. |
| copilot_launch_proposals | warranty_document | 255 | 173 | 54 | 228 | fits 27 to spare. |
| copilot_launch_proposals | service_agreement | 255 | 173 | 54 | 228 | fits 27 to spare. |
| equipment_models | files | 255 | 125 | 54 | 180 | fits per element — but the column is comma-separated, so the joined length is what matters. Widen or convert to text. |
| document_manager | name · file_path | 256 | 107 | 54 | 162 | fits |
| camp_inventory_items | images (element) | 255 | 104 | 30 | 135 | fits including the thumb/ variant. |
| inventory_sticker_details | sticker_name | 256 | 104 | 24 | 129 | fits |
| warranties | file | 255 | 88 | 54 | 143 | fits |
| users | profile | 150 | 76 | 29 | 106 | fits — narrowest column in the set, still 44 to spare. |
| feedbacks | attachment | 200 | 46 | 54 | 101 | fits |
| profiles · file_import_logs · po_requests · bill_of_sale_requests · transactions · equipment_categories | 6 columns | 255 | ≤51 | ≤54 | ≤102 | fits comfortably. |
Net answer to the open question. One column overflows, and it is one that should stay bare. Every other column fits without a width change. The migration table's own varchar(512) path columns are more than adequate — the longest possible key is 54 + 217 = 271.
Counted by the two things that break when a file moves: somewhere a folder is glued to a filename, and somewhere a public URL is glued to a folder.
backendApi is on CAP-2031-same-bucket-migration with uncommitted work in place: upload-targets.ts (the 45-entry destination map, {SCOPE} substitution, temp/final prefix builders), presigned-url-links.entity.ts, and storage.service.ts carrying getPreSignedUrlForUpload, recordUpload, commitUploads, commitRow, markSkipped and a getPreSignedUrlForView with a fallback lookup. The shared rule exists. What remains is adoption.
UploadModule: chatbot and denovo-external-link.getPreSignedUrlForView.BucketFolder.| Sites | File | Sites | File |
|---|---|---|---|
| 8 | src/shared/modules/document-manager/document-manager.service.ts | 1 | src/primary/modules/warranties/warranties.service.ts |
| 6 | src/import/queue/attachment/capture.service.ts | 1 | src/primary/modules/transaction/transaction.service.ts |
| 4 | src/shared/queue/mail/mail.service.ts | 1 | src/primary/modules/qrcodes/qrcodes.service.ts |
| 3 | src/primary/modules/vendor-offers/task-queue/vendor-offer-transaction.service.ts | 1 | src/primary/modules/inventory/divestiture/inventory-divestiture.service.ts |
| 3 | src/import/modules/thread.ts | 1 | src/primary/modules/ez-estimators/ez-estimators.service.ts |
| 2 | src/shared/modules/storage/storage.service.ts | 1 | src/primary/modules/developers-tab/developers-tab.service.ts |
| 2 | src/shared/common/utils/constant.ts | 1 | src/primary/modules/content/feedback/feedback.service.ts |
| 2 | src/primary/modules/gcp-agent-tools/gcp-agent-tools.controller.ts | 1 | src/primary/modules/carts/carts.service.ts |
| 2 | src/primary/modules/copilot/copilot-utilities/copilot-utilities.service.ts | 1 | src/import/queue/vertex-ai/vertex-ai.service.ts |
| 2 | src/import/queue/copilot-quote/copilot-quote-extractor.service.ts | 1 | src/import/queue/mail/pa-mail.service.ts |
| 2 | src/import/modules/qr-codes/qr-codes.service.ts | 1 | src/import/queue/imap/imap.service.ts |
| 2 | src/import/modules/qr-codes/export-qr-pdf.service.ts | 1 | src/import/queue/attachment/helpers/capture-helper.ts |
| 1 | src/shared/queue/mail/copilot/copilot-mail.service.ts | 1 | src/import/modules/thread-helper/marketplace-export.ts |
| 1 | src/primary/queue/mail/marketplace/shipping-mail.service.ts | 1 | src/import/modules/thread-helper/divest-export.ts |
| 1 | src/primary/queue/mail/marketplace/marketplace-mail.service.ts | 1 | src/import/modules/thread-helper/capture-export.ts |
| 1 | src/import/modules/reports/report-generation.job.ts |
47 sites gluing gcp.publicUrl / GCP_PUBLIC_URL to gcp.mediaBucket and a key. The heaviest are import/queue/attachment/capture.service.ts (8), shared/queue/mail/mail.service.ts (5), shared/queue/mail/copilot/copilot-mail.service.ts (4), shared/common/utils/constant.ts (3) and import/modules/thread.ts (3). Three further files — shared/core/config/configuration.ts, shared/core/config/validation.ts, shared/modules/storage/storage-config.ts — define the base and are not hand-built call sites.
Four of these emit site-images/logo.png into notifications and email footers: static-pages.controller.ts, notification.service.ts, mail-common-code.service.ts, imap.service.ts. That prefix has no UploadModule destination yet.
Every one is environment.GCS_BUCKET_URL or sharedService.GCS_BUCKET_URL concatenated with an inline folder string; there is no BucketFolder enum on the front end, so the 397 folder literals across 60 files are loose strings. Concentrations: services/shared.service.ts (11), modules/inventory/add-edit/add-edit.component.ts (10), modules/copilot/bid-submit-collective/… (7), modules/manage-users/add-account/add-account.hydrate.ts (6), modules/copilot/bid-submit-tabbing/bid-submit.component.ts (6), common/modules/chat/chat-widget/chat-widget.component.ts (6).
Introduce the enum here first. With folder names as free-text literals in 60 files, there is nothing for the compiler to catch when a folder moves. A shared constant plus one resolver function is the change that makes the other 170 mechanical.
| Version | Files | Refs | Notes |
|---|---|---|---|
| v9 | inventory/v9/inventory.service.ts | 19 | Oldest live inventory surface. |
| v10 | inventory · audit-view (get-asset, get-location-stickers) · market-place (service, get-all-marketplace-items) · copilot (create-copilot-request, copilot-request-created) | 17 + 12 + 6 + 2 | Widest version. |
| v11 | inventory · audit-view ×2 · market-place ×2 | 19 + 12 + 6 | |
| v12 | inventory · audit-view ×2 | 19 + 12 | |
| v13 | inventory · audit-view ×2 | 19 + 12 | |
| v14 | inventory · inventory-locations/fetch-consumable-group-items | 19 + 1 | Current. |
| v6 | misc/v6/misc.service.ts · misc/v6/dto/misc.dto.ts | 15 + 11 | Where the upload link is issued; 3 calls to getPreSignedUrlForUpload. |
| shared | modules/storage/storage.service.ts · shared/services/end-user/services/helper.service.ts · shared/utils/enum/index.ts | 3 + 1 + 42 | The enum here holds the three prefixes backendApi is missing. |
38 GCS_BUCKET_URL concatenations and 29 BucketFolderEnum references (overlapping), against a 26-entry enum in src/app/core/constants/index.constant.ts that is a strict subset of backendApi's. Heaviest: pages/authorized-container/audit/update/update.page.ts, pages/authorized-container/audit/list/list.page.ts, pages/authorized-container/marketplace/add/add.page.ts, pages/authorized-container/copilot/copilot-detail/copilot-detail.ts.
update.page.ts:1569 already does the right thing — `${environment.GCS_BUCKET_URL}${imgData.src}`, no folder glued on, because src already holds a path. That line is the target shape for the other 46.
One migration exists: 20260901134627-create-presigned-url-links.js, already applied to clone1. The Step-1 backfill migrations — one per column in §03 and §06 — do not exist yet.
So the numbers can be re-derived rather than trusted.
text, varchar, bpchar, json, jsonb, text[], varchar[], json[] and jsonb[] column in public and analytics — 1,253 columns across 169 tables — cast to text and matched against a 40-extension pattern over the first 20,000 rows of each table. 186 columns matched.http(s)://, gs://, contains-slash, or bare. Comma-separated cells counted separately so multi-value columns are visible..key for objects, [] for arrays), then filtered to file-like leaves and grouped by path. Sampled at 3,000 rows per column.BucketFolder enums in each repo.tableName declaration exists in backendApi/src/shared/entity or mobileApi/src/entity. 44 of the 59 matched tables are live.character_maximum_length against max(length(…)), plus the longest prefix each column can receive under UPLOAD_MODULE_TARGETS.Known limits. JSON key counts are sample-based. A file with no extension, or one whose extension is outside the 40 matched, would be missed. email_logs.data was sampled rather than fully walked because the table is 2.8 GB. Nothing was written to clone1.