Skip to main content

thingd/
model.rs

1//! Data model types shared by storage adapters.
2
3use crate::{u64_to_i64, unix_timestamp_millis};
4
5/// Default queue lease duration in milliseconds.
6pub const DEFAULT_QUEUE_LEASE_MS: u64 = 30_000;
7
8/// Stable object key inside a collection.
9#[derive(
10    Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, serde::Serialize, serde::Deserialize,
11)]
12#[serde(rename_all = "camelCase")]
13pub struct ObjectKey {
14    /// Collection name, such as `decisions`, `documents`, or `customers`.
15    pub collection: String,
16    /// Stable object identifier inside the collection.
17    pub id: String,
18}
19
20impl ObjectKey {
21    /// Create a new object key.
22    pub fn new(collection: impl Into<String>, id: impl Into<String>) -> Self {
23        Self {
24            collection: collection.into(),
25            id: id.into(),
26        }
27    }
28}
29
30/// An object stored in a thingd collection.
31#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Serialize, serde::Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct MemoryObject {
34    /// Stable object key.
35    pub key: ObjectKey,
36    /// Serialized object body.
37    pub body: String,
38    /// Monotonic object version assigned by the store.
39    pub version: u64,
40    /// ISO 8601 creation timestamp, e.g. "2026-06-01T12:00:00.000Z". Empty if not set.
41    pub created_at: String,
42    /// ISO 8601 last-update timestamp. Empty if not set.
43    pub updated_at: String,
44}
45
46impl MemoryObject {
47    /// Create a new object record.
48    pub fn new(
49        collection: impl Into<String>,
50        id: impl Into<String>,
51        body: impl Into<String>,
52    ) -> Self {
53        Self {
54            key: ObjectKey::new(collection, id),
55            body: body.into(),
56            version: 0,
57            created_at: String::new(),
58            updated_at: String::new(),
59        }
60    }
61}
62
63/// An append-only event stored in a thingd stream.
64#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Serialize, serde::Deserialize)]
65#[serde(rename_all = "camelCase")]
66pub struct MemoryEvent {
67    /// Stream name, such as `project:thingd` or `customer:cus_123`.
68    pub stream: String,
69    /// Event kind, such as `decision.made`.
70    pub event_type: String,
71    /// Serialized event body.
72    pub body: String,
73    /// Monotonic sequence assigned by the event log.
74    pub sequence: u64,
75    /// ISO 8601 creation timestamp. Empty if not set.
76    pub created_at: String,
77    /// Optional idempotency key for deduplication on retry.
78    /// When set, appending an event with the same (stream, `idempotency_key`)
79    /// pair returns the existing event instead of creating a duplicate.
80    pub idempotency_key: String,
81}
82
83impl MemoryEvent {
84    /// Create a new event record.
85    pub fn new(
86        stream: impl Into<String>,
87        event_type: impl Into<String>,
88        body: impl Into<String>,
89    ) -> Self {
90        Self {
91            stream: stream.into(),
92            event_type: event_type.into(),
93            body: body.into(),
94            sequence: 0,
95            created_at: String::new(),
96            idempotency_key: String::new(),
97        }
98    }
99}
100
101/// Queue job lifecycle state.
102#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Serialize, serde::Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub enum QueueJobStatus {
105    /// Ready to be claimed by a worker.
106    Ready,
107    /// Claimed by a worker and awaiting ack/nack.
108    Leased,
109    /// Completed successfully.
110    Completed,
111    /// Exhausted retries and moved to the dead-letter set.
112    Dead,
113}
114
115/// A queued unit of work.
116#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct QueueJob {
119    /// Queue name.
120    pub queue: String,
121    /// Stable job identifier.
122    pub id: String,
123    /// Serialized job payload.
124    pub body: String,
125    /// Number of attempts already made.
126    pub attempts: u32,
127    /// Maximum attempts before the job should be considered dead.
128    pub max_attempts: u32,
129    /// Current job status.
130    pub status: QueueJobStatus,
131    /// Unix timestamp in milliseconds when this job becomes claimable.
132    pub available_at_ms: i64,
133    /// Unix timestamp in milliseconds when this job was leased.
134    pub leased_at_ms: Option<i64>,
135    /// Unix timestamp in milliseconds when this job lease expires.
136    pub lease_expires_at_ms: Option<i64>,
137    /// Unix timestamp in milliseconds when this job completed.
138    pub completed_at_ms: Option<i64>,
139    /// Unix timestamp in milliseconds when this job moved to dead-letter state.
140    pub dead_at_ms: Option<i64>,
141    /// ISO 8601 creation timestamp. Empty if not set.
142    pub created_at: String,
143    /// Error message from last nack. Empty if not set.
144    pub last_error: String,
145}
146
147impl QueueJob {
148    /// Create a new ready job.
149    pub fn new(
150        queue: impl Into<String>,
151        id: impl Into<String>,
152        body: impl Into<String>,
153        max_attempts: u32,
154    ) -> Self {
155        Self {
156            queue: queue.into(),
157            id: id.into(),
158            body: body.into(),
159            attempts: 0,
160            max_attempts,
161            status: QueueJobStatus::Ready,
162            available_at_ms: 0,
163            leased_at_ms: None,
164            lease_expires_at_ms: None,
165            completed_at_ms: None,
166            dead_at_ms: None,
167            created_at: String::new(),
168            last_error: String::new(),
169        }
170    }
171
172    /// Make this job available after a delay.
173    #[must_use]
174    pub fn delay_by_ms(mut self, delay_ms: u64) -> Self {
175        self.available_at_ms = unix_timestamp_millis().saturating_add(u64_to_i64(delay_ms));
176        self
177    }
178
179    /// Set the exact Unix timestamp in milliseconds when this job is claimable.
180    #[must_use]
181    pub const fn available_at_ms(mut self, available_at_ms: i64) -> Self {
182        self.available_at_ms = available_at_ms;
183        self
184    }
185}
186
187/// Options for listing events.
188#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
189#[serde(rename_all = "camelCase")]
190pub struct ListEventsOptions {
191    /// Only return events with sequence greater than this value.
192    pub from_sequence: Option<u64>,
193    /// Maximum number of events to return.
194    pub limit: Option<u64>,
195}
196
197/// Sort direction for list queries.
198#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
199#[serde(rename_all = "camelCase")]
200pub enum SortDirection {
201    /// Ascending order (A→Z, oldest→newest, smallest→largest).
202    #[default]
203    Asc,
204    /// Descending order (Z→A, newest→oldest, largest→smallest).
205    Desc,
206}
207
208/// Sort specification for list queries.
209#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
210#[serde(rename_all = "camelCase")]
211pub struct SortBy {
212    /// Field name: `id`, `collection`, `created_at`, `updated_at`, `version`.
213    pub field: String,
214    /// Sort direction.
215    pub direction: SortDirection,
216}
217
218impl SortBy {
219    /// Create ascending sort by field name.
220    pub fn asc(field: impl Into<String>) -> Self {
221        Self {
222            field: field.into(),
223            direction: SortDirection::Asc,
224        }
225    }
226
227    /// Create descending sort by field name.
228    pub fn desc(field: impl Into<String>) -> Self {
229        Self {
230            field: field.into(),
231            direction: SortDirection::Desc,
232        }
233    }
234}
235
236/// Options for listing objects in a collection.
237#[derive(Clone, Debug, Default)]
238pub struct ListObjectsOptions {
239    /// Filter key-value pairs serialised as JSON pairs: only objects whose body
240    /// contains every listed top-level key with the exact JSON value are returned.
241    /// Each string is `"key":<json-value>` without surrounding braces.
242    pub filter: Vec<(String, serde_json::Value)>,
243    /// Sort specification. Default is insertion order.
244    pub sort_by: Option<SortBy>,
245    /// Maximum number of objects to return.
246    pub limit: Option<u64>,
247    /// Number of objects to skip before returning results.
248    pub offset: Option<u64>,
249}
250
251/// Options for putting an object.
252#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
253#[serde(rename_all = "camelCase")]
254pub struct PutObjectOptions {
255    /// Whether to update the FTS search index. Default: `true`.
256    /// Set to `false` when only metadata changes (e.g. timestamp dedup)
257    /// and the body text is identical — skips FTS DELETE + INSERT.
258    pub index: bool,
259    /// Optional expected version for optimistic locking (CAS).
260    /// When `Some(v)`, the put succeeds only if the current version
261    /// equals `v`. If the object does not exist, returns `Conflict`.
262    /// When `None`, no version check is performed (default).
263    pub expected_version: Option<u64>,
264}
265
266impl Default for PutObjectOptions {
267    fn default() -> Self {
268        Self {
269            index: true,
270            expected_version: None,
271        }
272    }
273}
274
275/// Options used when claiming a queue job.
276#[derive(Clone, Copy, Debug, Eq, PartialEq)]
277pub struct QueueClaimOptions {
278    /// Lease duration in milliseconds.
279    pub lease_ms: u64,
280}
281
282impl Default for QueueClaimOptions {
283    fn default() -> Self {
284        Self {
285            lease_ms: DEFAULT_QUEUE_LEASE_MS,
286        }
287    }
288}
289
290impl QueueClaimOptions {
291    /// Create queue claim options with the given lease duration.
292    #[must_use]
293    pub const fn new(lease_ms: u64) -> Self {
294        Self { lease_ms }
295    }
296}
297
298/// Options used when rejecting a leased queue job.
299#[derive(Clone, Debug, Eq, PartialEq, Default)]
300pub struct QueueNackOptions {
301    /// Delay before a retry can be claimed.
302    pub delay_ms: u64,
303    /// Error message from the worker, stored as `last_error` on the job.
304    pub error: String,
305}
306
307impl QueueNackOptions {
308    /// Create queue nack options with the given retry delay.
309    #[must_use]
310    pub const fn new(delay_ms: u64) -> Self {
311        Self {
312            delay_ms,
313            error: String::new(),
314        }
315    }
316
317    /// Create queue nack options with retry delay and an error message.
318    #[must_use]
319    pub fn with_error(delay_ms: u64, error: impl Into<String>) -> Self {
320        Self {
321            delay_ms,
322            error: error.into(),
323        }
324    }
325}
326
327/// Options used when performing a search.
328#[derive(Clone, Debug, Eq, PartialEq, Default)]
329pub struct SearchOptions {
330    /// Limit search to these collection or stream names.
331    pub collections: Option<Vec<String>>,
332    /// Maximum number of hits to return.
333    pub limit: Option<usize>,
334    /// Metadata filters to match custom fields in the JSON body.
335    pub filter: Option<serde_json::Value>,
336}
337
338/// A single match returned by a search query.
339#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
340#[serde(rename_all = "camelCase")]
341pub struct SearchHit {
342    /// Result kind: "object" or "event".
343    pub kind: String,
344    /// Collection or stream name.
345    pub collection: String,
346    /// Object id or event sequence number.
347    pub id: String,
348    /// The indexed text that matched.
349    pub text: String,
350    /// Relevancy score.
351    pub score: f64,
352    /// The serialized body.
353    pub body: String,
354    /// Object version (only populated for objects).
355    pub version: Option<u64>,
356    /// Created timestamp.
357    pub created_at: String,
358    /// Updated timestamp (only populated for objects).
359    pub updated_at: Option<String>,
360    /// Event type (only populated for events).
361    pub event_type: Option<String>,
362}
363
364/// A graph link connecting two references.
365#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
366#[serde(rename_all = "camelCase")]
367pub struct Link {
368    /// Unique link identifier.
369    pub id: String,
370    /// Source reference (e.g. "collection/id" or "stream/sequence").
371    pub from_ref: String,
372    /// Relationship type (e.g. "supports", "`depends_on`", "`chunk_of`").
373    pub link_type: String,
374    /// Target reference.
375    pub to_ref: String,
376    /// Optional weight for ranking (0.0 to 1.0).
377    pub weight: Option<f64>,
378    /// Optional metadata as JSON string.
379    pub metadata_json: String,
380    /// ISO 8601 creation timestamp.
381    pub created_at: String,
382}
383
384impl Link {
385    /// Create a new graph link.
386    pub fn new(
387        from_ref: impl Into<String>,
388        link_type: impl Into<String>,
389        to_ref: impl Into<String>,
390    ) -> Self {
391        Self {
392            id: String::new(),
393            from_ref: from_ref.into(),
394            link_type: link_type.into(),
395            to_ref: to_ref.into(),
396            weight: None,
397            metadata_json: "{}".to_string(),
398            created_at: String::new(),
399        }
400    }
401
402    /// Set the link weight.
403    #[must_use]
404    pub const fn with_weight(mut self, weight: f64) -> Self {
405        self.weight = Some(weight);
406        self
407    }
408
409    /// Set the metadata JSON.
410    #[must_use]
411    pub fn with_metadata(mut self, metadata: impl Into<String>) -> Self {
412        self.metadata_json = metadata.into();
413        self
414    }
415}
416
417/// Options for querying graph links.
418#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
419#[serde(rename_all = "camelCase")]
420pub struct LinkQueryOptions {
421    /// Filter by relationship type.
422    pub link_type: Option<String>,
423    /// Maximum number of results.
424    pub limit: Option<usize>,
425}
426
427/// Direction for neighbor queries.
428#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
429#[serde(rename_all = "camelCase")]
430pub enum LinkDirection {
431    /// Only outgoing links (`from_ref` matches).
432    Outgoing,
433    /// Only incoming links (`to_ref` matches).
434    Incoming,
435    /// Both directions.
436    #[default]
437    Both,
438}