@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.
- package/README.md +15 -1
- package/dist/cli.js +133 -34
- package/package.json +1 -1
- package/templates/vite-extension/AGENTS.md.tpl +13 -5
- package/templates/vite-extension/docs/inwink-api/README.md +65 -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
|
@@ -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
|
-
◂
|
|
1
|
+
◂ Back to [README](./README.md)
|
|
2
2
|
|
|
3
|
-
# InWink APIs — Assets (
|
|
3
|
+
# InWink APIs — Assets (API usage guide)
|
|
4
4
|
|
|
5
|
-
> **
|
|
6
|
-
>
|
|
7
|
-
> CustomerAPI).
|
|
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
|
|
10
|
-
> `edit`
|
|
11
|
-
>
|
|
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
|
-
>
|
|
13
|
+
> JSON convention: request bodies are accepted case-insensitively; responses are in `camelCase`.
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
-
## 1.
|
|
17
|
+
## 1. Principle (read first)
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
**
|
|
24
|
+
**Shape of an asset (on read)**:
|
|
25
25
|
|
|
26
26
|
```json
|
|
27
27
|
{
|
|
28
28
|
"id": "3fa85f64-....",
|
|
29
|
-
"title": "
|
|
30
|
-
"assetType": 0, //
|
|
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": { ... }, //
|
|
33
|
-
"folderId": "....", //
|
|
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
|
-
**
|
|
40
|
+
**Asset types (`assetType`)**: `0`=Image, `1`=Video, `2`=Document, `3`=Audio, `4`=Font, `5`=Model3D.
|
|
41
41
|
|
|
42
|
-
> ⚠️ **
|
|
43
|
-
> **route
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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.
|
|
51
|
+
## 2. Base routes per API
|
|
52
52
|
|
|
53
|
-
| API |
|
|
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` (
|
|
60
|
-
|
|
59
|
+
`{type}` ∈ `image`, `video`, `audio`, `document`, `font`, `model3d` (depending on the API — EventAPI
|
|
60
|
+
exposes all 6, CommunityAPI 4, CustomerAPI 3).
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
Below, `{base}` = the typed CRUD base (e.g. `{eventId}/image-asset`).
|
|
63
63
|
|
|
64
64
|
---
|
|
65
65
|
|
|
66
|
-
## 3.
|
|
66
|
+
## 3. Create an asset (upload + creation)
|
|
67
67
|
|
|
68
|
-
###
|
|
68
|
+
### Step 1 — Upload the file (temporary flow)
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
The upload endpoints are available under the asset base:
|
|
71
71
|
|
|
72
72
|
```
|
|
73
|
-
POST {base}/file { "type": "image/png", "name": "logo.png" } →
|
|
74
|
-
(
|
|
75
|
-
POST {base}/file/renew { "blobName": "...", "sas": "..." } →
|
|
76
|
-
POST {base}/file/try-renew { "blobName": "...", "sas": "..." } →
|
|
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
|
-
|
|
80
|
+
Repeat for the thumbnail if you provide one.
|
|
81
81
|
|
|
82
|
-
###
|
|
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": "
|
|
90
|
-
"file": "<
|
|
91
|
-
"thumbnail": "<
|
|
92
|
-
"folderId": "3fa85f64-....", //
|
|
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
|
-
|
|
99
|
-
|
|
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.
|
|
103
|
+
## 4. Search / count
|
|
104
104
|
|
|
105
105
|
```
|
|
106
106
|
POST {base}/query
|
|
107
107
|
{
|
|
108
108
|
"applicationScope": "...",
|
|
109
|
-
"applicationScopeId": "...", //
|
|
110
|
-
"folderId": "3fa85f64-....", //
|
|
111
|
-
"id": "....", //
|
|
112
|
-
"search": "logo", //
|
|
113
|
-
"fileUrl": "...", //
|
|
114
|
-
"page": { "index": 0, "size": 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
|
|
117
|
+
→ 200 paginated list of assets
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
```
|
|
121
|
-
POST {base}/count (
|
|
121
|
+
POST {base}/count (same body) → 200 <integer>
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
Filtering always includes the application scope and the route's type.
|
|
125
125
|
|
|
126
|
-
>
|
|
126
|
+
> To list **all types combined**, use the search base `{scopeId}/asset/query`.
|
|
127
127
|
|
|
128
128
|
---
|
|
129
129
|
|
|
130
|
-
## 5.
|
|
130
|
+
## 5. Update / delete / move
|
|
131
131
|
|
|
132
132
|
```
|
|
133
|
-
POST {base}/{assetId} (
|
|
133
|
+
POST {base}/{assetId} (partial edit)
|
|
134
134
|
```
|
|
135
|
-
-
|
|
136
|
-
- `thumbnail` **
|
|
137
|
-
- `folderId`/`title`
|
|
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 (
|
|
141
|
+
POST {base}/delete { "assetIds": ["...", "..."] } → 200 (bulk delete)
|
|
142
142
|
POST {base}/movetofolder { "assetIds": ["..."], "folderId": "..." } → 200
|
|
143
143
|
```
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
Deletion also removes the files (main + thumbnail + cache).
|
|
146
146
|
|
|
147
147
|
---
|
|
148
148
|
|
|
149
|
-
## 6.
|
|
149
|
+
## 6. Asset folders
|
|
150
150
|
|
|
151
|
-
|
|
151
|
+
A folder has the shape `{ name, parentFolderId?, assetType }` and is hierarchical.
|
|
152
152
|
|
|
153
|
-
- **
|
|
154
|
-
|
|
155
|
-
(
|
|
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
|
-
- **
|
|
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
|
-
- **
|
|
162
|
-
**
|
|
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.
|
|
166
|
+
## 7. Reference an asset from content (`inwinkasset://`)
|
|
167
167
|
|
|
168
|
-
|
|
169
|
-
builder),
|
|
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
|
|
173
|
-
inwinkasset://{assetId}/{shardId} // asset
|
|
172
|
+
inwinkasset://{assetId} // asset of the current scope
|
|
173
|
+
inwinkasset://{assetId}/{shardId} // asset of another tenant (cross-tenant)
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
-
**
|
|
176
|
+
**Do not** write a file object or a direct URL there: just the `inwinkasset://{id}` string.
|
|
177
177
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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.
|
|
185
|
+
## 8. Public delivery (anonymous read)
|
|
186
186
|
|
|
187
|
-
|
|
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
|
-
(
|
|
192
|
+
(equivalent prefixes: tenant/{tenantId}/..., community/{communityId}/...)
|
|
193
193
|
```
|
|
194
194
|
|
|
195
|
-
|
|
196
|
-
`b` (
|
|
197
|
-
(
|
|
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.
|
|
201
|
+
## 9. Examples
|
|
202
202
|
|
|
203
|
-
- **
|
|
203
|
+
- **Search**:
|
|
204
204
|
```
|
|
205
205
|
POST {eventId}/image-asset/query
|
|
206
206
|
{ "orders": [ { "desc": true, "value": { "createdAt": {} } } ] }
|
|
207
|
-
→ 200
|
|
207
|
+
→ 200 list of assets
|
|
208
208
|
```
|
|
209
|
-
- **
|
|
210
|
-
- **
|
|
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": "
|
|
214
|
-
→ 200 asset
|
|
213
|
+
{ "title": "Banner", "file": "<blobName>", "applicationScope": "...", "applicationScopeId": "..." }
|
|
214
|
+
→ 200 created asset
|
|
215
215
|
```
|
|
216
|
-
- **
|
|
217
|
-
`302` (
|
|
218
|
-
- **
|
|
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.
|
|
223
|
+
## 10. Caveats
|
|
224
224
|
|
|
225
|
-
- **
|
|
226
|
-
|
|
227
|
-
type
|
|
228
|
-
(
|
|
229
|
-
- **
|
|
230
|
-
|
|
231
|
-
- **`title`**
|
|
232
|
-
- **`file`
|
|
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
|
-
*
|
|
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
|
-
◂
|
|
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 (
|
|
19
|
+
- `selects` — optional. Controls which properties are returned on success (see [query-language.md](./query-language.md#projection-selects)).
|
|
20
20
|
|
|
21
|
-
>
|
|
21
|
+
> To create several entities efficiently, prefer [import.md](./import.md).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
◂
|
|
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.
|
|
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)).
|