@feltdb/core 0.5.0 → 0.5.2
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/README.md +24 -14
- package/dist/cell.d.ts +233 -0
- package/dist/cell.d.ts.map +1 -0
- package/dist/cell.js +1078 -0
- package/dist/cli/index.js +1 -1
- package/dist/collection.d.ts +48 -1
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +95 -10
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/application.rs +183 -8
- package/dist/create/server-source/crates/feltdb/src/lib.rs +173 -0
- package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +231 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +314 -46
- package/dist/create/server-source/crates/feltdb-server/src/request_telemetry.rs +1 -1
- package/dist/create/server-source/crates/feltdb-server/tests/revision_recovery_integration_test.rs +219 -0
- package/dist/db.d.ts +16 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +21 -0
- package/dist/feltdb.d.ts +7 -1
- package/dist/feltdb.d.ts.map +1 -1
- package/dist/file-db.d.ts +5 -0
- package/dist/file-db.d.ts.map +1 -1
- package/dist/file-db.js +72 -9
- package/dist/http-db.d.ts +13 -0
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +41 -0
- package/dist/http-server.d.ts +42 -0
- package/dist/http-server.d.ts.map +1 -0
- package/dist/http-server.js +182 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/indexeddb-conformance.spec.d.ts +21 -0
- package/dist/indexeddb-conformance.spec.d.ts.map +1 -0
- package/dist/indexeddb-conformance.spec.js +103 -0
- package/dist/indexeddb-db.d.ts +8 -0
- package/dist/indexeddb-db.d.ts.map +1 -1
- package/dist/indexeddb-db.js +69 -0
- package/dist/memory-db.d.ts +14 -0
- package/dist/memory-db.d.ts.map +1 -1
- package/dist/memory-db.js +42 -0
- package/dist/state-contract.d.ts +1 -1
- package/dist/state-contract.d.ts.map +1 -1
- package/dist/state-contract.js +1 -1
- package/dist/studio-app/assets/{feltdb_wasm-CBGD0zRu.js → feltdb_wasm-h9mxesnH.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-B8U4A1n1.wasm +0 -0
- package/dist/studio-app/assets/index-BMaQv3zF.js +28 -0
- package/dist/studio-app/index.html +1 -1
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/package.json +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-C6ATF9mJ.wasm +0 -0
- package/dist/studio-app/assets/index-3cvTQ0Mv.js +0 -28
|
@@ -40,8 +40,8 @@ use feltdb::{
|
|
|
40
40
|
authorize as authorize_resource, AuthorizationRequest as ResourceAuthorizationRequest,
|
|
41
41
|
Grant, GrantSigner, GrantStore, Subject as GrantSubject,
|
|
42
42
|
},
|
|
43
|
-
policy_evaluation::{Actor, PolicyContext, PolicyEvaluator, PolicySubject},
|
|
44
43
|
cardinality_endpoint::{CardinalityContext, CardinalityDiagnosticResponse},
|
|
44
|
+
policy_evaluation::{Actor, PolicyContext, PolicyEvaluator, PolicySubject},
|
|
45
45
|
state_contract::{
|
|
46
46
|
begin_read, compare_schemas, execute_query as execute_state_query, execute_transaction,
|
|
47
47
|
schema_from_revision, validate_schema, AuthorizationContext, CanonicalQuery, QueryFilter,
|
|
@@ -56,7 +56,7 @@ use feltdb::{
|
|
|
56
56
|
WorkerRegistration,
|
|
57
57
|
},
|
|
58
58
|
workload::{CreateWorkload, WorkloadStore},
|
|
59
|
-
DatabaseSnapshot, FeltDb, Operation, PeerAdvertisement, PeerId, StoredRow,
|
|
59
|
+
DatabaseSnapshot, FeltDb, JsonCasResult, Operation, PeerAdvertisement, PeerId, StoredRow,
|
|
60
60
|
};
|
|
61
61
|
use feltdb_server::{
|
|
62
62
|
app_state::AppState,
|
|
@@ -76,7 +76,7 @@ use feltdb_server::{
|
|
|
76
76
|
connections::{ConnectionProvider, ConnectionStatus, ConnectionStore, SecretReference},
|
|
77
77
|
content::ContentStore,
|
|
78
78
|
identity::IdentityStore,
|
|
79
|
-
key_management::{
|
|
79
|
+
key_management::{create_key, list_keys, revoke_key},
|
|
80
80
|
key_provider::provider_from_environment,
|
|
81
81
|
leases::LeaseStore,
|
|
82
82
|
metrics::Metrics,
|
|
@@ -103,7 +103,9 @@ struct ApiError(StatusCode, String);
|
|
|
103
103
|
|
|
104
104
|
impl IntoResponse for ApiError {
|
|
105
105
|
fn into_response(self) -> Response {
|
|
106
|
-
|
|
106
|
+
let body =
|
|
107
|
+
serde_json::from_str::<Value>(&self.1).unwrap_or_else(|_| json!({ "error": self.1 }));
|
|
108
|
+
(self.0, Json(body)).into_response()
|
|
107
109
|
}
|
|
108
110
|
}
|
|
109
111
|
|
|
@@ -2027,9 +2029,10 @@ fn is_platform_owner(principal: &Principal) -> bool {
|
|
|
2027
2029
|
.any(|owner| !owner.is_empty() && owner == identity)
|
|
2028
2030
|
}
|
|
2029
2031
|
fn require_platform_owner(principal: &Principal) -> Result<(), ApiError> {
|
|
2030
|
-
is_platform_owner(principal)
|
|
2031
|
-
|
|
2032
|
-
|
|
2032
|
+
is_platform_owner(principal).then_some(()).ok_or(ApiError(
|
|
2033
|
+
StatusCode::FORBIDDEN,
|
|
2034
|
+
"platform owner access required".into(),
|
|
2035
|
+
))
|
|
2033
2036
|
}
|
|
2034
2037
|
async fn platform_access(Extension(principal): Extension<Principal>) -> Json<Value> {
|
|
2035
2038
|
let platform_role = is_platform_owner(&principal).then_some("super_owner");
|
|
@@ -2365,7 +2368,11 @@ async fn evaluate_application_contract(
|
|
|
2365
2368
|
) -> Result<Json<Value>, ApiError> {
|
|
2366
2369
|
Ok(Json(json!(state
|
|
2367
2370
|
.contracts
|
|
2368
|
-
.evaluate(
|
|
2371
|
+
.evaluate(
|
|
2372
|
+
&user_contract_key(&principal, &id),
|
|
2373
|
+
&input.outcome,
|
|
2374
|
+
&principal.key_id
|
|
2375
|
+
)
|
|
2369
2376
|
.map_err(control_error)?)))
|
|
2370
2377
|
}
|
|
2371
2378
|
async fn patch_application_contract(
|
|
@@ -2384,7 +2391,10 @@ async fn application_contract_gaps(
|
|
|
2384
2391
|
Extension(principal): Extension<Principal>,
|
|
2385
2392
|
Path(id): Path<String>,
|
|
2386
2393
|
) -> Result<Json<Value>, ApiError> {
|
|
2387
|
-
let c = state
|
|
2394
|
+
let c = state
|
|
2395
|
+
.contracts
|
|
2396
|
+
.get(&user_contract_key(&principal, &id))
|
|
2397
|
+
.map_err(control_error)?;
|
|
2388
2398
|
Ok(Json(json!({"gaps":c.gaps})))
|
|
2389
2399
|
}
|
|
2390
2400
|
async fn application_contract_readiness(
|
|
@@ -4495,12 +4505,7 @@ async fn run_runtime_action(
|
|
|
4495
4505
|
schema_version: contract.state.schema.schema_version,
|
|
4496
4506
|
state_namespace: Some(contract.environment.state_namespace.clone()),
|
|
4497
4507
|
causal_parent: None,
|
|
4498
|
-
authorization: state_authorization(
|
|
4499
|
-
&principal,
|
|
4500
|
-
&contract,
|
|
4501
|
-
&definition.collection,
|
|
4502
|
-
"write",
|
|
4503
|
-
),
|
|
4508
|
+
authorization: state_authorization(&principal, &contract, &definition.collection, "write"),
|
|
4504
4509
|
operations: vec![TransactionOperation {
|
|
4505
4510
|
kind,
|
|
4506
4511
|
collection: definition.collection.clone(),
|
|
@@ -4603,12 +4608,7 @@ async fn run_runtime_query(
|
|
|
4603
4608
|
&state.db,
|
|
4604
4609
|
&contract.state.schema,
|
|
4605
4610
|
&contract.environment.state_namespace,
|
|
4606
|
-
state_authorization(
|
|
4607
|
-
&principal,
|
|
4608
|
-
&contract,
|
|
4609
|
-
&definition.collection,
|
|
4610
|
-
"read",
|
|
4611
|
-
),
|
|
4611
|
+
state_authorization(&principal, &contract, &definition.collection, "read"),
|
|
4612
4612
|
)
|
|
4613
4613
|
.map_err(state_contract_error)?;
|
|
4614
4614
|
|
|
@@ -4712,7 +4712,7 @@ fn state_authorization(
|
|
|
4712
4712
|
principal: &Principal,
|
|
4713
4713
|
contract: &ApplicationRuntimeContract,
|
|
4714
4714
|
collection: &str,
|
|
4715
|
-
operation: &str,
|
|
4715
|
+
operation: &str, // "read" or "write"
|
|
4716
4716
|
) -> AuthorizationContext {
|
|
4717
4717
|
let subject = format!("{}:{}", principal.subject_type, principal.key_id);
|
|
4718
4718
|
|
|
@@ -4888,12 +4888,7 @@ async fn execute_canonical_query(
|
|
|
4888
4888
|
&state.db,
|
|
4889
4889
|
&contract.state.schema,
|
|
4890
4890
|
&contract.environment.state_namespace,
|
|
4891
|
-
state_authorization(
|
|
4892
|
-
&principal,
|
|
4893
|
-
&contract,
|
|
4894
|
-
&input.query.collection,
|
|
4895
|
-
"read",
|
|
4896
|
-
),
|
|
4891
|
+
state_authorization(&principal, &contract, &input.query.collection, "read"),
|
|
4897
4892
|
)
|
|
4898
4893
|
.map_err(state_contract_error)?;
|
|
4899
4894
|
|
|
@@ -4905,8 +4900,14 @@ async fn execute_canonical_query(
|
|
|
4905
4900
|
.find(|p| p.resource == input.query.collection)
|
|
4906
4901
|
.and_then(|p| p.read.as_ref().and_then(|s| PolicySubject::from_str(s)));
|
|
4907
4902
|
|
|
4908
|
-
let result = execute_state_query(
|
|
4909
|
-
.
|
|
4903
|
+
let result = execute_state_query(
|
|
4904
|
+
&state.db,
|
|
4905
|
+
&contract.state.schema,
|
|
4906
|
+
&context,
|
|
4907
|
+
&input.query,
|
|
4908
|
+
read_policy,
|
|
4909
|
+
)
|
|
4910
|
+
.map_err(state_contract_error)?;
|
|
4910
4911
|
audit(
|
|
4911
4912
|
&state,
|
|
4912
4913
|
&principal.key_id,
|
|
@@ -4917,6 +4918,156 @@ async fn execute_canonical_query(
|
|
|
4917
4918
|
);
|
|
4918
4919
|
Ok(Json(json!(result)))
|
|
4919
4920
|
}
|
|
4921
|
+
|
|
4922
|
+
#[derive(serde::Deserialize)]
|
|
4923
|
+
struct RevisionRecoveryRequest {
|
|
4924
|
+
#[serde(rename = "applicationId")]
|
|
4925
|
+
application_id: String,
|
|
4926
|
+
#[serde(rename = "targetRevision")]
|
|
4927
|
+
target_revision: String,
|
|
4928
|
+
#[serde(rename = "expectedCurrentRevision")]
|
|
4929
|
+
expected_current_revision: String,
|
|
4930
|
+
authorization: String,
|
|
4931
|
+
actor: String,
|
|
4932
|
+
reason: String,
|
|
4933
|
+
#[serde(default)]
|
|
4934
|
+
environment: Option<String>,
|
|
4935
|
+
#[serde(default)]
|
|
4936
|
+
#[serde(rename = "recoveryId")]
|
|
4937
|
+
recovery_id: Option<String>,
|
|
4938
|
+
}
|
|
4939
|
+
|
|
4940
|
+
#[derive(serde::Serialize, serde::Deserialize)]
|
|
4941
|
+
struct RevisionRecoveryResponse {
|
|
4942
|
+
success: bool,
|
|
4943
|
+
#[serde(rename = "pointerMoved")]
|
|
4944
|
+
pointer_moved: bool,
|
|
4945
|
+
#[serde(rename = "sourceMarkedUntrusted")]
|
|
4946
|
+
source_marked_untrusted: bool,
|
|
4947
|
+
#[serde(rename = "auditDurable")]
|
|
4948
|
+
audit_durable: bool,
|
|
4949
|
+
#[serde(rename = "recoveryId")]
|
|
4950
|
+
recovery_id: String,
|
|
4951
|
+
#[serde(rename = "targetRevision")]
|
|
4952
|
+
target_revision: String,
|
|
4953
|
+
#[serde(rename = "currentRevision")]
|
|
4954
|
+
current_revision: String,
|
|
4955
|
+
audit: serde_json::Value,
|
|
4956
|
+
}
|
|
4957
|
+
|
|
4958
|
+
fn revision_recovery_error(
|
|
4959
|
+
status: StatusCode,
|
|
4960
|
+
code: &str,
|
|
4961
|
+
message: String,
|
|
4962
|
+
request_id: &str,
|
|
4963
|
+
transaction_id: &str,
|
|
4964
|
+
) -> ApiError {
|
|
4965
|
+
ApiError(
|
|
4966
|
+
status,
|
|
4967
|
+
json!({"code":code,"message":message,"request_id":request_id,
|
|
4968
|
+
"transaction_id":transaction_id,"http_status":status.as_u16()})
|
|
4969
|
+
.to_string(),
|
|
4970
|
+
)
|
|
4971
|
+
}
|
|
4972
|
+
|
|
4973
|
+
async fn execute_revision_recovery(
|
|
4974
|
+
State(state): State<AppState>,
|
|
4975
|
+
Extension(principal): Extension<Principal>,
|
|
4976
|
+
Json(input): Json<RevisionRecoveryRequest>,
|
|
4977
|
+
) -> Result<Json<RevisionRecoveryResponse>, ApiError> {
|
|
4978
|
+
let request_id = uuid::Uuid::new_v4().to_string();
|
|
4979
|
+
let transaction_id = input
|
|
4980
|
+
.recovery_id
|
|
4981
|
+
.clone()
|
|
4982
|
+
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
|
|
4983
|
+
|
|
4984
|
+
// Validate authorization level
|
|
4985
|
+
let auth_level = input.authorization.as_str();
|
|
4986
|
+
|
|
4987
|
+
if !["ELEVATED", "ADMIN", "EMERGENCY"].contains(&auth_level) {
|
|
4988
|
+
return Err(revision_recovery_error(
|
|
4989
|
+
StatusCode::FORBIDDEN,
|
|
4990
|
+
"PERMISSION_DENIED",
|
|
4991
|
+
"Recovery requires ELEVATED, ADMIN, or EMERGENCY authorization".into(),
|
|
4992
|
+
&request_id,
|
|
4993
|
+
&transaction_id,
|
|
4994
|
+
));
|
|
4995
|
+
}
|
|
4996
|
+
|
|
4997
|
+
// Validate input
|
|
4998
|
+
if input.reason.len() < 10 {
|
|
4999
|
+
return Err(revision_recovery_error(
|
|
5000
|
+
StatusCode::UNPROCESSABLE_ENTITY,
|
|
5001
|
+
"PRECONDITION_FAILED",
|
|
5002
|
+
"Recovery reason must be at least 10 characters".into(),
|
|
5003
|
+
&request_id,
|
|
5004
|
+
&transaction_id,
|
|
5005
|
+
));
|
|
5006
|
+
}
|
|
5007
|
+
|
|
5008
|
+
let recovery_id = transaction_id.clone();
|
|
5009
|
+
let environment = input
|
|
5010
|
+
.environment
|
|
5011
|
+
.unwrap_or_else(|| "production".to_string());
|
|
5012
|
+
let tenant = application_scope(
|
|
5013
|
+
&state,
|
|
5014
|
+
&principal.key_id,
|
|
5015
|
+
&input.application_id,
|
|
5016
|
+
"application:revision:promote",
|
|
5017
|
+
)?;
|
|
5018
|
+
let recovery = state
|
|
5019
|
+
.applications
|
|
5020
|
+
.recover_environment_pointer(
|
|
5021
|
+
&tenant,
|
|
5022
|
+
&input.application_id,
|
|
5023
|
+
&environment,
|
|
5024
|
+
&input.expected_current_revision,
|
|
5025
|
+
&input.target_revision,
|
|
5026
|
+
&input.actor,
|
|
5027
|
+
&input.reason,
|
|
5028
|
+
auth_level,
|
|
5029
|
+
&recovery_id,
|
|
5030
|
+
)
|
|
5031
|
+
.map_err(|message| {
|
|
5032
|
+
let (status, code) = if message.starts_with("expected_revision_mismatch")
|
|
5033
|
+
|| message == "recovery_id_conflict"
|
|
5034
|
+
{
|
|
5035
|
+
(StatusCode::CONFLICT, "CONFLICT")
|
|
5036
|
+
} else if message.contains("not found") {
|
|
5037
|
+
(StatusCode::NOT_FOUND, "PRECONDITION_FAILED")
|
|
5038
|
+
} else if message.contains("integrity") || message.contains("untrusted") {
|
|
5039
|
+
(StatusCode::UNPROCESSABLE_ENTITY, "VALIDATION_FAILED")
|
|
5040
|
+
} else {
|
|
5041
|
+
(StatusCode::SERVICE_UNAVAILABLE, "STORAGE_FAILURE")
|
|
5042
|
+
};
|
|
5043
|
+
revision_recovery_error(status, code, message, &request_id, &transaction_id)
|
|
5044
|
+
})?;
|
|
5045
|
+
|
|
5046
|
+
// Build response
|
|
5047
|
+
let response = RevisionRecoveryResponse {
|
|
5048
|
+
success: true,
|
|
5049
|
+
pointer_moved: true,
|
|
5050
|
+
source_marked_untrusted: true,
|
|
5051
|
+
audit_durable: true,
|
|
5052
|
+
recovery_id: recovery_id.clone(),
|
|
5053
|
+
target_revision: recovery.target_revision.clone(),
|
|
5054
|
+
current_revision: recovery.target_revision.clone(),
|
|
5055
|
+
audit: json!(recovery),
|
|
5056
|
+
};
|
|
5057
|
+
|
|
5058
|
+
// Audit log
|
|
5059
|
+
audit(
|
|
5060
|
+
&state,
|
|
5061
|
+
&principal.key_id,
|
|
5062
|
+
"revision.recover",
|
|
5063
|
+
&recovery_id,
|
|
5064
|
+
"allowed",
|
|
5065
|
+
200,
|
|
5066
|
+
);
|
|
5067
|
+
|
|
5068
|
+
Ok(Json(response))
|
|
5069
|
+
}
|
|
5070
|
+
|
|
4920
5071
|
async fn execute_canonical_transaction(
|
|
4921
5072
|
State(state): State<AppState>,
|
|
4922
5073
|
Extension(principal): Extension<Principal>,
|
|
@@ -4942,24 +5093,27 @@ async fn execute_canonical_transaction(
|
|
|
4942
5093
|
input.transaction.state_namespace = Some(contract.environment.state_namespace.clone());
|
|
4943
5094
|
// For transactions with operations, use the first operation's collection for policy evaluation
|
|
4944
5095
|
// Multi-collection transactions will still be checked at the operation level in execute_transaction
|
|
4945
|
-
let collection = input
|
|
5096
|
+
let collection = input
|
|
5097
|
+
.transaction
|
|
5098
|
+
.operations
|
|
4946
5099
|
.first()
|
|
4947
5100
|
.map(|op| op.collection.clone())
|
|
4948
5101
|
.unwrap_or_default();
|
|
4949
|
-
input.transaction.authorization =
|
|
4950
|
-
&principal,
|
|
4951
|
-
&contract,
|
|
4952
|
-
&collection,
|
|
4953
|
-
"write",
|
|
4954
|
-
);
|
|
5102
|
+
input.transaction.authorization =
|
|
5103
|
+
state_authorization(&principal, &contract, &collection, "write");
|
|
4955
5104
|
let write_policy = contract
|
|
4956
5105
|
.policies
|
|
4957
5106
|
.definitions
|
|
4958
5107
|
.iter()
|
|
4959
5108
|
.find(|p| p.resource == collection)
|
|
4960
5109
|
.and_then(|p| p.write.as_ref().and_then(|s| PolicySubject::from_str(s)));
|
|
4961
|
-
let result = execute_transaction(
|
|
4962
|
-
.
|
|
5110
|
+
let result = execute_transaction(
|
|
5111
|
+
&state.db,
|
|
5112
|
+
&contract.state.schema,
|
|
5113
|
+
&input.transaction,
|
|
5114
|
+
write_policy,
|
|
5115
|
+
)
|
|
5116
|
+
.map_err(state_contract_error)?;
|
|
4963
5117
|
audit(
|
|
4964
5118
|
&state,
|
|
4965
5119
|
&principal.key_id,
|
|
@@ -4975,13 +5129,19 @@ async fn get_cardinality_diagnostic(
|
|
|
4975
5129
|
State(state): State<AppState>,
|
|
4976
5130
|
Path(pattern): Path<String>,
|
|
4977
5131
|
) -> Result<Json<CardinalityDiagnosticResponse>, ApiError> {
|
|
4978
|
-
let all_cardinalities = state
|
|
5132
|
+
let all_cardinalities = state
|
|
5133
|
+
.db
|
|
5134
|
+
.list_cardinalities()
|
|
4979
5135
|
.map_err(|e| ApiError(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
4980
5136
|
|
|
4981
|
-
let actual_rows = state
|
|
5137
|
+
let actual_rows = state
|
|
5138
|
+
.db
|
|
5139
|
+
.diagnostic_row_count(&pattern)
|
|
4982
5140
|
.map_err(|e| ApiError(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
4983
5141
|
|
|
4984
|
-
let persisted_capability_keys = state
|
|
5142
|
+
let persisted_capability_keys = state
|
|
5143
|
+
.db
|
|
5144
|
+
.diagnostic_capability_keys(&pattern)
|
|
4985
5145
|
.map_err(|e| ApiError(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
|
4986
5146
|
|
|
4987
5147
|
let matching_keys: Vec<_> = all_cardinalities
|
|
@@ -4992,10 +5152,7 @@ async fn get_cardinality_diagnostic(
|
|
|
4992
5152
|
|
|
4993
5153
|
let cardinality_map_keys: Vec<String> = matching_keys.iter().map(|(k, _)| k.clone()).collect();
|
|
4994
5154
|
|
|
4995
|
-
let maintained = matching_keys
|
|
4996
|
-
.iter()
|
|
4997
|
-
.map(|(_, v)| v)
|
|
4998
|
-
.sum::<u64>();
|
|
5155
|
+
let maintained = matching_keys.iter().map(|(_, v)| v).sum::<u64>();
|
|
4999
5156
|
|
|
5000
5157
|
let now_ms = std::time::SystemTime::now()
|
|
5001
5158
|
.duration_since(std::time::UNIX_EPOCH)
|
|
@@ -5032,6 +5189,19 @@ struct CreateRecord {
|
|
|
5032
5189
|
value: Map<String, Value>,
|
|
5033
5190
|
}
|
|
5034
5191
|
|
|
5192
|
+
#[derive(Deserialize)]
|
|
5193
|
+
#[serde(rename_all = "camelCase")]
|
|
5194
|
+
struct CasRecordRequest {
|
|
5195
|
+
expected_version: u64,
|
|
5196
|
+
#[serde(default)]
|
|
5197
|
+
expected_epoch: Option<u64>,
|
|
5198
|
+
#[serde(default)]
|
|
5199
|
+
expected_lease_id: Option<String>,
|
|
5200
|
+
#[serde(default)]
|
|
5201
|
+
reject_if_diverged: bool,
|
|
5202
|
+
value: Value,
|
|
5203
|
+
}
|
|
5204
|
+
|
|
5035
5205
|
#[derive(Serialize, Deserialize)]
|
|
5036
5206
|
struct SyncPullRequest {
|
|
5037
5207
|
#[serde(default)]
|
|
@@ -5752,6 +5922,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
5752
5922
|
"/v1/transactions",
|
|
5753
5923
|
axum::routing::post(execute_canonical_transaction),
|
|
5754
5924
|
)
|
|
5925
|
+
.route(
|
|
5926
|
+
"/v1/revision/recover",
|
|
5927
|
+
axum::routing::post(execute_revision_recovery),
|
|
5928
|
+
)
|
|
5755
5929
|
.route(
|
|
5756
5930
|
"/debug/cardinality/{pattern}",
|
|
5757
5931
|
get(get_cardinality_diagnostic),
|
|
@@ -6031,6 +6205,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
6031
6205
|
"/collections/{collection}/{id}",
|
|
6032
6206
|
get(get_record).patch(update_record).delete(delete_record),
|
|
6033
6207
|
)
|
|
6208
|
+
.route(
|
|
6209
|
+
"/collections/{collection}/{id}/cas",
|
|
6210
|
+
axum::routing::post(compare_and_set_record),
|
|
6211
|
+
)
|
|
6034
6212
|
.route("/events", get(events))
|
|
6035
6213
|
.route("/sync/pull", axum::routing::post(sync_pull))
|
|
6036
6214
|
.route("/sync/push", axum::routing::post(sync_push))
|
|
@@ -8730,6 +8908,96 @@ async fn update_record(
|
|
|
8730
8908
|
}))
|
|
8731
8909
|
}
|
|
8732
8910
|
|
|
8911
|
+
async fn compare_and_set_record(
|
|
8912
|
+
State(state): State<AppState>,
|
|
8913
|
+
Path((collection, id)): Path<(String, String)>,
|
|
8914
|
+
Json(request): Json<CasRecordRequest>,
|
|
8915
|
+
) -> Result<Response, ApiError> {
|
|
8916
|
+
let key = record_key(&collection, &id)?;
|
|
8917
|
+
let result = state.db.compare_and_set_json(
|
|
8918
|
+
&key,
|
|
8919
|
+
request.expected_version,
|
|
8920
|
+
request.expected_epoch,
|
|
8921
|
+
request.expected_lease_id.as_deref(),
|
|
8922
|
+
request.reject_if_diverged,
|
|
8923
|
+
request.value,
|
|
8924
|
+
)?;
|
|
8925
|
+
let response = match result {
|
|
8926
|
+
JsonCasResult::Updated {
|
|
8927
|
+
current_version,
|
|
8928
|
+
current_epoch,
|
|
8929
|
+
value,
|
|
8930
|
+
} => (
|
|
8931
|
+
StatusCode::OK,
|
|
8932
|
+
Json(json!({
|
|
8933
|
+
"updated": true,
|
|
8934
|
+
"currentVersion": current_version,
|
|
8935
|
+
"currentEpoch": current_epoch,
|
|
8936
|
+
"item": value,
|
|
8937
|
+
})),
|
|
8938
|
+
)
|
|
8939
|
+
.into_response(),
|
|
8940
|
+
JsonCasResult::VersionConflict {
|
|
8941
|
+
current_version,
|
|
8942
|
+
current_epoch,
|
|
8943
|
+
} => (
|
|
8944
|
+
StatusCode::CONFLICT,
|
|
8945
|
+
Json(json!({
|
|
8946
|
+
"updated": false,
|
|
8947
|
+
"code": "VERSION_CONFLICT",
|
|
8948
|
+
"currentVersion": current_version,
|
|
8949
|
+
"currentEpoch": current_epoch,
|
|
8950
|
+
})),
|
|
8951
|
+
)
|
|
8952
|
+
.into_response(),
|
|
8953
|
+
JsonCasResult::AuthorityConflict {
|
|
8954
|
+
current_version,
|
|
8955
|
+
current_epoch,
|
|
8956
|
+
} => (
|
|
8957
|
+
StatusCode::CONFLICT,
|
|
8958
|
+
Json(json!({
|
|
8959
|
+
"updated": false,
|
|
8960
|
+
"code": "AUTHORITY_CONFLICT",
|
|
8961
|
+
"currentVersion": current_version,
|
|
8962
|
+
"currentEpoch": current_epoch,
|
|
8963
|
+
})),
|
|
8964
|
+
)
|
|
8965
|
+
.into_response(),
|
|
8966
|
+
JsonCasResult::LeaseConflict {
|
|
8967
|
+
current_version,
|
|
8968
|
+
current_epoch,
|
|
8969
|
+
} => (
|
|
8970
|
+
StatusCode::CONFLICT,
|
|
8971
|
+
Json(json!({
|
|
8972
|
+
"updated": false,
|
|
8973
|
+
"code": "LEASE_CONFLICT",
|
|
8974
|
+
"currentVersion": current_version,
|
|
8975
|
+
"currentEpoch": current_epoch,
|
|
8976
|
+
})),
|
|
8977
|
+
)
|
|
8978
|
+
.into_response(),
|
|
8979
|
+
JsonCasResult::ReplicationConflict {
|
|
8980
|
+
current_version,
|
|
8981
|
+
current_epoch,
|
|
8982
|
+
} => (
|
|
8983
|
+
StatusCode::CONFLICT,
|
|
8984
|
+
Json(json!({
|
|
8985
|
+
"updated": false,
|
|
8986
|
+
"code": "REPLICATION_CONFLICT",
|
|
8987
|
+
"currentVersion": current_version,
|
|
8988
|
+
"currentEpoch": current_epoch,
|
|
8989
|
+
})),
|
|
8990
|
+
)
|
|
8991
|
+
.into_response(),
|
|
8992
|
+
JsonCasResult::NotFound => (
|
|
8993
|
+
StatusCode::NOT_FOUND,
|
|
8994
|
+
Json(json!({ "updated": false, "code": "NOT_FOUND" })),
|
|
8995
|
+
)
|
|
8996
|
+
.into_response(),
|
|
8997
|
+
};
|
|
8998
|
+
Ok(response)
|
|
8999
|
+
}
|
|
9000
|
+
|
|
8733
9001
|
async fn delete_record(
|
|
8734
9002
|
State(state): State<AppState>,
|
|
8735
9003
|
Path((collection, id)): Path<(String, String)>,
|