@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
|
@@ -1,144 +1,145 @@
|
|
|
1
|
-
◂
|
|
2
|
-
# InWink APIs — Champs de type fichier (guide d'utilisation de l'API)
|
|
1
|
+
◂ Back to [README](./README.md)
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
>
|
|
3
|
+
# InWink APIs — File-type fields (API usage guide)
|
|
4
|
+
|
|
5
|
+
> **Purpose.** Describe how to **upload**, **read**, **replace** and **delete** a file
|
|
6
|
+
> carried by an entity field, through the InWink admin APIs (EventAPI / CommunityAPI /
|
|
7
|
+
> CustomerAPI). Everything is described at the HTTP level: verb + route + JSON body + JSON response.
|
|
7
8
|
>
|
|
8
|
-
> Auth
|
|
9
|
-
>
|
|
9
|
+
> Auth: Bearer token with the scope claims. The public FrontAPI (participant,
|
|
10
|
+
> anonymous) has different routes and is not covered (note at the end of the document).
|
|
10
11
|
>
|
|
11
|
-
>
|
|
12
|
-
>
|
|
12
|
+
> JSON convention: request bodies are accepted case-insensitively; responses
|
|
13
|
+
> are in `camelCase`. The examples below use `camelCase`.
|
|
13
14
|
|
|
14
15
|
---
|
|
15
16
|
|
|
16
|
-
## 1.
|
|
17
|
+
## 1. Principle (read first)
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
`videofile`
|
|
20
|
-
|
|
19
|
+
A "file-type field" is an entity field (e.g. `prop3` on `person`,
|
|
20
|
+
`videofile` on `vod`, `audiofile` on `podcast`…) whose type declared in the template
|
|
21
|
+
is one of:
|
|
21
22
|
|
|
22
|
-
|
|
|
23
|
+
| Field type | Effect |
|
|
23
24
|
|---------------|-------|
|
|
24
|
-
| `file` |
|
|
25
|
-
| `image` | image (
|
|
26
|
-
| `filevideo` |
|
|
25
|
+
| `file` | generic file (document, etc.) |
|
|
26
|
+
| `image` | image (thumbnails generated automatically) |
|
|
27
|
+
| `filevideo` | video (encoding + transcripts triggered) |
|
|
27
28
|
|
|
28
|
-
**
|
|
29
|
+
**The field value changes shape depending on direction:**
|
|
29
30
|
|
|
30
|
-
- **
|
|
31
|
-
|
|
32
|
-
- **
|
|
31
|
+
- **On write**: a plain **string = the temporary blob name** (`blobName`) obtained at
|
|
32
|
+
the upload step.
|
|
33
|
+
- **On read**: a full JSON **file object**
|
|
33
34
|
(`{ blobName, container, url, fileName, mimeType, size, ... }`).
|
|
34
35
|
|
|
35
|
-
**
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
**The upload follows a 4-step "direct upload link" pattern.** The API is never in the
|
|
37
|
+
bytes path: it issues a signed upload URL (SAS), and **the client sends the
|
|
38
|
+
bytes directly to storage**.
|
|
38
39
|
|
|
39
40
|
```
|
|
40
|
-
1. POST .../file →
|
|
41
|
-
2. PUT
|
|
42
|
-
3. POST .../file/finalize-tmp →
|
|
43
|
-
4. PATCH/PUT
|
|
44
|
-
|
|
41
|
+
1. POST .../file → the API creates a temporary location, returns a signed URL
|
|
42
|
+
2. PUT the bytes → the client uploads directly to the signed URL
|
|
43
|
+
3. POST .../file/finalize-tmp → the API validates and freezes the temporary file
|
|
44
|
+
4. PATCH/PUT the entity → set the field value = blob name; the API moves it
|
|
45
|
+
to its final location and rewrites the value as a file object
|
|
45
46
|
```
|
|
46
47
|
|
|
47
|
-
> ⚠️ **
|
|
48
|
-
>
|
|
49
|
-
>
|
|
48
|
+
> ⚠️ **Crucial point**: there is **no "attach a file" endpoint**. You reference
|
|
49
|
+
> the temporary blob name in the field, then **save the entity through its usual
|
|
50
|
+
> edit endpoint**. That save is what materializes the file.
|
|
50
51
|
|
|
51
|
-
###
|
|
52
|
+
### Route shape
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
An entity's file entry point is `{scopeId}/{entity}/file`. Examples:
|
|
54
55
|
`{eventId}/person/file`, `{eventId}/session/file`, `{communityId}/vod/file`.
|
|
55
56
|
|
|
56
|
-
`{scopeId}`
|
|
57
|
-
- EventAPI
|
|
58
|
-
- CommunityAPI
|
|
59
|
-
- CustomerAPI
|
|
57
|
+
`{scopeId}` depending on the API:
|
|
58
|
+
- EventAPI: `{eventId}`
|
|
59
|
+
- CommunityAPI: `{communityId}`
|
|
60
|
+
- CustomerAPI: `{customerId}/audience/{tenantId}` (or `{customerId}`)
|
|
60
61
|
|
|
61
62
|
---
|
|
62
63
|
|
|
63
|
-
## 2.
|
|
64
|
+
## 2. Upload a file into an entity field (full flow)
|
|
64
65
|
|
|
65
|
-
###
|
|
66
|
+
### Step 1 — Create the temporary location
|
|
66
67
|
|
|
67
68
|
```
|
|
68
69
|
POST {scopeId}/{entity}/file
|
|
69
70
|
Content-Type: application/json
|
|
70
71
|
|
|
71
72
|
{
|
|
72
|
-
"entity": "person", //
|
|
73
|
-
"field": "prop3", //
|
|
74
|
-
"type": "image/png", //
|
|
75
|
-
"name": "photo.png", //
|
|
76
|
-
"blobType": "Block" // "Block" (
|
|
73
|
+
"entity": "person", // entity name
|
|
74
|
+
"field": "prop3", // file field key
|
|
75
|
+
"type": "image/png", // file MIME type
|
|
76
|
+
"name": "photo.png", // file name
|
|
77
|
+
"blobType": "Block" // "Block" (default) | "append" | "page"
|
|
77
78
|
}
|
|
78
79
|
```
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
Response `200` — temporary access object:
|
|
81
82
|
|
|
82
83
|
```json
|
|
83
84
|
{
|
|
84
|
-
"url": "https://<
|
|
85
|
+
"url": "https://<storage>.blob.core.windows.net",
|
|
85
86
|
"container": "tmp",
|
|
86
87
|
"blobName": "a7f3c1e0-....png",
|
|
87
88
|
"sas": "?sv=...&sig=..."
|
|
88
89
|
}
|
|
89
90
|
```
|
|
90
91
|
|
|
91
|
-
###
|
|
92
|
+
### Step 2 — Upload the bytes directly (outside the API)
|
|
92
93
|
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
The client builds the target URL and **sends the bytes** to it (read/write rights on
|
|
95
|
+
the temporary location):
|
|
95
96
|
|
|
96
97
|
```
|
|
97
98
|
PUT {url}/{container}/{blobName}{sas}
|
|
98
99
|
x-ms-blob-type: BlockBlob
|
|
99
100
|
Content-Type: image/png
|
|
100
101
|
|
|
101
|
-
<
|
|
102
|
+
<file bytes>
|
|
102
103
|
```
|
|
103
104
|
|
|
104
|
-
|
|
105
|
+
For a long upload, renew the signed link if needed:
|
|
105
106
|
|
|
106
107
|
```
|
|
107
|
-
POST {scopeId}/{entity}/file/renew { "blobName": "...", "sas": "..." } →
|
|
108
|
-
POST {scopeId}/{entity}/file/try-renew { "blobName": "...", "sas": "..." } →
|
|
108
|
+
POST {scopeId}/{entity}/file/renew { "blobName": "...", "sas": "..." } → new access object
|
|
109
|
+
POST {scopeId}/{entity}/file/try-renew { "blobName": "...", "sas": "..." } → renewed if expired
|
|
109
110
|
```
|
|
110
111
|
|
|
111
|
-
###
|
|
112
|
+
### Step 3 — Finalize the temporary file
|
|
112
113
|
|
|
113
114
|
```
|
|
114
115
|
POST {scopeId}/{entity}/file/finalize-tmp
|
|
115
116
|
{ "blobName": "a7f3c1e0-....png", "sas": "?sv=...&sig=..." }
|
|
116
117
|
```
|
|
117
118
|
|
|
118
|
-
- `200
|
|
119
|
-
- `400`
|
|
120
|
-
- `400`
|
|
119
|
+
- `200`: the content matches the declared MIME type; the temporary file is frozen.
|
|
120
|
+
- `400` with body `101`: the content does not match the announced MIME (rejected).
|
|
121
|
+
- `400` with body `102`: unsupported file type.
|
|
121
122
|
|
|
122
|
-
###
|
|
123
|
+
### Step 4 — Save the entity referencing the file
|
|
123
124
|
|
|
124
|
-
|
|
125
|
+
Through the **entity's usual edit endpoint**, set the field value = the `blobName`:
|
|
125
126
|
|
|
126
127
|
```
|
|
127
128
|
PATCH {scopeId}/person/{personId}
|
|
128
129
|
{ "prop3": "a7f3c1e0-....png" }
|
|
129
130
|
```
|
|
130
131
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
On save, the API validates (size, allowed MIMEs, content consistency), **moves the
|
|
133
|
+
file to its final location**, **rewrites the field value** as a full file
|
|
134
|
+
object, and triggers the associated processing (video encoding, thumbnails, document analysis).
|
|
134
135
|
|
|
135
|
-
|
|
136
|
+
The value read back then becomes, for example:
|
|
136
137
|
|
|
137
138
|
```json
|
|
138
139
|
{
|
|
139
140
|
"blobName": "prop3/9f...png",
|
|
140
141
|
"container": "3fa85f64-...._person-prop3",
|
|
141
|
-
"url": "https://<
|
|
142
|
+
"url": "https://<storage>.blob.core.windows.net/...",
|
|
142
143
|
"fileName": "photo.png",
|
|
143
144
|
"mimeType": "image/png",
|
|
144
145
|
"size": 20481,
|
|
@@ -148,17 +149,17 @@ La valeur relue devient par exemple :
|
|
|
148
149
|
|
|
149
150
|
---
|
|
150
151
|
|
|
151
|
-
## 3.
|
|
152
|
+
## 3. Read / download a file
|
|
152
153
|
|
|
153
154
|
```
|
|
154
155
|
GET {scopeId}/{entity}/file/{entityId}/{field}?lang=fr&withoutExpiration=false
|
|
155
156
|
```
|
|
156
157
|
|
|
157
|
-
|
|
158
|
+
Response `200` — file object with access link:
|
|
158
159
|
|
|
159
160
|
```json
|
|
160
161
|
{
|
|
161
|
-
"url": "https://<
|
|
162
|
+
"url": "https://<storage>.blob.core.windows.net",
|
|
162
163
|
"container": "3fa85f64-...._person-prop3",
|
|
163
164
|
"blobName": "prop3/9f...png",
|
|
164
165
|
"fileName": "photo.png",
|
|
@@ -169,44 +170,44 @@ Réponse `200` — objet fichier avec lien d'accès :
|
|
|
169
170
|
}
|
|
170
171
|
```
|
|
171
172
|
|
|
172
|
-
|
|
173
|
+
The client then downloads the bytes directly from **`url` + `/{container}/{blobName}` + `sas`**.
|
|
173
174
|
|
|
174
|
-
- `404`
|
|
175
|
-
- `lang
|
|
176
|
-
- `withoutExpiration=true
|
|
175
|
+
- `404` if the field has no file.
|
|
176
|
+
- `lang`: language for a localized field (default = the scope's default language).
|
|
177
|
+
- `withoutExpiration=true`: long-lived access link.
|
|
177
178
|
|
|
178
|
-
**
|
|
179
|
+
**File of a sub-entity (collection)**:
|
|
179
180
|
```
|
|
180
181
|
GET {scopeId}/{entity}/file/{entityId}/{nestedField}/{index}/{field}?lang=&withoutExpiration=
|
|
181
182
|
```
|
|
182
183
|
|
|
183
|
-
**
|
|
184
|
+
**Video field**: the response is enriched (see §5).
|
|
184
185
|
|
|
185
186
|
---
|
|
186
187
|
|
|
187
|
-
## 4.
|
|
188
|
+
## 4. Replace / re-edit / delete a file
|
|
188
189
|
|
|
189
|
-
**
|
|
190
|
-
|
|
190
|
+
**Get an editable copy** of the current file in the temporary space, then resume
|
|
191
|
+
the finalize + save flow:
|
|
191
192
|
```
|
|
192
193
|
GET {scopeId}/{entity}/file/{entityId}/{field}/copytotmp?lang=fr
|
|
193
194
|
→ 200 { "blobName": "...", "container": "tmp", ... }
|
|
194
195
|
```
|
|
195
196
|
|
|
196
|
-
**
|
|
197
|
-
|
|
197
|
+
**Replace**: upload a new temporary file (§2) and re-edit the entity with the
|
|
198
|
+
new `blobName`. The old file is removed automatically.
|
|
198
199
|
|
|
199
|
-
**
|
|
200
|
+
**Delete**: edit the entity setting the field to `null`.
|
|
200
201
|
|
|
201
202
|
---
|
|
202
203
|
|
|
203
|
-
## 5.
|
|
204
|
+
## 5. Video fields — advanced features
|
|
204
205
|
|
|
205
|
-
|
|
206
|
-
|
|
206
|
+
`filevideo` fields trigger encoding and expose transcripts and document analysis.
|
|
207
|
+
All routes are under `{scopeId}/{entity}/file/...`.
|
|
207
208
|
|
|
208
|
-
###
|
|
209
|
-
`GET {scopeId}/{entity}/file/{entityId}/{field}`
|
|
209
|
+
### Reading a video field
|
|
210
|
+
`GET {scopeId}/{entity}/file/{entityId}/{field}` returns, when the video is ready:
|
|
210
211
|
|
|
211
212
|
```json
|
|
212
213
|
{
|
|
@@ -220,94 +221,94 @@ Toutes les routes sont sous `{scopeId}/{entity}/file/...`.
|
|
|
220
221
|
}
|
|
221
222
|
```
|
|
222
223
|
|
|
223
|
-
###
|
|
224
|
+
### Encoding
|
|
224
225
|
```
|
|
225
|
-
POST {scopeId}/{entity}/file/video/{entityId}/{field}/encoding/retry?lang=fr →
|
|
226
|
-
GET {scopeId}/{entity}/file/{entityId}/{field}/status?lang=fr →
|
|
226
|
+
POST {scopeId}/{entity}/file/video/{entityId}/{field}/encoding/retry?lang=fr → retries the encoding
|
|
227
|
+
GET {scopeId}/{entity}/file/{entityId}/{field}/status?lang=fr → encoding status
|
|
227
228
|
```
|
|
228
229
|
|
|
229
|
-
### Transcripts (
|
|
230
|
+
### Transcripts (requires an AI license)
|
|
230
231
|
```
|
|
231
|
-
POST {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/enabled →
|
|
232
|
+
POST {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/enabled → enables processing
|
|
232
233
|
GET {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/status → { status, progress, duration }
|
|
233
234
|
GET {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/manifest?language=fr
|
|
234
|
-
POST {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/manifest?language=fr (
|
|
235
|
+
POST {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/manifest?language=fr (body = manifest)
|
|
235
236
|
GET {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/{language}
|
|
236
|
-
POST {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/{language} (
|
|
237
|
-
POST {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/{language}/delete (
|
|
237
|
+
POST {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/{language} (body = transcript)
|
|
238
|
+
POST {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/{language}/delete (body = transcript)
|
|
238
239
|
POST {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/reset/from-artifacts
|
|
239
|
-
GET {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/artifacts/{data} (
|
|
240
|
+
GET {scopeId}/{entity}/file/video/{entityId}/{field}/transcripts/artifacts/{data} (302 redirect to the file)
|
|
240
241
|
```
|
|
241
|
-
`status` ∈ `NotSet`, `Pending`/`Processing`, `Ready`.
|
|
242
|
-
|
|
242
|
+
`status` ∈ `NotSet`, `Pending`/`Processing`, `Ready`. An unsaved `Ready` is
|
|
243
|
+
persisted automatically on the first call to `status`/`manifest`/`get`.
|
|
243
244
|
|
|
244
|
-
###
|
|
245
|
+
### Document analysis (requires an AI license)
|
|
245
246
|
```
|
|
246
|
-
GET {scopeId}/{entity}/file/document/{entityId}/{field}/analysis →
|
|
247
|
-
POST {scopeId}/{entity}/file/document/{entityId}/{field}/analysis/start →
|
|
247
|
+
GET {scopeId}/{entity}/file/document/{entityId}/{field}/analysis → operation status
|
|
248
|
+
POST {scopeId}/{entity}/file/document/{entityId}/{field}/analysis/start → starts the analysis
|
|
248
249
|
```
|
|
249
250
|
|
|
250
251
|
---
|
|
251
252
|
|
|
252
|
-
## 6.
|
|
253
|
+
## 6. Reference JSON shapes
|
|
253
254
|
|
|
254
|
-
**
|
|
255
|
-
-
|
|
256
|
-
-
|
|
255
|
+
**Request bodies**
|
|
256
|
+
- Create the temporary location: `{ entity, field, type, name, blobType }`
|
|
257
|
+
- Renew / finalize: `{ blobName, sas }`
|
|
257
258
|
|
|
258
|
-
**
|
|
259
|
-
-
|
|
260
|
-
-
|
|
259
|
+
**Responses**
|
|
260
|
+
- Temporary access object (create/renew): `{ url, container, blobName, sas }`
|
|
261
|
+
- File object with access link (get/download):
|
|
261
262
|
`{ url, container, blobName, fileName, size, mimeType, sas, lastModified }`
|
|
262
|
-
-
|
|
263
|
+
- Persisted file object (field value):
|
|
263
264
|
`{ url, container, blobName, fileName, size, mimeType, linkedBlobs, contentMD5, properties }`
|
|
264
|
-
- Image
|
|
265
|
-
-
|
|
266
|
-
|
|
265
|
+
- Image: adds `thumb`, `thumbSmall`, `thumbLarge` (each a file object).
|
|
266
|
+
- Video: adds `video { id, isReady, transcriptsEnabled, transcripts, transcriptData }`
|
|
267
|
+
and `encodings { <provider>: { hlsUrl, dashUrl, status, progress, timestamp } }`.
|
|
267
268
|
|
|
268
|
-
**
|
|
269
|
-
(
|
|
269
|
+
**Transfer error codes** (body of the `400` response): `101` = invalid file
|
|
270
|
+
(content ≠ MIME), `102` = unsupported file type.
|
|
270
271
|
|
|
271
272
|
---
|
|
272
273
|
|
|
273
|
-
## 7.
|
|
274
|
+
## 7. End-to-end example
|
|
274
275
|
|
|
275
|
-
|
|
276
|
+
Entity `person`, field `prop3` of type `file`.
|
|
276
277
|
|
|
277
|
-
1.
|
|
278
|
+
1. Create the temporary location:
|
|
278
279
|
```
|
|
279
280
|
POST {eventId}/person/file
|
|
280
281
|
{ "entity": "person", "field": "prop3", "name": "photo.jpg", "type": "image/jpeg" }
|
|
281
282
|
→ 200 { "blobName": "<blobName>", "container": "tmp", "url": "...", "sas": "..." }
|
|
282
283
|
```
|
|
283
|
-
2.
|
|
284
|
-
3.
|
|
285
|
-
4.
|
|
286
|
-
→
|
|
287
|
-
|
|
284
|
+
2. Upload the bytes to `url + /tmp/<blobName> + sas`.
|
|
285
|
+
3. Finalize: `POST {eventId}/person/file/finalize-tmp { "blobName": "<blobName>", "sas": "..." }` → `200`.
|
|
286
|
+
4. Edit the person: `PATCH {eventId}/person/<personId> { "prop3": "<blobName>" }` → `200`.
|
|
287
|
+
→ the file is moved from `tmp` to the `{eventId}_person-prop3` container, and the field
|
|
288
|
+
value is read back as a full file object.
|
|
288
289
|
|
|
289
290
|
---
|
|
290
291
|
|
|
291
|
-
## 8.
|
|
292
|
+
## 8. Caveats
|
|
292
293
|
|
|
293
|
-
- **Double
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
- **
|
|
297
|
-
|
|
298
|
-
- **
|
|
299
|
-
- **`withoutExpiration=true
|
|
300
|
-
- **
|
|
294
|
+
- **Double MIME validation**: at `finalize-tmp` (content vs declared MIME) **and** at
|
|
295
|
+
entity save (max size, field's allowed MIMEs, content consistency, SVG
|
|
296
|
+
safety). An invalid file makes the field fail/be emptied.
|
|
297
|
+
- **Final location** = container `{scopeId}_{entity}-{field}` (lowercase) — useful
|
|
298
|
+
for interpreting the returned URLs.
|
|
299
|
+
- **Language**: for a localized field, pass `lang` on read.
|
|
300
|
+
- **`withoutExpiration=true`**: long-lived link (reserve for uses that justify it).
|
|
301
|
+
- **Replacement**: re-uploading and re-editing is enough; the old file is cleaned up automatically.
|
|
301
302
|
|
|
302
303
|
---
|
|
303
304
|
|
|
304
|
-
## Note — FrontAPI (
|
|
305
|
+
## Note — FrontAPI (out of scope)
|
|
305
306
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
`GET community/{communityId}/file/{Entity}/{entityId}/{field}`,
|
|
309
|
-
|
|
307
|
+
On the public/participant side, an equivalent flow exists but with different routes and
|
|
308
|
+
auth: `POST {scopeId}/file` (+ `/renew`, `/try-renew`, `/finalize-tmp`), read
|
|
309
|
+
`GET community/{communityId}/file/{Entity}/{entityId}/{field}`, with anonymous access and
|
|
310
|
+
rights/group checks (may return `403` depending on membership).
|
|
310
311
|
|
|
311
312
|
---
|
|
312
313
|
|
|
313
|
-
*
|
|
314
|
+
*See also: [assets.md](./assets.md) (media library — same upload primitives, with its own identity and `inwinkasset://`), [update.md](./update.md).*
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
◂ Back to [README](./README.md)
|
|
2
|
+
|
|
3
|
+
# History: past versions and deleted entities
|
|
4
|
+
|
|
5
|
+
"With History" entities (see [entities.md](./entities.md)) are stored in **SQL Server temporal tables**: every modification archives the previous version in a history table. Three read endpoints, same request body as `query` / `count`:
|
|
6
|
+
|
|
7
|
+
| Action | Route | Purpose |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| Past versions | `POST {api}/{shardId}/{entity}/history/query` | Successive snapshots of an entity (still existing or not) |
|
|
10
|
+
| Deleted entities | `POST {api}/{shardId}/{entity}/history/removed/query` | Last snapshot of entities that no longer exist |
|
|
11
|
+
| Count deleted | `POST {api}/{shardId}/{entity}/history/removed/count` | Integer |
|
|
12
|
+
|
|
13
|
+
## Anatomy of a history row
|
|
14
|
+
|
|
15
|
+
A row is a **full snapshot** of the entity over a period, not a diff:
|
|
16
|
+
|
|
17
|
+
- `validFrom` — start of validity of this version (UTC). On the current table, it is the last modification date.
|
|
18
|
+
- `validTo` — end of validity, i.e. the instant of the next modification (UTC).
|
|
19
|
+
- `modifiedById` (UUID) and `modifiedByDisplayName` (string) — author of this version, on entities that track their author (Person, Event…). Set to `"anonymous"`, an application id or `"unknown"` depending on the context of the modification.
|
|
20
|
+
- All other fields hold the value they had during the period, custom fields included.
|
|
21
|
+
|
|
22
|
+
To find out **what changed**, compare two consecutive snapshots (sorted by `validFrom`).
|
|
23
|
+
|
|
24
|
+
## The current version
|
|
25
|
+
|
|
26
|
+
The history holds the versions that were **replaced**. While the entity exists, its current version stays in the current table: read it with [query](./query.md). The most recent history row then ends at its `validTo`, which is the `validFrom` of the current row, i.e. the date of the last modification. A deleted entity has no current row: its last snapshot records the deletion (see "Deleted entities").
|
|
27
|
+
|
|
28
|
+
To find out whether an entity changed since a date, compare that date with the `validFrom` of the current row. The history alone cannot tell: when no version starts after that date, the last modification may still be later, because its result is the current row. When [query](./query.md) finds no row for the id, the entity may have been deleted: `history/removed/query` returns its last snapshot.
|
|
29
|
+
|
|
30
|
+
## Deleted entities
|
|
31
|
+
|
|
32
|
+
Before deleting a row, the API writes one last version: on the last snapshot of a deleted entity, `validFrom` is the instant of deletion and `modifiedBy*` identifies who deleted it.
|
|
33
|
+
|
|
34
|
+
## Examples
|
|
35
|
+
|
|
36
|
+
Versions of an attendee, most recent first:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"filters": { "id": "3f2a…" },
|
|
41
|
+
"orders": [ { "desc": true, "value": { "validFrom": {} } } ],
|
|
42
|
+
"selects": { "$all": false, "mail": true, "company": true, "validFrom": true, "validTo": true, "modifiedByDisplayName": true },
|
|
43
|
+
"page": { "index": 0, "size": 50 }
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Attendees deleted since a date, and by whom:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"filters": { "validFrom": { "$gte": "2024-06-01T00:00:00Z" } },
|
|
52
|
+
"orders": [ { "desc": true, "value": { "validFrom": {} } } ],
|
|
53
|
+
"selects": { "$all": false, "mail": true, "firstname": true, "lastname": true, "validFrom": true, "modifiedByDisplayName": true },
|
|
54
|
+
"page": { "index": 0, "size": 50 }
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Limits
|
|
59
|
+
|
|
60
|
+
- **Cost**: history tables are large and history is never purged. Always filter on `id` (versions) or bound `validFrom` (deleted entities).
|
|
61
|
+
- **Flat projection only**: related entities cannot be expanded in `selects` on these endpoints.
|
|
62
|
+
- **Sorting**: no default sort; offset pagination is only stable with an explicit `orders`.
|
|
63
|
+
- **Coverage**: only "With History" entities expose these routes. A 404 means the entity has no history.
|
|
64
|
+
- `page` is required, `size` ≤ 300.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
*See also: [query.md](./query.md), [query-language.md](./query-language.md), [entities.md](./entities.md).*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
◂
|
|
1
|
+
◂ Back to [README](./README.md)
|
|
2
2
|
|
|
3
3
|
# Import (bulk create/update)
|
|
4
4
|
|
|
@@ -19,6 +19,6 @@ POST {api}/{shardId}/{entity}/import
|
|
|
19
19
|
}
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
- If an entity includes `id`, it
|
|
23
|
-
-
|
|
24
|
-
-
|
|
22
|
+
- If an entity includes `id`, it is treated as an **update**.
|
|
23
|
+
- Without `id`, the `identifiers` map acts as a uniqueness constraint to decide create vs. update. Above, `mail` is used to match existing records.
|
|
24
|
+
- Much faster than individual create/edit calls.
|