@inwink-mcp/backoffice-extension-helpers 1.0.10 → 1.0.12
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/vite-extension/AGENTS.md.tpl +12 -4
- package/templates/vite-extension/docs/inwink-api/README.md +63 -24
- package/templates/vite-extension/docs/inwink-api/aggregate.md +238 -0
- package/templates/vite-extension/docs/inwink-api/assets.md +107 -107
- package/templates/vite-extension/docs/inwink-api/create.md +3 -3
- package/templates/vite-extension/docs/inwink-api/delete.md +4 -2
- package/templates/vite-extension/docs/inwink-api/entities.md +24 -60
- package/templates/vite-extension/docs/inwink-api/entity-template.md +201 -0
- package/templates/vite-extension/docs/inwink-api/files.md +136 -135
- package/templates/vite-extension/docs/inwink-api/history.md +68 -0
- package/templates/vite-extension/docs/inwink-api/import.md +4 -4
- package/templates/vite-extension/docs/inwink-api/query-language.md +127 -38
- package/templates/vite-extension/docs/inwink-api/query.md +12 -10
- package/templates/vite-extension/docs/inwink-api/update.md +7 -5
- package/templates/vite-extension/src/hooks/useInwinkApi.ts +7 -4
package/package.json
CHANGED
|
@@ -60,15 +60,19 @@ Les outils MCP donnent les endpoints/champs **exacts** ; la doc statique dans `d
|
|
|
60
60
|
|
|
61
61
|
| Tâche | Fichier |
|
|
62
62
|
|-------|---------|
|
|
63
|
+
| URLs de base, shardId, routes d'audience, codes HTTP & règles générales | [docs/inwink-api/README.md](docs/inwink-api/README.md) |
|
|
63
64
|
| Lire / lister des entités (query, count, pagination, projection, joins) | [docs/inwink-api/query.md](docs/inwink-api/query.md) |
|
|
64
65
|
| Construire un filtre / tri / projection (langage de requête JSON) | [docs/inwink-api/query-language.md](docs/inwink-api/query-language.md) |
|
|
66
|
+
| Regrouper / agréger (GROUP BY, statistiques, facettes) | [docs/inwink-api/aggregate.md](docs/inwink-api/aggregate.md) |
|
|
65
67
|
| Créer une entité | [docs/inwink-api/create.md](docs/inwink-api/create.md) |
|
|
66
68
|
| Modifier (patch / massupdate) | [docs/inwink-api/update.md](docs/inwink-api/update.md) |
|
|
67
69
|
| Supprimer (delete / massdelete) | [docs/inwink-api/delete.md](docs/inwink-api/delete.md) |
|
|
68
70
|
| Import en masse (bulk create/update) | [docs/inwink-api/import.md](docs/inwink-api/import.md) |
|
|
71
|
+
| Versions passées et entités supprimées | [docs/inwink-api/history.md](docs/inwink-api/history.md) |
|
|
72
|
+
| Contrats d'entités, endpoints divers | [docs/inwink-api/entities.md](docs/inwink-api/entities.md) |
|
|
73
|
+
| Déclarer un champ custom (template d'entité) | [docs/inwink-api/entity-template.md](docs/inwink-api/entity-template.md) |
|
|
69
74
|
| Champs de type `file` (upload / download) | [docs/inwink-api/files.md](docs/inwink-api/files.md) |
|
|
70
|
-
|
|
|
71
|
-
| Codes HTTP & règles générales | [docs/inwink-api/README.md](docs/inwink-api/README.md) |
|
|
75
|
+
| Manipuler les `assets` (galerie media) | [docs/inwink-api/assets.md](docs/inwink-api/assets.md) |
|
|
72
76
|
|
|
73
77
|
## Appeler l'API inwink
|
|
74
78
|
|
|
@@ -90,7 +94,7 @@ function MonComposant() {
|
|
|
90
94
|
}
|
|
91
95
|
```
|
|
92
96
|
|
|
93
|
-
Méthodes disponibles sur chaque hook : `request<T>(path, init?)`, `get<T>`, `post<T>(path, body?)`, `put<T>(path, body?)`, `del<T>`.
|
|
97
|
+
Méthodes disponibles sur chaque hook : `request<T>(path, init?)`, `get<T>`, `post<T>(path, body?)`, `put<T>(path, body?)`, `patch<T>(path, body?)`, `del<T>`.
|
|
94
98
|
|
|
95
99
|
Pour appeler un **hôte externe déclaré** (hors API inwink), utilise le hook générique `useInwinkApi(baseUrl)`.
|
|
96
100
|
|
|
@@ -100,6 +104,7 @@ Bonnes pratiques :
|
|
|
100
104
|
- 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
105
|
- Au-delà de 429, une réponse non-OK lève une `InwinkApiError` (`status`, `body`) — à présenter via `ErrorState` ou un toast.
|
|
102
106
|
- Pour propager l'annulation, passer le `signal` reçu de `useAsync` dans `init` : `eventApi.get(path, { signal })`.
|
|
107
|
+
- Verbes disponibles : `get`, `post`, `put`, `patch`, `del`, plus `request` pour le reste. Une réponse OK sans corps — un 204, ou un 200 comme en renvoient les écritures de template d'entité — résout sur `undefined`.
|
|
103
108
|
- Ne déclarer dans `permissions.platform` que les scopes réellement utilisés.
|
|
104
109
|
- Déclarer les hôtes externes dans `permissions.externalHosts` (HTTPS uniquement).
|
|
105
110
|
|
|
@@ -129,10 +134,12 @@ import { useInwinkContext } from './hooks/useInwinkContext';
|
|
|
129
134
|
function MonComposant() {
|
|
130
135
|
const { scope, eventId, communityId, tenantId } = useInwinkContext();
|
|
131
136
|
// scope vaut 'event' | 'community' | 'tenant' | null (priorité event > community > tenant)
|
|
132
|
-
//
|
|
137
|
+
// En scope 'event' ou 'community', l'id correspondant est le shardId des chemins d'API.
|
|
133
138
|
}
|
|
134
139
|
```
|
|
135
140
|
|
|
141
|
+
En scope `tenant`, le `tenantId` **n'est pas un shardId** : les entités d'audience (`authtenant*`) vivent sous `{customerId}/audience/{tenantId}/{entity}/...` sur l'API Customer, où le shardId reste le `customerId` (cf. [docs/inwink-api/README.md](docs/inwink-api/README.md#audience-scoped-entities-customerapi)).
|
|
142
|
+
|
|
136
143
|
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
144
|
|
|
138
145
|
## Recettes
|
|
@@ -231,5 +238,6 @@ npx @inwink-mcp/backoffice-extension-helpers@latest verify # vérification de
|
|
|
231
238
|
|
|
232
239
|
> Ces consignes s'appliquent à **tout** assistant IA / outil agentique qui manipule cette extension, quel qu'il soit.
|
|
233
240
|
|
|
241
|
+
- **Langue de l'extension.** Ce guide est rédigé en français et la référence API (`docs/inwink-api/`) en anglais : cela ne dit rien de la langue de l'extension. Échange avec l'utilisateur dans sa langue. Pour le code, les commentaires et les textes d'interface, utilise la langue qu'il demande ou, à défaut, celle dans laquelle il t'écrit — pas celle de cette documentation ni des commentaires du squelette.
|
|
234
242
|
- **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
243
|
- **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`).
|
|
@@ -1,43 +1,82 @@
|
|
|
1
|
-
# inwink API
|
|
1
|
+
# inwink API reference
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Reference for consuming the inwink APIs from the extension. Calls always go through the `useInwink*Api` hooks, which set the Bearer token and the base URL: the paths below are relative to that base.
|
|
4
4
|
|
|
5
|
-
> **
|
|
5
|
+
> **Dynamic discovery first.** This documentation describes call *patterns* (verbs, payload shapes, query language). The **exact endpoints, fields and entities** are discovered with the MCP tools (`inwink_mcp_find_api_endpoint`, `inwink_mcp_get_entity_description`) — never guess a route or a schema.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Contents
|
|
8
8
|
|
|
9
|
-
|
|
|
9
|
+
| Task | File |
|
|
10
10
|
|---|---|
|
|
11
|
-
|
|
|
12
|
-
|
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
|
19
|
-
|
|
|
11
|
+
| Read / list entities (query, count, pagination, projection, joins) | [query.md](./query.md) |
|
|
12
|
+
| Build a filter / sort / projection (JSON query language) | [query-language.md](./query-language.md) |
|
|
13
|
+
| Group / aggregate (GROUP BY, statistics, facets) | [aggregate.md](./aggregate.md) |
|
|
14
|
+
| Create an entity | [create.md](./create.md) |
|
|
15
|
+
| Edit an entity (patch / massupdate) | [update.md](./update.md) |
|
|
16
|
+
| Delete an entity (delete / massdelete) | [delete.md](./delete.md) |
|
|
17
|
+
| Bulk import (bulk create/update) | [import.md](./import.md) |
|
|
18
|
+
| Past versions and deleted entities | [history.md](./history.md) |
|
|
19
|
+
| Entity contracts, other endpoints | [entities.md](./entities.md) |
|
|
20
|
+
| Declare a custom field (entity template) | [entity-template.md](./entity-template.md) |
|
|
21
|
+
| `file` fields (upload / download) | [files.md](./files.md) |
|
|
22
|
+
| Handle `assets` (media library) | [assets.md](./assets.md) |
|
|
20
23
|
|
|
21
24
|
---
|
|
22
25
|
|
|
23
|
-
##
|
|
26
|
+
## API Base URLs
|
|
27
|
+
|
|
28
|
+
| API | Base URL | Hook | Scope |
|
|
29
|
+
|---|---|---|---|
|
|
30
|
+
| EventAPI | `https://api-event.inwink.com` | `useInwinkEventApi` | Entities tied to an event (persons, sessions, exhibitors…) |
|
|
31
|
+
| CommunityAPI | `https://api-community.inwink.com` | `useInwinkCommunityApi` | Entities tied to a community (members, forums, forumsubjects…) |
|
|
32
|
+
| CustomerAPI | `https://api-customer.inwink.com` | `useInwinkCustomerApi` | Entities tied to the customer (events, contacts…) |
|
|
33
|
+
|
|
34
|
+
## URL pattern
|
|
35
|
+
|
|
36
|
+
- EventAPI: `{eventAPI}/{eventId}/{entity}/...`
|
|
37
|
+
- CommunityAPI: `{communityAPI}/{communityId}/{entity}/...`
|
|
38
|
+
- CustomerAPI: `{customerAPI}/{customerId}/{entity}/...`
|
|
39
|
+
|
|
40
|
+
The `{eventId}`, `{communityId}` or `{customerId}` is called the **shardId** — it scopes each request to one specific event, community or customer.
|
|
41
|
+
|
|
42
|
+
### Audience-scoped entities (CustomerAPI)
|
|
43
|
+
|
|
44
|
+
A customer holds one or more **audiences** (also called *tenants*). Entities that belong to an audience rather than to the whole customer have a name starting with `authtenant` (`authtenantcommunity`, `authtenantcontact`, `authtenantcompany`, `authtenantmailtemplate`…) and live on a two-segment route:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
{customerAPI}/{customerId}/audience/{tenantId}/{entity}/...
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The `{customerId}` remains the shardId; the `{tenantId}` identifies the audience. On these entities, `{shardId}` reads `{customerId}/audience/{tenantId}` in every route of this documentation. An `authtenant*` entity called without the audience segment returns 404.
|
|
51
|
+
|
|
52
|
+
## Common headers
|
|
53
|
+
|
|
54
|
+
Every request requires (set by the hooks):
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
Authorization: Bearer {access_token}
|
|
58
|
+
Content-Type: application/json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## General rules
|
|
24
64
|
|
|
25
65
|
- Property names are **case-insensitive**.
|
|
26
66
|
- Enum values are **case-insensitive**.
|
|
27
67
|
- All IDs are UUIDs.
|
|
28
68
|
- Dates are ISO 8601 / UTC.
|
|
29
|
-
-
|
|
30
|
-
|
|
31
|
-
---
|
|
69
|
+
- Write operations on entities (create, edit, delete, import) use the **POST** method. Only the partial edit of an entity's template uses `PATCH` (see [entity-template.md](./entity-template.md)).
|
|
32
70
|
|
|
33
|
-
## HTTP
|
|
71
|
+
## HTTP codes
|
|
34
72
|
|
|
35
73
|
| Code | Meaning |
|
|
36
74
|
|---|---|
|
|
37
|
-
| 200 | Success — parse body as JSON |
|
|
75
|
+
| 200 | Success — parse the body as JSON (some writes, such as entity template writes, return 200 with no body) |
|
|
38
76
|
| 204 | Success — no content |
|
|
39
|
-
| 400 | Bad request — body
|
|
40
|
-
| 401 | Unauthorized — token expired or invalid |
|
|
41
|
-
| 403 | Forbidden — missing rights, contact CSM |
|
|
42
|
-
|
|
|
77
|
+
| 400 | Bad request — the body holds the error details |
|
|
78
|
+
| 401 | Unauthorized — token expired or invalid (handled by the hooks) |
|
|
79
|
+
| 403 | Forbidden — missing rights, contact the CSM |
|
|
80
|
+
| 404 | Entity not found, empty body: entity name does not exist, entity exists but under another name on another API, or `authtenant*` entity called without the audience segment. Do not guess another name: run `inwink_mcp_find_api_endpoint` again with a natural-language description of the need |
|
|
81
|
+
| 500 | Server error — possibly transient, contact support if it persists. Also returned for a `shardId` that does not exist: check the identifier before concluding to an outage |
|
|
43
82
|
| 503 | Transient infrastructure error — retry later |
|
|
@@ -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).*
|