@inwink-mcp/backoffice-extension-helpers 1.0.9 → 1.0.11

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inwink-mcp/backoffice-extension-helpers",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "author": "inwink",
5
5
  "license": "ISC",
6
6
  "description": "CLI pour bootstrapper, packager et vérifier les extensions backoffice inwink.",
@@ -100,6 +100,7 @@ Bonnes pratiques :
100
100
  - Le 429 (rate limiting) est géré automatiquement par les hooks : rejeu avec backoff exponentiel respectant l'en-tête `Retry-After` (cf. `useInwinkApi.ts`). Ne pas le ré-implémenter.
101
101
  - Au-delà de 429, une réponse non-OK lève une `InwinkApiError` (`status`, `body`) — à présenter via `ErrorState` ou un toast.
102
102
  - Pour propager l'annulation, passer le `signal` reçu de `useAsync` dans `init` : `eventApi.get(path, { signal })`.
103
+ - Verbes disponibles : `get`, `post`, `put`, `patch`, `del`, plus `request` pour le reste. Une réponse OK sans corps — un 204, ou un 200 comme en renvoient les écritures de template d'entité — résout sur `undefined`.
103
104
  - Ne déclarer dans `permissions.platform` que les scopes réellement utilisés.
104
105
  - Déclarer les hôtes externes dans `permissions.externalHosts` (HTTPS uniquement).
105
106
 
@@ -16,6 +16,7 @@ This document explains how to consume inwink APIs. It is intended as a reference
16
16
  | Import en masse (bulk create/update) | [import.md](./import.md) |
17
17
  | Contrats & schémas d'entités, endpoints divers | [entities.md](./entities.md) |
18
18
  | Champs de type `file` (upload / download) | [files.md](./files.md) |
19
+ | Manipuler les `assets` (galerie media) | [assets.md](./assets.md) |
19
20
 
20
21
  ---
21
22
 
@@ -0,0 +1,236 @@
1
+ ◂ Retour à [README](./README.md)
2
+
3
+ # InWink APIs — Assets (guide d'utilisation de l'API)
4
+
5
+ > **But.** Décrire comment **créer**, **rechercher**, **modifier**, **supprimer**, **classer**
6
+ > et **référencer** des assets via les APIs d'administration InWink (EventAPI / CommunityAPI /
7
+ > CustomerAPI). Tout est décrit au niveau HTTP : verbe + route + corps JSON + réponse JSON.
8
+ >
9
+ > Auth : jeton porteur (Bearer) avec le claim `{scope}asset` (valeur `read` en lecture,
10
+ > `edit` en écriture). Le flux d'upload de fichier réutilise celui décrit dans
11
+ > la section `file-fields` — s'y référer pour le détail du lien d'upload signé.
12
+ >
13
+ > Convention JSON : corps de requête acceptés en casse insensible ; réponses en `camelCase`.
14
+
15
+ ---
16
+
17
+ ## 1. Principe (à lire en premier)
18
+
19
+ Un **asset** est une entrée de bibliothèque médias, scopée à un événement, une communauté ou
20
+ un tenant. Contrairement à un simple champ fichier, un asset a une identité propre, un titre,
21
+ un type, une miniature, peut être rangé dans un dossier et **référencé par d'autres contenus**
22
+ via le schéma `inwinkasset://`.
23
+
24
+ **Forme d'un asset (en lecture)** :
25
+
26
+ ```json
27
+ {
28
+ "id": "3fa85f64-....",
29
+ "title": "Logo principal", // localisable (peut être un objet multi-langues)
30
+ "assetType": 0, // voir table ci-dessous
31
+ "file": { "blobName": "...", "container": "...", "url": "...", "mimeType": "...", "size": 123 },
32
+ "thumbnail": { ... }, // objet fichier ou null
33
+ "folderId": "....", // ou null
34
+ "createdAt": "2026-07-01T10:00:00Z",
35
+ "applicationScope": "...",
36
+ "applicationScopeId": "..."
37
+ }
38
+ ```
39
+
40
+ **Types d'asset (`assetType`)** : `0`=Image, `1`=Video, `2`=Document, `3`=Audio, `4`=Font, `5`=Model3D.
41
+
42
+ > ⚠️ **Le type d'asset n'est jamais dans le corps de requête** : il est déterminé par la
43
+ > **route typée** sur laquelle on POST (`image-asset` → Image, `video-asset` → Video, …).
44
+
45
+ Les champs `file` et `thumbnail` sont alimentés par le **même flux d'upload temporaire** que
46
+ les champs fichier (voir la section `file-fields`) : on passe le **nom du blob temporaire** à la
47
+ création, et l'API le transfère puis le sérialise en objet fichier.
48
+
49
+ ---
50
+
51
+ ## 2. Routes de base par API
52
+
53
+ | API | Base CRUD typée | Base recherche (tous types) |
54
+ |-----|-----------------|-----------------------------|
55
+ | EventAPI | `{eventId}/{type}-asset` | `{eventId}/asset` |
56
+ | CommunityAPI | `{communityId}/{type}-asset` | `{communityId}/asset` |
57
+ | CustomerAPI | `{customerId}/audience/{tenantId}/{type}-asset` | `.../asset` |
58
+
59
+ `{type}` ∈ `image`, `video`, `audio`, `document`, `font`, `model3d` (selon l'API — EventAPI
60
+ expose les 6, CommunityAPI 4, CustomerAPI 3).
61
+
62
+ Dans la suite, `{base}` = la base CRUD typée (ex. `{eventId}/image-asset`).
63
+
64
+ ---
65
+
66
+ ## 3. Créer un asset (upload + création)
67
+
68
+ ### Étape 1 — Téléverser le fichier (flux temporaire)
69
+
70
+ Les endpoints d'upload sont disponibles sous la base de l'asset :
71
+
72
+ ```
73
+ POST {base}/file { "type": "image/png", "name": "logo.png" } → objet d'accès temporaire
74
+ (puis PUT des octets sur url+container+blobName+sas — voir section file-fields §2)
75
+ POST {base}/file/renew { "blobName": "...", "sas": "..." } → renouvellement
76
+ POST {base}/file/try-renew { "blobName": "...", "sas": "..." } → renouvellement si expiré
77
+ POST {base}/file/finalize-tmp { "blobName": "...", "sas": "..." } → 200
78
+ ```
79
+
80
+ Répéter pour la miniature si on en fournit une.
81
+
82
+ ### Étape 2 — Créer l'asset en référençant les blobs temporaires
83
+
84
+ ```
85
+ POST {base}
86
+ Content-Type: application/json
87
+
88
+ {
89
+ "title": "Logo principal",
90
+ "file": "<blobName_du_fichier>", // nom du blob temporaire (obligatoire)
91
+ "thumbnail": "<blobName_miniature>", // optionnel
92
+ "folderId": "3fa85f64-....", // optionnel
93
+ "applicationScope": "...",
94
+ "applicationScopeId": "..."
95
+ }
96
+ ```
97
+
98
+ Réponse `200` — l'asset créé (voir forme §1). L'API transfère le blob temporaire vers son
99
+ emplacement définitif et remplit `file` / `thumbnail` avec l'objet fichier résultant.
100
+
101
+ ---
102
+
103
+ ## 4. Rechercher / compter
104
+
105
+ ```
106
+ POST {base}/query
107
+ {
108
+ "applicationScope": "...",
109
+ "applicationScopeId": "...", // optionnel
110
+ "folderId": "3fa85f64-....", // optionnel — la présence de la clé active le filtre par dossier
111
+ "id": "....", // optionnel — un asset précis
112
+ "search": "logo", // optionnel — recherche dans le titre
113
+ "fileUrl": "...", // optionnel — recherche par URL de fichier
114
+ "page": { "index": 0, "size": 20 }, // ou { "skip": 0, "take": 20 }
115
+ "orders": [ { "desc": true, "value": { "createdAt": {} } } ]
116
+ }
117
+ → 200 liste paginée d'assets
118
+ ```
119
+
120
+ ```
121
+ POST {base}/count (même corps) → 200 <nombre entier>
122
+ ```
123
+
124
+ Le filtrage inclut toujours le scope de l'application et le type de la route.
125
+
126
+ > Pour lister **tous types confondus**, utiliser la base recherche `{scopeId}/asset/query`.
127
+
128
+ ---
129
+
130
+ ## 5. Modifier / supprimer / déplacer
131
+
132
+ ```
133
+ POST {base}/{assetId} (édition partielle)
134
+ ```
135
+ - Champs absents = inchangés. `file`/`thumbnail` optionnels (omettre = conserver).
136
+ - `thumbnail` **présent mais vide** = supprime la miniature.
137
+ - `folderId`/`title` mis à jour seulement si présents dans le corps.
138
+
139
+ ```
140
+ POST {base}/delete/{assetId} → 200
141
+ POST {base}/delete { "assetIds": ["...", "..."] } → 200 (suppression en masse)
142
+ POST {base}/movetofolder { "assetIds": ["..."], "folderId": "..." } → 200
143
+ ```
144
+
145
+ La suppression retire aussi les fichiers (principal + miniature + cache).
146
+
147
+ ---
148
+
149
+ ## 6. Dossiers d'assets
150
+
151
+ Un dossier a la forme `{ name, parentFolderId?, assetType }` et est hiérarchique.
152
+
153
+ - **Le CRUD des dossiers passe par les endpoints d'entité génériques** (pas par les routes
154
+ d'assets) : création / recherche / suppression standard sur `{scopeId}/{folderEntity}`
155
+ (ex. `{eventId}/eventassetfolder`, `{communityId}/communityassetfolder`,
156
+ `{customerId}/audience/{tenantId}/authtenantassetfolder`).
157
+ - **Route spécifique** — supprimer un dossier en **conservant** son contenu (sous-dossiers/assets) :
158
+ ```
159
+ POST {scopeId}/{folderEntity}/delete/{folderId}/keepSubContent
160
+ ```
161
+ - **Contrainte** : le dossier ciblé lors d'une création/déplacement doit exister et avoir le
162
+ **même type** que l'asset, sinon `400` (« Folder type does not match asset type »).
163
+
164
+ ---
165
+
166
+ ## 7. Référencer un asset depuis un contenu (`inwinkasset://`)
167
+
168
+ Pour lier un asset à un autre contenu (typiquement le contenu d'un ContentTemplate / page
169
+ builder), on **stocke une chaîne de référence** dans le champ concerné :
170
+
171
+ ```
172
+ inwinkasset://{assetId} // asset du scope courant
173
+ inwinkasset://{assetId}/{shardId} // asset d'un autre tenant (cross-tenant)
174
+ ```
175
+
176
+ **Ne pas** y écrire un objet fichier ni une URL directe : juste la chaîne `inwinkasset://{id}`.
177
+
178
+ À la lecture du contenu, l'API remplace automatiquement chaque référence par **l'objet asset
179
+ complet** (`{ id, tenantId, title, assetType, thumbnail, file, applicationScope, validFrom }`).
180
+ Les références **non résolvables sont retirées** de la sortie. Lors d'une copie d'événement,
181
+ les références sont remappées vers les nouveaux identifiants.
182
+
183
+ ---
184
+
185
+ ## 8. Livraison publique (lecture anonyme)
186
+
187
+ Le binaire d'un asset est servi et redimensionné en accès public :
188
+
189
+ ```
190
+ GET event/{eventId}/asset/{assetId}/file[/{language}/{fileName}]
191
+ GET event/{eventId}/asset/{assetId}/thumbnail[/{language}/{fileName}]
192
+ (préfixes équivalents : tenant/{tenantId}/..., community/{communityId}/...)
193
+ ```
194
+
195
+ Paramètres de requête (optionnels) : `mw` (largeur max), `mh` (hauteur max), `q` (qualité),
196
+ `b` (flou), `o` (format de sortie), `nocache`. Les images sont redimensionnées à la volée
197
+ (cache 30 jours + ETag). Les fichiers non-image sont streamés ou **redirigés (302)** vers le CDN.
198
+
199
+ ---
200
+
201
+ ## 9. Exemples
202
+
203
+ - **Recherche** :
204
+ ```
205
+ POST {eventId}/image-asset/query
206
+ { "orders": [ { "desc": true, "value": { "createdAt": {} } } ] }
207
+ → 200 liste d'assets
208
+ ```
209
+ - **Recherche par dossier** : ajouter `"folderId": "<guid>"` au corps.
210
+ - **Création** (après upload du fichier temporaire) :
211
+ ```
212
+ POST {eventId}/image-asset
213
+ { "title": "Bannière", "file": "<blobName>", "applicationScope": "...", "applicationScopeId": "..." }
214
+ → 200 asset créé
215
+ ```
216
+ - **Téléchargement public** : `GET event/{eventId}/asset/{assetId}/file` → `200` (image) ou
217
+ `302` (redirection, ex. vidéo).
218
+ - **Suppression de dossier en conservant le contenu** :
219
+ `POST {eventId}/eventassetfolder/delete/{folderId}/keepSubContent`.
220
+
221
+ ---
222
+
223
+ ## 10. Points d'attention
224
+
225
+ - **Pas de contrôle du MIME par type d'asset** à la création : l'API vérifie seulement que le
226
+ contenu correspond à son MIME déclaré (parmi les MIME supportés), pas qu'il correspond au
227
+ type de la route. Le type d'asset ne change que l'emplacement de stockage et le rendu public
228
+ (seules les images sont redimensionnées ; les autres sont streamées/redirigées).
229
+ - **Busting de cache** : à chaque changement de fichier, un nouvel emplacement est généré,
230
+ donc l'URL du fichier change — ne pas mettre en cache un objet fichier d'asset au-delà d'une édition.
231
+ - **`title`** peut être localisé (objet multi-langues).
232
+ - **`file` obligatoire à la création**, `thumbnail` optionnel ; à l'édition les deux sont optionnels.
233
+
234
+ ---
235
+
236
+ *Voir aussi : `file-fields` (détail du flux d'upload temporaire réutilisé ici), `mutations`, `entities`.*
@@ -1,189 +1,313 @@
1
1
  ◂ Retour à [README](./README.md)
2
+ # InWink APIs — Champs de type fichier (guide d'utilisation de l'API)
3
+
4
+ > **But.** Décrire comment **envoyer**, **lire**, **remplacer** et **supprimer** un fichier
5
+ > porté par un champ d'entité, via les APIs d'administration InWink (EventAPI / CommunityAPI /
6
+ > CustomerAPI). Tout est décrit au niveau HTTP : verbe + route + corps JSON + réponse JSON.
7
+ >
8
+ > Auth : jeton porteur (Bearer) avec les claims du scope. La FrontAPI publique (participant,
9
+ > anonyme) a des routes différentes et n'est pas couverte (note en fin de document).
10
+ >
11
+ > Convention JSON : les corps de requête sont acceptés en casse insensible ; les réponses
12
+ > sont en `camelCase`. Les exemples ci-dessous utilisent `camelCase`.
2
13
 
3
- # Manipuler les fichiers d'entites (champs de type `file`)
14
+ ---
4
15
 
5
- ## Regle d'architecture
16
+ ## 1. Principe (à lire en premier)
6
17
 
7
- Une extension backoffice ne doit pas appeler le serveur MCP en runtime depuis le navigateur. Le MCP sert uniquement a Claude Code / l'assistant IA pour decouvrir les endpoints et les schemas pendant le developpement. Le code final de l'extension doit appeler directement les APIs inwink avec le Bearer token utilisateur.
18
+ Un « champ de type fichier » est un champ d'entité (ex. `prop3` sur `person`,
19
+ `videofile` sur `vod`, `audiofile` sur `podcast`…) dont le type déclaré dans le template
20
+ est l'un de :
8
21
 
9
- Workflow a suivre pendant la generation du code :
22
+ | Type de champ | Effet |
23
+ |---------------|-------|
24
+ | `file` | fichier générique (document, etc.) |
25
+ | `image` | image (miniatures générées automatiquement) |
26
+ | `filevideo` | vidéo (encodage + transcripts déclenchés) |
10
27
 
11
- 1. Utiliser `inwink_mcp_get_entity_description` pour identifier le champ de type `file` et sa forme exacte.
12
- 2. Utiliser `inwink_mcp_find_api_endpoint` pour trouver l'endpoint d'upload ou de renouvellement SAS adapte au type de fichier.
13
- 3. Generer dans l'extension des appels directs aux APIs inwink, jamais des appels au MCP.
28
+ **La valeur du champ change de forme selon le sens :**
14
29
 
15
- ---
30
+ - **En écriture** : une simple **chaîne = le nom du blob temporaire** (`blobName`) obtenu à
31
+ l'étape d'upload.
32
+ - **En lecture** : un **objet fichier** JSON complet
33
+ (`{ blobName, container, url, fileName, mimeType, size, ... }`).
34
+
35
+ **L'upload suit un pattern « lien d'upload direct » en 4 temps.** L'API n'est jamais dans le
36
+ chemin des octets : elle délivre une URL d'upload signée (SAS), et **le client envoie les
37
+ octets directement sur le stockage**.
38
+
39
+ ```
40
+ 1. POST .../file → l'API crée un emplacement temporaire, renvoie une URL signée
41
+ 2. PUT des octets → le client téléverse directement sur l'URL signée
42
+ 3. POST .../file/finalize-tmp → l'API valide et fige le fichier temporaire
43
+ 4. PATCH/PUT de l'entité → on met la valeur du champ = nom du blob ; l'API le déplace
44
+ vers son emplacement définitif et réécrit la valeur en objet fichier
45
+ ```
46
+
47
+ > ⚠️ **Point crucial** : il n'existe **aucun endpoint « attacher un fichier »**. On référence
48
+ > le nom du blob temporaire dans le champ, puis **on sauvegarde l'entité via son endpoint
49
+ > d'édition habituel**. C'est cette sauvegarde qui matérialise le fichier.
16
50
 
17
- ## Lire ou telecharger un fichier prive
51
+ ### Forme des routes
18
52
 
19
- ### Problème
53
+ Le point d'entrée fichier d'une entité est `{scopeId}/{entity}/file`. Exemples :
54
+ `{eventId}/person/file`, `{eventId}/session/file`, `{communityId}/vod/file`.
20
55
 
21
- Les champs de type `file` retournés par l'API inwink (par exemple `url` dans `ExhibitorTaskAnswer`, `photo` dans `Person`, etc.) contiennent une URL Azure Blob Storage directe (ex. `https://[account].blob.core.windows.net/[container]/[path]`). Ces blobs sont **privés** : ils nécessitent une authentification Azure StorageSharedKey ou un token SAS signé par la clé du compte de stockage.
56
+ `{scopeId}` selon l'API :
57
+ - EventAPI : `{eventId}`
58
+ - CommunityAPI : `{communityId}`
59
+ - CustomerAPI : `{customerId}/audience/{tenantId}` (ou `{customerId}`)
22
60
 
23
- **Ce qui ne fonctionne pas** : envoyer le token Bearer inwink (`Authorization: Bearer …`) directement sur l'URL du blob. Azure Blob Storage ne reconnaît pas les JWT inwink — il retourne `403 Forbidden`.
61
+ ---
24
62
 
25
- ### Solution : endpoint `try-renew`
63
+ ## 2. Envoyer un fichier dans un champ d'entité (flux complet)
26
64
 
27
- L'API inwink expose des endpoints de renouvellement SAS pour chaque type d'asset. Pour la majorité des fichiers uploadés (PDF, Word, images, ZIP…), utiliser :
65
+ ### Étape 1 — Créer l'emplacement temporaire
28
66
 
29
67
  ```
30
- POST {eventAPI}/{eventId}/document-asset/file/try-renew
31
- Authorization: Bearer {inwink_token}
68
+ POST {scopeId}/{entity}/file
32
69
  Content-Type: application/json
33
70
 
34
71
  {
35
- "BlobName": "{container}/{path/to/blob}",
36
- "Sas": "{existing_sas_query_string_or_empty_string}"
72
+ "entity": "person", // nom de l'entité
73
+ "field": "prop3", // clé du champ fichier
74
+ "type": "image/png", // type MIME du fichier
75
+ "name": "photo.png", // nom de fichier
76
+ "blobType": "Block" // "Block" (défaut) | "append" | "page"
37
77
  }
38
78
  ```
39
79
 
40
- - **`BlobName`** : le chemin du blob **sans le hostname ni le `?`**. Correspond à `url.pathname.slice(1)` sur l'URL Azure.
41
- - **`Sas`** : le query string existant **sans le `?`** (`url.search.slice(1)`), ou `""` si l'URL ne contient pas de SAS.
80
+ Réponse `200` — objet d'accès temporaire :
42
81
 
43
- Le serveur inwink (qui possède la clé du compte de stockage) retourne une **URL signée SAS** fraîche. Cette URL peut ensuite être utilisée **sans aucun header `Authorization`** — le token SAS est intégré dans les query params.
82
+ ```json
83
+ {
84
+ "url": "https://<stockage>.blob.core.windows.net",
85
+ "container": "tmp",
86
+ "blobName": "a7f3c1e0-....png",
87
+ "sas": "?sv=...&sig=..."
88
+ }
89
+ ```
44
90
 
45
- ### Endpoints disponibles selon le type de fichier
91
+ ### Étape 2 — Téléverser les octets directement (hors API)
46
92
 
47
- | Type de fichier | Endpoint |
48
- |---|---|
49
- | Documents (PDF, Word, ZIP…) | `/{eventId}/document-asset/file/try-renew` |
50
- | Images | `/{eventId}/image-asset/file/try-renew` |
51
- | Vidéos | `/{eventId}/video-asset/file/try-renew` |
52
- | Audio | `/{eventId}/audio-asset/file/try-renew` |
93
+ Le client construit l'URL cible et **envoie les octets** dessus (droits lecture/écriture sur
94
+ l'emplacement temporaire) :
53
95
 
54
- En cas de doute sur le type, utiliser `document-asset` en premier (fonctionne pour la majorité des uploads exposants).
96
+ ```
97
+ PUT {url}/{container}/{blobName}{sas}
98
+ x-ms-blob-type: BlockBlob
99
+ Content-Type: image/png
55
100
 
56
- ### Pattern d'implémentation
101
+ <octets du fichier>
102
+ ```
57
103
 
58
- ```typescript
59
- async function getSignedFileUrl(absoluteUrl: string, eventId: string): Promise<string> {
60
- const parsed = new URL(absoluteUrl);
61
- const blobName = parsed.pathname.slice(1); // ex: "events/xxx/file.pdf"
62
- const sas = parsed.search ? parsed.search.slice(1) : '';
104
+ Pour un upload long, renouveler le lien signé si besoin :
63
105
 
64
- try {
65
- const result = await inwinkApiService.makeApiCall<any>(
66
- `/${eventId}/document-asset/file/try-renew`,
67
- { method: 'POST', body: JSON.stringify({ BlobName: blobName, Sas: sas }) }
68
- );
69
- if (typeof result === 'string' && result.startsWith('http')) return result;
70
- if (result?.url) return result.url;
71
- if (result?.Url) return result.Url;
72
- } catch { /* fallback sur l'URL d'origine */ }
106
+ ```
107
+ POST {scopeId}/{entity}/file/renew { "blobName": "...", "sas": "..." } → nouvel objet d'accès
108
+ POST {scopeId}/{entity}/file/try-renew { "blobName": "...", "sas": "..." } → renouvelé si expiré
109
+ ```
73
110
 
74
- return absoluteUrl;
75
- }
111
+ ### Étape 3 — Finaliser le fichier temporaire
76
112
 
77
- // Puis télécharger SANS header Authorization :
78
- const signedUrl = await getSignedFileUrl(fileField.url, eventId);
79
- const response = await fetch(signedUrl); // pas de Bearer token ici
113
+ ```
114
+ POST {scopeId}/{entity}/file/finalize-tmp
115
+ { "blobName": "a7f3c1e0-....png", "sas": "?sv=...&sig=..." }
80
116
  ```
81
117
 
82
- ---
118
+ - `200` : le contenu correspond au type MIME déclaré ; le fichier temporaire est figé.
119
+ - `400` avec corps `101` : le contenu ne correspond pas au MIME annoncé (rejet).
120
+ - `400` avec corps `102` : type de fichier non supporté.
83
121
 
84
- ## Uploader puis associer un fichier a une entite
122
+ ### Étape 4 — Sauvegarder l'entité en référençant le fichier
85
123
 
86
- L'upload d'un nouveau fichier se fait en deux etapes :
124
+ Via l'**endpoint d'édition habituel de l'entité**, mettre la valeur du champ = le `blobName` :
87
125
 
88
- 1. Uploader le binaire sur l'endpoint d'asset approprie (`document-asset`, `image-asset`, `video-asset`, `audio-asset`). L'endpoint exact et le nom du champ multipart doivent etre recuperes avec `inwink_mcp_find_api_endpoint` avant de coder.
89
- 2. Patcher l'entite avec le `BLOBID` retourne par l'upload. Ne pas envoyer le contenu binaire, du base64, ni un objet `{ url: ... }` dans le payload de l'entite sauf si l'endpoint decouvert l'exige explicitement.
126
+ ```
127
+ PATCH {scopeId}/person/{personId}
128
+ { "prop3": "a7f3c1e0-....png" }
129
+ ```
90
130
 
91
- Requetes MCP a utiliser avant de generer le code :
131
+ À la sauvegarde, l'API valide (taille, MIME autorisés, cohérence du contenu), **déplace le
132
+ fichier vers son emplacement définitif**, **réécrit la valeur** du champ en objet fichier
133
+ complet, et déclenche les traitements associés (encodage vidéo, miniatures, analyse de document).
92
134
 
93
- ```text
94
- inwink_mcp_find_api_endpoint:
95
- targetApi: eventapi
96
- shardId: {eventId}
97
- query: "upload an image asset file and return the file reference"
135
+ La valeur relue devient par exemple :
98
136
 
99
- inwink_mcp_get_entity_description:
100
- targetApi: eventapi
101
- shardId: {eventId}
102
- query: "person photo file field"
137
+ ```json
138
+ {
139
+ "blobName": "prop3/9f...png",
140
+ "container": "3fa85f64-...._person-prop3",
141
+ "url": "https://<stockage>.blob.core.windows.net/...",
142
+ "fileName": "photo.png",
143
+ "mimeType": "image/png",
144
+ "size": 20481,
145
+ "contentMD5": "..."
146
+ }
103
147
  ```
104
148
 
105
- Pattern TypeScript, a adapter aux champs exacts retournes par la discovery :
106
-
107
- ```typescript
108
- async function uploadImageAsset(eventId: string, file: File) {
109
- const form = new FormData();
110
- form.append('file', file, file.name);
111
-
112
- const uploaded = await inwinkApiService.makeApiCall<any>(
113
- `/${eventId}/image-asset/file`,
114
- {
115
- method: 'POST',
116
- body: form,
117
- // Ne pas forcer Content-Type ici : le navigateur ajoute le boundary multipart/form-data.
118
- },
119
- );
149
+ ---
120
150
 
121
- // Adapter la lecture ci-dessous a la forme exacte retournee par l'endpoint.
122
- return uploaded?.blobId ?? uploaded?.BlobId ?? uploaded?.id ?? uploaded;
123
- }
151
+ ## 3. Lire / télécharger un fichier
124
152
 
125
- async function updatePersonPhoto(eventId: string, personId: string, file: File) {
126
- const uploadedBlobId = await uploadImageAsset(eventId, file);
127
-
128
- return inwinkApiService.makeApiCall<any>(
129
- `/${eventId}/person/${personId}`,
130
- {
131
- method: 'POST',
132
- body: JSON.stringify({
133
- entity: {
134
- // Pour un champ file, l'API attend ici le BLOBID retourne par l'upload.
135
- photo: uploadedBlobId,
136
- },
137
- selects: {
138
- $all: true,
139
- photo: true,
140
- },
141
- }),
142
- },
143
- );
144
- }
153
+ ```
154
+ GET {scopeId}/{entity}/file/{entityId}/{field}?lang=fr&withoutExpiration=false
145
155
  ```
146
156
 
147
- Exemple JSON correct pour associer un fichier a une entite :
157
+ Réponse `200` — objet fichier avec lien d'accès :
148
158
 
149
159
  ```json
150
160
  {
151
- "entity": {
152
- "photo": "BLOBID"
153
- },
154
- "selects": {
155
- "$all": true
156
- }
161
+ "url": "https://<stockage>.blob.core.windows.net",
162
+ "container": "3fa85f64-...._person-prop3",
163
+ "blobName": "prop3/9f...png",
164
+ "fileName": "photo.png",
165
+ "mimeType": "image/png",
166
+ "size": 20481,
167
+ "sas": "?sv=...&sig=...",
168
+ "lastModified": "2026-07-01T10:00:00Z"
157
169
  }
158
170
  ```
159
171
 
160
- Exemple a ne pas faire :
172
+ Le client télécharge ensuite les octets directement sur **`url` + `/{container}/{blobName}` + `sas`**.
173
+
174
+ - `404` si le champ n'a pas de fichier.
175
+ - `lang` : langue pour un champ localisé (défaut = langue par défaut du scope).
176
+ - `withoutExpiration=true` : lien d'accès de longue durée.
177
+
178
+ **Fichier d'une sous-entité (collection)** :
179
+ ```
180
+ GET {scopeId}/{entity}/file/{entityId}/{nestedField}/{index}/{field}?lang=&withoutExpiration=
181
+ ```
182
+
183
+ **Champ vidéo** : la réponse est enrichie (voir §5).
184
+
185
+ ---
186
+
187
+ ## 4. Remplacer / ré-éditer / supprimer un fichier
188
+
189
+ **Récupérer une copie éditable** du fichier actuel dans l'espace temporaire, puis reprendre
190
+ le flux finalize + save :
191
+ ```
192
+ GET {scopeId}/{entity}/file/{entityId}/{field}/copytotmp?lang=fr
193
+ → 200 { "blobName": "...", "container": "tmp", ... }
194
+ ```
195
+
196
+ **Remplacer** : téléverser un nouveau fichier temporaire (§2) et ré-éditer l'entité avec le
197
+ nouveau `blobName`. L'ancien fichier est automatiquement retiré.
198
+
199
+ **Supprimer** : éditer l'entité en mettant le champ à `null`.
200
+
201
+ ---
202
+
203
+ ## 5. Champs vidéo — fonctionnalités avancées
204
+
205
+ Les champs `filevideo` déclenchent un encodage et exposent transcripts et analyse de document.
206
+ Toutes les routes sont sous `{scopeId}/{entity}/file/...`.
207
+
208
+ ### Lecture d'un champ vidéo
209
+ `GET {scopeId}/{entity}/file/{entityId}/{field}` renvoie, quand la vidéo est prête :
161
210
 
162
211
  ```json
163
212
  {
164
- "entity": {
165
- "photo": {
166
- "url": "..."
167
- }
168
- }
213
+ "url": "...", "blobName": "...", "container": "...", "fileName": "...",
214
+ "sas": "?...", "mimeType": "video/mp4", "size": 123456,
215
+ "token": null,
216
+ "transcripts": { "fr": { "url": "...", "sas": "?..." } },
217
+ "transcriptsEnabled": true,
218
+ "dashEnabled": true,
219
+ "hlsEnabled": true
169
220
  }
170
221
  ```
171
222
 
172
- Important : l'URL d'upload ci-dessus est un exemple de pattern. Toujours remplacer par l'endpoint exact retourne par `inwink_mcp_find_api_endpoint`. Selon le type d'asset ou l'API cible, le chemin, le nom du champ multipart ou le DTO de retour peuvent differer.
223
+ ### Encodage
224
+ ```
225
+ POST {scopeId}/{entity}/file/video/{entityId}/{field}/encoding/retry?lang=fr → relance l'encodage
226
+ GET {scopeId}/{entity}/file/{entityId}/{field}/status?lang=fr → statut d'encodage
227
+ ```
173
228
 
174
- ## Remplacer, supprimer ou conserver un fichier existant
229
+ ### Transcripts (requiert une licence IA)
230
+ ```
231
+ POST {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/enabled → active le traitement
232
+ GET {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/status → { status, progress, duration }
233
+ GET {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/manifest?language=fr
234
+ POST {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/manifest?language=fr (corps = manifeste)
235
+ GET {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/{language}
236
+ POST {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/{language} (corps = transcript)
237
+ POST {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/{language}/delete (corps = transcript)
238
+ POST {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/reset/from-artifacts
239
+ GET {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/artifacts/{data} (redirection 302 vers le fichier)
240
+ ```
241
+ `status` ∈ `NotSet`, `Pending`/`Processing`, `Ready`. Un `Ready` non enregistré est
242
+ persisté automatiquement au premier appel de `status`/`manifest`/`get`.
175
243
 
176
- - Remplacer un fichier : uploader le nouveau fichier, puis patcher uniquement le champ `file` avec le nouveau `BLOBID`.
177
- - Supprimer un fichier associe a une entite : patcher le champ avec `null` si le schema le permet.
178
- - Conserver un fichier existant : ne pas inclure le champ dans le payload de patch. Inclure un champ `file` incomplet peut effacer des metadonnees ou provoquer un `400`.
179
- - Champs multi-fichiers : si le schema indique une collection, envoyer le tableau complet attendu par l'API, pas seulement le nouvel element.
244
+ ### Analyse de document (requiert une licence IA)
245
+ ```
246
+ GET {scopeId}/{entity}/file/document/{entityId}/{field}/analysis → statut de l'opération
247
+ POST {scopeId}/{entity}/file/document/{entityId}/{field}/analysis/start → démarre l'analyse
248
+ ```
249
+
250
+ ---
180
251
 
181
- ## Erreurs frequentes
252
+ ## 6. Formes JSON de référence
253
+
254
+ **Corps de requête**
255
+ - Créer l'emplacement temporaire : `{ entity, field, type, name, blobType }`
256
+ - Renouveler / finaliser : `{ blobName, sas }`
257
+
258
+ **Réponses**
259
+ - Objet d'accès temporaire (create/renew) : `{ url, container, blobName, sas }`
260
+ - Objet fichier avec lien d'accès (get/download) :
261
+ `{ url, container, blobName, fileName, size, mimeType, sas, lastModified }`
262
+ - Objet fichier persisté (valeur du champ) :
263
+ `{ url, container, blobName, fileName, size, mimeType, linkedBlobs, contentMD5, properties }`
264
+ - Image : ajoute `thumb`, `thumbSmall`, `thumbLarge` (chacun un objet fichier).
265
+ - Vidéo : ajoute `video { id, isReady, transcriptsEnabled, transcripts, transcriptData }`
266
+ et `encodings { <provider>: { hlsUrl, dashUrl, status, progress, timestamp } }`.
267
+
268
+ **Codes d'erreur de transfert** (corps de la réponse `400`) : `101` = fichier invalide
269
+ (contenu ≠ MIME), `102` = type de fichier non supporté.
270
+
271
+ ---
272
+
273
+ ## 7. Exemple de bout en bout
274
+
275
+ Entité `person`, champ `prop3` de type `file`.
276
+
277
+ 1. Créer l'emplacement temporaire :
278
+ ```
279
+ POST {eventId}/person/file
280
+ { "entity": "person", "field": "prop3", "name": "photo.jpg", "type": "image/jpeg" }
281
+ → 200 { "blobName": "<blobName>", "container": "tmp", "url": "...", "sas": "..." }
282
+ ```
283
+ 2. Téléverser les octets sur `url + /tmp/<blobName> + sas`.
284
+ 3. Finaliser : `POST {eventId}/person/file/finalize-tmp { "blobName": "<blobName>", "sas": "..." }` → `200`.
285
+ 4. Éditer la personne : `PATCH {eventId}/person/<personId> { "prop3": "<blobName>" }` → `200`.
286
+ → le fichier est déplacé de `tmp` vers le conteneur `{eventId}_person-prop3`, et la valeur
287
+ du champ est relue comme objet fichier complet.
288
+
289
+ ---
290
+
291
+ ## 8. Points d'attention
292
+
293
+ - **Double validation MIME** : au `finalize-tmp` (contenu vs MIME déclaré) **et** à la
294
+ sauvegarde de l'entité (taille max, MIME autorisés du champ, cohérence du contenu, sûreté
295
+ SVG). Un fichier invalide fait échouer/vider le champ.
296
+ - **Emplacement définitif** = conteneur `{scopeId}_{entity}-{field}` (minuscules) — utile
297
+ pour interpréter les URLs retournées.
298
+ - **Langue** : pour un champ localisé, transmettre `lang` en lecture.
299
+ - **`withoutExpiration=true`** : lien de longue durée (à réserver aux usages qui le justifient).
300
+ - **Remplacement** : ré-uploader et ré-éditer suffit ; l'ancien fichier est nettoyé automatiquement.
301
+
302
+ ---
303
+
304
+ ## Note — FrontAPI (hors périmètre)
305
+
306
+ Côté public/participant, un flux équivalent existe mais avec des routes et une auth
307
+ différentes : `POST {scopeId}/file` (+ `/renew`, `/try-renew`, `/finalize-tmp`), lecture
308
+ `GET community/{communityId}/file/{Entity}/{entityId}/{field}`, en accès anonyme avec
309
+ contrôles par droits/groupes (peut renvoyer `403` selon l'appartenance).
310
+
311
+ ---
182
312
 
183
- | Symptome | Cause probable | Correction |
184
- |---|---|---|
185
- | `403 Forbidden` sur une URL `blob.core.windows.net` | Le Bearer inwink est envoye au blob Azure | Appeler `try-renew`, puis fetch l'URL signee sans header `Authorization` |
186
- | `401 Unauthorized` sur l'endpoint inwink | Token inwink expire ou absent | Relancer l'auth OIDC de l'extension, puis rejouer l'appel API |
187
- | `400 Bad Request` au patch de l'entite | Forme du champ `file` incorrecte | Relire `get_entity_description` et utiliser le `BLOBID` retourne par l'upload, pas un objet `{ url: ... }` |
188
- | CORS ou auth impossible en appelant `/mcp` depuis l'extension | Le MCP n'est pas une API runtime d'extension | Appeler directement les APIs inwink ; reserver le MCP a la generation/discovery |
189
- | Upload multipart vide ou casse | `Content-Type` force manuellement | Avec `FormData`, ne pas definir `Content-Type`; laisser le navigateur ajouter le boundary |
313
+ *Voir aussi : `assets` (bibliothèque médias — mêmes primitives d'upload, avec identité propre et `inwinkasset://`), `mutations`.*
@@ -17,6 +17,7 @@ export interface InwinkApi {
17
17
  get: <T = unknown>(path: string, init?: RequestInit) => Promise<T>;
18
18
  post: <T = unknown>(path: string, body?: unknown, init?: RequestInit) => Promise<T>;
19
19
  put: <T = unknown>(path: string, body?: unknown, init?: RequestInit) => Promise<T>;
20
+ patch: <T = unknown>(path: string, body?: unknown, init?: RequestInit) => Promise<T>;
20
21
  del: <T = unknown>(path: string, init?: RequestInit) => Promise<T>;
21
22
  }
22
23
 
@@ -115,10 +116,11 @@ export function useInwinkApi(baseUrl: string): InwinkApi {
115
116
  throw new InwinkApiError(response.status, await safeBody(response));
116
117
  }
117
118
 
118
- if (response.status === 204) {
119
- return undefined as T;
120
- }
121
- return (await response.json()) as T;
119
+ // Une réponse OK peut ne porter aucun corps : un 204, mais aussi un 200 — les endpoints
120
+ // d'écriture du template d'entité répondent ainsi. `response.json()` lève sur un corps
121
+ // vide, d'où la lecture en texte avant de tenter le parse.
122
+ const text = await response.text();
123
+ return (text ? (JSON.parse(text) as T) : (undefined as T));
122
124
  }
123
125
 
124
126
  const withBody =
@@ -136,6 +138,7 @@ export function useInwinkApi(baseUrl: string): InwinkApi {
136
138
  request<T>(path, { ...init, method: 'GET' }),
137
139
  post: withBody('POST'),
138
140
  put: withBody('PUT'),
141
+ patch: withBody('PATCH'),
139
142
  del: <T = unknown>(path: string, init?: RequestInit) =>
140
143
  request<T>(path, { ...init, method: 'DELETE' }),
141
144
  };