@@ -20,16 +20,21 @@ use http::StatusCode;
20
20
use serde:: Deserialize ;
21
21
use spacetimedb:: database_logger:: DatabaseLogger ;
22
22
use spacetimedb:: host:: module_host:: ClientConnectedError ;
23
- use spacetimedb:: host:: ReducerArgs ;
24
23
use spacetimedb:: host:: ReducerCallError ;
25
24
use spacetimedb:: host:: ReducerOutcome ;
26
25
use spacetimedb:: host:: UpdateDatabaseResult ;
26
+ use spacetimedb:: host:: { MigratePlanResult , ReducerArgs } ;
27
27
use spacetimedb:: identity:: Identity ;
28
28
use spacetimedb:: messages:: control_db:: { Database , HostType } ;
29
- use spacetimedb_client_api_messages:: name:: { self , DatabaseName , DomainName , PublishOp , PublishResult } ;
29
+ use spacetimedb_client_api_messages:: name:: {
30
+ self , DatabaseName , DomainName , MigrationPolicy , PrettyPrintStyle , PrintPlanResult , PublishOp , PublishResult ,
31
+ } ;
30
32
use spacetimedb_lib:: db:: raw_def:: v9:: RawModuleDefV9 ;
31
33
use spacetimedb_lib:: identity:: AuthCtx ;
32
34
use spacetimedb_lib:: { sats, Timestamp } ;
35
+ use spacetimedb_schema:: auto_migrate:: {
36
+ MigrationPolicy as SchemaMigrationPolicy , MigrationToken , PrettyPrintStyle as AutoMigratePrettyPrintStyle ,
37
+ } ;
33
38
34
39
use super :: subscribe:: { handle_websocket, HasWebSocketOptions } ;
35
40
@@ -469,6 +474,9 @@ pub struct PublishDatabaseQueryParams {
469
474
#[ serde( default ) ]
470
475
clear : bool ,
471
476
num_replicas : Option < usize > ,
477
+ // `Hash` of `MigrationToken` to be checked if `MigrationPolicy::BreakClients` is set.
478
+ token : Option < spacetimedb_lib:: Hash > ,
479
+ policy : Option < MigrationPolicy > ,
472
480
}
473
481
474
482
use std:: env;
@@ -496,7 +504,12 @@ fn allow_creation(auth: &SpacetimeAuth) -> Result<(), ErrorResponse> {
496
504
pub async fn publish < S : NodeDelegate + ControlStateDelegate > (
497
505
State ( ctx) : State < S > ,
498
506
Path ( PublishDatabaseParams { name_or_identity } ) : Path < PublishDatabaseParams > ,
499
- Query ( PublishDatabaseQueryParams { clear, num_replicas } ) : Query < PublishDatabaseQueryParams > ,
507
+ Query ( PublishDatabaseQueryParams {
508
+ clear,
509
+ num_replicas,
510
+ token,
511
+ policy,
512
+ } ) : Query < PublishDatabaseQueryParams > ,
500
513
Extension ( auth) : Extension < SpacetimeAuth > ,
501
514
body : Bytes ,
502
515
) -> axum:: response:: Result < axum:: Json < PublishResult > > {
@@ -546,6 +559,21 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate>(
546
559
}
547
560
} ;
548
561
562
+ let policy: SchemaMigrationPolicy = match policy. unwrap_or ( MigrationPolicy :: Compatible ) {
563
+ MigrationPolicy :: BreakClients => {
564
+ if let Some ( token) = token {
565
+ Ok ( SchemaMigrationPolicy :: BreakClients ( token) )
566
+ } else {
567
+ Err ( (
568
+ StatusCode :: BAD_REQUEST ,
569
+ "Migration policy is set to `BreakClients`, but no migration token was provided." ,
570
+ ) )
571
+ }
572
+ }
573
+
574
+ MigrationPolicy :: Compatible => Ok ( SchemaMigrationPolicy :: Compatible ) ,
575
+ } ?;
576
+
549
577
log:: trace!( "Publishing to the identity: {}" , database_identity. to_hex( ) ) ;
550
578
551
579
let op = {
@@ -587,6 +615,7 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate>(
587
615
num_replicas,
588
616
host_type : HostType :: Wasm ,
589
617
} ,
618
+ policy,
590
619
)
591
620
. await
592
621
. map_err ( log_and_500) ?;
@@ -614,6 +643,102 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate>(
614
643
} ) )
615
644
}
616
645
646
+ #[ derive( serde:: Deserialize ) ]
647
+ pub struct PrintPlanParams {
648
+ name_or_identity : NameOrIdentity ,
649
+ }
650
+
651
+ #[ derive( serde:: Deserialize ) ]
652
+ pub struct PrintPlanQueryParams {
653
+ style : Option < PrettyPrintStyle > ,
654
+ }
655
+
656
+ pub async fn print_migration_plan < S : NodeDelegate + ControlStateDelegate > (
657
+ State ( ctx) : State < S > ,
658
+ Path ( PrintPlanParams { name_or_identity } ) : Path < PrintPlanParams > ,
659
+ Query ( PrintPlanQueryParams { style } ) : Query < PrintPlanQueryParams > ,
660
+ Extension ( auth) : Extension < SpacetimeAuth > ,
661
+ body : Bytes ,
662
+ ) -> axum:: response:: Result < axum:: Json < PrintPlanResult > > {
663
+ // User should not be able to print migration plans for a database that they do not own
664
+ let database_identity = resolve_and_authenticate ( & ctx, & name_or_identity, & auth) . await ?;
665
+ let style = style
666
+ . map ( |s| match s {
667
+ PrettyPrintStyle :: NoColor => AutoMigratePrettyPrintStyle :: NoColor ,
668
+ PrettyPrintStyle :: AnsiColor => AutoMigratePrettyPrintStyle :: AnsiColor ,
669
+ } )
670
+ . unwrap_or_default ( ) ;
671
+
672
+ let migrate_plan = ctx
673
+ . migrate_plan (
674
+ DatabaseDef {
675
+ database_identity : database_identity. clone ( ) ,
676
+ program_bytes : body. into ( ) ,
677
+ num_replicas : None ,
678
+ host_type : HostType :: Wasm ,
679
+ } ,
680
+ style,
681
+ )
682
+ . await
683
+ . map_err ( log_and_500) ?;
684
+
685
+ match migrate_plan {
686
+ MigratePlanResult :: Success {
687
+ old_module_hash,
688
+ new_module_hash,
689
+ breaks_client,
690
+ plan,
691
+ } => {
692
+ let token = MigrationToken {
693
+ database_identity,
694
+ old_module_hash,
695
+ new_module_hash,
696
+ }
697
+ . hash ( ) ;
698
+
699
+ Ok ( PrintPlanResult {
700
+ token,
701
+ migrate_plan : plan,
702
+ break_clients : breaks_client,
703
+ } )
704
+ }
705
+ MigratePlanResult :: AutoMigrationError ( e) => Err ( (
706
+ StatusCode :: BAD_REQUEST ,
707
+ format ! ( "Automatic migration is not possible: {e}" ) ,
708
+ )
709
+ . into ( ) ) ,
710
+ }
711
+ . map ( axum:: Json )
712
+ }
713
+
714
+ /// Resolves the `NameOrIdentity` to a database identity and checks if the
715
+ /// `auth` identity owns the database.
716
+ async fn resolve_and_authenticate < S : ControlStateDelegate > (
717
+ ctx : & S ,
718
+ name_or_identity : & NameOrIdentity ,
719
+ auth : & SpacetimeAuth ,
720
+ ) -> axum:: response:: Result < Identity > {
721
+ let database_identity = name_or_identity. resolve ( ctx) . await ?;
722
+
723
+ let database = worker_ctx_find_database ( ctx, & database_identity)
724
+ . await ?
725
+ . ok_or ( NO_SUCH_DATABASE ) ?;
726
+
727
+ if database. owner_identity != auth. identity {
728
+ return Err ( (
729
+ StatusCode :: BAD_REQUEST ,
730
+ format ! (
731
+ "Identity does not own database, expected: {} got: {}" ,
732
+ database. owner_identity. to_hex( ) ,
733
+ auth. identity. to_hex( )
734
+ ) ,
735
+ )
736
+ . into ( ) ) ;
737
+ }
738
+
739
+ return Ok ( database_identity) ;
740
+ }
741
+
617
742
#[ derive( Deserialize ) ]
618
743
pub struct DeleteDatabaseParams {
619
744
name_or_identity : NameOrIdentity ,
@@ -783,7 +908,8 @@ pub struct DatabaseRoutes<S> {
783
908
pub logs_get : MethodRouter < S > ,
784
909
/// POST: /database/:name_or_identity/sql
785
910
pub sql_post : MethodRouter < S > ,
786
-
911
+ /// POST: /database/print-plan/:name_or_identity/sql
912
+ pub print_migration_plan : MethodRouter < S > ,
787
913
/// GET: /database/: name_or_identity/unstable/timestamp
788
914
pub timestamp_get : MethodRouter < S > ,
789
915
}
@@ -808,6 +934,7 @@ where
808
934
schema_get : get ( schema :: < S > ) ,
809
935
logs_get : get ( logs :: < S > ) ,
810
936
sql_post : post ( sql :: < S > ) ,
937
+ print_migration_plan : post ( print_migration_plan :: < S > ) ,
811
938
timestamp_get : get ( get_timestamp :: < S > ) ,
812
939
}
813
940
}
@@ -835,6 +962,7 @@ where
835
962
836
963
axum:: Router :: new ( )
837
964
. route ( "/" , self . root_post )
965
+ . route ( "/print-plan/:name_or_identity" , self . print_migration_plan )
838
966
. nest ( "/:name_or_identity" , db_router)
839
967
. route_layer ( axum:: middleware:: from_fn_with_state ( ctx, anon_auth_middleware :: < S > ) )
840
968
}
0 commit comments