@@ -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
@@ -474,6 +479,13 @@ pub struct PublishDatabaseQueryParams {
474
479
#[ serde( default ) ]
475
480
clear : bool ,
476
481
num_replicas : Option < usize > ,
482
+ /// [`Hash`] of [`MigrationToken`]` to be checked if `MigrationPolicy::BreakClients` is set.
483
+ ///
484
+ /// Users obtain such a hash via the `/database/:name_or_identity/pre-publish POST` route.
485
+ /// This is a safeguard to require explicit approval for updates which will break clients.
486
+ token : Option < spacetimedb_lib:: Hash > ,
487
+ #[ serde( default ) ]
488
+ policy : MigrationPolicy ,
477
489
}
478
490
479
491
use std:: env;
@@ -501,7 +513,12 @@ fn allow_creation(auth: &SpacetimeAuth) -> Result<(), ErrorResponse> {
501
513
pub async fn publish < S : NodeDelegate + ControlStateDelegate > (
502
514
State ( ctx) : State < S > ,
503
515
Path ( PublishDatabaseParams { name_or_identity } ) : Path < PublishDatabaseParams > ,
504
- Query ( PublishDatabaseQueryParams { clear, num_replicas } ) : Query < PublishDatabaseQueryParams > ,
516
+ Query ( PublishDatabaseQueryParams {
517
+ clear,
518
+ num_replicas,
519
+ token,
520
+ policy,
521
+ } ) : Query < PublishDatabaseQueryParams > ,
505
522
Extension ( auth) : Extension < SpacetimeAuth > ,
506
523
body : Bytes ,
507
524
) -> axum:: response:: Result < axum:: Json < PublishResult > > {
@@ -551,6 +568,21 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate>(
551
568
}
552
569
} ;
553
570
571
+ let policy: SchemaMigrationPolicy = match policy {
572
+ MigrationPolicy :: BreakClients => {
573
+ if let Some ( token) = token {
574
+ Ok ( SchemaMigrationPolicy :: BreakClients ( token) )
575
+ } else {
576
+ Err ( (
577
+ StatusCode :: BAD_REQUEST ,
578
+ "Migration policy is set to `BreakClients`, but no migration token was provided." ,
579
+ ) )
580
+ }
581
+ }
582
+
583
+ MigrationPolicy :: Compatible => Ok ( SchemaMigrationPolicy :: Compatible ) ,
584
+ } ?;
585
+
554
586
log:: trace!( "Publishing to the identity: {}" , database_identity. to_hex( ) ) ;
555
587
556
588
let op = {
@@ -592,6 +624,7 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate>(
592
624
num_replicas,
593
625
host_type : HostType :: Wasm ,
594
626
} ,
627
+ policy,
595
628
)
596
629
. await
597
630
. map_err ( log_and_500) ?;
@@ -619,6 +652,101 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate>(
619
652
} ) )
620
653
}
621
654
655
+ #[ derive( serde:: Deserialize ) ]
656
+ pub struct PrePublishParams {
657
+ name_or_identity : NameOrIdentity ,
658
+ }
659
+
660
+ #[ derive( serde:: Deserialize ) ]
661
+ pub struct PrePublishQueryParams {
662
+ #[ serde( default ) ]
663
+ style : PrettyPrintStyle ,
664
+ }
665
+
666
+ pub async fn pre_publish < S : NodeDelegate + ControlStateDelegate > (
667
+ State ( ctx) : State < S > ,
668
+ Path ( PrePublishParams { name_or_identity } ) : Path < PrePublishParams > ,
669
+ Query ( PrePublishQueryParams { style } ) : Query < PrePublishQueryParams > ,
670
+ Extension ( auth) : Extension < SpacetimeAuth > ,
671
+ body : Bytes ,
672
+ ) -> axum:: response:: Result < axum:: Json < PrintPlanResult > > {
673
+ // User should not be able to print migration plans for a database that they do not own
674
+ let database_identity = resolve_and_authenticate ( & ctx, & name_or_identity, & auth) . await ?;
675
+ let style = match style {
676
+ PrettyPrintStyle :: NoColor => AutoMigratePrettyPrintStyle :: NoColor ,
677
+ PrettyPrintStyle :: AnsiColor => AutoMigratePrettyPrintStyle :: AnsiColor ,
678
+ } ;
679
+
680
+ let migrate_plan = ctx
681
+ . migrate_plan (
682
+ DatabaseDef {
683
+ database_identity,
684
+ program_bytes : body. into ( ) ,
685
+ num_replicas : None ,
686
+ host_type : HostType :: Wasm ,
687
+ } ,
688
+ style,
689
+ )
690
+ . await
691
+ . map_err ( log_and_500) ?;
692
+
693
+ match migrate_plan {
694
+ MigratePlanResult :: Success {
695
+ old_module_hash,
696
+ new_module_hash,
697
+ breaks_client,
698
+ plan,
699
+ } => {
700
+ let token = MigrationToken {
701
+ database_identity,
702
+ old_module_hash,
703
+ new_module_hash,
704
+ }
705
+ . hash ( ) ;
706
+
707
+ Ok ( PrintPlanResult {
708
+ token,
709
+ migrate_plan : plan,
710
+ break_clients : breaks_client,
711
+ } )
712
+ }
713
+ MigratePlanResult :: AutoMigrationError ( e) => Err ( (
714
+ StatusCode :: BAD_REQUEST ,
715
+ format ! ( "Automatic migration is not possible: {e}" ) ,
716
+ )
717
+ . into ( ) ) ,
718
+ }
719
+ . map ( axum:: Json )
720
+ }
721
+
722
+ /// Resolves the [`NameOrIdentity`] to a database identity and checks if the
723
+ /// `auth` identity owns the database.
724
+ async fn resolve_and_authenticate < S : ControlStateDelegate > (
725
+ ctx : & S ,
726
+ name_or_identity : & NameOrIdentity ,
727
+ auth : & SpacetimeAuth ,
728
+ ) -> axum:: response:: Result < Identity > {
729
+ let database_identity = name_or_identity. resolve ( ctx) . await ?;
730
+
731
+ let database = worker_ctx_find_database ( ctx, & database_identity)
732
+ . await ?
733
+ . ok_or ( NO_SUCH_DATABASE ) ?;
734
+
735
+ if database. owner_identity != auth. identity {
736
+ return Err ( (
737
+ StatusCode :: UNAUTHORIZED ,
738
+ format ! (
739
+ "Identity does not own database, expected: {} got: {}" ,
740
+ database. owner_identity. to_hex( ) ,
741
+ auth. identity. to_hex( )
742
+ ) ,
743
+ )
744
+ . into ( ) ) ;
745
+ }
746
+
747
+ Ok ( database_identity)
748
+ }
749
+
622
750
#[ derive( Deserialize ) ]
623
751
pub struct DeleteDatabaseParams {
624
752
name_or_identity : NameOrIdentity ,
@@ -788,7 +916,8 @@ pub struct DatabaseRoutes<S> {
788
916
pub logs_get : MethodRouter < S > ,
789
917
/// POST: /database/:name_or_identity/sql
790
918
pub sql_post : MethodRouter < S > ,
791
-
919
+ /// POST: /database/:name_or_identity/pre-publish
920
+ pub pre_publish : MethodRouter < S > ,
792
921
/// GET: /database/: name_or_identity/unstable/timestamp
793
922
pub timestamp_get : MethodRouter < S > ,
794
923
}
@@ -813,6 +942,7 @@ where
813
942
schema_get : get ( schema :: < S > ) ,
814
943
logs_get : get ( logs :: < S > ) ,
815
944
sql_post : post ( sql :: < S > ) ,
945
+ pre_publish : post ( pre_publish :: < S > ) ,
816
946
timestamp_get : get ( get_timestamp :: < S > ) ,
817
947
}
818
948
}
@@ -836,7 +966,8 @@ where
836
966
. route ( "/schema" , self . schema_get )
837
967
. route ( "/logs" , self . logs_get )
838
968
. route ( "/sql" , self . sql_post )
839
- . route ( "/unstable/timestamp" , self . timestamp_get ) ;
969
+ . route ( "/unstable/timestamp" , self . timestamp_get )
970
+ . route ( "/pre-publish" , self . pre_publish ) ;
840
971
841
972
axum:: Router :: new ( )
842
973
. route ( "/" , self . root_post )
0 commit comments