Skip to main content

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