@inwink-mcp/backoffice-extension-helpers 1.0.9 → 1.0.10
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
|
@@ -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
|
-
|
|
14
|
+
---
|
|
4
15
|
|
|
5
|
-
##
|
|
16
|
+
## 1. Principe (à lire en premier)
|
|
6
17
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
+
### Forme des routes
|
|
18
52
|
|
|
19
|
-
|
|
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
|
-
|
|
56
|
+
`{scopeId}` selon l'API :
|
|
57
|
+
- EventAPI : `{eventId}`
|
|
58
|
+
- CommunityAPI : `{communityId}`
|
|
59
|
+
- CustomerAPI : `{customerId}/audience/{tenantId}` (ou `{customerId}`)
|
|
22
60
|
|
|
23
|
-
|
|
61
|
+
---
|
|
24
62
|
|
|
25
|
-
|
|
63
|
+
## 2. Envoyer un fichier dans un champ d'entité (flux complet)
|
|
26
64
|
|
|
27
|
-
|
|
65
|
+
### Étape 1 — Créer l'emplacement temporaire
|
|
28
66
|
|
|
29
67
|
```
|
|
30
|
-
POST {
|
|
31
|
-
Authorization: Bearer {inwink_token}
|
|
68
|
+
POST {scopeId}/{entity}/file
|
|
32
69
|
Content-Type: application/json
|
|
33
70
|
|
|
34
71
|
{
|
|
35
|
-
"
|
|
36
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
91
|
+
### Étape 2 — Téléverser les octets directement (hors API)
|
|
46
92
|
|
|
47
|
-
|
|
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
|
-
|
|
96
|
+
```
|
|
97
|
+
PUT {url}/{container}/{blobName}{sas}
|
|
98
|
+
x-ms-blob-type: BlockBlob
|
|
99
|
+
Content-Type: image/png
|
|
55
100
|
|
|
56
|
-
|
|
101
|
+
<octets du fichier>
|
|
102
|
+
```
|
|
57
103
|
|
|
58
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
75
|
-
}
|
|
111
|
+
### Étape 3 — Finaliser le fichier temporaire
|
|
76
112
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
122
|
+
### Étape 4 — Sauvegarder l'entité en référençant le fichier
|
|
85
123
|
|
|
86
|
-
|
|
124
|
+
Via l'**endpoint d'édition habituel de l'entité**, mettre la valeur du champ = le `blobName` :
|
|
87
125
|
|
|
88
|
-
|
|
89
|
-
|
|
126
|
+
```
|
|
127
|
+
PATCH {scopeId}/person/{personId}
|
|
128
|
+
{ "prop3": "a7f3c1e0-....png" }
|
|
129
|
+
```
|
|
90
130
|
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
return uploaded?.blobId ?? uploaded?.BlobId ?? uploaded?.id ?? uploaded;
|
|
123
|
-
}
|
|
151
|
+
## 3. Lire / télécharger un fichier
|
|
124
152
|
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
157
|
+
Réponse `200` — objet fichier avec lien d'accès :
|
|
148
158
|
|
|
149
159
|
```json
|
|
150
160
|
{
|
|
151
|
-
"
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
"
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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`.*
|