feat(storage): wire MinIO/S3 document upload & download into the API + web
Build and Push Images / Build jorgecuadros-web (push) Successful in 1m37s
Build and Push Images / Build jorgecuadros-api (push) Successful in 2m8s

The schema has carried `storageKey` pointers and the migration has written
blobs to MinIO since day one, but the API had no S3 client — documents could
only be deleted, never uploaded or retrieved. This adds the missing wiring.

API
- StorageModule/StorageService (@aws-sdk/client-s3, path-style for MinIO):
  put/getStream/delete, best-effort bucket ensure on boot, gracefully disabled
  when S3 env is absent (ServiceUnavailable on use).
- Reads S3_ENDPOINT/S3_BUCKET + S3_ACCESS_KEY/S3_SECRET_KEY, falling back to
  MINIO_ROOT_USER/MINIO_ROOT_PASSWORD so one credential set drives both the
  migration and the API.
- Property service documents: POST :id/documents (multipart), GET
  :id/documents/:childId/download (streamed), delete now also drops the blob.
- Policy documents: same upload/download/delete (previously had none).
- Keys stay under the service/<id>/… and policy/<id>/… prefixes the migration
  established.

Web
- api.ts: shared uploadFile() helper (uploadIngest refactored onto it),
  upload/download/remove helpers for property & policy documents.
- Servicios, polizas, clientes detail pages: real Descargar links and an
  upload control (gated by policy:update / property:update) replacing the
  "storage pending" notes.

Infra
- docker-compose: minio service (9000/9001, healthcheck, named volume) + S3
  env wired into the api service.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 19:19:57 -07:00
co-authored by Claude Opus 4.8
parent 45afb824ef
commit afe2411c86
15 changed files with 957 additions and 102 deletions
+302
View File
@@ -10,6 +10,9 @@ importers:
apps/api:
dependencies:
'@aws-sdk/client-s3':
specifier: ^3.665.0
version: 3.1093.0
'@jorgecuadros/database':
specifier: workspace:*
version: link:../../packages/database
@@ -145,6 +148,78 @@ packages:
resolution: {integrity: sha512-I5wviiIqiFwar9Pdk30Lujk8FczEEc18i22A5c6Z9lbmhPQdTroDnEQdsfXjy404wPe8H62s0I15o4pmMGfTYQ==}
engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
'@aws-sdk/checksums@3.1000.19':
resolution: {integrity: sha512-Hc4N100RdkuWshKBnhPzmpdftfi9mCLz+OHFELHM1QIgMH4QRUUWyWgfiebta/YX2Bd62wTcm3EqAP8TeXv0gA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/client-s3@3.1093.0':
resolution: {integrity: sha512-7452vEdp/nihIBWijnmcTBujXEFfbs4F02wyBDGqmNr6pwyo5GmQorx0zQIVg8QGFLXiBvsWKXBhCdiBcxNnGA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/core@3.976.0':
resolution: {integrity: sha512-0cjRaEdlVoOrsNb9pP5q1Syyc8pXw5xSj2Np2ryReRTr9FppIIRVSdZK4lbnfmc2Hvgux/xBOUU6baB7z8//uA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/credential-provider-env@3.972.60':
resolution: {integrity: sha512-BAkxdoe7tpDDqCghGpuOeHQRbm/2znVvOQm0AvpQbA2tbfMN46doN4zx65fv85ImP3KADwc2zQPmbrlI9MPfMg==}
engines: {node: '>=20.0.0'}
'@aws-sdk/credential-provider-http@3.972.62':
resolution: {integrity: sha512-g/0fGqKTb9xpKdd9AtpmV5Eo3DFKbnkpA2+w0peISSlu7NfAoWOuYBFxsu+yWBtxU89ka55ezoZBCbFaS8pjYQ==}
engines: {node: '>=20.0.0'}
'@aws-sdk/credential-provider-ini@3.973.5':
resolution: {integrity: sha512-ylubazcRfq2TVus/qXucSXeC42Qdjp5HQxTu68K/BsdMiZlcSLD1zkpoCgApXZX1Y6YJhtGGs7ZHhO/GuIgBlw==}
engines: {node: '>=20.0.0'}
'@aws-sdk/credential-provider-login@3.972.67':
resolution: {integrity: sha512-CCygIKJ9YbI3n84OClSaSppkgKKHVj2TGT33c6FRORZrYNZQ1POmD+ip0FLYokiJAK7sSdc3YVkOsBm90oxWMQ==}
engines: {node: '>=20.0.0'}
'@aws-sdk/credential-provider-node@3.972.71':
resolution: {integrity: sha512-HIg7Q2osBzajQwL+1Vkyh2E7Gim3eTNb9RHIsOxDGjW0eZg4oEKtRs5sioCnc73ilhaOm4gX2lHVF8J7+nt2rg==}
engines: {node: '>=20.0.0'}
'@aws-sdk/credential-provider-process@3.972.60':
resolution: {integrity: sha512-YIo3f99hM43QdYG8hDzwGemnR/pU95b0kramqSJUTleCqaB7+HwKf7YZFHqvOgTqZTPx/mRmNIqoDRr3U0Z3Tw==}
engines: {node: '>=20.0.0'}
'@aws-sdk/credential-provider-sso@3.973.4':
resolution: {integrity: sha512-BPdmL8sSBOCv4ngZ+3LHxyc3CNqDCEK37CHioCk7zGrTMY5sUtkH8q+o6qA80nn6w3/fyBPGNE7OIRlmoOxRQA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/credential-provider-web-identity@3.972.66':
resolution: {integrity: sha512-kSAziJboOmZmsR9/MTbiNjowl2BPes1bQuJpne4qAZ62ubi8fjfr/aupJSQje6udBoYxXTQbsL0e0kby2la3ng==}
engines: {node: '>=20.0.0'}
'@aws-sdk/middleware-sdk-s3@3.972.65':
resolution: {integrity: sha512-udwNhRfDTfCB98mAHjjgsnKQlxygB4e0X+Obne/XjJpvVsF0YCQC8ZErd/8Z6IPoLQjtiKHzwqEDbZiLrJEnOg==}
engines: {node: '>=20.0.0'}
'@aws-sdk/nested-clients@3.997.34':
resolution: {integrity: sha512-Y9REVrSwmLM+Qy6sZJ7ofMC2S3Hr3tPP/4CzL5U1olPP7OGoF+6+Px0E49cVQBtSxJtyeLJMf0UaBErfeSahAA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/signature-v4-multi-region@3.996.41':
resolution: {integrity: sha512-QMUytg+FQMGouc8gHS00KoYih3+N6cqmVI/pQGOIo7Nr7OpQaiXjSYOuL+vsPZ1tymY4LAQ8MYcHJmws5LRxng==}
engines: {node: '>=20.0.0'}
'@aws-sdk/token-providers@3.1092.0':
resolution: {integrity: sha512-hBYUAr6iBLNFcsiWTgtBb0stdSw39VOUq4Sp4A5caCNf66BAZplWN4FleKrVpJx5li2YgdnK2DqoFSMWC642FQ==}
engines: {node: '>=20.0.0'}
'@aws-sdk/types@3.974.2':
resolution: {integrity: sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/xml-builder@3.972.36':
resolution: {integrity: sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA==}
engines: {node: '>=20.0.0'}
'@aws/lambda-invoke-store@0.3.0':
resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==}
engines: {node: '>=18.0.0'}
'@babel/code-frame@7.29.7':
resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
engines: {node: '>=6.9.0'}
@@ -615,6 +690,30 @@ packages:
'@sinonjs/fake-timers@10.3.0':
resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==}
'@smithy/core@3.29.7':
resolution: {integrity: sha512-BiEE2bnnGoPKdlGe3L+gOYORDHFGPuYVRLP7iUow/Sflm0B4hC4XY3FC1MRuc7ltzpW2xNnXopKi34TTkULlKQ==}
engines: {node: '>=18.0.0'}
'@smithy/credential-provider-imds@4.4.12':
resolution: {integrity: sha512-ZZPDbl/aRp77aycuoMlo3BTayT4CE2a3uoqETYZU5ySnVbhpl5IJiY7dCZedn+ZusyDLqVv44IvKBiXd2/nK0Q==}
engines: {node: '>=18.0.0'}
'@smithy/fetch-http-handler@5.6.9':
resolution: {integrity: sha512-EJktha5m5MXCwzdXrlWyqb9UCNHNFKlg+PmTpRsdX3dncJPTiqYleM9OKj2mLgdVJHR01d2tU4alG+z2NdH5rQ==}
engines: {node: '>=18.0.0'}
'@smithy/node-http-handler@4.9.9':
resolution: {integrity: sha512-xVBZ3hptB99iNO9XyWqEhC7KD9bP9UPXhuy3h5Y2ItCfBv160D9IIC/Fmmp3EbnWwit4C+KVqlSE+E29Nk/pPg==}
engines: {node: '>=18.0.0'}
'@smithy/signature-v4@5.6.8':
resolution: {integrity: sha512-iGBm6hIwD2MGvVRSgrjVWa4FXtXDq3akxu0DCpnkmBo0xtEHZ/siMRt7ycfZAefYr2UdywUgmGtoRLaq5u56pg==}
engines: {node: '>=18.0.0'}
'@smithy/types@4.16.1':
resolution: {integrity: sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==}
engines: {node: '>=18.0.0'}
'@swc/counter@0.1.3':
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
@@ -942,6 +1041,9 @@ packages:
resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
bowser@2.14.1:
resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==}
brace-expansion@1.1.16:
resolution: {integrity: sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==}
@@ -2768,6 +2870,171 @@ snapshots:
transitivePeerDependencies:
- chokidar
'@aws-sdk/checksums@3.1000.19':
dependencies:
'@aws-sdk/core': 3.976.0
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.7
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/client-s3@3.1093.0':
dependencies:
'@aws-sdk/checksums': 3.1000.19
'@aws-sdk/core': 3.976.0
'@aws-sdk/credential-provider-node': 3.972.71
'@aws-sdk/middleware-sdk-s3': 3.972.65
'@aws-sdk/signature-v4-multi-region': 3.996.41
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.7
'@smithy/fetch-http-handler': 5.6.9
'@smithy/node-http-handler': 4.9.9
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/core@3.976.0':
dependencies:
'@aws-sdk/types': 3.974.2
'@aws-sdk/xml-builder': 3.972.36
'@aws/lambda-invoke-store': 0.3.0
'@smithy/core': 3.29.7
'@smithy/signature-v4': 5.6.8
'@smithy/types': 4.16.1
bowser: 2.14.1
tslib: 2.8.1
'@aws-sdk/credential-provider-env@3.972.60':
dependencies:
'@aws-sdk/core': 3.976.0
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.7
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/credential-provider-http@3.972.62':
dependencies:
'@aws-sdk/core': 3.976.0
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.7
'@smithy/fetch-http-handler': 5.6.9
'@smithy/node-http-handler': 4.9.9
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/credential-provider-ini@3.973.5':
dependencies:
'@aws-sdk/core': 3.976.0
'@aws-sdk/credential-provider-env': 3.972.60
'@aws-sdk/credential-provider-http': 3.972.62
'@aws-sdk/credential-provider-login': 3.972.67
'@aws-sdk/credential-provider-process': 3.972.60
'@aws-sdk/credential-provider-sso': 3.973.4
'@aws-sdk/credential-provider-web-identity': 3.972.66
'@aws-sdk/nested-clients': 3.997.34
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.7
'@smithy/credential-provider-imds': 4.4.12
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/credential-provider-login@3.972.67':
dependencies:
'@aws-sdk/core': 3.976.0
'@aws-sdk/nested-clients': 3.997.34
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.7
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/credential-provider-node@3.972.71':
dependencies:
'@aws-sdk/credential-provider-env': 3.972.60
'@aws-sdk/credential-provider-http': 3.972.62
'@aws-sdk/credential-provider-ini': 3.973.5
'@aws-sdk/credential-provider-process': 3.972.60
'@aws-sdk/credential-provider-sso': 3.973.4
'@aws-sdk/credential-provider-web-identity': 3.972.66
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.7
'@smithy/credential-provider-imds': 4.4.12
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/credential-provider-process@3.972.60':
dependencies:
'@aws-sdk/core': 3.976.0
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.7
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/credential-provider-sso@3.973.4':
dependencies:
'@aws-sdk/core': 3.976.0
'@aws-sdk/nested-clients': 3.997.34
'@aws-sdk/token-providers': 3.1092.0
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.7
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/credential-provider-web-identity@3.972.66':
dependencies:
'@aws-sdk/core': 3.976.0
'@aws-sdk/nested-clients': 3.997.34
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.7
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/middleware-sdk-s3@3.972.65':
dependencies:
'@aws-sdk/core': 3.976.0
'@aws-sdk/signature-v4-multi-region': 3.996.41
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.7
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/nested-clients@3.997.34':
dependencies:
'@aws-sdk/core': 3.976.0
'@aws-sdk/signature-v4-multi-region': 3.996.41
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.7
'@smithy/fetch-http-handler': 5.6.9
'@smithy/node-http-handler': 4.9.9
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/signature-v4-multi-region@3.996.41':
dependencies:
'@aws-sdk/types': 3.974.2
'@smithy/signature-v4': 5.6.8
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/token-providers@3.1092.0':
dependencies:
'@aws-sdk/core': 3.976.0
'@aws-sdk/nested-clients': 3.997.34
'@aws-sdk/types': 3.974.2
'@smithy/core': 3.29.7
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/types@3.974.2':
dependencies:
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws-sdk/xml-builder@3.972.36':
dependencies:
'@smithy/types': 4.16.1
tslib: 2.8.1
'@aws/lambda-invoke-store@0.3.0': {}
'@babel/code-frame@7.29.7':
dependencies:
'@babel/helper-validator-identifier': 7.29.7
@@ -3368,6 +3635,39 @@ snapshots:
dependencies:
'@sinonjs/commons': 3.0.1
'@smithy/core@3.29.7':
dependencies:
'@smithy/types': 4.16.1
tslib: 2.8.1
'@smithy/credential-provider-imds@4.4.12':
dependencies:
'@smithy/core': 3.29.7
'@smithy/types': 4.16.1
tslib: 2.8.1
'@smithy/fetch-http-handler@5.6.9':
dependencies:
'@smithy/core': 3.29.7
'@smithy/types': 4.16.1
tslib: 2.8.1
'@smithy/node-http-handler@4.9.9':
dependencies:
'@smithy/core': 3.29.7
'@smithy/types': 4.16.1
tslib: 2.8.1
'@smithy/signature-v4@5.6.8':
dependencies:
'@smithy/core': 3.29.7
'@smithy/types': 4.16.1
tslib: 2.8.1
'@smithy/types@4.16.1':
dependencies:
tslib: 2.8.1
'@swc/counter@0.1.3': {}
'@swc/helpers@0.5.5':
@@ -3795,6 +4095,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
bowser@2.14.1: {}
brace-expansion@1.1.16:
dependencies:
balanced-match: 1.0.2