@feltdb/core 0.4.12 → 0.4.14
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 +42 -25
- package/dist/cli/index.js +1 -1
- package/dist/create/cli.js +39 -4
- package/dist/create/create.js +30 -18
- package/dist/create/docker-compose-generator.js +68 -11
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +2238 -0
- package/dist/create/server-source/Cargo.toml +7 -0
- package/dist/create/server-source/crates/feltdb/Cargo.lock +175 -0
- package/dist/create/server-source/crates/feltdb/Cargo.toml +79 -0
- package/dist/create/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +370 -0
- package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +589 -0
- package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +259 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +446 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +272 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +378 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +414 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +486 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +273 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +587 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +513 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +395 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +469 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +409 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +627 -0
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +383 -0
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +298 -0
- package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +1698 -0
- package/dist/create/server-source/crates/feltdb/src/acquisition.rs +286 -0
- package/dist/create/server-source/crates/feltdb/src/admission.rs +192 -0
- package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +477 -0
- package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +566 -0
- package/dist/create/server-source/crates/feltdb/src/analytics.rs +475 -0
- package/dist/create/server-source/crates/feltdb/src/application.rs +2244 -0
- package/dist/create/server-source/crates/feltdb/src/application_runtime.rs +1070 -0
- package/dist/create/server-source/crates/feltdb/src/authorization.rs +1030 -0
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +266 -0
- package/dist/create/server-source/crates/feltdb/src/capabilities/mod.rs +8 -0
- package/dist/create/server-source/crates/feltdb/src/capabilities/search.rs +418 -0
- package/dist/create/server-source/crates/feltdb/src/capabilities/vector.rs +482 -0
- package/dist/create/server-source/crates/feltdb/src/capability.rs +903 -0
- package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +261 -0
- package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +78 -0
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +2214 -0
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +194 -0
- package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +427 -0
- package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +453 -0
- package/dist/create/server-source/crates/feltdb/src/content_distribution.rs +465 -0
- package/dist/create/server-source/crates/feltdb/src/convergence.rs +618 -0
- package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +418 -0
- package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +380 -0
- package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +362 -0
- package/dist/create/server-source/crates/feltdb/src/cron.rs +294 -0
- package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +474 -0
- package/dist/create/server-source/crates/feltdb/src/distributed_tests.rs +1003 -0
- package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +536 -0
- package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +364 -0
- package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +235 -0
- package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +207 -0
- package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +316 -0
- package/dist/create/server-source/crates/feltdb/src/execution.rs +426 -0
- package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +219 -0
- package/dist/create/server-source/crates/feltdb/src/indexing.rs +779 -0
- package/dist/create/server-source/crates/feltdb/src/lib.rs +2838 -0
- package/dist/create/server-source/crates/feltdb/src/materialization.rs +184 -0
- package/dist/create/server-source/crates/feltdb/src/metrics.rs +267 -0
- package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +311 -0
- package/dist/create/server-source/crates/feltdb/src/observability.rs +366 -0
- package/dist/create/server-source/crates/feltdb/src/operation.rs +251 -0
- package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +438 -0
- package/dist/create/server-source/crates/feltdb/src/operation_log.rs +344 -0
- package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +477 -0
- package/dist/create/server-source/crates/feltdb/src/peer_registry.rs +166 -0
- package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +261 -0
- package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +560 -0
- package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +3226 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +201 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +263 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +484 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +216 -0
- package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +281 -0
- package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +323 -0
- package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +573 -0
- package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +404 -0
- package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +418 -0
- package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +381 -0
- package/dist/create/server-source/crates/feltdb/src/phase_1c_three_node.rs +523 -0
- package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +334 -0
- package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +306 -0
- package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +355 -0
- package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +395 -0
- package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +346 -0
- package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +430 -0
- package/dist/create/server-source/crates/feltdb/src/production_api.rs +400 -0
- package/dist/create/server-source/crates/feltdb/src/provenance.rs +207 -0
- package/dist/create/server-source/crates/feltdb/src/query_performance.rs +217 -0
- package/dist/create/server-source/crates/feltdb/src/references.rs +404 -0
- package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +401 -0
- package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +160 -0
- package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +132 -0
- package/dist/create/server-source/crates/feltdb/src/routing.rs +409 -0
- package/dist/create/server-source/crates/feltdb/src/sharding.rs +523 -0
- package/dist/create/server-source/crates/feltdb/src/state_contract.rs +3118 -0
- package/dist/create/server-source/crates/feltdb/src/state_hash.rs +291 -0
- package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +376 -0
- package/dist/create/server-source/crates/feltdb/src/storage.rs +267 -0
- package/dist/create/server-source/crates/feltdb/src/submission.rs +477 -0
- package/dist/create/server-source/crates/feltdb/src/sync.rs +483 -0
- package/dist/create/server-source/crates/feltdb/src/sync_contract.rs +822 -0
- package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +366 -0
- package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +473 -0
- package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +949 -0
- package/dist/create/server-source/crates/feltdb/src/transactions.rs +646 -0
- package/dist/create/server-source/crates/feltdb/src/trigger.rs +390 -0
- package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +928 -0
- package/dist/create/server-source/crates/feltdb/src/workflow.rs +713 -0
- package/dist/create/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +510 -0
- package/dist/create/server-source/crates/feltdb/src/workflow_integration.rs +354 -0
- package/dist/create/server-source/crates/feltdb/src/workflow_runtime.rs +594 -0
- package/dist/create/server-source/crates/feltdb/src/workload.rs +1310 -0
- package/dist/create/server-source/crates/feltdb-server/Cargo.toml +23 -0
- package/dist/create/server-source/crates/feltdb-server/README.md +79 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +73 -0
- package/dist/create/server-source/crates/feltdb-server/src/application_contract.rs +425 -0
- package/dist/create/server-source/crates/feltdb-server/src/artifacts.rs +449 -0
- package/dist/create/server-source/crates/feltdb-server/src/audit.rs +59 -0
- package/dist/create/server-source/crates/feltdb-server/src/auth.rs +308 -0
- package/dist/create/server-source/crates/feltdb-server/src/authorization.rs +228 -0
- package/dist/create/server-source/crates/feltdb-server/src/backup.rs +416 -0
- package/dist/create/server-source/crates/feltdb-server/src/causal.rs +218 -0
- package/dist/create/server-source/crates/feltdb-server/src/certification.rs +223 -0
- package/dist/create/server-source/crates/feltdb-server/src/clock.rs +132 -0
- package/dist/create/server-source/crates/feltdb-server/src/cluster.rs +165 -0
- package/dist/create/server-source/crates/feltdb-server/src/connections.rs +1044 -0
- package/dist/create/server-source/crates/feltdb-server/src/content.rs +111 -0
- package/dist/create/server-source/crates/feltdb-server/src/identity.rs +250 -0
- package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +78 -0
- package/dist/create/server-source/crates/feltdb-server/src/key_provider.rs +247 -0
- package/dist/create/server-source/crates/feltdb-server/src/leases.rs +123 -0
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +25 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +8715 -0
- package/dist/create/server-source/crates/feltdb-server/src/metrics.rs +97 -0
- package/dist/create/server-source/crates/feltdb-server/src/portable_bundle.rs +350 -0
- package/dist/create/server-source/crates/feltdb-server/src/principals.rs +510 -0
- package/dist/create/server-source/crates/feltdb-server/src/providers.rs +269 -0
- package/dist/create/server-source/crates/feltdb-server/src/releases.rs +2563 -0
- package/dist/create/server-source/crates/feltdb-server/src/sessions.rs +156 -0
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +1097 -0
- package/dist/create/server-source/crates/feltdb-server/src/versions.rs +264 -0
- package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +31 -0
- package/dist/create/server-source/crates/feltdb-wasm/src/lib.rs +1086 -0
- package/dist/create/server-source/crates/feltdb-wasm/test.db +0 -0
- package/dist/flowspec.d.ts +2 -0
- package/dist/flowspec.d.ts.map +1 -1
- package/dist/flowspec.js +28 -4
- package/dist/state-contract.d.ts +7 -1
- package/dist/state-contract.d.ts.map +1 -1
- package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
- package/dist/studio/components/ConflictExplorer.d.ts.map +1 -1
- package/dist/studio/components/index.js +1 -1
- package/dist/studio/{components-Q0Nx6gLE.js → components-9kDSWiGL.js} +163 -47
- package/dist/studio/index.js +31 -28
- package/dist/studio-app/assets/{index-BHOmLZF-.js → index-DVdvmf69.js} +9 -9
- package/dist/studio-app/index.html +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1070 @@
|
|
|
1
|
+
use crate::{
|
|
2
|
+
application::*,
|
|
3
|
+
state_contract::{schema_from_revision, StateSchema},
|
|
4
|
+
FeltDb,
|
|
5
|
+
};
|
|
6
|
+
use serde::{Deserialize, Serialize};
|
|
7
|
+
use serde_json::{Map, Value};
|
|
8
|
+
use sha2::{Digest, Sha256};
|
|
9
|
+
use std::{
|
|
10
|
+
collections::{BTreeMap, BTreeSet},
|
|
11
|
+
fs,
|
|
12
|
+
path::PathBuf,
|
|
13
|
+
sync::{Arc, RwLock},
|
|
14
|
+
time::{SystemTime, UNIX_EPOCH},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
pub const APPLICATION_RUNTIME_CONTRACT_VERSION: u32 = 1;
|
|
18
|
+
pub const ENGINE_VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
19
|
+
|
|
20
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
21
|
+
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
|
22
|
+
pub enum ReadinessStatus {
|
|
23
|
+
Ready,
|
|
24
|
+
ReadyWithWarnings,
|
|
25
|
+
Blocked,
|
|
26
|
+
}
|
|
27
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
28
|
+
pub struct ReadinessCheck {
|
|
29
|
+
pub name: String,
|
|
30
|
+
pub passed: bool,
|
|
31
|
+
pub blocking: bool,
|
|
32
|
+
pub message: String,
|
|
33
|
+
}
|
|
34
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
35
|
+
pub struct RuntimeReadiness {
|
|
36
|
+
pub revision_id: String,
|
|
37
|
+
pub environment: String,
|
|
38
|
+
pub checks: Vec<ReadinessCheck>,
|
|
39
|
+
pub status: ReadinessStatus,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
43
|
+
pub struct StateContract {
|
|
44
|
+
pub schema: StateSchema,
|
|
45
|
+
pub collections: Vec<CollectionDefinition>,
|
|
46
|
+
pub indexes: Vec<IndexDefinition>,
|
|
47
|
+
pub references: Vec<ReferenceDefinition>,
|
|
48
|
+
pub mutation_preconditions: bool,
|
|
49
|
+
}
|
|
50
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
51
|
+
pub struct QueryContract {
|
|
52
|
+
pub definitions: Vec<QueryDefinition>,
|
|
53
|
+
pub live_bindings: bool,
|
|
54
|
+
}
|
|
55
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
56
|
+
pub struct ActionContract {
|
|
57
|
+
pub definitions: Vec<ActionDefinition>,
|
|
58
|
+
}
|
|
59
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
60
|
+
pub struct WorkflowContract {
|
|
61
|
+
pub definitions: Vec<WorkflowDefinition>,
|
|
62
|
+
pub revision_bound: bool,
|
|
63
|
+
}
|
|
64
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
65
|
+
pub struct TriggerContract {
|
|
66
|
+
pub definitions: Vec<TriggerDefinition>,
|
|
67
|
+
}
|
|
68
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
69
|
+
pub struct ScheduleContract {
|
|
70
|
+
pub definitions: Vec<ScheduleDefinition>,
|
|
71
|
+
}
|
|
72
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
73
|
+
pub struct AgentContract {
|
|
74
|
+
pub definitions: Vec<AgentDefinition>,
|
|
75
|
+
pub immutable_capability_snapshot: bool,
|
|
76
|
+
}
|
|
77
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
78
|
+
pub struct CapabilityContract {
|
|
79
|
+
pub bindings: Vec<CapabilityBinding>,
|
|
80
|
+
}
|
|
81
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
82
|
+
pub struct ResolvedConnection {
|
|
83
|
+
pub name: String,
|
|
84
|
+
pub connection_id: String,
|
|
85
|
+
pub secret_bindings: Vec<String>,
|
|
86
|
+
}
|
|
87
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
88
|
+
pub struct ConnectionContract {
|
|
89
|
+
pub bindings: Vec<ResolvedConnection>,
|
|
90
|
+
}
|
|
91
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
92
|
+
pub struct PolicyContract {
|
|
93
|
+
pub definitions: Vec<PolicyDefinition>,
|
|
94
|
+
pub required_layers: Vec<String>,
|
|
95
|
+
}
|
|
96
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
97
|
+
pub struct UiRuntimeContract {
|
|
98
|
+
pub routes: Vec<RouteDefinition>,
|
|
99
|
+
pub forms: Vec<FormDefinition>,
|
|
100
|
+
pub dashboards: Vec<DashboardDefinition>,
|
|
101
|
+
pub bindings: Vec<DataBinding>,
|
|
102
|
+
pub actions: Vec<String>,
|
|
103
|
+
pub navigation: Vec<String>,
|
|
104
|
+
}
|
|
105
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
106
|
+
pub struct ArtifactContract {
|
|
107
|
+
pub references: Vec<ArtifactReference>,
|
|
108
|
+
}
|
|
109
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
110
|
+
pub struct EnvironmentContract {
|
|
111
|
+
pub name: String,
|
|
112
|
+
pub configuration: BTreeMap<String, String>,
|
|
113
|
+
pub state_namespace: String,
|
|
114
|
+
pub isolated: bool,
|
|
115
|
+
}
|
|
116
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
117
|
+
pub struct RuntimeCompatibility {
|
|
118
|
+
pub manifest_version: u32,
|
|
119
|
+
pub runtime_contract_version: u32,
|
|
120
|
+
pub engine_version: String,
|
|
121
|
+
pub minimum_engine: String,
|
|
122
|
+
pub maximum_engine_exclusive: Option<String>,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
126
|
+
pub struct ApplicationRuntimeContract {
|
|
127
|
+
pub application_id: String,
|
|
128
|
+
pub tenant_id: String,
|
|
129
|
+
pub revision_id: String,
|
|
130
|
+
pub revision_number: u64,
|
|
131
|
+
pub metadata: ApplicationMetadata,
|
|
132
|
+
pub state: StateContract,
|
|
133
|
+
pub queries: QueryContract,
|
|
134
|
+
pub actions: ActionContract,
|
|
135
|
+
pub workflows: WorkflowContract,
|
|
136
|
+
pub triggers: TriggerContract,
|
|
137
|
+
pub schedules: ScheduleContract,
|
|
138
|
+
pub agents: AgentContract,
|
|
139
|
+
pub capabilities: CapabilityContract,
|
|
140
|
+
pub connections: ConnectionContract,
|
|
141
|
+
pub policies: PolicyContract,
|
|
142
|
+
pub ui: UiRuntimeContract,
|
|
143
|
+
pub artifacts: ArtifactContract,
|
|
144
|
+
pub environment: EnvironmentContract,
|
|
145
|
+
pub compatibility: RuntimeCompatibility,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#[derive(Debug, Clone, Default)]
|
|
149
|
+
pub struct RuntimeInventory {
|
|
150
|
+
pub capabilities: BTreeSet<String>,
|
|
151
|
+
pub connections: BTreeSet<String>,
|
|
152
|
+
pub enforce_availability: bool,
|
|
153
|
+
}
|
|
154
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
155
|
+
pub struct RuntimeIssue {
|
|
156
|
+
pub path: String,
|
|
157
|
+
pub message: String,
|
|
158
|
+
}
|
|
159
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
160
|
+
pub struct RuntimeValidation {
|
|
161
|
+
pub valid: bool,
|
|
162
|
+
pub contract_hash: Option<String>,
|
|
163
|
+
pub issues: Vec<RuntimeIssue>,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fn semver_tuple(value: &str) -> Option<(u64, u64, u64)> {
|
|
167
|
+
let mut p = value.split('.').map(|v| v.parse::<u64>().ok());
|
|
168
|
+
Some((
|
|
169
|
+
p.next()??,
|
|
170
|
+
p.next().unwrap_or(Some(0))?,
|
|
171
|
+
p.next().unwrap_or(Some(0))?,
|
|
172
|
+
))
|
|
173
|
+
}
|
|
174
|
+
fn compatible(requirement: &RuntimeRequirement) -> bool {
|
|
175
|
+
if requirement.contract != APPLICATION_RUNTIME_CONTRACT_VERSION {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
let Some(engine) = semver_tuple(ENGINE_VERSION) else {
|
|
179
|
+
return false;
|
|
180
|
+
};
|
|
181
|
+
if semver_tuple(&requirement.minimum_engine).is_some_and(|minimum| engine < minimum) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
if requirement
|
|
185
|
+
.maximum_engine_exclusive
|
|
186
|
+
.as_deref()
|
|
187
|
+
.and_then(semver_tuple)
|
|
188
|
+
.is_some_and(|maximum| engine >= maximum)
|
|
189
|
+
{
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
true
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
pub fn resolve_runtime(
|
|
196
|
+
revision: &ApplicationRevision,
|
|
197
|
+
environment: &str,
|
|
198
|
+
inventory: &RuntimeInventory,
|
|
199
|
+
) -> Result<ApplicationRuntimeContract, RuntimeValidation> {
|
|
200
|
+
let mut issues = vec![];
|
|
201
|
+
match manifest_hash(&revision.manifest) {
|
|
202
|
+
Ok(hash) if hash == revision.manifest_hash => {}
|
|
203
|
+
Ok(_) => issues.push(RuntimeIssue {
|
|
204
|
+
path: "manifest_hash".into(),
|
|
205
|
+
message: "revision integrity check failed".into(),
|
|
206
|
+
}),
|
|
207
|
+
Err(error) => issues.push(RuntimeIssue {
|
|
208
|
+
path: "manifest".into(),
|
|
209
|
+
message: error,
|
|
210
|
+
}),
|
|
211
|
+
}
|
|
212
|
+
let validation = validate_manifest(
|
|
213
|
+
&revision.manifest,
|
|
214
|
+
&revision.tenant_id,
|
|
215
|
+
&revision.application_id,
|
|
216
|
+
None,
|
|
217
|
+
);
|
|
218
|
+
for issue in validation.issues {
|
|
219
|
+
issues.push(RuntimeIssue {
|
|
220
|
+
path: issue.path,
|
|
221
|
+
message: issue.message,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
if !compatible(&revision.manifest.runtime) {
|
|
225
|
+
issues.push(RuntimeIssue{path:"runtime".into(),message:format!("runtime requires contract {} and engine >= {} < {:?}; current contract is {} and engine is {}",revision.manifest.runtime.contract,revision.manifest.runtime.minimum_engine,revision.manifest.runtime.maximum_engine_exclusive,APPLICATION_RUNTIME_CONTRACT_VERSION,ENGINE_VERSION)});
|
|
226
|
+
}
|
|
227
|
+
let environment_definition_name = if environment.starts_with("preview/") {
|
|
228
|
+
"preview"
|
|
229
|
+
} else {
|
|
230
|
+
environment
|
|
231
|
+
};
|
|
232
|
+
let environment_definition = revision
|
|
233
|
+
.manifest
|
|
234
|
+
.environments
|
|
235
|
+
.iter()
|
|
236
|
+
.find(|v| v.name == environment_definition_name)
|
|
237
|
+
.cloned();
|
|
238
|
+
if environment_definition.is_none() {
|
|
239
|
+
issues.push(RuntimeIssue {
|
|
240
|
+
path: "environment".into(),
|
|
241
|
+
message: "environment is not declared by the revision".into(),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
if inventory.enforce_availability {
|
|
245
|
+
for capability in &revision.manifest.capabilities {
|
|
246
|
+
if capability.provider.is_some() && !inventory.capabilities.contains(&capability.name) {
|
|
247
|
+
issues.push(RuntimeIssue {
|
|
248
|
+
path: format!("capabilities.{}", capability.name),
|
|
249
|
+
message: "required capability provider is unavailable".into(),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
for connection in &revision.manifest.connections {
|
|
254
|
+
if !inventory.connections.contains(&connection.connection_id) {
|
|
255
|
+
issues.push(RuntimeIssue {
|
|
256
|
+
path: format!("connections.{}", connection.name),
|
|
257
|
+
message: "connection is unavailable or unauthorized".into(),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if !issues.is_empty() {
|
|
263
|
+
return Err(RuntimeValidation {
|
|
264
|
+
valid: false,
|
|
265
|
+
contract_hash: None,
|
|
266
|
+
issues,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
let environment_definition = environment_definition.expect("checked above");
|
|
270
|
+
let isolated = environment == "preview"
|
|
271
|
+
|| environment.starts_with("preview/")
|
|
272
|
+
|| environment == "development";
|
|
273
|
+
let revision_key = revision
|
|
274
|
+
.manifest_hash
|
|
275
|
+
.strip_prefix("sha256:")
|
|
276
|
+
.unwrap_or(&revision.manifest_hash)
|
|
277
|
+
.chars()
|
|
278
|
+
.take(12)
|
|
279
|
+
.collect::<String>();
|
|
280
|
+
let state_namespace = if isolated {
|
|
281
|
+
format!(
|
|
282
|
+
"{}:{}:{}:{revision_key}",
|
|
283
|
+
revision.tenant_id, revision.application_id, environment
|
|
284
|
+
)
|
|
285
|
+
} else {
|
|
286
|
+
format!(
|
|
287
|
+
"{}:{}:{}",
|
|
288
|
+
revision.tenant_id, revision.application_id, environment
|
|
289
|
+
)
|
|
290
|
+
};
|
|
291
|
+
let mut actions = revision
|
|
292
|
+
.manifest
|
|
293
|
+
.actions
|
|
294
|
+
.iter()
|
|
295
|
+
.map(|v| v.name.clone())
|
|
296
|
+
.collect::<Vec<_>>();
|
|
297
|
+
actions.sort();
|
|
298
|
+
let mut navigation = revision
|
|
299
|
+
.manifest
|
|
300
|
+
.ui
|
|
301
|
+
.routes
|
|
302
|
+
.iter()
|
|
303
|
+
.map(|v| v.path.clone())
|
|
304
|
+
.collect::<Vec<_>>();
|
|
305
|
+
navigation.sort();
|
|
306
|
+
Ok(ApplicationRuntimeContract {
|
|
307
|
+
application_id: revision.application_id.clone(),
|
|
308
|
+
tenant_id: revision.tenant_id.clone(),
|
|
309
|
+
revision_id: revision.revision_id.clone(),
|
|
310
|
+
revision_number: revision.revision_number,
|
|
311
|
+
metadata: revision.manifest.metadata.clone(),
|
|
312
|
+
state: StateContract {
|
|
313
|
+
schema: schema_from_revision(revision),
|
|
314
|
+
collections: revision.manifest.collections.clone(),
|
|
315
|
+
indexes: revision.manifest.indexes.clone(),
|
|
316
|
+
references: revision.manifest.references.clone(),
|
|
317
|
+
mutation_preconditions: true,
|
|
318
|
+
},
|
|
319
|
+
queries: QueryContract {
|
|
320
|
+
definitions: revision.manifest.queries.clone(),
|
|
321
|
+
live_bindings: true,
|
|
322
|
+
},
|
|
323
|
+
actions: ActionContract {
|
|
324
|
+
definitions: revision.manifest.actions.clone(),
|
|
325
|
+
},
|
|
326
|
+
workflows: WorkflowContract {
|
|
327
|
+
definitions: revision.manifest.workflows.clone(),
|
|
328
|
+
revision_bound: true,
|
|
329
|
+
},
|
|
330
|
+
triggers: TriggerContract {
|
|
331
|
+
definitions: revision.manifest.triggers.clone(),
|
|
332
|
+
},
|
|
333
|
+
schedules: ScheduleContract {
|
|
334
|
+
definitions: revision.manifest.schedules.clone(),
|
|
335
|
+
},
|
|
336
|
+
agents: AgentContract {
|
|
337
|
+
definitions: revision.manifest.agents.clone(),
|
|
338
|
+
immutable_capability_snapshot: true,
|
|
339
|
+
},
|
|
340
|
+
capabilities: CapabilityContract {
|
|
341
|
+
bindings: revision.manifest.capabilities.clone(),
|
|
342
|
+
},
|
|
343
|
+
connections: ConnectionContract {
|
|
344
|
+
bindings: revision
|
|
345
|
+
.manifest
|
|
346
|
+
.connections
|
|
347
|
+
.iter()
|
|
348
|
+
.map(|v| ResolvedConnection {
|
|
349
|
+
name: v.name.clone(),
|
|
350
|
+
connection_id: v.connection_id.clone(),
|
|
351
|
+
secret_bindings: v.secrets.iter().map(|s| s.binding.clone()).collect(),
|
|
352
|
+
})
|
|
353
|
+
.collect(),
|
|
354
|
+
},
|
|
355
|
+
policies: PolicyContract {
|
|
356
|
+
definitions: revision.manifest.policies.clone(),
|
|
357
|
+
required_layers: vec![
|
|
358
|
+
"tenant".into(),
|
|
359
|
+
"application".into(),
|
|
360
|
+
format!("environment:{environment}"),
|
|
361
|
+
"principal".into(),
|
|
362
|
+
"workload_snapshot".into(),
|
|
363
|
+
],
|
|
364
|
+
},
|
|
365
|
+
ui: UiRuntimeContract {
|
|
366
|
+
routes: revision.manifest.ui.routes.clone(),
|
|
367
|
+
forms: revision.manifest.ui.forms.clone(),
|
|
368
|
+
dashboards: revision.manifest.ui.dashboards.clone(),
|
|
369
|
+
bindings: revision.manifest.ui.bindings.clone(),
|
|
370
|
+
actions,
|
|
371
|
+
navigation,
|
|
372
|
+
},
|
|
373
|
+
artifacts: ArtifactContract {
|
|
374
|
+
references: revision.manifest.artifacts.clone(),
|
|
375
|
+
},
|
|
376
|
+
environment: EnvironmentContract {
|
|
377
|
+
name: environment.into(),
|
|
378
|
+
configuration: environment_definition.configuration,
|
|
379
|
+
state_namespace,
|
|
380
|
+
isolated,
|
|
381
|
+
},
|
|
382
|
+
compatibility: RuntimeCompatibility {
|
|
383
|
+
manifest_version: revision.manifest.schema,
|
|
384
|
+
runtime_contract_version: APPLICATION_RUNTIME_CONTRACT_VERSION,
|
|
385
|
+
engine_version: ENGINE_VERSION.into(),
|
|
386
|
+
minimum_engine: revision.manifest.runtime.minimum_engine.clone(),
|
|
387
|
+
maximum_engine_exclusive: revision.manifest.runtime.maximum_engine_exclusive.clone(),
|
|
388
|
+
},
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
pub fn runtime_readiness(
|
|
393
|
+
revision: &ApplicationRevision,
|
|
394
|
+
environment: &str,
|
|
395
|
+
inventory: &RuntimeInventory,
|
|
396
|
+
) -> RuntimeReadiness {
|
|
397
|
+
let report = validate_runtime(revision, environment, inventory);
|
|
398
|
+
let names = [
|
|
399
|
+
"manifest valid",
|
|
400
|
+
"runtime compatible",
|
|
401
|
+
"schema valid",
|
|
402
|
+
"references valid",
|
|
403
|
+
"capabilities available",
|
|
404
|
+
"connections available",
|
|
405
|
+
"policies valid",
|
|
406
|
+
"required artifacts available",
|
|
407
|
+
"environment valid",
|
|
408
|
+
"migration valid",
|
|
409
|
+
];
|
|
410
|
+
let mut checks = names
|
|
411
|
+
.into_iter()
|
|
412
|
+
.map(|name| ReadinessCheck {
|
|
413
|
+
name: name.into(),
|
|
414
|
+
passed: true,
|
|
415
|
+
blocking: true,
|
|
416
|
+
message: "passed".into(),
|
|
417
|
+
})
|
|
418
|
+
.collect::<Vec<_>>();
|
|
419
|
+
for error in &report.issues {
|
|
420
|
+
let index = if error.path.contains("capabilit") {
|
|
421
|
+
4
|
|
422
|
+
} else if error.path.contains("connection") {
|
|
423
|
+
5
|
|
424
|
+
} else if error.path.contains("environment") {
|
|
425
|
+
8
|
|
426
|
+
} else if error.path.contains("runtime") {
|
|
427
|
+
1
|
|
428
|
+
} else if error.path.contains("polic") {
|
|
429
|
+
6
|
|
430
|
+
} else if error.path.contains("artifact") {
|
|
431
|
+
7
|
|
432
|
+
} else if error.path.contains("reference") {
|
|
433
|
+
3
|
|
434
|
+
} else {
|
|
435
|
+
0
|
|
436
|
+
};
|
|
437
|
+
checks[index].passed = false;
|
|
438
|
+
checks[index].message = error.message.clone();
|
|
439
|
+
}
|
|
440
|
+
let status = if checks.iter().any(|v| !v.passed && v.blocking) {
|
|
441
|
+
ReadinessStatus::Blocked
|
|
442
|
+
} else if checks.iter().any(|v| !v.passed) {
|
|
443
|
+
ReadinessStatus::ReadyWithWarnings
|
|
444
|
+
} else {
|
|
445
|
+
ReadinessStatus::Ready
|
|
446
|
+
};
|
|
447
|
+
RuntimeReadiness {
|
|
448
|
+
revision_id: revision.revision_id.clone(),
|
|
449
|
+
environment: environment.into(),
|
|
450
|
+
checks,
|
|
451
|
+
status,
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
pub fn canonical_runtime_bytes(contract: &ApplicationRuntimeContract) -> Result<Vec<u8>, String> {
|
|
455
|
+
serde_json::to_vec(contract).map_err(|e| e.to_string())
|
|
456
|
+
}
|
|
457
|
+
pub fn runtime_hash(contract: &ApplicationRuntimeContract) -> Result<String, String> {
|
|
458
|
+
Ok(format!(
|
|
459
|
+
"sha256:{:x}",
|
|
460
|
+
Sha256::digest(canonical_runtime_bytes(contract)?)
|
|
461
|
+
))
|
|
462
|
+
}
|
|
463
|
+
pub fn validate_runtime(
|
|
464
|
+
revision: &ApplicationRevision,
|
|
465
|
+
environment: &str,
|
|
466
|
+
inventory: &RuntimeInventory,
|
|
467
|
+
) -> RuntimeValidation {
|
|
468
|
+
match resolve_runtime(revision, environment, inventory) {
|
|
469
|
+
Ok(contract) => RuntimeValidation {
|
|
470
|
+
valid: true,
|
|
471
|
+
contract_hash: runtime_hash(&contract).ok(),
|
|
472
|
+
issues: vec![],
|
|
473
|
+
},
|
|
474
|
+
Err(report) => report,
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
479
|
+
#[serde(rename_all = "snake_case")]
|
|
480
|
+
pub enum RuntimeStatus {
|
|
481
|
+
Running,
|
|
482
|
+
Stopped,
|
|
483
|
+
}
|
|
484
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
485
|
+
pub struct RuntimeInstance {
|
|
486
|
+
pub runtime_id: String,
|
|
487
|
+
pub tenant_id: String,
|
|
488
|
+
pub application_id: String,
|
|
489
|
+
pub revision_id: String,
|
|
490
|
+
pub environment: String,
|
|
491
|
+
pub contract_hash: String,
|
|
492
|
+
pub state_namespace: String,
|
|
493
|
+
pub status: RuntimeStatus,
|
|
494
|
+
pub started_by: String,
|
|
495
|
+
pub started_at: u64,
|
|
496
|
+
pub stopped_at: Option<u64>,
|
|
497
|
+
}
|
|
498
|
+
#[derive(Debug, Default, Serialize, Deserialize)]
|
|
499
|
+
struct RuntimeRecords {
|
|
500
|
+
instances: Vec<RuntimeInstance>,
|
|
501
|
+
}
|
|
502
|
+
#[derive(Clone)]
|
|
503
|
+
pub struct RuntimeStore {
|
|
504
|
+
path: PathBuf,
|
|
505
|
+
records: Arc<RwLock<RuntimeRecords>>,
|
|
506
|
+
}
|
|
507
|
+
fn now() -> u64 {
|
|
508
|
+
SystemTime::now()
|
|
509
|
+
.duration_since(UNIX_EPOCH)
|
|
510
|
+
.unwrap_or_default()
|
|
511
|
+
.as_secs()
|
|
512
|
+
}
|
|
513
|
+
impl RuntimeStore {
|
|
514
|
+
pub fn load(path: impl Into<PathBuf>) -> Result<Self, String> {
|
|
515
|
+
let path = path.into();
|
|
516
|
+
let records = if path.exists() {
|
|
517
|
+
serde_json::from_slice(&fs::read(&path).map_err(|e| e.to_string())?)
|
|
518
|
+
.map_err(|e| e.to_string())?
|
|
519
|
+
} else {
|
|
520
|
+
RuntimeRecords::default()
|
|
521
|
+
};
|
|
522
|
+
Ok(Self {
|
|
523
|
+
path,
|
|
524
|
+
records: Arc::new(RwLock::new(records)),
|
|
525
|
+
})
|
|
526
|
+
}
|
|
527
|
+
fn persist(&self, r: &RuntimeRecords) -> Result<(), String> {
|
|
528
|
+
if let Some(parent) = self.path.parent() {
|
|
529
|
+
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
|
|
530
|
+
}
|
|
531
|
+
let tmp = self.path.with_extension("tmp");
|
|
532
|
+
fs::write(
|
|
533
|
+
&tmp,
|
|
534
|
+
serde_json::to_vec_pretty(r).map_err(|e| e.to_string())?,
|
|
535
|
+
)
|
|
536
|
+
.map_err(|e| e.to_string())?;
|
|
537
|
+
fs::rename(tmp, &self.path).map_err(|e| e.to_string())
|
|
538
|
+
}
|
|
539
|
+
pub fn start(
|
|
540
|
+
&self,
|
|
541
|
+
contract: &ApplicationRuntimeContract,
|
|
542
|
+
actor: &str,
|
|
543
|
+
) -> Result<RuntimeInstance, String> {
|
|
544
|
+
let mut r = self
|
|
545
|
+
.records
|
|
546
|
+
.write()
|
|
547
|
+
.map_err(|_| "runtime store lock poisoned")?;
|
|
548
|
+
let hash = runtime_hash(contract)?;
|
|
549
|
+
if let Some(current) = r.instances.iter_mut().find(|v| {
|
|
550
|
+
v.application_id == contract.application_id
|
|
551
|
+
&& v.revision_id == contract.revision_id
|
|
552
|
+
&& v.environment == contract.environment.name
|
|
553
|
+
}) {
|
|
554
|
+
current.status = RuntimeStatus::Running;
|
|
555
|
+
current.stopped_at = None;
|
|
556
|
+
let result = current.clone();
|
|
557
|
+
self.persist(&r)?;
|
|
558
|
+
return Ok(result);
|
|
559
|
+
}
|
|
560
|
+
let identity = Sha256::digest(
|
|
561
|
+
format!(
|
|
562
|
+
"{}:{}:{}:{hash}",
|
|
563
|
+
contract.application_id, contract.revision_id, contract.environment.name
|
|
564
|
+
)
|
|
565
|
+
.as_bytes(),
|
|
566
|
+
);
|
|
567
|
+
let instance = RuntimeInstance {
|
|
568
|
+
runtime_id: format!("runtime_{:x}", identity),
|
|
569
|
+
tenant_id: contract.tenant_id.clone(),
|
|
570
|
+
application_id: contract.application_id.clone(),
|
|
571
|
+
revision_id: contract.revision_id.clone(),
|
|
572
|
+
environment: contract.environment.name.clone(),
|
|
573
|
+
contract_hash: hash,
|
|
574
|
+
state_namespace: contract.environment.state_namespace.clone(),
|
|
575
|
+
status: RuntimeStatus::Running,
|
|
576
|
+
started_by: actor.into(),
|
|
577
|
+
started_at: now(),
|
|
578
|
+
stopped_at: None,
|
|
579
|
+
};
|
|
580
|
+
r.instances.push(instance.clone());
|
|
581
|
+
self.persist(&r)?;
|
|
582
|
+
Ok(instance)
|
|
583
|
+
}
|
|
584
|
+
pub fn stop(
|
|
585
|
+
&self,
|
|
586
|
+
tenant: &str,
|
|
587
|
+
app: &str,
|
|
588
|
+
revision: &str,
|
|
589
|
+
environment: &str,
|
|
590
|
+
) -> Result<RuntimeInstance, String> {
|
|
591
|
+
let mut r = self
|
|
592
|
+
.records
|
|
593
|
+
.write()
|
|
594
|
+
.map_err(|_| "runtime store lock poisoned")?;
|
|
595
|
+
let instance = r
|
|
596
|
+
.instances
|
|
597
|
+
.iter_mut()
|
|
598
|
+
.find(|v| {
|
|
599
|
+
v.tenant_id == tenant
|
|
600
|
+
&& v.application_id == app
|
|
601
|
+
&& v.revision_id == revision
|
|
602
|
+
&& v.environment == environment
|
|
603
|
+
})
|
|
604
|
+
.ok_or("runtime is not running")?;
|
|
605
|
+
instance.status = RuntimeStatus::Stopped;
|
|
606
|
+
instance.stopped_at = Some(now());
|
|
607
|
+
let result = instance.clone();
|
|
608
|
+
self.persist(&r)?;
|
|
609
|
+
Ok(result)
|
|
610
|
+
}
|
|
611
|
+
pub fn active(
|
|
612
|
+
&self,
|
|
613
|
+
tenant: &str,
|
|
614
|
+
app: &str,
|
|
615
|
+
revision: &str,
|
|
616
|
+
environment: &str,
|
|
617
|
+
) -> Option<RuntimeInstance> {
|
|
618
|
+
self.records
|
|
619
|
+
.read()
|
|
620
|
+
.ok()?
|
|
621
|
+
.instances
|
|
622
|
+
.iter()
|
|
623
|
+
.find(|v| {
|
|
624
|
+
v.tenant_id == tenant
|
|
625
|
+
&& v.application_id == app
|
|
626
|
+
&& v.revision_id == revision
|
|
627
|
+
&& v.environment == environment
|
|
628
|
+
&& v.status == RuntimeStatus::Running
|
|
629
|
+
})
|
|
630
|
+
.cloned()
|
|
631
|
+
}
|
|
632
|
+
pub fn list(&self, tenant: &str, app: &str) -> Vec<RuntimeInstance> {
|
|
633
|
+
self.records
|
|
634
|
+
.read()
|
|
635
|
+
.map(|r| {
|
|
636
|
+
r.instances
|
|
637
|
+
.iter()
|
|
638
|
+
.filter(|v| v.tenant_id == tenant && v.application_id == app)
|
|
639
|
+
.cloned()
|
|
640
|
+
.collect()
|
|
641
|
+
})
|
|
642
|
+
.unwrap_or_default()
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
647
|
+
pub struct QueryExecution {
|
|
648
|
+
pub query: String,
|
|
649
|
+
pub revision_id: String,
|
|
650
|
+
pub state_version: u64,
|
|
651
|
+
pub subscription_id: String,
|
|
652
|
+
pub records: Vec<Value>,
|
|
653
|
+
}
|
|
654
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
655
|
+
pub struct ActionExecution {
|
|
656
|
+
pub action: String,
|
|
657
|
+
pub revision_id: String,
|
|
658
|
+
pub state_version: u64,
|
|
659
|
+
pub record_id: String,
|
|
660
|
+
pub value: Option<Value>,
|
|
661
|
+
}
|
|
662
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
663
|
+
pub struct RuntimeExecutionRequest {
|
|
664
|
+
pub execution_id: String,
|
|
665
|
+
pub application_id: String,
|
|
666
|
+
pub revision_id: String,
|
|
667
|
+
pub workflow_id: String,
|
|
668
|
+
pub trigger: String,
|
|
669
|
+
pub subject: String,
|
|
670
|
+
pub input: Value,
|
|
671
|
+
pub state: String,
|
|
672
|
+
pub checkpoint: Option<Value>,
|
|
673
|
+
pub result: Option<Value>,
|
|
674
|
+
}
|
|
675
|
+
pub fn trigger_requests(
|
|
676
|
+
contract: &ApplicationRuntimeContract,
|
|
677
|
+
event: &str,
|
|
678
|
+
subject: &str,
|
|
679
|
+
input: Value,
|
|
680
|
+
) -> Vec<RuntimeExecutionRequest> {
|
|
681
|
+
contract
|
|
682
|
+
.triggers
|
|
683
|
+
.definitions
|
|
684
|
+
.iter()
|
|
685
|
+
.filter(|trigger| trigger.event == event)
|
|
686
|
+
.map(|trigger| {
|
|
687
|
+
let identity = Sha256::digest(
|
|
688
|
+
format!(
|
|
689
|
+
"{}:{event}:{subject}:{}",
|
|
690
|
+
contract.revision_id,
|
|
691
|
+
serde_json::to_string(&input).unwrap_or_default()
|
|
692
|
+
)
|
|
693
|
+
.as_bytes(),
|
|
694
|
+
);
|
|
695
|
+
RuntimeExecutionRequest {
|
|
696
|
+
execution_id: format!("execution_{identity:x}"),
|
|
697
|
+
application_id: contract.application_id.clone(),
|
|
698
|
+
revision_id: contract.revision_id.clone(),
|
|
699
|
+
workflow_id: trigger.workflow.clone(),
|
|
700
|
+
trigger: trigger.name.clone(),
|
|
701
|
+
subject: subject.into(),
|
|
702
|
+
input: input.clone(),
|
|
703
|
+
state: "requested".into(),
|
|
704
|
+
checkpoint: None,
|
|
705
|
+
result: None,
|
|
706
|
+
}
|
|
707
|
+
})
|
|
708
|
+
.collect()
|
|
709
|
+
}
|
|
710
|
+
fn runtime_collection(contract: &ApplicationRuntimeContract, collection: &str) -> String {
|
|
711
|
+
format!("{}:{collection}", contract.environment.state_namespace)
|
|
712
|
+
}
|
|
713
|
+
fn matches_filter(value: &Value, filter: &Value) -> bool {
|
|
714
|
+
filter.as_object().is_none_or(|expected| {
|
|
715
|
+
expected
|
|
716
|
+
.iter()
|
|
717
|
+
.all(|(key, wanted)| value.get(key) == Some(wanted))
|
|
718
|
+
})
|
|
719
|
+
}
|
|
720
|
+
fn project(value: &Value, fields: &[String]) -> Value {
|
|
721
|
+
if fields.is_empty() {
|
|
722
|
+
return value.clone();
|
|
723
|
+
}
|
|
724
|
+
let mut out = Map::new();
|
|
725
|
+
for field in fields {
|
|
726
|
+
if let Some(value) = value.get(field) {
|
|
727
|
+
out.insert(field.clone(), value.clone());
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
Value::Object(out)
|
|
731
|
+
}
|
|
732
|
+
pub fn execute_query(
|
|
733
|
+
db: &FeltDb,
|
|
734
|
+
contract: &ApplicationRuntimeContract,
|
|
735
|
+
name: &str,
|
|
736
|
+
) -> Result<QueryExecution, String> {
|
|
737
|
+
let query = contract
|
|
738
|
+
.queries
|
|
739
|
+
.definitions
|
|
740
|
+
.iter()
|
|
741
|
+
.find(|v| v.name == name)
|
|
742
|
+
.ok_or("query not found")?;
|
|
743
|
+
let collection = runtime_collection(contract, &query.collection);
|
|
744
|
+
let rows = db
|
|
745
|
+
.export_snapshot()
|
|
746
|
+
.map_err(|e| e.to_string())?
|
|
747
|
+
.rows
|
|
748
|
+
.into_iter()
|
|
749
|
+
.filter(|row| {
|
|
750
|
+
row.capability == collection
|
|
751
|
+
&& !row.deleted
|
|
752
|
+
&& matches_filter(&row.value, &query.filter)
|
|
753
|
+
})
|
|
754
|
+
.map(|row| {
|
|
755
|
+
let mut value = project(&row.value, &query.fields);
|
|
756
|
+
if let Some(object) = value.as_object_mut() {
|
|
757
|
+
object.insert("_id".into(), Value::String(row.key));
|
|
758
|
+
}
|
|
759
|
+
value
|
|
760
|
+
})
|
|
761
|
+
.collect::<Vec<_>>();
|
|
762
|
+
let version = db.sequence().map_err(|e| e.to_string())?;
|
|
763
|
+
Ok(QueryExecution {
|
|
764
|
+
query: name.into(),
|
|
765
|
+
revision_id: contract.revision_id.clone(),
|
|
766
|
+
state_version: version,
|
|
767
|
+
subscription_id: format!("query://{}/{}/{version}", contract.revision_id, name),
|
|
768
|
+
records: rows,
|
|
769
|
+
})
|
|
770
|
+
}
|
|
771
|
+
fn validate_input(
|
|
772
|
+
contract: &ApplicationRuntimeContract,
|
|
773
|
+
action: &ActionDefinition,
|
|
774
|
+
input: &Value,
|
|
775
|
+
) -> Result<(), String> {
|
|
776
|
+
let collection = contract
|
|
777
|
+
.state
|
|
778
|
+
.collections
|
|
779
|
+
.iter()
|
|
780
|
+
.find(|v| v.name == action.collection)
|
|
781
|
+
.ok_or("action collection not found")?;
|
|
782
|
+
let object = input.as_object().ok_or("action input must be an object")?;
|
|
783
|
+
for field in &collection.fields {
|
|
784
|
+
if field.required
|
|
785
|
+
&& action.operation == ActionOperation::Create
|
|
786
|
+
&& !object.contains_key(&field.name)
|
|
787
|
+
{
|
|
788
|
+
return Err(format!("required field is missing: {}", field.name));
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
if object
|
|
792
|
+
.keys()
|
|
793
|
+
.any(|key| !collection.fields.iter().any(|field| &field.name == key) && key != "id")
|
|
794
|
+
{
|
|
795
|
+
return Err("action input contains an unknown field".into());
|
|
796
|
+
}
|
|
797
|
+
Ok(())
|
|
798
|
+
}
|
|
799
|
+
pub fn execute_action(
|
|
800
|
+
db: &FeltDb,
|
|
801
|
+
contract: &ApplicationRuntimeContract,
|
|
802
|
+
name: &str,
|
|
803
|
+
input: Value,
|
|
804
|
+
) -> Result<ActionExecution, String> {
|
|
805
|
+
let action = contract
|
|
806
|
+
.actions
|
|
807
|
+
.definitions
|
|
808
|
+
.iter()
|
|
809
|
+
.find(|v| v.name == name)
|
|
810
|
+
.ok_or("action not found")?;
|
|
811
|
+
validate_input(contract, action, &input)?;
|
|
812
|
+
let capability = db.capability(&runtime_collection(contract, &action.collection));
|
|
813
|
+
let id = input.get("id").and_then(Value::as_str).map(str::to_string);
|
|
814
|
+
let (record_id, value) = match action.operation {
|
|
815
|
+
ActionOperation::Create => {
|
|
816
|
+
let mut value = input;
|
|
817
|
+
if let Some(object) = value.as_object_mut() {
|
|
818
|
+
object.remove("id");
|
|
819
|
+
}
|
|
820
|
+
let key = if let Some(id) = id {
|
|
821
|
+
capability
|
|
822
|
+
.insert_with_key(&id, value.clone())
|
|
823
|
+
.map_err(|e| e.to_string())?;
|
|
824
|
+
id
|
|
825
|
+
} else {
|
|
826
|
+
capability
|
|
827
|
+
.insert(value.clone())
|
|
828
|
+
.map_err(|e| e.to_string())?
|
|
829
|
+
};
|
|
830
|
+
(key, Some(value))
|
|
831
|
+
}
|
|
832
|
+
ActionOperation::Update => {
|
|
833
|
+
let id = id.ok_or("update action requires id")?;
|
|
834
|
+
let mut value = input;
|
|
835
|
+
if let Some(object) = value.as_object_mut() {
|
|
836
|
+
object.remove("id");
|
|
837
|
+
}
|
|
838
|
+
capability
|
|
839
|
+
.update(&id, value.clone())
|
|
840
|
+
.map_err(|e| e.to_string())?;
|
|
841
|
+
(id, Some(value))
|
|
842
|
+
}
|
|
843
|
+
ActionOperation::Delete => {
|
|
844
|
+
let id = id.ok_or("delete action requires id")?;
|
|
845
|
+
capability.delete(&id).map_err(|e| e.to_string())?;
|
|
846
|
+
(id, None)
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
Ok(ActionExecution {
|
|
850
|
+
action: name.into(),
|
|
851
|
+
revision_id: contract.revision_id.clone(),
|
|
852
|
+
state_version: db.sequence().map_err(|e| e.to_string())?,
|
|
853
|
+
record_id,
|
|
854
|
+
value,
|
|
855
|
+
})
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/// Copies the current materialized application state into an isolated namespace.
|
|
859
|
+
/// This is intentionally a point-in-time copy: later production writes cannot flow
|
|
860
|
+
/// into the preview and preview writes cannot flow back to production.
|
|
861
|
+
pub fn snapshot_state_namespace(db: &FeltDb, from: &str, to: &str) -> Result<usize, String> {
|
|
862
|
+
if from == to {
|
|
863
|
+
return Err("source and destination namespaces must differ".into());
|
|
864
|
+
}
|
|
865
|
+
let prefix = format!("{from}:");
|
|
866
|
+
let rows = db
|
|
867
|
+
.export_snapshot()
|
|
868
|
+
.map_err(|e| e.to_string())?
|
|
869
|
+
.rows
|
|
870
|
+
.into_iter()
|
|
871
|
+
.filter(|row| !row.deleted && row.capability.starts_with(&prefix))
|
|
872
|
+
.collect::<Vec<_>>();
|
|
873
|
+
for row in &rows {
|
|
874
|
+
let suffix = row
|
|
875
|
+
.capability
|
|
876
|
+
.strip_prefix(&prefix)
|
|
877
|
+
.ok_or("invalid source namespace")?;
|
|
878
|
+
db.capability(&format!("{to}:{suffix}"))
|
|
879
|
+
.insert_with_key(&row.key, row.value.clone())
|
|
880
|
+
.map_err(|e| e.to_string())?;
|
|
881
|
+
}
|
|
882
|
+
Ok(rows.len())
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
#[cfg(test)]
|
|
886
|
+
mod tests {
|
|
887
|
+
use super::*;
|
|
888
|
+
fn revision() -> ApplicationRevision {
|
|
889
|
+
let mut manifest = ApplicationManifest::empty("tenant", "app", "App");
|
|
890
|
+
manifest.collections.push(CollectionDefinition {
|
|
891
|
+
name: "incidents".into(),
|
|
892
|
+
fields: vec![
|
|
893
|
+
FieldDefinition {
|
|
894
|
+
name: "title".into(),
|
|
895
|
+
field_type: "string".into(),
|
|
896
|
+
required: true,
|
|
897
|
+
nullable: false,
|
|
898
|
+
default: None,
|
|
899
|
+
enum_values: vec![],
|
|
900
|
+
reference: None,
|
|
901
|
+
computed: None,
|
|
902
|
+
},
|
|
903
|
+
FieldDefinition {
|
|
904
|
+
name: "status".into(),
|
|
905
|
+
field_type: "string".into(),
|
|
906
|
+
required: false,
|
|
907
|
+
nullable: false,
|
|
908
|
+
default: None,
|
|
909
|
+
enum_values: vec![],
|
|
910
|
+
reference: None,
|
|
911
|
+
computed: None,
|
|
912
|
+
},
|
|
913
|
+
],
|
|
914
|
+
});
|
|
915
|
+
manifest.queries.push(QueryDefinition {
|
|
916
|
+
name: "OpenIncidents".into(),
|
|
917
|
+
collection: "incidents".into(),
|
|
918
|
+
filter: serde_json::json!({"status":"open"}),
|
|
919
|
+
fields: vec![],
|
|
920
|
+
});
|
|
921
|
+
manifest.actions.push(ActionDefinition {
|
|
922
|
+
name: "CreateIncident".into(),
|
|
923
|
+
operation: ActionOperation::Create,
|
|
924
|
+
collection: "incidents".into(),
|
|
925
|
+
input_fields: vec!["title".into(), "status".into()],
|
|
926
|
+
required_capabilities: vec![],
|
|
927
|
+
audit: true,
|
|
928
|
+
});
|
|
929
|
+
let hash = manifest_hash(&manifest).unwrap();
|
|
930
|
+
ApplicationRevision {
|
|
931
|
+
revision_id: "apprev://tenant/app/test".into(),
|
|
932
|
+
application_id: "app".into(),
|
|
933
|
+
tenant_id: "tenant".into(),
|
|
934
|
+
revision_number: 1,
|
|
935
|
+
parent_revision_id: None,
|
|
936
|
+
manifest_hash: hash,
|
|
937
|
+
manifest,
|
|
938
|
+
created_by: "owner".into(),
|
|
939
|
+
created_at: 1,
|
|
940
|
+
status: RevisionStatus::Committed,
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
#[test]
|
|
944
|
+
fn integrity_and_runtime_hash_are_deterministic() {
|
|
945
|
+
let revision = revision();
|
|
946
|
+
let a = resolve_runtime(&revision, "preview", &RuntimeInventory::default()).unwrap();
|
|
947
|
+
let b = resolve_runtime(&revision, "preview", &RuntimeInventory::default()).unwrap();
|
|
948
|
+
assert_eq!(runtime_hash(&a).unwrap(), runtime_hash(&b).unwrap());
|
|
949
|
+
let mut corrupt = revision;
|
|
950
|
+
corrupt.manifest.metadata.name = "Changed".into();
|
|
951
|
+
assert!(!validate_runtime(&corrupt, "preview", &RuntimeInventory::default()).valid);
|
|
952
|
+
}
|
|
953
|
+
#[test]
|
|
954
|
+
fn preview_state_is_isolated_and_actions_feed_queries() {
|
|
955
|
+
let revision = revision();
|
|
956
|
+
let preview = resolve_runtime(&revision, "preview", &RuntimeInventory::default()).unwrap();
|
|
957
|
+
let production =
|
|
958
|
+
resolve_runtime(&revision, "production", &RuntimeInventory::default()).unwrap();
|
|
959
|
+
assert_ne!(
|
|
960
|
+
preview.environment.state_namespace,
|
|
961
|
+
production.environment.state_namespace
|
|
962
|
+
);
|
|
963
|
+
let db = FeltDb::open(std::env::temp_dir().join(format!("runtime-{}.log", now()))).unwrap();
|
|
964
|
+
execute_action(
|
|
965
|
+
&db,
|
|
966
|
+
&preview,
|
|
967
|
+
"CreateIncident",
|
|
968
|
+
serde_json::json!({"title":"Power outage","status":"open"}),
|
|
969
|
+
)
|
|
970
|
+
.unwrap();
|
|
971
|
+
assert_eq!(
|
|
972
|
+
execute_query(&db, &preview, "OpenIncidents")
|
|
973
|
+
.unwrap()
|
|
974
|
+
.records
|
|
975
|
+
.len(),
|
|
976
|
+
1
|
|
977
|
+
);
|
|
978
|
+
assert!(execute_query(&db, &production, "OpenIncidents")
|
|
979
|
+
.unwrap()
|
|
980
|
+
.records
|
|
981
|
+
.is_empty());
|
|
982
|
+
}
|
|
983
|
+
#[test]
|
|
984
|
+
fn preview_snapshot_is_a_point_in_time_copy() {
|
|
985
|
+
let revision = revision();
|
|
986
|
+
let production = resolve_runtime(&revision, "production", &Default::default()).unwrap();
|
|
987
|
+
let preview = resolve_runtime(&revision, "preview/editor", &Default::default()).unwrap();
|
|
988
|
+
let db = FeltDb::open(std::env::temp_dir().join(format!("runtime-snapshot-{}.log", now())))
|
|
989
|
+
.unwrap();
|
|
990
|
+
execute_action(
|
|
991
|
+
&db,
|
|
992
|
+
&production,
|
|
993
|
+
"CreateIncident",
|
|
994
|
+
serde_json::json!({"id":"one","title":"First","status":"open"}),
|
|
995
|
+
)
|
|
996
|
+
.unwrap();
|
|
997
|
+
assert_eq!(
|
|
998
|
+
snapshot_state_namespace(
|
|
999
|
+
&db,
|
|
1000
|
+
&production.environment.state_namespace,
|
|
1001
|
+
&preview.environment.state_namespace
|
|
1002
|
+
)
|
|
1003
|
+
.unwrap(),
|
|
1004
|
+
1
|
|
1005
|
+
);
|
|
1006
|
+
execute_action(
|
|
1007
|
+
&db,
|
|
1008
|
+
&production,
|
|
1009
|
+
"CreateIncident",
|
|
1010
|
+
serde_json::json!({"id":"two","title":"Second","status":"open"}),
|
|
1011
|
+
)
|
|
1012
|
+
.unwrap();
|
|
1013
|
+
assert_eq!(
|
|
1014
|
+
execute_query(&db, &preview, "OpenIncidents")
|
|
1015
|
+
.unwrap()
|
|
1016
|
+
.records
|
|
1017
|
+
.len(),
|
|
1018
|
+
1
|
|
1019
|
+
);
|
|
1020
|
+
assert_eq!(
|
|
1021
|
+
execute_query(&db, &production, "OpenIncidents")
|
|
1022
|
+
.unwrap()
|
|
1023
|
+
.records
|
|
1024
|
+
.len(),
|
|
1025
|
+
2
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
#[test]
|
|
1030
|
+
fn unavailable_connections_fail_closed_and_trigger_keeps_revision() {
|
|
1031
|
+
let mut revision = revision();
|
|
1032
|
+
revision.manifest.connections.push(ConnectionBinding {
|
|
1033
|
+
name: "dispatch".into(),
|
|
1034
|
+
connection_id: "connection_missing".into(),
|
|
1035
|
+
configuration: Value::Object(Default::default()),
|
|
1036
|
+
secrets: vec![SecretReference {
|
|
1037
|
+
binding: "token".into(),
|
|
1038
|
+
secret_id: "secret_1".into(),
|
|
1039
|
+
}],
|
|
1040
|
+
});
|
|
1041
|
+
revision.manifest.workflows.push(WorkflowDefinition {
|
|
1042
|
+
name: "Dispatch".into(),
|
|
1043
|
+
steps: vec![],
|
|
1044
|
+
});
|
|
1045
|
+
revision.manifest.triggers.push(TriggerDefinition {
|
|
1046
|
+
name: "incident-created".into(),
|
|
1047
|
+
event: "Incident.created".into(),
|
|
1048
|
+
workflow: "Dispatch".into(),
|
|
1049
|
+
});
|
|
1050
|
+
revision.manifest_hash = manifest_hash(&revision.manifest).unwrap();
|
|
1051
|
+
let inventory = RuntimeInventory {
|
|
1052
|
+
enforce_availability: true,
|
|
1053
|
+
..Default::default()
|
|
1054
|
+
};
|
|
1055
|
+
assert!(!validate_runtime(&revision, "production", &inventory).valid);
|
|
1056
|
+
let contract =
|
|
1057
|
+
resolve_runtime(&revision, "production", &RuntimeInventory::default()).unwrap();
|
|
1058
|
+
let requests = trigger_requests(
|
|
1059
|
+
&contract,
|
|
1060
|
+
"Incident.created",
|
|
1061
|
+
"agent_1",
|
|
1062
|
+
serde_json::json!({"id":"i1"}),
|
|
1063
|
+
);
|
|
1064
|
+
assert_eq!(requests[0].revision_id, revision.revision_id);
|
|
1065
|
+
assert_eq!(requests[0].workflow_id, "Dispatch");
|
|
1066
|
+
assert!(!serde_json::to_string(&contract)
|
|
1067
|
+
.unwrap()
|
|
1068
|
+
.contains("secret_1"));
|
|
1069
|
+
}
|
|
1070
|
+
}
|