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

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.
@@ -0,0 +1,238 @@
1
+ ◂ Back to [README](./README.md)
2
+
3
+ # Endpoint `aggregate` — native GROUP BY
4
+
5
+ Generic endpoint to **group and aggregate** inwink entities, pushed down to SQL (`GROUP BY`).
6
+
7
+ ## Principle
8
+
9
+ - **Route**: `POST <entity-route>/aggregate` (same base as `query`/`count`, e.g. `…/person/aggregate`,
10
+ `…/order/aggregate`). Available on every entity that exposes `query`.
11
+ - **Body**: a JSON object (`ExpressionEntityAggregateModel`). Response: `{ data, count, totalGroups }`.
12
+ - The computation is **pushed down to SQL** (`GROUP BY`); global filters and rights/scopes are applied automatically.
13
+ - `filters` use the same language as `query`. However, **`groupBy` keys and aggregate values are more
14
+ restrictive** than the fields accepted elsewhere: see "Referencing a field" below before grouping on
15
+ anything other than a field of the entity itself.
16
+
17
+ ## Payload structure
18
+
19
+ ```jsonc
20
+ {
21
+ "filters": { /* MongoDB-like predicate, optional, applied BEFORE grouping (WHERE) */ },
22
+ "groupBy": [ /* 0..N grouping keys (empty or absent = global aggregate, a single row) */ ],
23
+ "aggregates":[ /* 1..N aggregated values per group (required) */ ],
24
+ "having": [ /* 0..N filters AFTER grouping (HAVING), optional */ ],
25
+ "orders": [ /* 0..N sorts, optional */ ],
26
+ "limit": 100 /* required, positive integer, capped (default 1000) */
27
+ }
28
+ ```
29
+
30
+ ### `filters`
31
+ Same syntax as `query`: `{ "field": { "$eq": value } }`. Applied before grouping.
32
+ The operator list in [query-language.md](./query-language.md#filters) is authoritative: refer to it rather than guessing a name; an
33
+ operator outside that list returns 400.
34
+
35
+ ### `groupBy[]` — grouping keys
36
+ ```jsonc
37
+ {
38
+ "name": "month", // output name of the column (result key)
39
+ "value": { "registrationDate": {} }, // field reference (see "Referencing a field")
40
+ "dateTrunc": "month", // optional: truncates a DATE (year|quarter|month|week|day|hour)
41
+ "bucketSize": 100 // optional: buckets a NUMERIC (exclusive with dateTrunc)
42
+ }
43
+ ```
44
+ - `dateTrunc`: `year`, `quarter`, `month`, `week` (Monday), `day`, `hour`. The returned key is the truncated date (start of the bucket).
45
+ - `bucketSize`: for a numeric field, key = lower bound `floor(value / size) * size`.
46
+ - `dateTrunc` and `bucketSize` are **mutually exclusive**.
47
+ - A key can be a **multi-valued** field (multiselectlist/multiselecttree): the array is **denormalized**
48
+ (an entity `["a","b"]` counts in group `a` AND group `b`). **At most 3** multi-valued keys (Cartesian product).
49
+
50
+ ### `aggregates[]` — aggregation functions
51
+ ```jsonc
52
+ { "name": "total", "op": "count" } // count(*): no "value"
53
+ { "name": "distinct", "op": "countDistinct", "value": { "company": {} } }
54
+ { "name": "sumHt", "op": "sum", "value": { "totalHt": {} } }
55
+ { "name": "avgHt", "op": "avg", "value": { "totalHt": {} } }
56
+ { "name": "minHt", "op": "min", "value": { "totalHt": {} } }
57
+ { "name": "maxDate", "op": "max", "value": { "registrationDate": {} } }
58
+ { "name": "stdHt", "op": "stdDevSamp", "value": { "totalHt": {} } }
59
+ ```
60
+ Operations (`op`):
61
+
62
+ | op | description | field type | output type |
63
+ |---|---|---|---|
64
+ | `count` | number of rows in the group (no `value`) | — | integer |
65
+ | `countDistinct` | number of distinct values | scalar | integer |
66
+ | `sum` | sum | numeric | decimal (nullable) |
67
+ | `avg` | average | numeric | decimal (nullable) |
68
+ | `min` / `max` | min / max | scalar (num/date/text) | field type |
69
+ | `stdDevPop` / `stdDevSamp` | population / sample standard deviation | numeric | double (nullable) |
70
+ | `varPop` / `varSamp` | population / sample variance | numeric | double (nullable) |
71
+
72
+ > `*Samp` (sample) returns `null` if the group has fewer than 2 values.
73
+
74
+ ### `having[]` — post-grouping filters (SQL HAVING)
75
+ ```jsonc
76
+ { "name": "total", "op": "gte", "value": 5 } // keeps only groups where total >= 5
77
+ ```
78
+ - `op`: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`.
79
+ - `name` references an output name (`groupBy` key OR aggregate). Multiple entries = **AND**.
80
+ - Prefer `filters` (before grouping) when filtering on a field value; reserve `having` for **aggregates**.
81
+
82
+ ### `orders[]`
83
+ ```jsonc
84
+ { "name": "total", "desc": true } // name = output name of a key or an aggregate
85
+ ```
86
+ A deterministic secondary sort on the keys is added automatically (reproducible paging).
87
+
88
+ ### `limit`
89
+ Required, integer > 0, capped by `AggregateMaxGroups` (default 1000). Exceeding it → 400.
90
+
91
+ ## Referencing a field (`value`)
92
+ Syntax: `{ "<fieldName>": {} }`. A navigation is written by nesting: `{ "member": { "lastname": {} } }`.
93
+ - Works for **static** fields (entity properties) **and dynamic** fields (stored as JSON); case-insensitive.
94
+ - For a `selectlist`/`selecttree` field (single value), the returned key is the stored **option key** (e.g. `v1`), not the label.
95
+ - For a `multiselectlist`/`multiselecttree` field, see the denormalization described in `groupBy`.
96
+ - A **localizable** field (`title`, `description` — object `{ "fr": …, "en": … }`) used as a key returns the
97
+ label resolved in the default language, **provided it belongs to the root entity**.
98
+
99
+ ### What can be traversed, and what cannot
100
+
101
+ `aggregate` does not accept every path that `filters` accepts. The table applies to `groupBy[].value`
102
+ and to `aggregates[].value`:
103
+
104
+ | Key | Example | Result |
105
+ |---|---|---|
106
+ | Entity field, scalar | `{ "pinned": {} }` | ✅ |
107
+ | Entity field, localizable | `{ "title": {} }` | ✅ resolved label |
108
+ | **to-one** relation + scalar field | `{ "forum": { "status": {} } }` | ✅ |
109
+ | **to-one** relation + **localizable** field | `{ "forum": { "title": {} } }` | ❌ 400 |
110
+ | **to-many** relation (scalar or localizable) | `{ "categorySubjects": { "id": {} } }` | ❌ 400 |
111
+
112
+ Both failing cases return the same message, which **names the last segment of the path** (`property
113
+ resolution error for title`) even though the field does exist on the related entity: it is the **traversal**
114
+ that is rejected, not the field. Do not go looking for another field name, and do not retry the same path in dot
115
+ notation (`"forum.title"`) — the two forms are strictly equivalent on the server side.
116
+
117
+ **Working around a to-many relation:** there is no native `groupBy`. Two options, both server-side:
118
+ - one `count` per value, with the `$any` quantifier in `filters` (see [query-language.md](./query-language.md#collection-operators)):
119
+ `{ "categorySubjects": { "$any": { "title": { "$eq": "Archived" } } } }`;
120
+ - or `query` requesting the relation in `selects`, then grouping client-side.
121
+
122
+ **Working around a to-one localizable field:** group on the foreign key (`{ "forumId": {} }`) then resolve
123
+ the labels with an `query` on the related entity.
124
+
125
+ ## Response
126
+ ```jsonc
127
+ {
128
+ "data": [ { "<key1>": …, "<key2>": …, "<agg1>": …, "<agg2>": … }, … ],
129
+ "count": 2, // number of rows returned (after limit)
130
+ "totalGroups": 2 // total number of distinct groups (independent of limit)
131
+ }
132
+ ```
133
+ - **Global aggregate** (no `groupBy`) → a single row. On an empty set → one row `{ count: 0, … : null }`.
134
+
135
+ ## Constraints / errors (400)
136
+ - `sum`/`avg`/`var*`/`stdDev*` require a **numeric** field.
137
+ - A multi-valued field is allowed as a **key** (denormalized) but **not** as an aggregate value.
138
+ - At most **3** multi-valued keys.
139
+ - `limit` required and ≤ cap.
140
+ - Field not found, inconsistent op×type, `dateTrunc` on a non-date, `bucketSize` on a non-numeric → 400 with message.
141
+ - **Not supported**: a key traversing a **to-many** relation, or a **localizable** field reached through
142
+ a relation (see "What can be traversed") → 400 `property resolution error for <last segment>`.
143
+ - **Not supported**: median / percentiles (limitation of the SQL `GROUP BY` translation).
144
+
145
+ ## Examples
146
+
147
+ ### 1. Registrations per month (date bucket) + average, filtered
148
+ ```json
149
+ {
150
+ "filters": { "registrationStatus": { "$eq": "registered" } },
151
+ "groupBy": [ { "name": "month", "value": { "registrationDate": {} }, "dateTrunc": "month" } ],
152
+ "aggregates": [
153
+ { "name": "total", "op": "count" },
154
+ { "name": "avgScore", "op": "avg", "value": { "score": {} } }
155
+ ],
156
+ "orders": [ { "name": "month", "desc": false } ],
157
+ "limit": 100
158
+ }
159
+ ```
160
+
161
+ ### 2. Facets: values of a multi-select field + count
162
+ ```json
163
+ {
164
+ "filters": { "country": { "$eq": "FR" } },
165
+ "groupBy": [ { "name": "choice", "value": { "testselect": {} } } ],
166
+ "aggregates": [ { "name": "total", "op": "count" } ],
167
+ "orders": [ { "name": "total", "desc": true } ],
168
+ "limit": 100
169
+ }
170
+ ```
171
+
172
+ ### 3. Cross-tab: country × month
173
+ ```json
174
+ {
175
+ "groupBy": [
176
+ { "name": "country", "value": { "country": {} } },
177
+ { "name": "month", "value": { "registrationDate": {} }, "dateTrunc": "month" }
178
+ ],
179
+ "aggregates": [ { "name": "total", "op": "count" } ],
180
+ "limit": 500
181
+ }
182
+ ```
183
+
184
+ ### 4. Amount ranges (numeric bucket) with HAVING
185
+ ```json
186
+ {
187
+ "groupBy": [ { "name": "bucket", "value": { "totalHt": {} }, "bucketSize": 100 } ],
188
+ "aggregates": [
189
+ { "name": "nbOrders", "op": "count" },
190
+ { "name": "sumHt", "op": "sum", "value": { "totalHt": {} } }
191
+ ],
192
+ "having": [ { "name": "nbOrders", "op": "gte", "value": 5 } ],
193
+ "orders": [ { "name": "bucket", "desc": false } ],
194
+ "limit": 100
195
+ }
196
+ ```
197
+
198
+ ### 5. Global statistics (no grouping)
199
+ ```json
200
+ {
201
+ "aggregates": [
202
+ { "name": "count", "op": "count" },
203
+ { "name": "avgHt", "op": "avg", "value": { "totalHt": {} } },
204
+ { "name": "minHt", "op": "min", "value": { "totalHt": {} } },
205
+ { "name": "maxHt", "op": "max", "value": { "totalHt": {} } },
206
+ { "name": "stdHt", "op": "stdDevSamp", "value": { "totalHt": {} } }
207
+ ],
208
+ "limit": 1
209
+ }
210
+ ```
211
+
212
+ ### 6. Distinct + sort by aggregate
213
+ ```json
214
+ {
215
+ "groupBy": [ { "name": "country", "value": { "country": {} } } ],
216
+ "aggregates": [
217
+ { "name": "members", "op": "count" },
218
+ { "name": "companies", "op": "countDistinct", "value": { "company": {} } }
219
+ ],
220
+ "orders": [ { "name": "members", "desc": true } ],
221
+ "limit": 50
222
+ }
223
+ ```
224
+
225
+ ### 7. Grouping on a to-one relation
226
+ ```json
227
+ {
228
+ "groupBy": [ { "name": "author", "value": { "member": { "lastname": {} } } } ],
229
+ "aggregates": [ { "name": "n", "op": "count" } ],
230
+ "orders": [ { "name": "n", "desc": true } ],
231
+ "limit": 50
232
+ }
233
+ ```
234
+ The `null` group gathers the entities whose relation is empty.
235
+
236
+ ---
237
+
238
+ *See also: [query-language.md](./query-language.md) (WHERE predicates, sorting), [entities.md](./entities.md).*
@@ -1,236 +1,236 @@
1
- ◂ Retour à [README](./README.md)
1
+ ◂ Back to [README](./README.md)
2
2
 
3
- # InWink APIs — Assets (guide d'utilisation de l'API)
3
+ # InWink APIs — Assets (API usage guide)
4
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.
5
+ > **Purpose.** Describe how to **create**, **search**, **update**, **delete**, **organize**
6
+ > and **reference** assets through the InWink admin APIs (EventAPI / CommunityAPI /
7
+ > CustomerAPI). Everything is described at the HTTP level: verb + route + JSON body + JSON response.
8
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é.
9
+ > Auth: Bearer token with the `{scope}asset` claim (value `read` for reads,
10
+ > `edit` for writes). The file upload flow reuses the one described in
11
+ > [files.md](./files.md) — refer to it for details on the signed upload link.
12
12
  >
13
- > Convention JSON : corps de requête acceptés en casse insensible ; réponses en `camelCase`.
13
+ > JSON convention: request bodies are accepted case-insensitively; responses are in `camelCase`.
14
14
 
15
15
  ---
16
16
 
17
- ## 1. Principe (à lire en premier)
17
+ ## 1. Principle (read first)
18
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://`.
19
+ An **asset** is a media library entry, scoped to an event, a community or
20
+ a tenant. Unlike a plain file field, an asset has its own identity, a title,
21
+ a type, a thumbnail, can be placed in a folder and **referenced by other content**
22
+ through the `inwinkasset://` scheme.
23
23
 
24
- **Forme d'un asset (en lecture)** :
24
+ **Shape of an asset (on read)**:
25
25
 
26
26
  ```json
27
27
  {
28
28
  "id": "3fa85f64-....",
29
- "title": "Logo principal", // localisable (peut être un objet multi-langues)
30
- "assetType": 0, // voir table ci-dessous
29
+ "title": "Main logo", // localizable (can be a multi-language object)
30
+ "assetType": 0, // see table below
31
31
  "file": { "blobName": "...", "container": "...", "url": "...", "mimeType": "...", "size": 123 },
32
- "thumbnail": { ... }, // objet fichier ou null
33
- "folderId": "....", // ou null
32
+ "thumbnail": { ... }, // file object or null
33
+ "folderId": "....", // or null
34
34
  "createdAt": "2026-07-01T10:00:00Z",
35
35
  "applicationScope": "...",
36
36
  "applicationScopeId": "..."
37
37
  }
38
38
  ```
39
39
 
40
- **Types d'asset (`assetType`)** : `0`=Image, `1`=Video, `2`=Document, `3`=Audio, `4`=Font, `5`=Model3D.
40
+ **Asset types (`assetType`)**: `0`=Image, `1`=Video, `2`=Document, `3`=Audio, `4`=Font, `5`=Model3D.
41
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, …).
42
+ > ⚠️ **The asset type is never in the request body**: it is determined by the
43
+ > **typed route** you POST to (`image-asset` → Image, `video-asset` → Video, …).
44
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.
45
+ The `file` and `thumbnail` fields are populated by the **same temporary upload flow** as
46
+ file fields (see [files.md](./files.md)): you pass the **temporary blob name** at
47
+ creation, and the API moves it and then serializes it as a file object.
48
48
 
49
49
  ---
50
50
 
51
- ## 2. Routes de base par API
51
+ ## 2. Base routes per API
52
52
 
53
- | API | Base CRUD typée | Base recherche (tous types) |
53
+ | API | Typed CRUD base | Search base (all types) |
54
54
  |-----|-----------------|-----------------------------|
55
55
  | EventAPI | `{eventId}/{type}-asset` | `{eventId}/asset` |
56
56
  | CommunityAPI | `{communityId}/{type}-asset` | `{communityId}/asset` |
57
57
  | CustomerAPI | `{customerId}/audience/{tenantId}/{type}-asset` | `.../asset` |
58
58
 
59
- `{type}` ∈ `image`, `video`, `audio`, `document`, `font`, `model3d` (selon l'API — EventAPI
60
- expose les 6, CommunityAPI 4, CustomerAPI 3).
59
+ `{type}` ∈ `image`, `video`, `audio`, `document`, `font`, `model3d` (depending on the API — EventAPI
60
+ exposes all 6, CommunityAPI 4, CustomerAPI 3).
61
61
 
62
- Dans la suite, `{base}` = la base CRUD typée (ex. `{eventId}/image-asset`).
62
+ Below, `{base}` = the typed CRUD base (e.g. `{eventId}/image-asset`).
63
63
 
64
64
  ---
65
65
 
66
- ## 3. Créer un asset (upload + création)
66
+ ## 3. Create an asset (upload + creation)
67
67
 
68
- ### Étape 1 — Téléverser le fichier (flux temporaire)
68
+ ### Step 1 — Upload the file (temporary flow)
69
69
 
70
- Les endpoints d'upload sont disponibles sous la base de l'asset :
70
+ The upload endpoints are available under the asset base:
71
71
 
72
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é
73
+ POST {base}/file { "type": "image/png", "name": "logo.png" } → temporary access object
74
+ (then PUT the bytes to url+container+blobName+sas — see section file-fields §2)
75
+ POST {base}/file/renew { "blobName": "...", "sas": "..." } → renewal
76
+ POST {base}/file/try-renew { "blobName": "...", "sas": "..." } → renewal if expired
77
77
  POST {base}/file/finalize-tmp { "blobName": "...", "sas": "..." } → 200
78
78
  ```
79
79
 
80
- Répéter pour la miniature si on en fournit une.
80
+ Repeat for the thumbnail if you provide one.
81
81
 
82
- ### Étape 2 — Créer l'asset en référençant les blobs temporaires
82
+ ### Step 2 — Create the asset referencing the temporary blobs
83
83
 
84
84
  ```
85
85
  POST {base}
86
86
  Content-Type: application/json
87
87
 
88
88
  {
89
- "title": "Logo principal",
90
- "file": "<blobName_du_fichier>", // nom du blob temporaire (obligatoire)
91
- "thumbnail": "<blobName_miniature>", // optionnel
92
- "folderId": "3fa85f64-....", // optionnel
89
+ "title": "Main logo",
90
+ "file": "<file_blobName>", // temporary blob name (required)
91
+ "thumbnail": "<thumbnail_blobName>", // optional
92
+ "folderId": "3fa85f64-....", // optional
93
93
  "applicationScope": "...",
94
94
  "applicationScopeId": "..."
95
95
  }
96
96
  ```
97
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.
98
+ Response `200` — the created asset (see shape §1). The API moves the temporary blob to its
99
+ final location and fills `file` / `thumbnail` with the resulting file object.
100
100
 
101
101
  ---
102
102
 
103
- ## 4. Rechercher / compter
103
+ ## 4. Search / count
104
104
 
105
105
  ```
106
106
  POST {base}/query
107
107
  {
108
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 }
109
+ "applicationScopeId": "...", // optional
110
+ "folderId": "3fa85f64-....", // optional — the presence of the key enables folder filtering
111
+ "id": "....", // optional — a specific asset
112
+ "search": "logo", // optional — search in the title
113
+ "fileUrl": "...", // optional — search by file URL
114
+ "page": { "index": 0, "size": 20 }, // or { "skip": 0, "take": 20 }
115
115
  "orders": [ { "desc": true, "value": { "createdAt": {} } } ]
116
116
  }
117
- → 200 liste paginée d'assets
117
+ → 200 paginated list of assets
118
118
  ```
119
119
 
120
120
  ```
121
- POST {base}/count (même corps) → 200 <nombre entier>
121
+ POST {base}/count (same body) → 200 <integer>
122
122
  ```
123
123
 
124
- Le filtrage inclut toujours le scope de l'application et le type de la route.
124
+ Filtering always includes the application scope and the route's type.
125
125
 
126
- > Pour lister **tous types confondus**, utiliser la base recherche `{scopeId}/asset/query`.
126
+ > To list **all types combined**, use the search base `{scopeId}/asset/query`.
127
127
 
128
128
  ---
129
129
 
130
- ## 5. Modifier / supprimer / déplacer
130
+ ## 5. Update / delete / move
131
131
 
132
132
  ```
133
- POST {base}/{assetId} (édition partielle)
133
+ POST {base}/{assetId} (partial edit)
134
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.
135
+ - Missing fields = unchanged. `file`/`thumbnail` optional (omit = keep).
136
+ - `thumbnail` **present but empty** = removes the thumbnail.
137
+ - `folderId`/`title` updated only if present in the body.
138
138
 
139
139
  ```
140
140
  POST {base}/delete/{assetId} → 200
141
- POST {base}/delete { "assetIds": ["...", "..."] } → 200 (suppression en masse)
141
+ POST {base}/delete { "assetIds": ["...", "..."] } → 200 (bulk delete)
142
142
  POST {base}/movetofolder { "assetIds": ["..."], "folderId": "..." } → 200
143
143
  ```
144
144
 
145
- La suppression retire aussi les fichiers (principal + miniature + cache).
145
+ Deletion also removes the files (main + thumbnail + cache).
146
146
 
147
147
  ---
148
148
 
149
- ## 6. Dossiers d'assets
149
+ ## 6. Asset folders
150
150
 
151
- Un dossier a la forme `{ name, parentFolderId?, assetType }` et est hiérarchique.
151
+ A folder has the shape `{ name, parentFolderId?, assetType }` and is hierarchical.
152
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`,
153
+ - **Folder CRUD goes through the generic entity endpoints** (not through the asset
154
+ routes): standard create / search / delete on `{scopeId}/{folderEntity}`
155
+ (e.g. `{eventId}/eventassetfolder`, `{communityId}/communityassetfolder`,
156
156
  `{customerId}/audience/{tenantId}/authtenantassetfolder`).
157
- - **Route spécifique** — supprimer un dossier en **conservant** son contenu (sous-dossiers/assets) :
157
+ - **Specific route** — delete a folder while **keeping** its content (subfolders/assets):
158
158
  ```
159
159
  POST {scopeId}/{folderEntity}/delete/{folderId}/keepSubContent
160
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 »).
161
+ - **Constraint**: the folder targeted during a create/move must exist and have the
162
+ **same type** as the asset, otherwise `400` ("Folder type does not match asset type").
163
163
 
164
164
  ---
165
165
 
166
- ## 7. Référencer un asset depuis un contenu (`inwinkasset://`)
166
+ ## 7. Reference an asset from content (`inwinkasset://`)
167
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é :
168
+ To link an asset to other content (typically the content of a ContentTemplate / page
169
+ builder), you **store a reference string** in the relevant field:
170
170
 
171
171
  ```
172
- inwinkasset://{assetId} // asset du scope courant
173
- inwinkasset://{assetId}/{shardId} // asset d'un autre tenant (cross-tenant)
172
+ inwinkasset://{assetId} // asset of the current scope
173
+ inwinkasset://{assetId}/{shardId} // asset of another tenant (cross-tenant)
174
174
  ```
175
175
 
176
- **Ne pas** y écrire un objet fichier ni une URL directe : juste la chaîne `inwinkasset://{id}`.
176
+ **Do not** write a file object or a direct URL there: just the `inwinkasset://{id}` string.
177
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.
178
+ When the content is read, the API automatically replaces each reference with the **full asset
179
+ object** (`{ id, tenantId, title, assetType, thumbnail, file, applicationScope, validFrom }`).
180
+ **Unresolvable references are removed** from the output. When an event is copied,
181
+ references are remapped to the new identifiers.
182
182
 
183
183
  ---
184
184
 
185
- ## 8. Livraison publique (lecture anonyme)
185
+ ## 8. Public delivery (anonymous read)
186
186
 
187
- Le binaire d'un asset est servi et redimensionné en accès public :
187
+ An asset's binary is served and resized with public access:
188
188
 
189
189
  ```
190
190
  GET event/{eventId}/asset/{assetId}/file[/{language}/{fileName}]
191
191
  GET event/{eventId}/asset/{assetId}/thumbnail[/{language}/{fileName}]
192
- (préfixes équivalents : tenant/{tenantId}/..., community/{communityId}/...)
192
+ (equivalent prefixes: tenant/{tenantId}/..., community/{communityId}/...)
193
193
  ```
194
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.
195
+ Query parameters (optional): `mw` (max width), `mh` (max height), `q` (quality),
196
+ `b` (blur), `o` (output format), `nocache`. Images are resized on the fly
197
+ (30-day cache + ETag). Non-image files are streamed or **redirected (302)** to the CDN.
198
198
 
199
199
  ---
200
200
 
201
- ## 9. Exemples
201
+ ## 9. Examples
202
202
 
203
- - **Recherche** :
203
+ - **Search**:
204
204
  ```
205
205
  POST {eventId}/image-asset/query
206
206
  { "orders": [ { "desc": true, "value": { "createdAt": {} } } ] }
207
- → 200 liste d'assets
207
+ → 200 list of assets
208
208
  ```
209
- - **Recherche par dossier** : ajouter `"folderId": "<guid>"` au corps.
210
- - **Création** (après upload du fichier temporaire) :
209
+ - **Search by folder**: add `"folderId": "<guid>"` to the body.
210
+ - **Creation** (after uploading the temporary file):
211
211
  ```
212
212
  POST {eventId}/image-asset
213
- { "title": "Bannière", "file": "<blobName>", "applicationScope": "...", "applicationScopeId": "..." }
214
- → 200 asset créé
213
+ { "title": "Banner", "file": "<blobName>", "applicationScope": "...", "applicationScopeId": "..." }
214
+ → 200 created asset
215
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** :
216
+ - **Public download**: `GET event/{eventId}/asset/{assetId}/file` → `200` (image) or
217
+ `302` (redirect, e.g. video).
218
+ - **Folder deletion keeping the content**:
219
219
  `POST {eventId}/eventassetfolder/delete/{folderId}/keepSubContent`.
220
220
 
221
221
  ---
222
222
 
223
- ## 10. Points d'attention
223
+ ## 10. Caveats
224
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.
225
+ - **No MIME check per asset type** at creation: the API only checks that the
226
+ content matches its declared MIME (among supported MIMEs), not that it matches the
227
+ route's type. The asset type only changes the storage location and the public rendering
228
+ (only images are resized; the others are streamed/redirected).
229
+ - **Cache busting**: on every file change, a new location is generated,
230
+ so the file URL changes — do not cache an asset file object beyond an edit.
231
+ - **`title`** can be localized (multi-language object).
232
+ - **`file` required at creation**, `thumbnail` optional; on edit both are optional.
233
233
 
234
234
  ---
235
235
 
236
- *Voir aussi : `file-fields` (détail du flux d'upload temporaire réutilisé ici), `mutations`, `entities`.*
236
+ *See also: [files.md](./files.md) (details of the temporary upload flow reused here), [create.md](./create.md), [entities.md](./entities.md).*
@@ -1,4 +1,4 @@
1
- ◂ Retour à [README](./README.md)
1
+ ◂ Back to [README](./README.md)
2
2
 
3
3
  # Create
4
4
 
@@ -16,6 +16,6 @@ POST {api}/{shardId}/{entity}
16
16
  ```
17
17
 
18
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)).
19
+ - `selects` — optional. Controls which properties are returned on success (see [query-language.md](./query-language.md#projection-selects)).
20
20
 
21
- > Pour créer plusieurs entités efficacement, préférer [import.md](./import.md).
21
+ > To create several entities efficiently, prefer [import.md](./import.md).
@@ -1,4 +1,4 @@
1
- ◂ Retour à [README](./README.md)
1
+ ◂ Back to [README](./README.md)
2
2
 
3
3
  # Delete & Massdelete
4
4
 
@@ -18,4 +18,6 @@ No body required.
18
18
  POST {api}/{shardId}/{entity}/massdelete
19
19
  ```
20
20
 
21
- Bulk DELETE like `DELETE … WHERE` in SQL. S'appuie sur un filtre (voir [query-language.md](./query-language.md)).
21
+ Bulk DELETE, like `DELETE … WHERE` in SQL. Relies on a filter (see [query-language.md](./query-language.md#filters)).
22
+
23
+ > A deleted "With History" entity remains readable through `history/removed/query` (see [history.md](./history.md)).