@ddd-ts-kit/s3 0.16.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.fr.md +211 -0
  2. package/README.md +135 -10
  3. package/package.json +4 -4
package/README.fr.md ADDED
@@ -0,0 +1,211 @@
1
+ [English](README.md) · **Français**
2
+ <!-- translation-of: README.md sha256:1ff5474228b48807 -->
3
+
4
+ # @ddd-ts-kit/s3
5
+
6
+ Le `FileStorage` du noyau sur tout fournisseur du protocole S3 : AWS S3, MinIO, OVHcloud Object
7
+ Storage, Scaleway, Cloudflare R2, Garage. Le contenu d'un fichier part en flux vers le bucket par
8
+ parties, jamais entier en mémoire et jamais dans la base de données ; l'agrégat garde le
9
+ `StoredFile` que renvoie `put`. Rien dans `domain/` ou `application/` n'importe ce *package* : les
10
+ gestionnaires (handlers) dépendent du port `FileStorage`, et la plateforme lie cet adaptateur.
11
+
12
+ ```
13
+ npm i @ddd-ts-kit/s3 @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner
14
+ ```
15
+
16
+ Les trois *packages* du SDK AWS v3 officiel sont des dépendances *peer* optionnelles, écrites pour
17
+ la version 3.1150.
18
+
19
+ ![Les cas d'usage et FileStaging dépendent du port FileStorage du noyau ; S3FileStorage l'implémente, place chaque clé sous son préfixe et envoie ses commandes par un S3Client construit par s3ClientOf ; le client envoie et télécharge directement vers le bucket par des liens signés ; le bucket garde les fichiers promus sous leurs clés et les fichiers mis en attente sous staging/](https://raw.githubusercontent.com/gbourgeat/ddd-ts-kit/main/docs/assets/diagrams/s3-place-in-an-application.fr.svg)
20
+
21
+ Les cas d'usage parlent au port avec des clés comme `appointment-documents/2026/10/7f3c`,
22
+ générées par `TenantFileKeys`, le `FileKeys` du noyau : la zone, l'année, le mois, puis un
23
+ identifiant issu de l'`IdGenerator` (précédés de `tenants/<tenant>/` quand le travail en cours
24
+ désigne un tenant). L'adaptateur place son préfixe devant chaque clé
25
+ (`scheduling/appointment-documents/2026/10/7f3c` dans le bucket), et `FileStaging` place
26
+ `staging/` devant la clé d'un fichier qui attend sa validation
27
+ (`scheduling/staging/appointment-documents/2026/10/9a1b`). Les octets d'un fichier vont du client
28
+ au bucket, ou de l'API au bucket par parties, et la base de données ne garde que la référence.
29
+
30
+ ## Racine de composition
31
+
32
+ <!-- checked sample -->
33
+ ```ts
34
+ import type { FileStorage } from '@ddd-ts-kit/kernel';
35
+ import { S3FileStorage, s3ClientOf } from '@ddd-ts-kit/s3';
36
+
37
+ declare const config: {
38
+ readonly storage: {
39
+ readonly endpoint?: string;
40
+ readonly region: string;
41
+ readonly accessKeyId: string;
42
+ readonly secretAccessKey: string;
43
+ readonly bucket: string;
44
+ };
45
+ };
46
+
47
+ const { endpoint, region, accessKeyId, secretAccessKey, bucket } = config.storage;
48
+
49
+ export const files: FileStorage = new S3FileStorage({
50
+ client: s3ClientOf({
51
+ ...(endpoint === undefined ? {} : { endpoint }), // omis pour AWS ; http://localhost:9000 pour MinIO
52
+ region, // 'auto' pour R2
53
+ accessKeyId,
54
+ secretAccessKey,
55
+ }),
56
+ bucket, // créé par le code d'infrastructure, jamais par l'application
57
+ prefix: 'scheduling/', // facultatif : plusieurs applications dans un seul bucket
58
+ });
59
+ ```
60
+
61
+ `s3ClientOf` active l'adressage par chemin (path-style) quand un endpoint est donné (MinIO et la
62
+ plupart des serveurs auto-hébergés en ont besoin) et ne calcule d'empreinte (checksum) que
63
+ lorsqu'une opération en exige une : par défaut, le SDK envoie une empreinte avec chaque requête,
64
+ ce que refusent plusieurs fournisseurs qui ne sont pas AWS, et ce qui lierait un lien d'envoi
65
+ signé à un corps vide. Un client construit à la main fonctionne aussi.
66
+
67
+ Les options de `S3FileStorage` :
68
+
69
+ | Option | Par défaut | |
70
+ |---|---|---|
71
+ | `client` | requis | un `S3Client`, issu de `s3ClientOf` ou construit à la main |
72
+ | `bucket` | requis | il doit exister : l'adaptateur n'en crée jamais |
73
+ | `prefix` | aucun | une clé valide terminée par `/`, placée devant chaque clé ; le constructeur en refuse toute autre |
74
+ | `partSize` | 8 Mio | la taille de chaque partie d'un envoi, au moins 5 Mio : le maximum gardé en mémoire par envoi |
75
+ | `clock` | l'horloge du système | donne l'heure des `expiresAt` que renvoient les liens signés |
76
+
77
+ Un gestionnaire dépend du port, jamais du SDK :
78
+
79
+ ```ts
80
+ // À éviter : le gestionnaire parle S3, garde tout le fichier en mémoire et nomme le bucket
81
+ const body = Buffer.concat(await Array.fromAsync(rows));
82
+ await s3.send(new PutObjectCommand({ Bucket: 'acme-files', Key: `exports/${id}.csv`, Body: body }));
83
+ ```
84
+
85
+ <!-- checked sample -->
86
+ ```ts
87
+ import { FileKeys, FileStorage, type StoredFile } from '@ddd-ts-kit/kernel';
88
+
89
+ // Écrire : le port du noyau ; la plateforme y lie S3FileStorage, un test InMemoryFileStorage.
90
+ export class ExportAppointments {
91
+ constructor(
92
+ private readonly files: FileStorage,
93
+ private readonly keys: FileKeys,
94
+ ) {}
95
+
96
+ async execute(csv: AsyncIterable<Uint8Array>): Promise<{ readonly file: StoredFile; readonly link: string }> {
97
+ const key = this.keys.next('appointment-exports');
98
+
99
+ // Part en flux vers le bucket par parties ; au-delà de 50 Mio, FILE_TOO_LARGE, et rien ne reste sous la clé.
100
+ const file = await this.files.put(key, csv, { mediaType: 'text/csv', maxBytes: 50 * 1_048_576 });
101
+
102
+ // Un GetObject présigné, calculé localement : aucune requête n'atteint le bucket.
103
+ const { url } = await this.files.signedDownloadUrl(key, { expiresInSeconds: 300, fileName: 'appointments.csv' });
104
+
105
+ return { file, link: url };
106
+ }
107
+ }
108
+ ```
109
+
110
+ ## Ce qu'il fait
111
+
112
+ | Méthode du port | Sur le bucket |
113
+ |---|---|
114
+ | `put(key, stream, { mediaType, maxBytes })` | un envoi *multipart* en parties de `partSize` (8 Mio par défaut, une seule en mémoire à la fois), ou un seul `PutObject` quand le contenu tient dans une partie ; le contenu est compté et haché au passage, et refusé avec `FILE_TOO_LARGE` au-delà de `maxBytes`, ce qui abandonne l'envoi et laisse l'objet précédent |
115
+ | `get`, `stat` | `GetObject` en flux, `HeadObject` ; `undefined` quand l'objet n'existe pas, un rejet quand le service est injoignable |
116
+ | `delete` | `DeleteObject`, idempotent |
117
+ | `copy`, `move` | `CopyObject`, fait par le service (et `DeleteObject` pour un déplacement), jusqu'à 5 Gio par objet |
118
+ | `list(prefix)` | `ListObjectsV2`, page après page |
119
+ | `signedDownloadUrl` | un `GetObject` présigné, servi avec `Content-Disposition: attachment`, avec le nom du fichier quand il est donné |
120
+ | `signedUploadUrl` | un `PutObject` présigné dont la signature couvre le `content-type` : un autre type de média est refusé par le service |
121
+
122
+ L'empreinte (`sha256:<hex>`) est renvoyée par `put`, calculée pendant que le contenu passe ; le
123
+ bucket ne la garde pas, si bien que `stat` n'en renvoie aucune. Un fichier envoyé par un lien signé
124
+ est mesuré, vérifié et haché par `FileStaging.confirm` du noyau, qui le relit.
125
+
126
+ Une clé que le port refuse (`../escape`) est refusée avec `INVALID_FILE_KEY`, et un lien demandé
127
+ pour plus d'une heure avec `INVALID_FILE_URL_EXPIRY`, avant toute requête. La signature ne
128
+ demande pas de requête non plus : un lien signé est calculé à partir des identifiants d'accès, et
129
+ seul le fournisseur le vérifie, quand le client l'utilise.
130
+
131
+ ![Un envoi par lien signé : le client demande un lien, FileStaging.uploadUrl fait signer un PUT localement par S3FileStorage et garde l'autorisation d'envoi sur l'agrégat, le client envoie le fichier directement au bucket, qui vérifie la clé, la méthode, le content-type et l'expiration mais ne peut pas plafonner la taille ; puis le client confirme, et FileStaging.confirm relit l'objet mis en attente pour vérifier sa taille contre le plafond, son type de média, son empreinte et l'analyse](https://raw.githubusercontent.com/gbourgeat/ddd-ts-kit/main/docs/assets/diagrams/s3-signed-upload.fr.svg)
132
+
133
+ Le client envoie le fichier exactement comme le dit le lien, avec les en-têtes qu'il a reçus :
134
+
135
+ <!-- checked sample -->
136
+ ```ts
137
+ import type { SignedUpload } from '@ddd-ts-kit/kernel';
138
+
139
+ // Dans le client : la signature couvre le content-type, les en-têtes sont donc envoyés tels quels.
140
+ export async function sendFile(link: SignedUpload, file: Blob): Promise<void> {
141
+ const response = await fetch(link.url, { method: link.method, headers: link.headers, body: file });
142
+
143
+ if (!response.ok) {
144
+ throw new Error(`The storage refused the upload: ${response.status}`); // 403 une fois expiré, ou pour un autre type
145
+ }
146
+ }
147
+ ```
148
+
149
+ ## Limites
150
+
151
+ - Un lien d'envoi signé ne peut pas plafonner la taille de ce qui est envoyé (seule une politique
152
+ de POST présigné le peut) : son `maxBytes` n'est pas appliqué par le service, et
153
+ `FileStaging.confirm` mesure le fichier contre le plafond de l'autorisation d'envoi que le
154
+ serveur a gardée, et le supprime au-delà.
155
+ - La copie et le déplacement passent par `CopyObject`, limité à 5 Gio par objet par le protocole.
156
+ - Un envoi compte au plus 10 000 parties : avec des parties de 8 Mio, un fichier d'environ 78 Gio.
157
+ Augmentez `partSize` pour des fichiers plus gros.
158
+ - Les règles de cycle de vie, le versionnement, le verrouillage d'objet (object lock) et le
159
+ chiffrement du bucket sont des réglages du bucket, faits par le code d'infrastructure du
160
+ déploiement, pas par cet adaptateur.
161
+
162
+ ## Tests
163
+
164
+ Le contrat du port est tenu contre un vrai serveur : `fileStorageContract` de
165
+ `@ddd-ts-kit/testing/contracts` (réexporté par `@ddd-ts-kit/s3/testing`) s'exécute sur MinIO dans
166
+ le *job* d'intégration du *pipeline*, avec la variable `DDD_TS_KIT_TEST_S3_ENDPOINT` définie ;
167
+ sans elle, les *specs* qui ont besoin d'un serveur sont ignorées. Le projet MinIO amont a cessé
168
+ de publier des images en 2025 : le *pipeline* exécute `pgsty/minio`, construit à partir des
169
+ mêmes sources, épinglé par *digest* (`pipeline/github/scripts/start-minio.sh`).
170
+
171
+ ```
172
+ docker run -d --name minio -p 127.0.0.1:9000:9000 -e MINIO_ROOT_USER=ddd-ts-kit -e MINIO_ROOT_PASSWORD=ddd-ts-kit-secret pgsty/minio:RELEASE.2026-08-04T00-00-00Z server /data
173
+ DDD_TS_KIT_TEST_S3_ENDPOINT=http://localhost:9000 npx vitest run packages/s3
174
+ ```
175
+
176
+ Un projet tient sa propre liaison à la même suite, sur le MinIO de sa *stack* compose : chaque
177
+ test écrit sous un préfixe à lui, si bien que les tests partagent un seul bucket sans voir les
178
+ fichiers des autres.
179
+
180
+ <!-- checked sample -->
181
+ ```ts
182
+ import { randomBytes } from 'node:crypto';
183
+ import { beforeEach, describe, expect, it } from 'vitest';
184
+ import { S3FileStorage, s3ClientOf } from '@ddd-ts-kit/s3';
185
+ import { fileStorageContract } from '@ddd-ts-kit/s3/testing';
186
+
187
+ const endpoint = process.env['S3_TEST_ENDPOINT'] ?? '';
188
+
189
+ // Ignoré sans serveur ; le bucket est créé au préalable, comme dans un déploiement.
190
+ fileStorageContract(
191
+ { describe: describe.runIf(endpoint !== ''), it, expect, beforeEach },
192
+ {
193
+ name: 'S3FileStorage on MinIO',
194
+ createStorage: () =>
195
+ new S3FileStorage({
196
+ client: s3ClientOf({ endpoint, region: 'us-east-1', accessKeyId: 'minio', secretAccessKey: 'minio-secret' }),
197
+ bucket: 'acme-files-test',
198
+ prefix: `run-${randomBytes(6).toString('hex')}/`,
199
+ }),
200
+ // Les liens d'un vrai serveur expirent en temps réel.
201
+ elapse: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
202
+ },
203
+ );
204
+ ```
205
+
206
+ ## Un autre fournisseur
207
+
208
+ Google Cloud Storage et Azure Blob Storage parlent leurs propres protocoles. Un connecteur pour
209
+ l'un d'eux est une classe qui étend `FileStorage` du noyau et passe `fileStorageContract` contre
210
+ le vrai service ou son émulateur (fake-gcs-server, Azurite) : voir
211
+ [Connecteurs de stockage de fichiers](../../docs/tools/file-storage.fr.md).
package/README.md CHANGED
@@ -1,9 +1,11 @@
1
+ **English** · [Français](README.fr.md)
2
+
1
3
  # @ddd-ts-kit/s3
2
4
 
3
5
  The kernel `FileStorage` on any provider of the S3 protocol: AWS S3, MinIO, OVHcloud Object
4
6
  Storage, Scaleway, Cloudflare R2, Garage. The content of a file streams to the bucket in parts,
5
7
  never whole in memory and never in the database; the aggregate keeps the `StoredFile` that `put`
6
- answers. Nothing in `domain/` or `application/` imports this package: handlers depend on the
8
+ returns. Nothing in `domain/` or `application/` imports this package: handlers depend on the
7
9
  `FileStorage` port, and the platform binds this adapter.
8
10
 
9
11
  ```
@@ -12,19 +14,44 @@ npm i @ddd-ts-kit/s3 @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request
12
14
 
13
15
  The three packages of the official AWS SDK v3 are optional peers, written against 3.1150.
14
16
 
17
+ ![The use cases and FileStaging depend on the FileStorage port of the kernel; S3FileStorage implements it, maps each key under its prefix and sends its commands through an S3Client built by s3ClientOf; the client uploads and downloads straight to the bucket with signed links; the bucket holds promoted files under their keys and staged files under staging/](https://raw.githubusercontent.com/gbourgeat/ddd-ts-kit/main/docs/assets/diagrams/s3-place-in-an-application.svg)
18
+
19
+ The use cases speak to the port with keys such as `appointment-documents/2026/10/7f3c`,
20
+ generated by `TenantFileKeys`, the `FileKeys` of the kernel: the area, the year, the month, then
21
+ an id from the `IdGenerator` (and `tenants/<tenant>/` before them when the work names a tenant).
22
+ The adapter puts its prefix before every key (`scheduling/appointment-documents/2026/10/7f3c` in
23
+ the bucket), and `FileStaging` puts `staging/` before the key of a file that waits for its commit
24
+ (`scheduling/staging/appointment-documents/2026/10/9a1b`). The bytes of a file go from the
25
+ client to the bucket, or from the API to the bucket in parts, and the database keeps only the
26
+ reference.
27
+
15
28
  ## Composition root
16
29
 
30
+ <!-- checked sample -->
17
31
  ```ts
32
+ import type { FileStorage } from '@ddd-ts-kit/kernel';
18
33
  import { S3FileStorage, s3ClientOf } from '@ddd-ts-kit/s3';
19
34
 
20
- const files = new S3FileStorage({
35
+ declare const config: {
36
+ readonly storage: {
37
+ readonly endpoint?: string;
38
+ readonly region: string;
39
+ readonly accessKeyId: string;
40
+ readonly secretAccessKey: string;
41
+ readonly bucket: string;
42
+ };
43
+ };
44
+
45
+ const { endpoint, region, accessKeyId, secretAccessKey, bucket } = config.storage;
46
+
47
+ export const files: FileStorage = new S3FileStorage({
21
48
  client: s3ClientOf({
22
- endpoint: config.storage.endpoint, // left out for AWS; http://localhost:9000 for MinIO
23
- region: config.storage.region, // 'auto' for R2
24
- accessKeyId: config.storage.accessKeyId,
25
- secretAccessKey: config.storage.secretAccessKey,
49
+ ...(endpoint === undefined ? {} : { endpoint }), // left out for AWS; http://localhost:9000 for MinIO
50
+ region, // 'auto' for R2
51
+ accessKeyId,
52
+ secretAccessKey,
26
53
  }),
27
- bucket: config.storage.bucket, // created by the infrastructure code, never by the application
54
+ bucket, // created by the infrastructure code, never by the application
28
55
  prefix: 'scheduling/', // optional: several applications in one bucket
29
56
  });
30
57
  ```
@@ -34,11 +61,54 @@ servers need it) and computes checksums only when an operation requires one: the
34
61
  SDK sends a checksum with every request, which several providers that are not AWS refuse, and
35
62
  which would bind a signed upload link to an empty body. A client built by hand works too.
36
63
 
64
+ The options of `S3FileStorage`:
65
+
66
+ | Option | Default | |
67
+ |---|---|---|
68
+ | `client` | required | an `S3Client`, from `s3ClientOf` or built by hand |
69
+ | `bucket` | required | it must exist: the adapter never creates one |
70
+ | `prefix` | none | a valid key ending with `/`, put before every key; the constructor refuses another |
71
+ | `partSize` | 8 MiB | the size of each part of an upload, at least 5 MiB: the most held in memory per upload |
72
+ | `clock` | the system clock | gives the `expiresAt` the signed links return |
73
+
74
+ A handler depends on the port, never on the SDK:
75
+
76
+ ```ts
77
+ // Instead of: the handler speaks S3, holds the whole file in memory, and names the bucket
78
+ const body = Buffer.concat(await Array.fromAsync(rows));
79
+ await s3.send(new PutObjectCommand({ Bucket: 'acme-files', Key: `exports/${id}.csv`, Body: body }));
80
+ ```
81
+
82
+ <!-- checked sample -->
83
+ ```ts
84
+ import { FileKeys, FileStorage, type StoredFile } from '@ddd-ts-kit/kernel';
85
+
86
+ // Write: the port of the kernel; the platform binds S3FileStorage to it, a test InMemoryFileStorage.
87
+ export class ExportAppointments {
88
+ constructor(
89
+ private readonly files: FileStorage,
90
+ private readonly keys: FileKeys,
91
+ ) {}
92
+
93
+ async execute(csv: AsyncIterable<Uint8Array>): Promise<{ readonly file: StoredFile; readonly link: string }> {
94
+ const key = this.keys.next('appointment-exports');
95
+
96
+ // Streams to the bucket in parts; beyond 50 MiB, FILE_TOO_LARGE, and nothing is left under the key.
97
+ const file = await this.files.put(key, csv, { mediaType: 'text/csv', maxBytes: 50 * 1_048_576 });
98
+
99
+ // A presigned GetObject, computed locally: no request reaches the bucket.
100
+ const { url } = await this.files.signedDownloadUrl(key, { expiresInSeconds: 300, fileName: 'appointments.csv' });
101
+
102
+ return { file, link: url };
103
+ }
104
+ }
105
+ ```
106
+
37
107
  ## What it does
38
108
 
39
109
  | Port method | On the bucket |
40
110
  |---|---|
41
- | `put(key, stream, { mediaType, maxBytes })` | a multipart upload of `partSize` parts (8 MiB by default, one in memory at a time); the content is counted and hashed on its way, and refused with `FILE_TOO_LARGE` beyond `maxBytes`, which aborts the upload and leaves the previous object |
111
+ | `put(key, stream, { mediaType, maxBytes })` | a multipart upload of `partSize` parts (8 MiB by default, one in memory at a time), or a single `PutObject` when the content fits in one part; the content is counted and hashed on its way, and refused with `FILE_TOO_LARGE` beyond `maxBytes`, which aborts the upload and leaves the previous object |
42
112
  | `get`, `stat` | `GetObject` as a stream, `HeadObject`; `undefined` when the object does not exist, a rejection when the service cannot be reached |
43
113
  | `delete` | `DeleteObject`, idempotent |
44
114
  | `copy`, `move` | `CopyObject` done by the service (and `DeleteObject` for a move), up to 5 GiB per object |
@@ -46,16 +116,41 @@ which would bind a signed upload link to an empty body. A client built by hand w
46
116
  | `signedDownloadUrl` | a presigned `GetObject` answered with `Content-Disposition: attachment`, with the file name when one is given |
47
117
  | `signedUploadUrl` | a presigned `PutObject` whose signature covers the `content-type`: another media type is refused by the service |
48
118
 
49
- The checksum (`sha256:<hex>`) is answered by `put`, computed while the content streams; the
50
- bucket does not keep it, so `stat` does not answer one. A file uploaded through a signed link
119
+ The checksum (`sha256:<hex>`) is returned by `put`, computed while the content streams; the
120
+ bucket does not keep it, so `stat` does not return one. A file uploaded through a signed link
51
121
  is measured, checked and hashed by `FileStaging.confirm` of the kernel, which reads it back.
52
122
 
123
+ A key the port refuses (`../escape`) is refused with `INVALID_FILE_KEY`, and a link asked for
124
+ more than an hour with `INVALID_FILE_URL_EXPIRY`, before any request. Signing needs no request
125
+ either: a signed link is computed from the credentials, and only the provider checks it, when
126
+ the client uses it.
127
+
128
+ ![An upload through a signed link: the client asks for a link, FileStaging.uploadUrl has S3FileStorage sign a PUT locally and keeps the grant on the aggregate, the client sends the file straight to the bucket, which checks the key, method, content-type and expiry but cannot cap the size; then the client confirms, and FileStaging.confirm reads the staged object back to check its size against the cap, its media type, its checksum and the scan](https://raw.githubusercontent.com/gbourgeat/ddd-ts-kit/main/docs/assets/diagrams/s3-signed-upload.svg)
129
+
130
+ The client sends the file exactly as the link says, with the headers it was given:
131
+
132
+ <!-- checked sample -->
133
+ ```ts
134
+ import type { SignedUpload } from '@ddd-ts-kit/kernel';
135
+
136
+ // In the client: the signature covers the content-type, so the headers are sent as they are.
137
+ export async function sendFile(link: SignedUpload, file: Blob): Promise<void> {
138
+ const response = await fetch(link.url, { method: link.method, headers: link.headers, body: file });
139
+
140
+ if (!response.ok) {
141
+ throw new Error(`The storage refused the upload: ${response.status}`); // 403 once expired, or for another type
142
+ }
143
+ }
144
+ ```
145
+
53
146
  ## Limits
54
147
 
55
148
  - A signed upload link cannot cap the size of what is sent (only a presigned POST policy can):
56
149
  its `maxBytes` is not enforced by the service, and `FileStaging.confirm` measures the file
57
150
  against the cap of the grant the server kept and discards it beyond.
58
151
  - Copy and move go through `CopyObject`, limited to 5 GiB per object by the protocol.
152
+ - An upload has at most 10 000 parts: with parts of 8 MiB, a file of about 78 GiB. Raise
153
+ `partSize` for larger files.
59
154
  - Lifecycle rules, versioning, object lock and the encryption of the bucket are settings of the
60
155
  bucket, made by the infrastructure code of the deployment, not by this adapter.
61
156
 
@@ -73,6 +168,36 @@ docker run -d --name minio -p 127.0.0.1:9000:9000 -e MINIO_ROOT_USER=ddd-ts-kit
73
168
  DDD_TS_KIT_TEST_S3_ENDPOINT=http://localhost:9000 npx vitest run packages/s3
74
169
  ```
75
170
 
171
+ A project holds its own binding to the same suite, on the MinIO of its compose stack: each test
172
+ writes under a prefix of its own, so the tests share one bucket without seeing each other's
173
+ files.
174
+
175
+ <!-- checked sample -->
176
+ ```ts
177
+ import { randomBytes } from 'node:crypto';
178
+ import { beforeEach, describe, expect, it } from 'vitest';
179
+ import { S3FileStorage, s3ClientOf } from '@ddd-ts-kit/s3';
180
+ import { fileStorageContract } from '@ddd-ts-kit/s3/testing';
181
+
182
+ const endpoint = process.env['S3_TEST_ENDPOINT'] ?? '';
183
+
184
+ // Skipped without a server; the bucket is created beforehand, as in a deployment.
185
+ fileStorageContract(
186
+ { describe: describe.runIf(endpoint !== ''), it, expect, beforeEach },
187
+ {
188
+ name: 'S3FileStorage on MinIO',
189
+ createStorage: () =>
190
+ new S3FileStorage({
191
+ client: s3ClientOf({ endpoint, region: 'us-east-1', accessKeyId: 'minio', secretAccessKey: 'minio-secret' }),
192
+ bucket: 'acme-files-test',
193
+ prefix: `run-${randomBytes(6).toString('hex')}/`,
194
+ }),
195
+ // The links of a real server expire in real time.
196
+ elapse: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
197
+ },
198
+ );
199
+ ```
200
+
76
201
  ## Another provider
77
202
 
78
203
  Google Cloud Storage and Azure Blob Storage speak their own protocols. A connector for one of
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ddd-ts-kit/s3",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "description": "Adapter of the kernel FileStorage port on the S3 protocol (AWS S3, MinIO, OVHcloud, Scaleway, Cloudflare R2): streamed uploads, signed links, copy, move, listing.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,13 +30,13 @@
30
30
  "dist"
31
31
  ],
32
32
  "dependencies": {
33
- "@ddd-ts-kit/kernel": "0.16.0"
33
+ "@ddd-ts-kit/kernel": "0.16.1"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "@aws-sdk/client-s3": "^3.1150.0",
37
37
  "@aws-sdk/lib-storage": "^3.1150.0",
38
38
  "@aws-sdk/s3-request-presigner": "^3.1150.0",
39
- "@ddd-ts-kit/testing": "0.16.0"
39
+ "@ddd-ts-kit/testing": "0.16.1"
40
40
  },
41
41
  "peerDependenciesMeta": {
42
42
  "@aws-sdk/client-s3": {
@@ -56,6 +56,6 @@
56
56
  "@aws-sdk/client-s3": "3.1150.0",
57
57
  "@aws-sdk/lib-storage": "3.1150.0",
58
58
  "@aws-sdk/s3-request-presigner": "3.1150.0",
59
- "@ddd-ts-kit/testing": "0.16.0"
59
+ "@ddd-ts-kit/testing": "0.16.1"
60
60
  }
61
61
  }