@feltdb/core 0.5.7 → 0.6.1
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/dist/cli/commands.js +110 -5
- package/dist/cli/index.js +1 -1
- package/dist/cli/workspace-integration.js +127 -0
- package/dist/create/cli.js +1 -1
- package/dist/create/create.js +21 -0
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +10 -0
- package/dist/create/server-source/crates/feltdb-server/Cargo.toml +1 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +5 -1
- package/dist/create/server-source/crates/feltdb-server/src/authenticated_principal.rs +273 -0
- package/dist/create/server-source/crates/feltdb-server/src/certification_harness.rs +528 -0
- package/dist/create/server-source/crates/feltdb-server/src/delegation_token.rs +472 -0
- package/dist/create/server-source/crates/feltdb-server/src/durable_operations.rs +992 -0
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +9 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +157 -9
- package/dist/create/server-source/crates/feltdb-server/src/managed_diagnostics.rs +413 -0
- package/dist/create/server-source/crates/feltdb-server/src/membership_policy.rs +488 -0
- package/dist/create/server-source/crates/feltdb-server/src/snapshot_cursor.rs +378 -0
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +49 -0
- package/dist/create/server-source/crates/feltdb-server/src/tenant_policies.rs +525 -0
- package/dist/create/server-source/crates/feltdb-server/src/transaction_recovery.rs +461 -0
- package/dist/create/template/dot-feltdb-README.md +58 -0
- package/dist/create/workspace-initialization.js +77 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/studio-app/assets/{feltdb_wasm-h9mxesnH.js → feltdb_wasm-B4wq4mqp.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-Ceyi7l21.wasm +0 -0
- package/dist/studio-app/assets/{index-B_EMnTaE.js → index-LQmvJSq6.js} +2 -2
- package/dist/studio-app/index.html +1 -1
- package/dist/telemetry.js +1 -1
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workspace/development-node.d.ts +42 -0
- package/dist/workspace/development-node.d.ts.map +1 -0
- package/dist/workspace/development-node.js +208 -0
- package/dist/workspace/index.d.ts +20 -0
- package/dist/workspace/index.d.ts.map +1 -0
- package/dist/workspace/index.js +16 -0
- package/dist/workspace/workspace-connection.d.ts +174 -0
- package/dist/workspace/workspace-connection.d.ts.map +1 -0
- package/dist/workspace/workspace-connection.js +290 -0
- package/dist/workspace/workspace-identity.d.ts +13 -0
- package/dist/workspace/workspace-identity.d.ts.map +1 -0
- package/dist/workspace/workspace-identity.js +70 -0
- package/dist/workspace/workspace-types.d.ts +82 -0
- package/dist/workspace/workspace-types.d.ts.map +1 -0
- package/dist/workspace/workspace-types.js +7 -0
- package/package.json +5 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-B8U4A1n1.wasm +0 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
use base64::Engine;
|
|
2
|
+
use serde::{Deserialize, Serialize};
|
|
3
|
+
use sha2::{Digest, Sha256};
|
|
4
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
5
|
+
|
|
6
|
+
/// Snapshot-bound pagination cursor.
|
|
7
|
+
/// Prevents reading from different snapshots within a single pagination session.
|
|
8
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
9
|
+
pub struct SnapshotCursor {
|
|
10
|
+
/// Unique identifier for the snapshot version.
|
|
11
|
+
pub snapshot_id: String,
|
|
12
|
+
|
|
13
|
+
/// Position/offset within the snapshot.
|
|
14
|
+
pub position: usize,
|
|
15
|
+
|
|
16
|
+
/// Collection being paginated.
|
|
17
|
+
pub collection: String,
|
|
18
|
+
|
|
19
|
+
/// Snapshot version number.
|
|
20
|
+
pub snapshot_version: u64,
|
|
21
|
+
|
|
22
|
+
/// Hash of authorization context (prevents reuse with different auth).
|
|
23
|
+
pub auth_context_hash: String,
|
|
24
|
+
|
|
25
|
+
/// When this cursor was created.
|
|
26
|
+
pub created_at: u64,
|
|
27
|
+
|
|
28
|
+
/// Hash of the cursor payload for verification.
|
|
29
|
+
pub checksum: String,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Pagination request parameters.
|
|
33
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
34
|
+
pub struct PaginationRequest {
|
|
35
|
+
pub collection: String,
|
|
36
|
+
pub limit: usize,
|
|
37
|
+
pub cursor: Option<String>, // Base64-encoded SnapshotCursor
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Pagination response.
|
|
41
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
42
|
+
pub struct PaginationResponse<T> {
|
|
43
|
+
pub records: Vec<T>,
|
|
44
|
+
pub next_cursor: Option<String>, // Base64-encoded cursor or None if end
|
|
45
|
+
pub snapshot_version: u64,
|
|
46
|
+
pub total_returned: usize,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Pagination cursor codec.
|
|
50
|
+
pub struct CursorCodec;
|
|
51
|
+
|
|
52
|
+
impl CursorCodec {
|
|
53
|
+
/// Encodes a cursor to a base64 string.
|
|
54
|
+
pub fn encode(cursor: &SnapshotCursor) -> Result<String, String> {
|
|
55
|
+
let json = serde_json::to_string(cursor)
|
|
56
|
+
.map_err(|e| format!("failed to serialize cursor: {e}"))?;
|
|
57
|
+
Ok(base64::engine::general_purpose::STANDARD.encode(json))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Decodes a cursor from a base64 string.
|
|
61
|
+
pub fn decode(encoded: &str) -> Result<SnapshotCursor, String> {
|
|
62
|
+
let bytes = base64::engine::general_purpose::STANDARD
|
|
63
|
+
.decode(encoded)
|
|
64
|
+
.map_err(|e| format!("invalid cursor encoding: {e}"))?;
|
|
65
|
+
let json = String::from_utf8(bytes)
|
|
66
|
+
.map_err(|e| format!("cursor not valid UTF-8: {e}"))?;
|
|
67
|
+
serde_json::from_str(&json).map_err(|e| format!("invalid cursor JSON: {e}"))
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Snapshot cursor factory and validator.
|
|
72
|
+
pub struct SnapshotCursorManager {
|
|
73
|
+
/// Expected collection (prevents cursor reuse across collections).
|
|
74
|
+
expected_collection: String,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
impl SnapshotCursorManager {
|
|
78
|
+
pub fn new(collection: String) -> Self {
|
|
79
|
+
Self {
|
|
80
|
+
expected_collection: collection,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Creates a new cursor for pagination.
|
|
85
|
+
pub fn create_cursor(
|
|
86
|
+
&self,
|
|
87
|
+
snapshot_id: String,
|
|
88
|
+
snapshot_version: u64,
|
|
89
|
+
position: usize,
|
|
90
|
+
auth_context_hash: String,
|
|
91
|
+
) -> Result<String, String> {
|
|
92
|
+
let cursor = SnapshotCursor {
|
|
93
|
+
snapshot_id: snapshot_id.clone(),
|
|
94
|
+
position,
|
|
95
|
+
collection: self.expected_collection.clone(),
|
|
96
|
+
snapshot_version,
|
|
97
|
+
auth_context_hash: auth_context_hash.clone(),
|
|
98
|
+
created_at: now(),
|
|
99
|
+
checksum: String::new(), // Will be computed below
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
let checksum = self.compute_checksum(&cursor)?;
|
|
103
|
+
let cursor_with_checksum = SnapshotCursor {
|
|
104
|
+
checksum,
|
|
105
|
+
..cursor
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
CursorCodec::encode(&cursor_with_checksum)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Validates a cursor.
|
|
112
|
+
pub fn validate_cursor(
|
|
113
|
+
&self,
|
|
114
|
+
encoded_cursor: &str,
|
|
115
|
+
current_snapshot_id: &str,
|
|
116
|
+
current_snapshot_version: u64,
|
|
117
|
+
current_auth_context_hash: &str,
|
|
118
|
+
) -> Result<SnapshotCursor, String> {
|
|
119
|
+
let cursor = CursorCodec::decode(encoded_cursor)?;
|
|
120
|
+
|
|
121
|
+
// Verify collection
|
|
122
|
+
if cursor.collection != self.expected_collection {
|
|
123
|
+
return Err(format!(
|
|
124
|
+
"cursor collection mismatch: expected {}, got {}",
|
|
125
|
+
self.expected_collection, cursor.collection
|
|
126
|
+
));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Verify snapshot hasn't changed
|
|
130
|
+
if cursor.snapshot_id != current_snapshot_id {
|
|
131
|
+
return Err(format!(
|
|
132
|
+
"snapshot changed: cursor has {} but current is {}",
|
|
133
|
+
cursor.snapshot_id, current_snapshot_id
|
|
134
|
+
));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Verify snapshot version consistency
|
|
138
|
+
if cursor.snapshot_version != current_snapshot_version {
|
|
139
|
+
return Err(format!(
|
|
140
|
+
"snapshot version mismatch: cursor version {}, current version {}",
|
|
141
|
+
cursor.snapshot_version, current_snapshot_version
|
|
142
|
+
));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Verify authorization context hasn't changed
|
|
146
|
+
if cursor.auth_context_hash != current_auth_context_hash {
|
|
147
|
+
return Err(
|
|
148
|
+
"authorization context changed since cursor creation".into(),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Verify checksum
|
|
153
|
+
self.verify_checksum(&cursor)?;
|
|
154
|
+
|
|
155
|
+
Ok(cursor)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// Gets the next position for pagination.
|
|
159
|
+
pub fn next_position(&self, cursor: &SnapshotCursor, returned_count: usize) -> usize {
|
|
160
|
+
cursor.position + returned_count
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Checks if this is likely the end of results.
|
|
164
|
+
pub fn is_end(&self, returned_count: usize, limit: usize) -> bool {
|
|
165
|
+
returned_count < limit
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// Computes hash of authorization context.
|
|
169
|
+
pub fn hash_auth_context(actor_id: &str, tenant_id: &str, required_role: Option<&str>) -> String {
|
|
170
|
+
let mut hasher = Sha256::new();
|
|
171
|
+
hasher.update(actor_id.as_bytes());
|
|
172
|
+
hasher.update(tenant_id.as_bytes());
|
|
173
|
+
if let Some(role) = required_role {
|
|
174
|
+
hasher.update(role.as_bytes());
|
|
175
|
+
}
|
|
176
|
+
format!("{:x}", hasher.finalize())
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fn compute_checksum(&self, cursor: &SnapshotCursor) -> Result<String, String> {
|
|
180
|
+
let mut hasher = Sha256::new();
|
|
181
|
+
|
|
182
|
+
hasher.update(cursor.snapshot_id.as_bytes());
|
|
183
|
+
hasher.update(cursor.position.to_le_bytes());
|
|
184
|
+
hasher.update(cursor.collection.as_bytes());
|
|
185
|
+
hasher.update(cursor.snapshot_version.to_le_bytes());
|
|
186
|
+
hasher.update(cursor.auth_context_hash.as_bytes());
|
|
187
|
+
hasher.update(cursor.created_at.to_le_bytes());
|
|
188
|
+
|
|
189
|
+
Ok(format!("{:x}", hasher.finalize()))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
fn verify_checksum(&self, cursor: &SnapshotCursor) -> Result<(), String> {
|
|
193
|
+
let mut cursor_copy = cursor.clone();
|
|
194
|
+
let expected_checksum = cursor_copy.checksum.clone();
|
|
195
|
+
cursor_copy.checksum = String::new();
|
|
196
|
+
|
|
197
|
+
let computed = self.compute_checksum(&cursor_copy)?;
|
|
198
|
+
if computed != expected_checksum {
|
|
199
|
+
return Err("cursor checksum verification failed".into());
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
Ok(())
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
fn now() -> u64 {
|
|
207
|
+
SystemTime::now()
|
|
208
|
+
.duration_since(UNIX_EPOCH)
|
|
209
|
+
.unwrap_or_default()
|
|
210
|
+
.as_secs()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
#[cfg(test)]
|
|
214
|
+
mod tests {
|
|
215
|
+
use super::*;
|
|
216
|
+
|
|
217
|
+
#[test]
|
|
218
|
+
fn encode_and_decode_cursor() {
|
|
219
|
+
let cursor = SnapshotCursor {
|
|
220
|
+
snapshot_id: "snap_123".into(),
|
|
221
|
+
position: 100,
|
|
222
|
+
collection: "release_jobs".into(),
|
|
223
|
+
snapshot_version: 5,
|
|
224
|
+
auth_context_hash: "hash_abc".into(),
|
|
225
|
+
created_at: 1234567890,
|
|
226
|
+
checksum: "checksum_xyz".into(),
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
let encoded = CursorCodec::encode(&cursor).expect("encode failed");
|
|
230
|
+
let decoded = CursorCodec::decode(&encoded).expect("decode failed");
|
|
231
|
+
|
|
232
|
+
assert_eq!(decoded.snapshot_id, cursor.snapshot_id);
|
|
233
|
+
assert_eq!(decoded.position, cursor.position);
|
|
234
|
+
assert_eq!(decoded.collection, cursor.collection);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#[test]
|
|
238
|
+
fn create_and_validate_cursor() {
|
|
239
|
+
let manager = SnapshotCursorManager::new("release_jobs".into());
|
|
240
|
+
let auth_hash = SnapshotCursorManager::hash_auth_context("user_123", "tenant_prod", None);
|
|
241
|
+
|
|
242
|
+
let encoded = manager
|
|
243
|
+
.create_cursor("snap_123".into(), 5, 0, auth_hash.clone())
|
|
244
|
+
.expect("create failed");
|
|
245
|
+
|
|
246
|
+
let validated = manager
|
|
247
|
+
.validate_cursor(&encoded, "snap_123", 5, &auth_hash)
|
|
248
|
+
.expect("validate failed");
|
|
249
|
+
|
|
250
|
+
assert_eq!(validated.position, 0);
|
|
251
|
+
assert_eq!(validated.snapshot_version, 5);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#[test]
|
|
255
|
+
fn reject_cursor_from_different_collection() {
|
|
256
|
+
let manager = SnapshotCursorManager::new("release_jobs".into());
|
|
257
|
+
let auth_hash = SnapshotCursorManager::hash_auth_context("user_123", "tenant_prod", None);
|
|
258
|
+
|
|
259
|
+
let cursor = SnapshotCursor {
|
|
260
|
+
snapshot_id: "snap_123".into(),
|
|
261
|
+
position: 100,
|
|
262
|
+
collection: "workflows".into(), // Wrong collection
|
|
263
|
+
snapshot_version: 5,
|
|
264
|
+
auth_context_hash: auth_hash.clone(),
|
|
265
|
+
created_at: now(),
|
|
266
|
+
checksum: "checksum_xyz".into(),
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
let encoded = CursorCodec::encode(&cursor).expect("encode failed");
|
|
270
|
+
|
|
271
|
+
let result = manager.validate_cursor(&encoded, "snap_123", 5, &auth_hash);
|
|
272
|
+
assert!(result.is_err());
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#[test]
|
|
276
|
+
fn reject_cursor_when_snapshot_changes() {
|
|
277
|
+
let manager = SnapshotCursorManager::new("release_jobs".into());
|
|
278
|
+
let auth_hash = SnapshotCursorManager::hash_auth_context("user_123", "tenant_prod", None);
|
|
279
|
+
|
|
280
|
+
let encoded = manager
|
|
281
|
+
.create_cursor("snap_123".into(), 5, 100, auth_hash.clone())
|
|
282
|
+
.expect("create failed");
|
|
283
|
+
|
|
284
|
+
// Try to use with different snapshot
|
|
285
|
+
let result = manager.validate_cursor(&encoded, "snap_456", 5, &auth_hash);
|
|
286
|
+
assert!(result.is_err());
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
#[test]
|
|
290
|
+
fn reject_cursor_when_auth_context_changes() {
|
|
291
|
+
let manager = SnapshotCursorManager::new("release_jobs".into());
|
|
292
|
+
let auth_hash1 =
|
|
293
|
+
SnapshotCursorManager::hash_auth_context("user_123", "tenant_prod", None);
|
|
294
|
+
let auth_hash2 =
|
|
295
|
+
SnapshotCursorManager::hash_auth_context("user_123", "tenant_other", None);
|
|
296
|
+
|
|
297
|
+
let encoded = manager
|
|
298
|
+
.create_cursor("snap_123".into(), 5, 0, auth_hash1)
|
|
299
|
+
.expect("create failed");
|
|
300
|
+
|
|
301
|
+
// Try to use with different auth context
|
|
302
|
+
let result = manager.validate_cursor(&encoded, "snap_123", 5, &auth_hash2);
|
|
303
|
+
assert!(result.is_err());
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
#[test]
|
|
307
|
+
fn compute_next_position() {
|
|
308
|
+
let manager = SnapshotCursorManager::new("release_jobs".into());
|
|
309
|
+
let cursor = SnapshotCursor {
|
|
310
|
+
snapshot_id: "snap_123".into(),
|
|
311
|
+
position: 100,
|
|
312
|
+
collection: "release_jobs".into(),
|
|
313
|
+
snapshot_version: 5,
|
|
314
|
+
auth_context_hash: "hash_abc".into(),
|
|
315
|
+
created_at: now(),
|
|
316
|
+
checksum: "checksum_xyz".into(),
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
let next = manager.next_position(&cursor, 50);
|
|
320
|
+
assert_eq!(next, 150);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
#[test]
|
|
324
|
+
fn detect_end_of_results() {
|
|
325
|
+
let manager = SnapshotCursorManager::new("release_jobs".into());
|
|
326
|
+
|
|
327
|
+
// Returned less than limit = end
|
|
328
|
+
assert!(manager.is_end(10, 20));
|
|
329
|
+
// Returned equals limit = might have more
|
|
330
|
+
assert!(!manager.is_end(20, 20));
|
|
331
|
+
// Returned 0 = definitely end
|
|
332
|
+
assert!(manager.is_end(0, 20));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
#[test]
|
|
336
|
+
fn auth_context_hash_includes_all_components() {
|
|
337
|
+
let hash1 = SnapshotCursorManager::hash_auth_context("user_123", "tenant_prod", None);
|
|
338
|
+
let hash2 =
|
|
339
|
+
SnapshotCursorManager::hash_auth_context("user_123", "tenant_prod", Some("admin"));
|
|
340
|
+
let hash3 = SnapshotCursorManager::hash_auth_context("user_456", "tenant_prod", None);
|
|
341
|
+
|
|
342
|
+
// Different role = different hash
|
|
343
|
+
assert_ne!(hash1, hash2);
|
|
344
|
+
// Different actor = different hash
|
|
345
|
+
assert_ne!(hash1, hash3);
|
|
346
|
+
// Same inputs = same hash
|
|
347
|
+
let hash1_repeat =
|
|
348
|
+
SnapshotCursorManager::hash_auth_context("user_123", "tenant_prod", None);
|
|
349
|
+
assert_eq!(hash1, hash1_repeat);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
#[test]
|
|
353
|
+
fn invalid_base64_rejected() {
|
|
354
|
+
let result = CursorCodec::decode("not valid base64!!!");
|
|
355
|
+
assert!(result.is_err());
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
#[test]
|
|
359
|
+
fn tampered_checksum_rejected() {
|
|
360
|
+
let manager = SnapshotCursorManager::new("release_jobs".into());
|
|
361
|
+
let auth_hash = SnapshotCursorManager::hash_auth_context("user_123", "tenant_prod", None);
|
|
362
|
+
|
|
363
|
+
let cursor = SnapshotCursor {
|
|
364
|
+
snapshot_id: "snap_123".into(),
|
|
365
|
+
position: 100,
|
|
366
|
+
collection: "release_jobs".into(),
|
|
367
|
+
snapshot_version: 5,
|
|
368
|
+
auth_context_hash: auth_hash.clone(),
|
|
369
|
+
created_at: now(),
|
|
370
|
+
checksum: "fake_checksum".into(),
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
let encoded = CursorCodec::encode(&cursor).expect("encode failed");
|
|
374
|
+
|
|
375
|
+
let result = manager.validate_cursor(&encoded, "snap_123", 5, &auth_hash);
|
|
376
|
+
assert!(result.is_err());
|
|
377
|
+
}
|
|
378
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
use std::{
|
|
2
|
+
collections::BTreeSet,
|
|
2
3
|
fs,
|
|
3
4
|
path::PathBuf,
|
|
4
5
|
sync::{Arc, RwLock},
|
|
@@ -578,6 +579,54 @@ impl TenancyStore {
|
|
|
578
579
|
Ok(tenant)
|
|
579
580
|
}
|
|
580
581
|
|
|
582
|
+
/// Removes an isolated certification tenant selected by an exact application ID.
|
|
583
|
+
/// This deliberately refuses non-certification resources and avoids global inventory access.
|
|
584
|
+
pub fn delete_certification_fixture(&self, application_id: &str) -> Result<String, String> {
|
|
585
|
+
let mut records = self
|
|
586
|
+
.records
|
|
587
|
+
.write()
|
|
588
|
+
.map_err(|_| "tenancy store lock poisoned")?;
|
|
589
|
+
let application = records
|
|
590
|
+
.applications
|
|
591
|
+
.iter()
|
|
592
|
+
.find(|application| application.id == application_id)
|
|
593
|
+
.cloned()
|
|
594
|
+
.ok_or("application not found")?;
|
|
595
|
+
let tenant = records
|
|
596
|
+
.tenants
|
|
597
|
+
.iter()
|
|
598
|
+
.find(|tenant| tenant.id == application.tenant_id)
|
|
599
|
+
.cloned()
|
|
600
|
+
.ok_or("tenant not found")?;
|
|
601
|
+
if !application.name.to_lowercase().contains("certification")
|
|
602
|
+
|| !tenant.name.to_lowercase().contains("certification")
|
|
603
|
+
{
|
|
604
|
+
return Err("refusing to delete a non-certification fixture".into());
|
|
605
|
+
}
|
|
606
|
+
let tenant_id = tenant.id.clone();
|
|
607
|
+
let application_ids = records
|
|
608
|
+
.applications
|
|
609
|
+
.iter()
|
|
610
|
+
.filter(|candidate| candidate.tenant_id == tenant_id)
|
|
611
|
+
.map(|candidate| candidate.id.clone())
|
|
612
|
+
.collect::<BTreeSet<_>>();
|
|
613
|
+
records
|
|
614
|
+
.applications
|
|
615
|
+
.retain(|candidate| candidate.tenant_id != tenant_id);
|
|
616
|
+
records
|
|
617
|
+
.application_memberships
|
|
618
|
+
.retain(|membership| !application_ids.contains(&membership.application_id));
|
|
619
|
+
records
|
|
620
|
+
.tenant_memberships
|
|
621
|
+
.retain(|membership| membership.tenant_id != tenant_id);
|
|
622
|
+
records
|
|
623
|
+
.invitations
|
|
624
|
+
.retain(|invitation| invitation.tenant_id != tenant_id);
|
|
625
|
+
records.tenants.retain(|candidate| candidate.id != tenant_id);
|
|
626
|
+
self.persist(&records)?;
|
|
627
|
+
Ok(tenant_id)
|
|
628
|
+
}
|
|
629
|
+
|
|
581
630
|
pub fn create_application(
|
|
582
631
|
&self,
|
|
583
632
|
actor: &str,
|