orion-server 1.8.1

Turn business logic into live REST/Kafka services, declared as JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
//! Row structs: exactly what a `SELECT` decodes into, and nothing else.
//!
//! # The rule
//!
//! **A row struct never derives `Serialize` or `ToSchema`** (D27, D28). It is a
//! picture of a table, so it holds whatever the table holds — including columns
//! that exist to be compared, never shown, like [`Trace::access_token_hash`].
//! The moment such a struct is also a wire type, "does this field leave the
//! process?" stops being a property of the type and becomes a property of
//! whichever `#[serde(skip_serializing)]` attribute someone remembered.
//!
//! So the wire shape is always a separate type in [`super::dto`], reached
//! through a `From`/`TryFrom`. Adding a column therefore cannot leak it; it has
//! to be copied into a DTO by hand first.
//!
//! `row_structs_are_not_wire_types`
//! scans this file and fails if a derive here names either trait, and
//! `no_storage_row_struct_is_published_unless_it_is_the_wire_shape` in
//! `server::routes::openapi` fails if one of these names reaches the published
//! document.

use chrono::NaiveDateTime;

// ============================================================
// Workflow
// ============================================================

#[derive(Debug, Clone, sqlx::FromRow)]
pub struct Workflow {
    pub workflow_id: String,
    pub version: i64,
    pub name: String,
    pub description: Option<String>,
    pub priority: i64,
    pub status: String,
    pub rollout_percentage: i64,
    pub condition_json: String,
    pub tasks_json: String,
    /// JSON array of tag strings. Named for the column (D26), not for the
    /// `tags` field the admin API publishes — that lives on
    /// [`super::dto::WorkflowResponse`] and is a `Value`, already decoded.
    pub tags_json: String,
    /// The engine-managed loop over this workflow's task list, stored as the
    /// `LoopConfig` object verbatim — `{counter, init, increment, max}` — or
    /// `None` for a workflow that runs its tasks exactly once. Nullable
    /// because absent and empty are different statements, and because a
    /// stored `NULL` keeps the [`content_hash`](crate::storage::content)
    /// projection identical to what it was before the column existed.
    pub loop_json: Option<String>,
    pub continue_on_error: bool,
    pub created_at: NaiveDateTime,
    pub updated_at: NaiveDateTime,
}

// ============================================================
// Plugin
// ============================================================

/// One version of a plugin: the manifest it was uploaded with and the
/// digest of the component it names. The bytes live in
/// [`PluginArtifact`], keyed by that digest, so a version is small and an
/// artifact is stored once however many versions name it.
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct Plugin {
    pub plugin_id: String,
    pub version: i64,
    pub status: String,
    /// `sha256:<hex>` of the component bytes, computed by the server.
    pub digest: String,
    /// The manifest as JSON — the parsed, validated form, not the TOML text.
    pub manifest_json: String,
    pub tags_json: String,
    /// A detached signature over `digest`, base64, when the upload carried
    /// one. Verified against `[plugins.trust]` at upload and again at every
    /// load; `None` on a row uploaded to a node with no trust keys.
    pub signature: Option<String>,
    pub created_at: NaiveDateTime,
    pub updated_at: NaiveDateTime,
}

/// A component's bytes, addressed by their digest. Immutable: a row is
/// inserted once and deleted only when no plugin version names it.
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct PluginArtifact {
    pub digest: String,
    pub bytes: Vec<u8>,
    pub size: i64,
    pub created_at: NaiveDateTime,
}

// ============================================================
// Model
// ============================================================

/// One version of an ONNX model: the manifest it was registered with and a
/// reference to its artifact in object storage. Unlike [`Plugin`] there is
/// no bytes table — the row points at a bucket, the digest is claimed at
/// registration, and a node confirms it against what it fetches.
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct Model {
    pub model_id: String,
    pub version: i64,
    pub status: String,
    /// `sha256:<hex>` of the artifact bytes as claimed at registration — a
    /// copy of the `digest` inside [`Self::artifact_json`], as a column so it
    /// can be indexed.
    pub digest: String,
    /// The manifest as JSON — the parsed, validated form.
    pub manifest_json: String,
    /// `{"connector","key","digest","size"}`: where the bytes are.
    pub artifact_json: String,
    /// The admission verdict — `{"state":"pending"}` until a node has probed
    /// the artifact. Derived, not authored: outside the active-immutability
    /// trigger, and written only by `ModelRepository::set_admission`.
    pub admission_json: String,
    /// What the admission probe read out of the model; `None` until it
    /// passes. Derived like `admission_json`, written only by
    /// `ModelRepository::set_stats`.
    pub stats_json: Option<String>,
    pub tags_json: String,
    /// A detached signature over `digest`, base64, when the registration
    /// carried one; `None` otherwise.
    pub signature: Option<String>,
    pub created_at: NaiveDateTime,
    pub updated_at: NaiveDateTime,
}

// ============================================================
// Channel
// ============================================================

#[derive(Debug, Clone, sqlx::FromRow)]
pub struct Channel {
    pub channel_id: String,
    pub version: i64,
    pub name: String,
    pub description: Option<String>,
    pub channel_type: String,
    pub protocol: String,
    /// JSON array of HTTP method names, `None` for non-REST channels. Named
    /// for the column (D26); the admin API's `methods` field lives on
    /// [`super::dto::ChannelResponse`].
    pub methods_json: Option<String>,
    pub route_pattern: Option<String>,
    pub topic: Option<String>,
    pub consumer_group: Option<String>,
    pub transport_config_json: String,
    pub workflow_id: Option<String>,
    pub config_json: String,
    pub status: String,
    pub priority: i64,
    /// JSON array of tag strings (K6), same contract as
    /// [`Workflow::tags_json`]: the column is `tags_json`, the wire says
    /// `tags`.
    pub tags_json: String,
    pub created_at: NaiveDateTime,
    pub updated_at: NaiveDateTime,
}

impl Channel {
    /// Tolerant decode of the [`Self::methods_json`] column: a corrupt value
    /// contributes no methods.
    ///
    /// This is the rule the *runtime* wants — the route table and the
    /// activation gate must keep working on a row they cannot parse, and the
    /// update validator treats an undecodable column as "the request must
    /// supply them". The admin response deliberately uses the strict decode in
    /// [`super::dto::ChannelResponse`] instead, so a corrupt row is visible
    /// rather than silently empty.
    pub fn methods(&self) -> Option<Vec<String>> {
        self.methods_json
            .as_deref()
            .and_then(|m| serde_json::from_str(m).ok())
    }
}

// ============================================================
// Connector OAuth2 runtime state (#268)
// ============================================================

/// One `connector_oauth_state` row: the managed-OAuth2 token state for a
/// connector. `state_json` is served decrypted by the repository (encrypted
/// at rest with `storage.connector_encryption_key` when set, exactly like
/// `connectors.config_json`); `fingerprint` hashes the oauth2 auth block the
/// state was minted under, so stale state is discarded when the connector's
/// config changes.
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct ConnectorOauthStateRow {
    pub fingerprint: String,
    pub state_json: String,
}

// ============================================================
// Connector
// ============================================================

#[derive(Debug, Clone, sqlx::FromRow)]
pub struct Connector {
    pub id: String,
    pub name: String,
    pub connector_type: String,
    pub config_json: String,
    pub enabled: bool,
    /// JSON array of tag strings (K6); the wire says `tags`.
    pub tags_json: String,
    pub created_at: NaiveDateTime,
    pub updated_at: NaiveDateTime,
}

// ============================================================
// Trace
// ============================================================

/// One execution record, read whole. Carries the payloads *and*
/// [`Self::access_token_hash`], so it is only ever fetched for a single trace
/// the caller has already been authorised for — list pages read
/// [`TraceListRow`] instead.
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct Trace {
    pub id: String,
    /// The channel **name** as it was at execution time — an immutable
    /// snapshot, not a lookup key (D26). It is deliberately kept alongside
    /// `channel_id` and deliberately not refreshed: renaming a channel must
    /// not rewrite the history of what already ran, and a trace has to stay
    /// readable after its channel is deleted. Filter and group by
    /// `channel_id` when you mean "this channel"; read `channel` when you
    /// mean "what it was called then".
    pub channel: String,
    /// Stable identity of the channel that ran, when one was resolved. `None`
    /// for rows written before the column existed.
    pub channel_id: Option<String>,
    pub mode: String,
    pub status: String,
    pub input_json: Option<String>,
    pub result_json: Option<String>,
    pub error_message: Option<String>,
    pub duration_ms: Option<f64>,
    pub started_at: Option<NaiveDateTime>,
    pub completed_at: Option<NaiveDateTime>,
    pub created_at: NaiveDateTime,
    pub updated_at: NaiveDateTime,
    /// Per-task `dataflow_rs::ExecutionTrace` JSON, captured only when the
    /// channel has `config.tracing.task_details = true`. Workflow authors
    /// can inspect intermediate inputs/outputs for each task to debug
    /// pipelines without re-running them in dry-run.
    pub task_trace_json: Option<String>,
    /// SHA-256 hash of the capability token returned with the async 202 (R12).
    /// A credential verifier: compared against a presented token, never shown.
    /// It is safe to hold here precisely because this struct cannot be
    /// serialized — see the module rule.
    pub access_token_hash: Option<String>,
}

/// List-view projection over `traces`. Deliberately omits `input_json`,
/// `result_json`, `task_trace_json` and `access_token_hash` (D27): a trace
/// listing would otherwise carry every caller's request body, the full engine
/// message, and one credential verifier per row — all of it read out of the
/// database and into process memory for rows the response never shows.
///
/// The list query names these columns explicitly, so `SELECT *` cannot quietly
/// widen the page again.
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct TraceListRow {
    pub id: String,
    /// See [`Trace::channel`] — the name at execution time, not a key.
    pub channel: String,
    pub channel_id: Option<String>,
    pub mode: String,
    pub status: String,
    pub error_message: Option<String>,
    pub duration_ms: Option<f64>,
    pub started_at: Option<NaiveDateTime>,
    pub completed_at: Option<NaiveDateTime>,
    pub created_at: NaiveDateTime,
    pub updated_at: NaiveDateTime,
}

// ============================================================
// Trace DLQ
// ============================================================

#[derive(Debug, Clone, sqlx::FromRow)]
pub struct TraceDlqEntry {
    pub id: String,
    pub trace_id: String,
    pub channel: String,
    pub payload_json: String,
    pub metadata_json: String,
    pub error_message: String,
    pub retry_count: i64,
    pub max_retries: i64,
    pub next_retry_at: NaiveDateTime,
    pub created_at: NaiveDateTime,
    pub updated_at: NaiveDateTime,
}

/// List-view projection over `trace_dlq`. Deliberately omits `payload_json` /
/// `metadata_json`: a DLQ listing would otherwise dump every failed request's
/// body — and, on rows written before S10, its headers — into one response.
/// Payloads are served one at a time by `get_by_id`.
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct TraceDlqSummary {
    pub id: String,
    pub trace_id: String,
    pub channel: String,
    pub error_message: String,
    pub retry_count: i64,
    pub max_retries: i64,
    pub next_retry_at: NaiveDateTime,
    pub created_at: NaiveDateTime,
    pub updated_at: NaiveDateTime,
}

// ============================================================
// Package receipt (K14)
// ============================================================

/// One package version's receipt: what was applied (or staged) here, with
/// what content hash, by whom. The applied-immutability rule is enforced
/// against these rows — see `repositories::packages`.
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct PackageReceipt {
    pub name: String,
    pub version: String,
    pub content_hash: String,
    /// `staged` (drafts landed, mutable in place) or `applied` (activated,
    /// immutable — content changes require a version bump).
    pub state: String,
    pub principal: String,
    pub created_at: NaiveDateTime,
    pub updated_at: NaiveDateTime,
}

// ============================================================
// Audit log
// ============================================================

#[derive(Debug, Clone, sqlx::FromRow)]
pub struct AuditLogEntry {
    pub id: String,
    pub principal: String,
    pub action: String,
    pub resource_type: String,
    pub resource_id: String,
    pub details: Option<String>,
    pub created_at: NaiveDateTime,
}

// ============================================================
// Cron scheduling rows
// ============================================================

/// Where one channel's schedule has got to.
///
/// Keyed by `channel_id` alone: the cursor is the schedule's position in time
/// and follows the *channel*, not the version. `config_hash` is what decides
/// whether a new version inherits it.
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct CronScheduleState {
    pub channel_id: String,
    pub channel_version: i64,
    pub config_hash: String,
    pub next_fire_at: NaiveDateTime,
    /// Set when the channel left the active set. A paused cursor resumes from
    /// the reactivation moment rather than filling in the gap.
    pub paused_at: Option<NaiveDateTime>,
    pub updated_at: NaiveDateTime,
}

/// One scheduled instant of one channel: the durable record that it was due,
/// and of what happened to it.
///
/// Read whole — unlike a trace, there is no payload column to keep off a list
/// page, so the listing DTO narrows for readability rather than for cost.
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct CronOccurrence {
    pub id: String,
    pub channel_id: String,
    /// The channel name as it was when this was materialised, like
    /// [`Trace::channel`]: an immutable snapshot, not a lookup key.
    pub channel_name: String,
    /// The version that materialised it…
    pub channel_version: i64,
    /// …and the version that claimed it, which may differ. `None` until
    /// claimed.
    pub executing_version: Option<i64>,
    pub workflow_id: Option<String>,
    /// `cron` or `manual`.
    pub trigger: String,
    pub scheduled_for: NaiveDateTime,
    pub status: String,
    pub attempt: i64,
    pub claimed_by: Option<String>,
    pub claimed_until: Option<NaiveDateTime>,
    pub singleton_key: Option<String>,
    pub fencing_token: Option<i64>,
    pub trace_id: Option<String>,
    pub error_message: Option<String>,
    pub started_at: Option<NaiveDateTime>,
    pub completed_at: Option<NaiveDateTime>,
    pub created_at: NaiveDateTime,
    pub updated_at: NaiveDateTime,
}

/// One held singleton key. The row's *existence* is the lock.
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct CronSingleton {
    pub singleton_key: String,
    pub occurrence_id: String,
    pub holder: String,
    pub fencing_token: i64,
    pub lease_until: NaiveDateTime,
    pub updated_at: NaiveDateTime,
}

#[cfg(test)]
mod tests {
    /// The module rule, checked against the module (D27, D28).
    ///
    /// There is no way to write "`T` does *not* implement `Serialize`" as a
    /// bound in stable Rust, so the rule is enforced against the source: every
    /// `#[derive(...)]` in this file must name neither trait. A row struct that
    /// picks one up — and with it the ability to put a column like
    /// `access_token_hash` on the wire by accident — fails here.
    #[test]
    fn row_structs_are_not_wire_types() {
        const SOURCE: &str = include_str!("rows.rs");
        let mut rest = SOURCE;
        while let Some(start) = rest.find("#[derive(") {
            rest = &rest[start + "#[derive(".len()..];
            let end = rest.find(")]").expect("unterminated #[derive(...)]");
            let derives = &rest[..end];
            for banned in ["Serialize", "ToSchema"] {
                assert!(
                    !derives.contains(banned),
                    "`{banned}` derived on a row struct in models/rows.rs \
                     (derive list: `{derives}`). A row struct is a picture of a table, \
                     not a wire shape — give it a DTO in models/dto.rs and a \
                     `From`/`TryFrom` instead."
                );
            }
            rest = &rest[end..];
        }
    }
}