@@ -20,16 +20,21 @@ use http::StatusCode;
2020use serde:: Deserialize ;
2121use spacetimedb:: database_logger:: DatabaseLogger ;
2222use spacetimedb:: host:: module_host:: ClientConnectedError ;
23- use spacetimedb:: host:: ReducerArgs ;
2423use spacetimedb:: host:: ReducerCallError ;
2524use spacetimedb:: host:: ReducerOutcome ;
2625use spacetimedb:: host:: UpdateDatabaseResult ;
26+ use spacetimedb:: host:: { MigratePlanResult , ReducerArgs } ;
2727use spacetimedb:: identity:: Identity ;
2828use 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+ } ;
3032use spacetimedb_lib:: db:: raw_def:: v9:: RawModuleDefV9 ;
3133use spacetimedb_lib:: identity:: AuthCtx ;
3234use spacetimedb_lib:: { sats, Timestamp } ;
35+ use spacetimedb_schema:: auto_migrate:: {
36+ MigrationPolicy as SchemaMigrationPolicy , MigrationToken , PrettyPrintStyle as AutoMigratePrettyPrintStyle ,
37+ } ;
3338
3439use super :: subscribe:: { handle_websocket, HasWebSocketOptions } ;
3540
@@ -474,6 +479,13 @@ pub struct PublishDatabaseQueryParams {
474479 #[ serde( default ) ]
475480 clear : bool ,
476481 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 ,
477489}
478490
479491use std:: env;
@@ -501,7 +513,12 @@ fn allow_creation(auth: &SpacetimeAuth) -> Result<(), ErrorResponse> {
501513pub async fn publish < S : NodeDelegate + ControlStateDelegate > (
502514 State ( ctx) : State < S > ,
503515 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 > ,
505522 Extension ( auth) : Extension < SpacetimeAuth > ,
506523 body : Bytes ,
507524) -> axum:: response:: Result < axum:: Json < PublishResult > > {
@@ -551,6 +568,21 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate>(
551568 }
552569 } ;
553570
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+
554586 log:: trace!( "Publishing to the identity: {}" , database_identity. to_hex( ) ) ;
555587
556588 let op = {
@@ -592,6 +624,7 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate>(
592624 num_replicas,
593625 host_type : HostType :: Wasm ,
594626 } ,
627+ policy,
595628 )
596629 . await
597630 . map_err ( log_and_500) ?;
@@ -619,6 +652,101 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate>(
619652 } ) )
620653}
621654
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+
622750#[ derive( Deserialize ) ]
623751pub struct DeleteDatabaseParams {
624752 name_or_identity : NameOrIdentity ,
@@ -788,7 +916,8 @@ pub struct DatabaseRoutes<S> {
788916 pub logs_get : MethodRouter < S > ,
789917 /// POST: /database/:name_or_identity/sql
790918 pub sql_post : MethodRouter < S > ,
791-
919+ /// POST: /database/:name_or_identity/pre-publish
920+ pub pre_publish : MethodRouter < S > ,
792921 /// GET: /database/: name_or_identity/unstable/timestamp
793922 pub timestamp_get : MethodRouter < S > ,
794923}
@@ -813,6 +942,7 @@ where
813942 schema_get : get ( schema :: < S > ) ,
814943 logs_get : get ( logs :: < S > ) ,
815944 sql_post : post ( sql :: < S > ) ,
945+ pre_publish : post ( pre_publish :: < S > ) ,
816946 timestamp_get : get ( get_timestamp :: < S > ) ,
817947 }
818948 }
@@ -836,7 +966,8 @@ where
836966 . route ( "/schema" , self . schema_get )
837967 . route ( "/logs" , self . logs_get )
838968 . 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 ) ;
840971
841972 axum:: Router :: new ( )
842973 . route ( "/" , self . root_post )
0 commit comments