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

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 (32) hide show
  1. package/package.json +1 -1
  2. package/templates/test-app/src/components/EventApiTester.tsx +51 -39
  3. package/templates/vite-extension/AGENTS.md.tpl +104 -1
  4. package/templates/vite-extension/docs/inwink-api/README.md +42 -0
  5. package/templates/vite-extension/docs/inwink-api/create.md +21 -0
  6. package/templates/vite-extension/docs/inwink-api/delete.md +21 -0
  7. package/templates/vite-extension/docs/inwink-api/entities.md +68 -0
  8. package/templates/vite-extension/docs/inwink-api/files.md +189 -0
  9. package/templates/vite-extension/docs/inwink-api/import.md +24 -0
  10. package/templates/vite-extension/docs/inwink-api/query-language.md +120 -0
  11. package/templates/vite-extension/docs/inwink-api/query.md +68 -0
  12. package/templates/vite-extension/docs/inwink-api/update.md +34 -0
  13. package/templates/vite-extension/src/components/ConfirmDialog.tsx +61 -0
  14. package/templates/vite-extension/src/components/ErrorBoundary.css +18 -0
  15. package/templates/vite-extension/src/components/ErrorBoundary.tsx +46 -0
  16. package/templates/vite-extension/src/components/Layout.tsx +4 -1
  17. package/templates/vite-extension/src/components/Modal.css +34 -0
  18. package/templates/vite-extension/src/components/Modal.tsx +69 -0
  19. package/templates/vite-extension/src/components/feedback/EmptyState.css +15 -0
  20. package/templates/vite-extension/src/components/feedback/EmptyState.tsx +20 -0
  21. package/templates/vite-extension/src/components/feedback/ErrorState.css +12 -0
  22. package/templates/vite-extension/src/components/feedback/ErrorState.tsx +28 -0
  23. package/templates/vite-extension/src/components/feedback/Spinner.css +21 -0
  24. package/templates/vite-extension/src/components/feedback/Spinner.tsx +15 -0
  25. package/templates/vite-extension/src/components/toast/ToastProvider.css +59 -0
  26. package/templates/vite-extension/src/components/toast/ToastProvider.tsx +67 -0
  27. package/templates/vite-extension/src/hooks/useAsync.ts +71 -0
  28. package/templates/vite-extension/src/hooks/useConfirm.ts +11 -0
  29. package/templates/vite-extension/src/hooks/useInwinkApi.ts +26 -0
  30. package/templates/vite-extension/src/hooks/useInwinkPagedQuery.ts +100 -0
  31. package/templates/vite-extension/src/hooks/useToast.ts +11 -0
  32. package/templates/vite-extension/src/main.tsx +7 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inwink-mcp/backoffice-extension-helpers",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "author": "inwink",
5
5
  "license": "ISC",
6
6
  "description": "CLI pour bootstrapper, packager et vérifier les extensions backoffice inwink.",
@@ -1,54 +1,66 @@
1
- import { useEffect, useState } from 'react';
2
- import { FiRefreshCw } from 'react-icons/fi';
1
+ import { FiRefreshCw, FiTrash2 } from 'react-icons/fi';
3
2
  import { useInwinkEventApi } from '../hooks/useInwinkEventApi';
3
+ import { useAsync } from '../hooks/useAsync';
4
+ import { useToast } from '../hooks/useToast';
5
+ import { useConfirm } from '../hooks/useConfirm';
6
+ import Spinner from './feedback/Spinner';
7
+ import ErrorState from './feedback/ErrorState';
8
+ import EmptyState from './feedback/EmptyState';
4
9
 
5
- // Composant de test (overlay "test-app") : exerce useInwinkEventApi en lançant
6
- // l'appel automatiquement au montage. N'est monté que lorsque l'utilisateur est
7
- // authentifié (cf. HomePage), mais l'appel passe de toute façon par le gate
8
- // `whenReady` du hook, qui attend la résolution d'auth avant de lire le token.
10
+ // Payload de requête de sessions (cf. docs/inwink-api/query.md).
11
+ const SESSION_QUERY = {
12
+ selects: { id: true, title: true, status: true, startDate: true, endDate: true },
13
+ orders: [{ desc: true, value: { validFrom: {} } }],
14
+ page: { index: 0, size: 50 },
15
+ };
16
+
17
+ // Composant de test (overlay "test-app") : exerce d'un coup les building blocks
18
+ // useAsync (chargement/erreur/annulation), Spinner/ErrorState/EmptyState,
19
+ // useToast et useConfirm — et force leur compilation dans le build de test.
9
20
  export default function EventApiTester() {
10
21
  const eventApi = useInwinkEventApi();
11
- const [result, setResult] = useState<string>('');
12
- const [error, setError] = useState<string>('');
13
- const [isLoading, setIsLoading] = useState(false);
22
+ const toast = useToast();
23
+ const confirm = useConfirm();
24
+
25
+ const { data, error, isLoading, refetch } = useAsync(
26
+ (signal) =>
27
+ eventApi.post<{ data?: unknown[] }>(
28
+ '/dae274d9-e86e-f011-8dca-0022488a3cad/session/query',
29
+ SESSION_QUERY,
30
+ { signal },
31
+ ),
32
+ [eventApi],
33
+ );
14
34
 
15
- async function callEventApi(signal?: { aborted: boolean }) {
16
- setError('');
17
- setResult('');
18
- setIsLoading(true);
19
- try {
20
- const data = await eventApi.post('/dae274d9-e86e-f011-8dca-0022488a3cad/session/query', {
21
- "selects": { "id": true, "title": true, "status": true, "moderators": { "person": { "id": true, "firstname": true, "lastname": true, "company": true, "mail": true } }, "startDate": true, "endDate": true, "room": { "name": true, "id": true }, "speakers": { "id": true, "firstname": true, "lastname": true, "company": true, "mail": true }, "exhibitors": { "id": true, "name": true }, "infos": { "registered": true, "attended": true, "attendedOnsite": true, "attendedOnlineLive": true, "attendedOnlineReplay": true } },
22
- "orders": [{ "desc": true, "value": { "validFrom": {} } }],
23
- "page": { "index": 0, "size": 200 }
24
- });
25
- if (!signal?.aborted) setResult(JSON.stringify(data, null, 2));
26
- } catch (err) {
27
- if (!signal?.aborted) setError(err instanceof Error ? err.message : String(err));
28
- } finally {
29
- if (!signal?.aborted) setIsLoading(false);
30
- }
35
+ async function handleFakeDelete() {
36
+ const ok = await confirm({
37
+ title: 'Action de test',
38
+ message: 'Confirmer cette action factice ? (aucune suppression réelle)',
39
+ confirmLabel: 'Confirmer',
40
+ });
41
+ if (ok) toast.success('Action confirmée (démo).');
31
42
  }
32
43
 
33
- // Appel automatique au montage ; on ignore le résultat si le composant est
34
- // démonté entre-temps (évite un setState sur composant démonté).
35
- useEffect(() => {
36
- const signal = { aborted: false };
37
- void callEventApi(signal);
38
- return () => {
39
- signal.aborted = true;
40
- };
41
- // eslint-disable-next-line react-hooks/exhaustive-deps
42
- }, []);
44
+ if (isLoading) return <Spinner label="Appel de l'API Event…" />;
45
+ if (error) return <ErrorState error={error} onRetry={refetch} />;
46
+
47
+ const sessions = data?.data ?? [];
43
48
 
44
49
  return (
45
50
  <section>
46
- <button disabled={isLoading} onClick={() => void callEventApi()}>
51
+ <button onClick={refetch}>
47
52
  <FiRefreshCw aria-hidden />
48
- {isLoading ? 'Appel en cours…' : "Rappeler l'API Event"}
53
+ Rappeler l'API Event
54
+ </button>
55
+ <button onClick={() => void handleFakeDelete()}>
56
+ <FiTrash2 aria-hidden />
57
+ Action de test (confirm + toast)
49
58
  </button>
50
- {result && <pre>{result}</pre>}
51
- {error && <p role="alert">Erreur : {error}</p>}
59
+ {sessions.length === 0 ? (
60
+ <EmptyState message="La requête n'a retourné aucune session." />
61
+ ) : (
62
+ <pre>{JSON.stringify(data, null, 2)}</pre>
63
+ )}
52
64
  </section>
53
65
  );
54
66
  }
@@ -54,6 +54,22 @@ Avant d'écrire **tout** appel API, utilise les outils MCP inwink pour découvri
54
54
 
55
55
  **Ne devine jamais une route ou un format de payload.** Le MCP est un outil de développement : l'extension n'appelle jamais le serveur MCP en runtime.
56
56
 
57
+ ### Référence des patterns d'API (`docs/inwink-api/`)
58
+
59
+ Les outils MCP donnent les endpoints/champs **exacts** ; la doc statique dans `docs/inwink-api/` décrit les **patterns d'appel** (verbes, forme des payloads, langage de requête). Lis uniquement le fichier correspondant à ta tâche :
60
+
61
+ | Tâche | Fichier |
62
+ |-------|---------|
63
+ | Lire / lister des entités (query, count, pagination, projection, joins) | [docs/inwink-api/query.md](docs/inwink-api/query.md) |
64
+ | Construire un filtre / tri / projection (langage de requête JSON) | [docs/inwink-api/query-language.md](docs/inwink-api/query-language.md) |
65
+ | Créer une entité | [docs/inwink-api/create.md](docs/inwink-api/create.md) |
66
+ | Modifier (patch / massupdate) | [docs/inwink-api/update.md](docs/inwink-api/update.md) |
67
+ | Supprimer (delete / massdelete) | [docs/inwink-api/delete.md](docs/inwink-api/delete.md) |
68
+ | Import en masse (bulk create/update) | [docs/inwink-api/import.md](docs/inwink-api/import.md) |
69
+ | Champs de type `file` (upload / download) | [docs/inwink-api/files.md](docs/inwink-api/files.md) |
70
+ | Contrats & schémas d'entités, endpoints divers | [docs/inwink-api/entities.md](docs/inwink-api/entities.md) |
71
+ | Codes HTTP & règles générales | [docs/inwink-api/README.md](docs/inwink-api/README.md) |
72
+
57
73
  ## Appeler l'API inwink
58
74
 
59
75
  **N'appelle jamais `fetch` directement.** Utilise les hooks dédiés (`useInwinkEventApi`, `useInwinkCommunityApi`, `useInwinkCustomerApi`). Ils encapsulent l'envoi du Bearer token, le renouvellement automatique sur 401 (`signinSilent` puis rejeu, sinon login interactif) et la gestion d'erreurs (`InwinkApiError`).
@@ -81,10 +97,90 @@ Pour appeler un **hôte externe déclaré** (hors API inwink), utilise le hook g
81
97
  Bonnes pratiques :
82
98
  - Toujours passer par un hook `useInwink*Api` — jamais de `fetch` direct.
83
99
  - Le 401 est géré automatiquement (renouvellement puis reconnexion) ; ne pas le ré-implémenter.
84
- - Gérer le 429 (rate limiting) avec un backoff exponentiel côté appelant si nécessaire.
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
+ - Au-delà de 429, une réponse non-OK lève une `InwinkApiError` (`status`, `body`) — à présenter via `ErrorState` ou un toast.
102
+ - Pour propager l'annulation, passer le `signal` reçu de `useAsync` dans `init` : `eventApi.get(path, { signal })`.
85
103
  - Ne déclarer dans `permissions.platform` que les scopes réellement utilisés.
86
104
  - Déclarer les hôtes externes dans `permissions.externalHosts` (HTTPS uniquement).
87
105
 
106
+ ## Building blocks fournis
107
+
108
+ Le squelette inclut des primitives réutilisables. **Réutilise-les** plutôt que de les ré-implémenter :
109
+
110
+ | Bloc | Import | Rôle |
111
+ |------|--------|------|
112
+ | `useAsync(fn, deps)` | `hooks/useAsync` | Exécute une opération async → `{ data, error, isLoading, refetch }`, avec annulation (`AbortSignal`) et déduplication des réponses obsolètes. Moyen canonique de consommer un `useInwink*Api` dans un composant. |
113
+ | `useInwinkPagedQuery(loadPage, getId)` | `hooks/useInwinkPagedQuery` | Pagination offset-based pour les endpoints `/query` (load more, dédup par id). |
114
+ | `useToast()` | `hooks/useToast` | `toast.success/error/info(message)` — notifications transitoires. |
115
+ | `useConfirm()` | `hooks/useConfirm` | `await confirm({ title, message })` → `boolean`. À utiliser avant toute action destructrice. |
116
+ | `Spinner`, `EmptyState`, `ErrorState` | `components/feedback/*` | États standard de chargement / liste vide / erreur (gère `InwinkApiError`). |
117
+ | `ErrorBoundary` | `components/ErrorBoundary` | Capture les erreurs de **rendu** (déjà monté autour de l'`Outlet`). |
118
+ | `Modal` | `components/Modal` | Boîte modale (overlay, focus trap, Échap) pour des dialogues custom. |
119
+
120
+ `ToastProvider` et `ConfirmProvider` sont déjà montés dans `main.tsx`.
121
+
122
+ ## Lire le contexte inwink
123
+
124
+ Le contexte d'exécution (`eventid` / `communityid` / `tenantid`) est extrait de l'URL au chargement, **persisté en sessionStorage** (le callback OIDC nettoie la query string) et exposé via `useInwinkContext()` :
125
+
126
+ ```typescript
127
+ import { useInwinkContext } from './hooks/useInwinkContext';
128
+
129
+ function MonComposant() {
130
+ const { scope, eventId, communityId, tenantId } = useInwinkContext();
131
+ // scope vaut 'event' | 'community' | 'tenant' | null (priorité event > community > tenant)
132
+ // Utilise l'id pertinent comme shardId dans les chemins d'API.
133
+ }
134
+ ```
135
+
136
+ Pour laisser l'utilisateur **choisir** un contexte, monte un picker (`EventPicker`, `CommunityPicker`, `AudiencePicker`) — typiquement en **surchargeant le `Header`** (le `Header` par défaut est minimal et prévu pour ça). Les pickers ne sont pas montés par défaut : décide selon le besoin de l'extension.
137
+
138
+ ## Recettes
139
+
140
+ **Charger et afficher une liste :**
141
+
142
+ ```tsx
143
+ const eventApi = useInwinkEventApi();
144
+ const { eventId } = useInwinkContext();
145
+ const { data, error, isLoading, refetch } = useAsync(
146
+ (signal) => eventApi.post<{ data: Session[] }>(`/${eventId}/session/query`, QUERY, { signal }),
147
+ [eventApi, eventId],
148
+ );
149
+
150
+ if (isLoading) return <Spinner />;
151
+ if (error) return <ErrorState error={error} onRetry={refetch} />;
152
+ if (!data?.data.length) return <EmptyState message="Aucune session." />;
153
+ return <ul>{data.data.map((s) => <li key={s.id}>{s.title}</li>)}</ul>;
154
+ ```
155
+
156
+ **Créer / modifier puis notifier :**
157
+
158
+ ```tsx
159
+ const toast = useToast();
160
+ async function save(values: Partial<Session>) {
161
+ try {
162
+ await eventApi.post(`/${eventId}/session`, { entity: values, selects: { $all: true } });
163
+ toast.success('Enregistré.');
164
+ } catch (err) {
165
+ toast.error(err instanceof InwinkApiError ? `Échec (${err.status}).` : 'Échec.');
166
+ }
167
+ }
168
+ ```
169
+
170
+ **Action destructrice avec confirmation :**
171
+
172
+ ```tsx
173
+ const confirm = useConfirm();
174
+ const toast = useToast();
175
+ async function remove(id: string) {
176
+ if (!(await confirm({ title: 'Supprimer ?', message: 'Action irréversible.' }))) return;
177
+ await eventApi.post(`/${eventId}/session/delete/${id}`);
178
+ toast.success('Supprimé.');
179
+ }
180
+ ```
181
+
182
+ **Ajouter une page/route :** créer le composant dans `pages/`, puis ajouter une `<Route>` dans `App.tsx` (sous le `Layout`, encadrée par `ProtectedRoute` si l'auth est requise).
183
+
88
184
  ## Règles et contraintes
89
185
 
90
186
  | Règle | Détail |
@@ -130,3 +226,10 @@ npx @inwink-mcp/backoffice-extension-helpers@latest verify # vérification de
130
226
  - Les appels API passent **toujours** par un hook `useInwink*Api` ; jamais de `fetch` direct dans les composants.
131
227
  - Les icônes proviennent de **`react-icons`** (ex. `react-icons/fi`, `react-icons/md`) ; pas de SVG inline ni d'images d'icônes.
132
228
  - Le squelette (auth, api, config, structure) est généré par la CLI helper — ne pas le régénérer à la main. Ajouter la logique métier dans `App.tsx` et de nouveaux composants/hooks.
229
+
230
+ ## Consignes opérationnelles pour l'agent IA
231
+
232
+ > Ces consignes s'appliquent à **tout** assistant IA / outil agentique qui manipule cette extension, quel qu'il soit.
233
+
234
+ - **Ne jamais publier sans validation explicite de l'utilisateur.** Le build, le packaging et la vérification **en local** sont autorisés sans demander. En revanche, toute étape qui **expose l'extension côté inwink** (upload d'une version, `publish`, promotion en `latest`) doit être **confirmée explicitement par l'utilisateur à chaque fois**. En cas de doute, demande avant d'agir.
235
+ - **Tester dans un vrai navigateur, pas dans un aperçu intégré.** Ne lance pas l'extension dans un panneau d'aperçu ou une webview embarquée de l'outil agentique : le flux OIDC y échoue. L'authentification exige que l'origine soit le serveur de dev local — le `redirect_uri` vaut `${window.location.origin}${window.location.pathname}` et n'est enregistré côté IdP que pour `http://localhost:<port>`, alors qu'un aperçu intégré sert l'application depuis une autre origine. **Recommandé** : lancer `npm run dev` et ouvrir un **vrai onglet de navigateur** (Chrome de préférence) sur l'URL locale affichée (ex. `http://localhost:5173`).
@@ -0,0 +1,42 @@
1
+ # inwink API Reference
2
+
3
+ This document explains how to consume inwink APIs. It is intended as a reference for building API calls programmatically.
4
+
5
+ > **Découverte dynamique d'abord.** Cette doc décrit les *patterns* d'appel (verbes, forme des payloads, langage de requête). Les **endpoints exacts, champs et entités** se découvrent via les outils MCP (`inwink_mcp_find_api_endpoint`, `inwink_mcp_get_entity_description`) — ne devine jamais une route ou un schéma.
6
+
7
+ ## Sommaire
8
+
9
+ | Tâche | Fichier |
10
+ |---|---|
11
+ | Lire / lister des entités (query, count, pagination, projection, joins) | [query.md](./query.md) |
12
+ | Construire un filtre / tri / projection (langage de requête JSON) | [query-language.md](./query-language.md) |
13
+ | Créer une entité | [create.md](./create.md) |
14
+ | Modifier une entité (patch / massupdate) | [update.md](./update.md) |
15
+ | Supprimer une entité (delete / massdelete) | [delete.md](./delete.md) |
16
+ | Import en masse (bulk create/update) | [import.md](./import.md) |
17
+ | Contrats & schémas d'entités, endpoints divers | [entities.md](./entities.md) |
18
+ | Champs de type `file` (upload / download) | [files.md](./files.md) |
19
+
20
+ ---
21
+
22
+ ## General Rules
23
+
24
+ - Property names are **case-insensitive**.
25
+ - Enum values are **case-insensitive**.
26
+ - All IDs are UUIDs.
27
+ - Dates are ISO 8601 / UTC.
28
+ - All write operations (create, edit, delete, import) use **POST** method.
29
+
30
+ ---
31
+
32
+ ## HTTP Status Codes
33
+
34
+ | Code | Meaning |
35
+ |---|---|
36
+ | 200 | Success — parse body as JSON |
37
+ | 204 | Success — no content |
38
+ | 400 | Bad request — body contains error details |
39
+ | 401 | Unauthorized — token expired or invalid |
40
+ | 403 | Forbidden — missing rights, contact CSM |
41
+ | 500 | Server error — may be transient, contact support if persistent |
42
+ | 503 | Transient infrastructure error — retry later |
@@ -0,0 +1,21 @@
1
+ ◂ Retour à [README](./README.md)
2
+
3
+ # Create
4
+
5
+ ```
6
+ POST {api}/{shardId}/{entity}
7
+ ```
8
+
9
+ **Request body:**
10
+
11
+ ```json
12
+ {
13
+ "entity": { "mail": "user@example.com", "firstname": "John" },
14
+ "selects": { "$all": true }
15
+ }
16
+ ```
17
+
18
+ - `entity` — **required**. The object to create. Missing required properties cause a 400 error.
19
+ - `selects` — optional. Controls which properties are returned on success (voir [query-language.md](./query-language.md)).
20
+
21
+ > Pour créer plusieurs entités efficacement, préférer [import.md](./import.md).
@@ -0,0 +1,21 @@
1
+ ◂ Retour à [README](./README.md)
2
+
3
+ # Delete & Massdelete
4
+
5
+ ## Delete
6
+
7
+ ```
8
+ POST {api}/{shardId}/{entity}/delete/{entityId}
9
+ ```
10
+
11
+ No body required.
12
+
13
+ ---
14
+
15
+ ## Massdelete
16
+
17
+ ```
18
+ POST {api}/{shardId}/{entity}/massdelete
19
+ ```
20
+
21
+ Bulk DELETE like `DELETE … WHERE` in SQL. S'appuie sur un filtre (voir [query-language.md](./query-language.md)).
@@ -0,0 +1,68 @@
1
+ ◂ Retour à [README](./README.md)
2
+
3
+ # Entity Contracts, Schemas & Other Endpoints
4
+
5
+ > Les champs exacts d'une entité se découvrent via `inwink_mcp_get_entity_description`. Les schémas ci-dessous sont indicatifs.
6
+
7
+ ## Other Endpoints
8
+
9
+ | Endpoint | Pattern | Purpose |
10
+ |---|---|---|
11
+ | Worktemplate | `GET {api}/{shardId}/{entity}/worktemplate` | Returns the entity template (list of properties, types, required flags, localization info) |
12
+ | History | `POST {api}/{shardId}/{entity}/history/query` | Query past modifications (same body as query). Use sparingly — filter by `id` for performance. |
13
+ | Massupdate | `POST {api}/{shardId}/{entity}/massupdate` | Bulk EDIT like `UPDATE … WHERE` in SQL (voir [update.md](./update.md)) |
14
+ | Massdelete | `POST {api}/{shardId}/{entity}/massdelete` | Bulk DELETE like `DELETE … WHERE` in SQL (voir [delete.md](./delete.md)) |
15
+ | Export | Returns entities as an Excel file | |
16
+ | File | Upload files (voir [files.md](./files.md)) | |
17
+
18
+ ---
19
+
20
+ ## Entity Contracts
21
+
22
+ ### Event Entity
23
+
24
+ Scoped to EventAPI. Always has an `eventId` property. URL uses `{eventId}` as shardId.
25
+
26
+ ### Customer Entity
27
+
28
+ Scoped to CustomerAPI. Always has a `customerId` property. URL uses `{customerId}` as shardId.
29
+
30
+ ### Dynamic Entity
31
+
32
+ Can have custom properties added via backoffice or template API. Custom properties behave identically to built-in properties in all endpoints.
33
+
34
+ ### With Publication Status
35
+
36
+ Has a `status` property (string, case-insensitive): `Draft`, `Preview`, `Published`, `Canceled`, `Rejected`.
37
+
38
+ ### With History
39
+
40
+ Tracked modifications. Read-only properties: `validFrom` (last modification date, UTC), `modifiedById` (UUID), `modifiedByDisplayName` (string).
41
+
42
+ ---
43
+
44
+ ## Common Entities & Schemas
45
+
46
+ ### Person (EventAPI)
47
+
48
+ Contracts: Event entity, Dynamic, With Publication Status, With History.
49
+
50
+ Key properties: `id` (uuid), `mail`, `firstname`, `lastname`, `company`, `jobTitle`, `preferredLanguage`, `isRegistered` (bool), `hasAttended` (bool), `status`, `registrationDate`, `registrationMode` (enum: BackOffice, Website, OnSite, Order, Invitation, ExhibitorAccount), `checkinDate`, `kinds` (string[]), `badgeRef`, `userId`, `contactId`, `description` (localizable), `photo` (file ref).
51
+
52
+ Expandable relations: `exhibitor`, `infos`, `roomingInfos`, `exhibitorAccounts`, `orders`, `hotelRoomNights`, `sessions`, `favoriteExhibitors`, `animatedSessions`, `orderItems`, `personScans`, `tasks`, `meetingPersons`, `availabilities`, `agenda`, `activityView`.
53
+
54
+ ### EventDetail (EventAPI)
55
+
56
+ Contracts: Event entity, With History.
57
+
58
+ Key properties: `id`, `eventId`, `eventType` (array: Onsite/Online), `configuration`, `agenda`, `previewHost`.
59
+
60
+ Expandable relations: `sessions`, `exhibitors`.
61
+
62
+ ### Event (CustomerAPI)
63
+
64
+ Contracts: Customer entity, Dynamic, With History.
65
+
66
+ Key properties: `id`, `customerId`, `title`, `description` (localizable), `startDate`, `endDate`, `city`, `country`, `eventKey`, `computedUrl`, `status` (Active/Archived/Deleted), `eventType`, `isMaster`, `timeZone`, `parentEventId`, `goalRegistered`, `goalAttendees`, `goalSales`.
67
+
68
+ Expandable relations: `stat`, `authTenant`, `eventHttpHostAliases`, `websites`.
@@ -0,0 +1,189 @@
1
+ ◂ Retour à [README](./README.md)
2
+
3
+ # Manipuler les fichiers d'entites (champs de type `file`)
4
+
5
+ ## Regle d'architecture
6
+
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.
8
+
9
+ Workflow a suivre pendant la generation du code :
10
+
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.
14
+
15
+ ---
16
+
17
+ ## Lire ou telecharger un fichier prive
18
+
19
+ ### Problème
20
+
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.
22
+
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`.
24
+
25
+ ### Solution : endpoint `try-renew`
26
+
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 :
28
+
29
+ ```
30
+ POST {eventAPI}/{eventId}/document-asset/file/try-renew
31
+ Authorization: Bearer {inwink_token}
32
+ Content-Type: application/json
33
+
34
+ {
35
+ "BlobName": "{container}/{path/to/blob}",
36
+ "Sas": "{existing_sas_query_string_or_empty_string}"
37
+ }
38
+ ```
39
+
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.
42
+
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.
44
+
45
+ ### Endpoints disponibles selon le type de fichier
46
+
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` |
53
+
54
+ En cas de doute sur le type, utiliser `document-asset` en premier (fonctionne pour la majorité des uploads exposants).
55
+
56
+ ### Pattern d'implémentation
57
+
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) : '';
63
+
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 */ }
73
+
74
+ return absoluteUrl;
75
+ }
76
+
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
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Uploader puis associer un fichier a une entite
85
+
86
+ L'upload d'un nouveau fichier se fait en deux etapes :
87
+
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.
90
+
91
+ Requetes MCP a utiliser avant de generer le code :
92
+
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"
98
+
99
+ inwink_mcp_get_entity_description:
100
+ targetApi: eventapi
101
+ shardId: {eventId}
102
+ query: "person photo file field"
103
+ ```
104
+
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
+ );
120
+
121
+ // Adapter la lecture ci-dessous a la forme exacte retournee par l'endpoint.
122
+ return uploaded?.blobId ?? uploaded?.BlobId ?? uploaded?.id ?? uploaded;
123
+ }
124
+
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
+ }
145
+ ```
146
+
147
+ Exemple JSON correct pour associer un fichier a une entite :
148
+
149
+ ```json
150
+ {
151
+ "entity": {
152
+ "photo": "BLOBID"
153
+ },
154
+ "selects": {
155
+ "$all": true
156
+ }
157
+ }
158
+ ```
159
+
160
+ Exemple a ne pas faire :
161
+
162
+ ```json
163
+ {
164
+ "entity": {
165
+ "photo": {
166
+ "url": "..."
167
+ }
168
+ }
169
+ }
170
+ ```
171
+
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.
173
+
174
+ ## Remplacer, supprimer ou conserver un fichier existant
175
+
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.
180
+
181
+ ## Erreurs frequentes
182
+
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 |
@@ -0,0 +1,24 @@
1
+ ◂ Retour à [README](./README.md)
2
+
3
+ # Import (bulk create/update)
4
+
5
+ ```
6
+ POST {api}/{shardId}/{entity}/import
7
+ ```
8
+
9
+ **Request body:**
10
+
11
+ ```json
12
+ {
13
+ "entities": [
14
+ { "mail": "user0@mail.com" },
15
+ { "id": "existing-uuid", "mail": "user1@mail.com" }
16
+ ],
17
+ "identifiers": { "mail": {} },
18
+ "selects": { }
19
+ }
20
+ ```
21
+
22
+ - If an entity includes `id`, it's treated as an **update**.
23
+ - If no `id` is present, the `identifiers` map is used as a unique constraint to determine create vs. update. In the example above, `mail` is used to match existing records.
24
+ - This is much more performant than individual create/edit calls.