@inwink-mcp/backoffice-extension-helpers 1.0.6 → 1.0.8
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 +1 -1
- package/templates/test-app/src/components/EventApiTester.tsx +66 -0
- package/templates/test-app/src/pages/HomePage.tsx.tpl +4 -28
- package/templates/vite-extension/AGENTS.md.tpl +104 -1
- package/templates/vite-extension/docs/inwink-api/README.md +42 -0
- package/templates/vite-extension/docs/inwink-api/create.md +21 -0
- package/templates/vite-extension/docs/inwink-api/delete.md +21 -0
- package/templates/vite-extension/docs/inwink-api/entities.md +68 -0
- package/templates/vite-extension/docs/inwink-api/files.md +189 -0
- package/templates/vite-extension/docs/inwink-api/import.md +24 -0
- package/templates/vite-extension/docs/inwink-api/query-language.md +120 -0
- package/templates/vite-extension/docs/inwink-api/query.md +68 -0
- package/templates/vite-extension/docs/inwink-api/update.md +34 -0
- package/templates/vite-extension/src/auth/AuthProvider.tsx +41 -7
- package/templates/vite-extension/src/components/ConfirmDialog.tsx +61 -0
- package/templates/vite-extension/src/components/ErrorBoundary.css +18 -0
- package/templates/vite-extension/src/components/ErrorBoundary.tsx +46 -0
- package/templates/vite-extension/src/components/Layout.tsx +4 -1
- package/templates/vite-extension/src/components/Modal.css +34 -0
- package/templates/vite-extension/src/components/Modal.tsx +69 -0
- package/templates/vite-extension/src/components/feedback/EmptyState.css +15 -0
- package/templates/vite-extension/src/components/feedback/EmptyState.tsx +20 -0
- package/templates/vite-extension/src/components/feedback/ErrorState.css +12 -0
- package/templates/vite-extension/src/components/feedback/ErrorState.tsx +28 -0
- package/templates/vite-extension/src/components/feedback/Spinner.css +21 -0
- package/templates/vite-extension/src/components/feedback/Spinner.tsx +15 -0
- package/templates/vite-extension/src/components/toast/ToastProvider.css +59 -0
- package/templates/vite-extension/src/components/toast/ToastProvider.tsx +67 -0
- package/templates/vite-extension/src/hooks/useAsync.ts +71 -0
- package/templates/vite-extension/src/hooks/useConfirm.ts +11 -0
- package/templates/vite-extension/src/hooks/useInwinkApi.ts +32 -1
- package/templates/vite-extension/src/hooks/useInwinkPagedQuery.ts +100 -0
- package/templates/vite-extension/src/hooks/useToast.ts +11 -0
- package/templates/vite-extension/src/main.tsx +7 -1
package/package.json
CHANGED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { FiRefreshCw, FiTrash2 } from 'react-icons/fi';
|
|
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';
|
|
9
|
+
|
|
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.
|
|
20
|
+
export default function EventApiTester() {
|
|
21
|
+
const eventApi = useInwinkEventApi();
|
|
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
|
+
);
|
|
34
|
+
|
|
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).');
|
|
42
|
+
}
|
|
43
|
+
|
|
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 ?? [];
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<section>
|
|
51
|
+
<button onClick={refetch}>
|
|
52
|
+
<FiRefreshCw aria-hidden />
|
|
53
|
+
Rappeler l'API Event
|
|
54
|
+
</button>
|
|
55
|
+
<button onClick={() => void handleFakeDelete()}>
|
|
56
|
+
<FiTrash2 aria-hidden />
|
|
57
|
+
Action de test (confirm + toast)
|
|
58
|
+
</button>
|
|
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
|
+
)}
|
|
64
|
+
</section>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -1,31 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { FiLogIn, FiLogOut, FiRefreshCw } from 'react-icons/fi';
|
|
1
|
+
import { FiLogIn, FiLogOut } from 'react-icons/fi';
|
|
3
2
|
import { useAuth } from '../hooks/useAuth';
|
|
4
|
-
import
|
|
3
|
+
import EventApiTester from '../components/EventApiTester';
|
|
5
4
|
|
|
6
5
|
// Version de HomePage générée par le template overlay "test-app".
|
|
7
6
|
// Sert à vérifier que le squelette bootstrappé compile et que les hooks API
|
|
8
|
-
// (ici useInwinkEventApi) sont utilisables.
|
|
7
|
+
// (ici useInwinkEventApi, via EventApiTester) sont utilisables.
|
|
9
8
|
export default function HomePage() {
|
|
10
9
|
const { isAuthenticated, isLoading, login, logout, user } = useAuth();
|
|
11
|
-
const eventApi = useInwinkEventApi();
|
|
12
|
-
const [result, setResult] = useState<string>('');
|
|
13
|
-
const [error, setError] = useState<string>('');
|
|
14
|
-
|
|
15
|
-
async function callEventApi() {
|
|
16
|
-
setError('');
|
|
17
|
-
setResult('');
|
|
18
|
-
try {
|
|
19
|
-
const data = await eventApi.post('/dae274d9-e86e-f011-8dca-0022488a3cad/session/query', {
|
|
20
|
-
"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 } },
|
|
21
|
-
"orders": [{ "desc": true, "value": { "validFrom": {} } }],
|
|
22
|
-
"page": { "index": 0, "size": 200 }
|
|
23
|
-
});
|
|
24
|
-
setResult(JSON.stringify(data, null, 2));
|
|
25
|
-
} catch (err) {
|
|
26
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
10
|
|
|
30
11
|
return (
|
|
31
12
|
<main>
|
|
@@ -38,16 +19,11 @@ export default function HomePage() {
|
|
|
38
19
|
) : (
|
|
39
20
|
<>
|
|
40
21
|
<p>Connecté en tant que : {user?.profile.email ?? 'Inconnu'}</p>
|
|
41
|
-
<button onClick={() => void callEventApi()}>
|
|
42
|
-
<FiRefreshCw aria-hidden />
|
|
43
|
-
Appeler l'API Event
|
|
44
|
-
</button>
|
|
45
22
|
<button onClick={() => void logout()}>
|
|
46
23
|
<FiLogOut aria-hidden />
|
|
47
24
|
Se déconnecter
|
|
48
25
|
</button>
|
|
49
|
-
|
|
50
|
-
{error && <p role="alert">Erreur : {error}</p>}
|
|
26
|
+
<EventApiTester />
|
|
51
27
|
</>
|
|
52
28
|
)}
|
|
53
29
|
</main>
|
|
@@ -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
|
-
-
|
|
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.
|