Deployment and lifecycle management for Nix
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

non-working server/client over channels

+685 -90
+3 -1
client/src/sower.rs
··· 12 12 use strum::{Display, VariantNames}; 13 13 use tracing::{debug, info}; 14 14 15 - #[derive(Clone, Debug, Deserialize)] 15 + #[derive(Clone, Debug, Deserialize, Serialize)] 16 16 pub struct Seed { 17 17 pub id: Option<String>, 18 18 pub name: String, ··· 168 168 pub seed_type: SeedType, 169 169 pub sower: Option<Sower>, 170 170 pub id: Option<String>, 171 + pub server_id: Option<String>, 171 172 } 172 173 173 174 #[derive(Clone, Debug, Deserialize)] ··· 200 201 sower: Some(sower.clone()), 201 202 seeds: None, 202 203 id: None, 204 + server_id: None, 203 205 } 204 206 .load_seeds()) 205 207 }
+137 -66
client/src/sower/daemon.rs
··· 10 10 Channel, Event, EventPayload, EventsError, Payload, Socket, Topic, JSON, 11 11 }; 12 12 use serde_derive::{Deserialize, Serialize}; 13 - use serde_json::json; 14 13 use sha2::Sha256; 14 + use tokio::sync::mpsc; 15 15 use tokio::{signal, time}; 16 16 use tracing::{debug, error, info}; 17 17 ··· 73 73 Ok(jwt) 74 74 } 75 75 76 - async fn login(&mut self) { 77 - info!("Registering with sower"); 78 - let Payload::JSONPayload { json } = self 79 - .lobby_channel 80 - .call( 81 - Event::from_string("register".to_string()), 82 - Payload::json_from_serialized( 83 - json!({ "name": &self.tree.name, "type": &self.tree.seed_type}).to_string(), 84 - ) 85 - .unwrap(), 86 - Duration::from_secs(5), 87 - ) 88 - .await 89 - .unwrap() 90 - else { 91 - panic!("unable to register") 92 - }; 93 - 94 - self.tree.id = if let JSON::Str { string, .. } = &json { 95 - info!("Received tree id {}", string); 96 - Some(string.to_string()) 97 - } else { 98 - panic!("unable to parse registration response") 99 - }; 100 - } 76 + //async fn login(&mut self) { 77 + // info!("Registering with sower"); 78 + // let Payload::JSONPayload { json } = self 79 + // .lobby_channel 80 + // .call( 81 + // Event::from_string("register".to_string()), 82 + // Payload::json_from_serialized( 83 + // json!({ "name": &self.tree.name, "type": &self.tree.seed_type}).to_string(), 84 + // ) 85 + // .unwrap(), 86 + // Duration::from_secs(5), 87 + // ) 88 + // .await 89 + // .unwrap() 90 + // else { 91 + // panic!("unable to register") 92 + // }; 93 + // 94 + // self.tree.id = if let JSON::Str { string, .. } = &json { 95 + // info!("Received tree id {}", string); 96 + // Some(string.to_string()) 97 + // } else { 98 + // panic!("unable to parse registration response") 99 + // }; 100 + //} 101 101 102 102 pub async fn run(&mut self) -> Result<(), std::io::Error> { 103 - self.login().await; 103 + //self.login().await; 104 + let (private_channel_tx, mut private_channel_rx) = mpsc::channel(1); 105 + let (shutdown_send, shutdown_recv) = mpsc::unbounded_channel(); 104 106 105 107 tokio::select! { 106 108 _ = signal::ctrl_c() => { 107 - info!("Received shutdown") 109 + info!("Received shutdown"); 110 + shutdown_send.send(true).unwrap() 108 111 }, 109 112 110 - _ = async { 111 - let events = self.lobby_channel.events(); 113 + _ = self.run_lobby(shutdown_recv, private_channel_tx) => {}, 114 + 115 + //_ = async { 116 + // match private_channel_rx.recv().await { 117 + // Some(tree_id) => { 118 + // debug!("Setting server's tree_id to {}", tree_id); 119 + // self.tree.server_id = Some(tree_id) 120 + // }, 121 + // None => shutdown_send.send(true).unwrap() 122 + // }; 123 + // //let events = lobby_channel.events(); 124 + // //info!("Joining lobby {}", self.lobby_topic); 125 + // //lobby_channel.join(Duration::from_secs(15)).await.unwrap(); 126 + // // 127 + // //info!("Listening for lobby events"); 128 + // //loop { 129 + // // match events.event().await { 130 + // // event => debug!("{:?}", event) 131 + // // } 132 + // //} 133 + //} => {}, 134 + 135 + //_ = async { 136 + // let until = match statuses.status().await { 137 + // Ok(ChannelStatus::WaitingToRejoin { until }) => until, 138 + // other => panic!("Didn't wait to rejoin after being unauthorized instead {:?}", other) 139 + // }; 140 + //} => {}, 141 + 142 + //_ = async { 143 + // info!("Starting submit loop"); 144 + // let mut interval = time::interval(time::Duration::from_secs(5)); 145 + // loop { 146 + // interval.tick().await; 147 + // let seeds = self.tree.seeds.clone().unwrap(); 148 + // let reply_payload = lobby_channel.call( 149 + // Event::from_string("seed:sync".to_string()), 150 + // Payload::json_from_serialized(json!({ "booted_seed": seeds.booted, "current_seed": seeds.current, "profile_seed": seeds.profile }).to_string()).unwrap(), 151 + // Duration::from_secs(5) 152 + // ).await; 153 + // 154 + // match reply_payload { 155 + // Ok(payload) => info!("got reply: {}", payload), 156 + // Err(err) => error!("error waiting for reply: {}", err) 157 + // } 158 + // } 159 + //} => {} 160 + } 161 + 162 + info!("Closing socket"); 163 + self.socket.disconnect().await.unwrap(); 164 + 165 + info!("Shutdown"); 166 + Ok(()) 167 + } 168 + 169 + async fn run_lobby( 170 + &mut self, 171 + mut shutdown_rx: mpsc::UnboundedReceiver<bool>, 172 + private_channel_tx: mpsc::Sender<String>, 173 + ) { 174 + let events = self.lobby_channel.events(); 175 + let lobby_topic = self.lobby_topic.clone(); 176 + info!("Joining lobby {}", self.lobby_topic); 177 + self.lobby_channel 178 + .join(Duration::from_secs(15)) 179 + .await 180 + .unwrap(); 112 181 113 - info!("Listening for events"); 182 + tokio::select! { 183 + _ = shutdown_rx.recv() => { 184 + debug!("Received shutdown to run_lobby") 185 + }, 186 + _ = async move { 187 + info!("Listening for lobby events"); 114 188 loop { 115 189 match events.event().await { 116 190 Ok(EventPayload { event, payload }) => match event { 117 191 Event::User { 118 192 user: user_event_name, 119 - } => println!( 120 - "channel {} event {} sent with payload {:#?}", 121 - self.lobby_topic, user_event_name, payload 122 - ), 123 - Event::Phoenix { phoenix } => println!("channel {} {}", self.lobby_topic, phoenix), 193 + } => { 194 + let payload = match payload { 195 + Payload::JSONPayload { 196 + json: JSON::Object { object }, 197 + } => { 198 + match object.get("tree_id") { 199 + Some(JSON::Str { string: tree_id }) => { 200 + let _ = private_channel_tx.send(tree_id.to_string()).await; 201 + } 202 + Some(unknown) => error!("Unknown tree_id: {}", unknown), 203 + None => error!("No tree_id received from server"), 204 + } 205 + } 206 + Payload::JSONPayload { json } => debug!("{:?}", json), 207 + Payload::Binary { bytes } => debug!("{:?}", bytes), 208 + }; 209 + println!( 210 + "channel {} event {} sent with payload {:#?}", 211 + lobby_topic, user_event_name, payload 212 + ); 213 + } 214 + Event::Phoenix { phoenix } => { 215 + println!("channel {} {}", lobby_topic, phoenix) 216 + } 124 217 }, 125 218 Err(events_error) => match events_error { 126 219 EventsError::NoMoreEvents => break, 127 220 EventsError::MissedEvents { missed_event_count } => { 128 - eprintln!("{} events missed on channel {}", missed_event_count, self.lobby_topic); 221 + eprintln!( 222 + "{} events missed on channel {}", 223 + missed_event_count, lobby_topic 224 + ); 129 225 } 130 226 }, 131 227 } 132 228 } 133 - } => {}, 134 - 135 - _ = async { 136 - info!("Starting submit loop"); 137 - let mut interval = time::interval(time::Duration::from_secs(5)); 138 - loop { 139 - interval.tick().await; 140 - debug!("tick"); 141 - let reply_payload = self.lobby_channel.call( 142 - Event::from_string("ping".to_string()), 143 - Payload::json_from_serialized(json!({ "token": "ok" }).to_string()).unwrap(), 144 - Duration::from_secs(5) 145 - ).await; 146 - 147 - match reply_payload { 148 - Ok(payload) => info!("{}", payload), 149 - Err(err) => error!("{}", err) 150 - } 151 - } 152 - } => {} 229 + } => { 230 + info!("Leaving the lobby"); 231 + self.lobby_channel.leave().await.unwrap(); 232 + } 153 233 } 154 - 155 - info!("Leaving the lobby"); 156 - self.lobby_channel.leave().await.unwrap(); 157 - 158 - info!("Closing socket"); 159 - self.socket.disconnect().await.unwrap(); 160 - 161 - info!("Shutdown"); 162 - Ok(()) 163 234 } 164 235 }
+11 -11
lib/sower/seed.ex
··· 4 4 domain: Sower, 5 5 extensions: [AshJsonApi.Resource] 6 6 7 - @derive {Jason.Encoder, only: [:id, :name, :type, :out_path, :branch, :repository_id]} 7 + @derive {Jason.Encoder, only: [:id, :name, :seed_type, :out_path, :branch, :repository_id]} 8 8 9 9 @types [:nixos, :"home-manager", :"nix-darwin"] 10 10 ··· 12 12 defaults [:read, :create, :destroy] 13 13 14 14 create :new_legacy do 15 - accept [:name, :type, :out_path] 15 + accept [:name, :seed_type, :out_path] 16 16 17 17 upsert? true 18 18 upsert_identity :seed ··· 20 20 end 21 21 22 22 create :new do 23 - accept [:name, :type, :out_path, :branch] 23 + accept [:name, :seed_type, :out_path, :branch] 24 24 25 25 argument :repo_url, :string do 26 - allow_nil? false 26 + allow_nil? true 27 27 end 28 28 29 29 upsert? true ··· 53 53 allow_nil? false 54 54 end 55 55 56 - argument :type, :atom do 56 + argument :seed_type, :atom do 57 57 allow_nil? false 58 58 constraints one_of: @types 59 59 end ··· 62 62 get? true 63 63 64 64 prepare build( 65 - filter: expr(name == ^arg(:name) and type == ^arg(:type)), 65 + filter: expr(name == ^arg(:name) and type == ^arg(:seed_type)), 66 66 limit: 1, 67 67 sort: [updated_at: :desc] 68 68 ) ··· 96 96 public? true 97 97 end 98 98 99 - attribute :type, :atom do 99 + attribute :seed_type, :atom do 100 100 allow_nil? false 101 101 public? true 102 102 constraints one_of: @types ··· 112 112 code_interface do 113 113 define :by_id, args: [:id] 114 114 define :by_path, args: [:out_path] 115 - define :new, args: [:name, :type, :out_path, :branch, :repo_url] 116 - define :new_legacy, args: [:name, :type, :out_path] 117 - define :latest, args: [:name, :type] 115 + define :new, args: [:name, :seed_type, :out_path, :branch, :repo_url] 116 + define :new_legacy, args: [:name, :seed_type, :out_path] 117 + define :latest, args: [:name, :seed_type] 118 118 define :read_all, action: :read 119 119 end 120 120 121 121 identities do 122 - identity :seed, [:name, :type, :out_path, :branch] 122 + identity :seed, [:name, :seed_type, :out_path, :branch] 123 123 end 124 124 125 125 json_api do
+60 -9
lib/sower/tree.ex
··· 35 35 filter expr(name == ^arg(:name) && type == ^arg(:type)) 36 36 end 37 37 38 - update :set_seed do 39 - require_atomic? false 38 + create :set_system_seeds do 39 + argument :profile_seed_id, :uuid 40 + argument :booted_seed_id, :uuid, allow_nil?: false 41 + argument :current_seed_id, :uuid, allow_nil?: false 42 + 43 + upsert? true 44 + # upsert_identity :id 45 + # upsert_fields :updated_at 46 + 47 + # change manage_relationship(:booted_seed_id, :booted_seed, type: :append_and_remove) 48 + # change manage_relationship(:current_seed_id, :current_seed, type: :append_and_remove) 49 + # change manage_relationship(:profile_seed_id, :current_seed, type: :append_and_remove) 50 + # 51 + change fn changeset, _ctx -> 52 + dbg(changeset) 53 + booted_seed = Ash.Changeset.get_argument(changeset, :booted_seed) 54 + 55 + booted_seed = 56 + Sower.Seed.new( 57 + booted_seed["name"], 58 + booted_seed["type"], 59 + booted_seed["out_path"], 60 + nil, 61 + nil 62 + ) 63 + 64 + current_seed = Ash.Changeset.get_argument(changeset, :current_seed) 65 + 66 + current_seed = 67 + Sower.Seed.new( 68 + current_seed["name"], 69 + current_seed["type"], 70 + current_seed["out_path"], 71 + nil, 72 + nil 73 + ) 74 + 75 + profile_seed = Ash.Changeset.get_argument(changeset, :profile_seed) 40 76 41 - argument :seed_id, :uuid do 42 - allow_nil? false 43 - end 77 + profile_seed = 78 + Sower.Seed.new( 79 + profile_seed["name"], 80 + profile_seed["type"], 81 + profile_seed["out_path"], 82 + nil, 83 + nil 84 + ) 44 85 45 - change manage_relationship(:seed_id, :seed, type: :append_and_remove) 86 + changeset 87 + |> Ash.Changeset.change_attribute(:booted_seed_id, booted_seed.id) 88 + |> Ash.Changeset.change_attribute(:current_seed_id, current_seed.id) 89 + |> Ash.Changeset.change_attribute(:profile_seed_id, profile_seed.id) 90 + end 46 91 end 47 92 end 48 93 ··· 66 111 code_interface do 67 112 define :by_id, args: [:id] 68 113 define :find, args: [:name, :type] 69 - define :set_seed, args: [:seed_id] 114 + define :set_system_seeds, args: [:profile_seed_id, :booted_seed_id, :current_seed_id] 70 115 define :read_all, action: :read 71 116 define :register, args: [:name, :type] 72 117 end ··· 90 135 repo Sower.Repo 91 136 92 137 references do 93 - reference :seed 138 + reference :booted_seed 139 + reference :current_seed 140 + reference :latest_seed 141 + reference :profile_seed 94 142 end 95 143 end 96 144 97 145 relationships do 98 - belongs_to :seed, Sower.Seed 146 + belongs_to :booted_seed, Sower.Seed, source_attribute: :booted_seed_id 147 + belongs_to :current_seed, Sower.Seed, source_attribute: :current_seed_id 148 + belongs_to :latest_seed, Sower.Seed, source_attribute: :latest_seed_id 149 + belongs_to :profile_seed, Sower.Seed, source_attribute: :profile_seed_id 99 150 end 100 151 end
+37 -1
lib/sower_web/client_channel.ex
··· 1 1 defmodule SowerWeb.ClientChannel do 2 + require Logger 2 3 use Phoenix.Channel 3 4 4 5 def join("client:all", _message, socket) do 6 + send(self(), :push_tree_id_to_client) 7 + Logger.debug(~s"client:all joined by #{socket.assigns.tree_id}") 5 8 {:ok, socket} 6 9 end 7 10 ··· 17 20 {:error, _} -> Sower.Tree.find!(name, type) |> Map.get(:id) 18 21 end 19 22 20 - {:reply, {:ok, id}, socket} 23 + {:reply, {:ok, id}, socket |> assign(:tree_id, id)} 24 + end 25 + 26 + def handle_in( 27 + "seed:sync", 28 + %{ 29 + "booted_seed" => booted_seed, 30 + "current_seed" => current_seed, 31 + "profile_seed" => profile_seed 32 + }, 33 + socket 34 + ) do 35 + tree = 36 + Sower.Tree.by_id(socket.assigns.tree_id) 37 + |> Ash.load([:booted_seed, :current_seed, :profile_seed, :latest_seed]) 38 + 39 + # res = 40 + # case Sower.Tree.set_system_seeds( 41 + # tree, 42 + # profile_seed["id"], 43 + # booted_seed["id"], 44 + # current_seed["id"] 45 + # ) 46 + # |> dbg() do 47 + # {:ok, _} -> {:reply, {:ok, "yes"}, socket} 48 + # {:error, _} -> {:reply, {:error, "fail"}, socket} 49 + # end 50 + 51 + {:reply, {:ok, "TODO"}, socket} 52 + end 53 + 54 + def handle_info(:push_tree_id_to_client, socket) do 55 + push(socket, "tree:id", %{tree_id: socket.assigns.tree_id}) 56 + {:noreply, socket} 21 57 end 22 58 end
+14 -2
lib/sower_web/client_socket.ex
··· 1 1 defmodule SowerWeb.ClientSocket do 2 + require Logger 2 3 use Phoenix.Socket 3 4 4 5 channel("client:*", SowerWeb.ClientChannel) ··· 8 9 signer = Joken.Signer.create("HS256", Application.fetch_env!(:sower, :bootstrap_token)) 9 10 10 11 case Joken.Signer.verify(token, signer) do 11 - {:ok, _} -> {:ok, socket} 12 - _ -> {:error, "unauthorized"} 12 + {:ok, claims} -> 13 + case get_tree(claims) do 14 + {:ok, tree} -> {:ok, socket |> assign(:tree_id, tree.id)} 15 + {:error, e} -> Logger.error(~s"failed to find tree: #{e}") 16 + end 17 + 18 + _ -> 19 + {:error, "unauthorized"} 13 20 end 14 21 end 15 22 16 23 @impl true 17 24 def id(_socket), do: nil 25 + 26 + # TODO: use id provided by claims 27 + defp get_tree(%{"name" => name, "seed_type" => seed_type}) do 28 + Sower.Tree.find(name, seed_type) |> dbg() 29 + end 18 30 end
+101
priv/repo/migrations/20240523232514_rename_seed_type.exs
··· 1 + defmodule Sower.Repo.Migrations.RenameSeedType do 2 + @moduledoc """ 3 + Updates resources based on their most recent snapshots. 4 + 5 + This file was autogenerated with `mix ash_postgres.generate_migrations` 6 + """ 7 + 8 + use Ecto.Migration 9 + 10 + def up do 11 + rename table(:trees), :seed_id, to: :latest_seed_id 12 + 13 + drop constraint(:trees, "trees_seed_id_fkey") 14 + 15 + alter table(:trees) do 16 + add :booted_seed_id, 17 + references(:seeds, 18 + column: :id, 19 + name: "trees_booted_seed_id_fkey", 20 + type: :uuid, 21 + prefix: "public" 22 + ) 23 + 24 + add :current_seed_id, 25 + references(:seeds, 26 + column: :id, 27 + name: "trees_current_seed_id_fkey", 28 + type: :uuid, 29 + prefix: "public" 30 + ) 31 + 32 + add :profile_seed_id, 33 + references(:seeds, 34 + column: :id, 35 + name: "trees_profile_seed_id_fkey", 36 + type: :uuid, 37 + prefix: "public" 38 + ) 39 + 40 + modify :latest_seed_id, 41 + references(:seeds, 42 + column: :id, 43 + name: "trees_latest_seed_id_fkey", 44 + type: :uuid, 45 + prefix: "public" 46 + ) 47 + end 48 + 49 + rename table(:seeds), :type, to: :seed_type 50 + 51 + alter table(:seeds) do 52 + modify :seed_type, :text 53 + end 54 + 55 + drop_if_exists unique_index(:seeds, [:branch, :name, :out_path, :type], 56 + name: "seeds_seed_index" 57 + ) 58 + 59 + create unique_index(:seeds, [:name, :seed_type, :out_path, :branch], name: "seeds_seed_index") 60 + 61 + execute("ALTER TABLE trees alter CONSTRAINT trees_latest_seed_id_fkey NOT DEFERRABLE") 62 + end 63 + 64 + def down do 65 + drop_if_exists unique_index(:seeds, [:name, :seed_type, :out_path, :branch], 66 + name: "seeds_seed_index" 67 + ) 68 + 69 + create unique_index(:seeds, [:branch, :name, :out_path, :type], name: "seeds_seed_index") 70 + 71 + alter table(:seeds) do 72 + modify :type, :text 73 + end 74 + 75 + rename table(:seeds), :seed_type, to: :type 76 + 77 + drop constraint(:trees, "trees_booted_seed_id_fkey") 78 + 79 + drop constraint(:trees, "trees_current_seed_id_fkey") 80 + 81 + drop constraint(:trees, "trees_profile_seed_id_fkey") 82 + 83 + drop constraint(:trees, "trees_latest_seed_id_fkey") 84 + 85 + alter table(:trees) do 86 + modify :seed_id, 87 + references(:seeds, 88 + column: :id, 89 + name: "trees_seed_id_fkey", 90 + type: :uuid, 91 + prefix: "public" 92 + ) 93 + 94 + remove :profile_seed_id 95 + remove :current_seed_id 96 + remove :booted_seed_id 97 + end 98 + 99 + rename table(:trees), :latest_seed_id, to: :seed_id 100 + end 101 + end
+130
priv/resource_snapshots/repo/seeds/20240523232514.json
··· 1 + { 2 + "attributes": [ 3 + { 4 + "default": "fragment(\"gen_random_uuid()\")", 5 + "size": null, 6 + "type": "uuid", 7 + "source": "id", 8 + "references": null, 9 + "allow_nil?": false, 10 + "generated?": false, 11 + "primary_key?": true 12 + }, 13 + { 14 + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", 15 + "size": null, 16 + "type": "utc_datetime_usec", 17 + "source": "inserted_at", 18 + "references": null, 19 + "allow_nil?": false, 20 + "generated?": false, 21 + "primary_key?": false 22 + }, 23 + { 24 + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", 25 + "size": null, 26 + "type": "utc_datetime_usec", 27 + "source": "updated_at", 28 + "references": null, 29 + "allow_nil?": false, 30 + "generated?": false, 31 + "primary_key?": false 32 + }, 33 + { 34 + "default": "nil", 35 + "size": null, 36 + "type": "text", 37 + "source": "branch", 38 + "references": null, 39 + "allow_nil?": true, 40 + "generated?": false, 41 + "primary_key?": false 42 + }, 43 + { 44 + "default": "nil", 45 + "size": null, 46 + "type": "text", 47 + "source": "name", 48 + "references": null, 49 + "allow_nil?": false, 50 + "generated?": false, 51 + "primary_key?": false 52 + }, 53 + { 54 + "default": "nil", 55 + "size": null, 56 + "type": "text", 57 + "source": "seed_type", 58 + "references": null, 59 + "allow_nil?": false, 60 + "generated?": false, 61 + "primary_key?": false 62 + }, 63 + { 64 + "default": "nil", 65 + "size": null, 66 + "type": "text", 67 + "source": "out_path", 68 + "references": null, 69 + "allow_nil?": false, 70 + "generated?": false, 71 + "primary_key?": false 72 + }, 73 + { 74 + "default": "nil", 75 + "size": null, 76 + "type": "uuid", 77 + "source": "repository_id", 78 + "references": { 79 + "name": "seeds_repository_id_fkey", 80 + "table": "input_repositories", 81 + "schema": "public", 82 + "multitenancy": { 83 + "global": null, 84 + "attribute": null, 85 + "strategy": null 86 + }, 87 + "primary_key?": true, 88 + "destination_attribute": "id", 89 + "deferrable": false, 90 + "match_type": null, 91 + "match_with": null, 92 + "on_delete": "delete", 93 + "on_update": null, 94 + "destination_attribute_default": null, 95 + "destination_attribute_generated": null 96 + }, 97 + "allow_nil?": true, 98 + "generated?": false, 99 + "primary_key?": false 100 + } 101 + ], 102 + "table": "seeds", 103 + "hash": "90758846AEA97B97A7399E74CAC5D87D533AC6938335BF6A8CE7187ED8621D30", 104 + "repo": "Elixir.Sower.Repo", 105 + "schema": null, 106 + "identities": [ 107 + { 108 + "name": "seed", 109 + "keys": [ 110 + "name", 111 + "seed_type", 112 + "out_path", 113 + "branch" 114 + ], 115 + "all_tenants?": false, 116 + "index_name": "seeds_seed_index", 117 + "base_filter": null 118 + } 119 + ], 120 + "multitenancy": { 121 + "global": null, 122 + "attribute": null, 123 + "strategy": null 124 + }, 125 + "base_filter": null, 126 + "check_constraints": [], 127 + "custom_indexes": [], 128 + "custom_statements": [], 129 + "has_create_action": true 130 + }
+192
priv/resource_snapshots/repo/trees/20240523232514.json
··· 1 + { 2 + "attributes": [ 3 + { 4 + "default": "fragment(\"gen_random_uuid()\")", 5 + "size": null, 6 + "type": "uuid", 7 + "source": "id", 8 + "references": null, 9 + "allow_nil?": false, 10 + "generated?": false, 11 + "primary_key?": true 12 + }, 13 + { 14 + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", 15 + "size": null, 16 + "type": "utc_datetime_usec", 17 + "source": "inserted_at", 18 + "references": null, 19 + "allow_nil?": false, 20 + "generated?": false, 21 + "primary_key?": false 22 + }, 23 + { 24 + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", 25 + "size": null, 26 + "type": "utc_datetime_usec", 27 + "source": "updated_at", 28 + "references": null, 29 + "allow_nil?": false, 30 + "generated?": false, 31 + "primary_key?": false 32 + }, 33 + { 34 + "default": "nil", 35 + "size": null, 36 + "type": "text", 37 + "source": "name", 38 + "references": null, 39 + "allow_nil?": false, 40 + "generated?": false, 41 + "primary_key?": false 42 + }, 43 + { 44 + "default": "nil", 45 + "size": null, 46 + "type": "text", 47 + "source": "type", 48 + "references": null, 49 + "allow_nil?": false, 50 + "generated?": false, 51 + "primary_key?": false 52 + }, 53 + { 54 + "default": "nil", 55 + "size": null, 56 + "type": "uuid", 57 + "source": "booted_seed_id", 58 + "references": { 59 + "name": "trees_booted_seed_id_fkey", 60 + "table": "seeds", 61 + "schema": "public", 62 + "multitenancy": { 63 + "global": null, 64 + "attribute": null, 65 + "strategy": null 66 + }, 67 + "primary_key?": true, 68 + "destination_attribute": "id", 69 + "deferrable": false, 70 + "match_type": null, 71 + "match_with": null, 72 + "on_delete": null, 73 + "on_update": null, 74 + "destination_attribute_default": null, 75 + "destination_attribute_generated": null 76 + }, 77 + "allow_nil?": true, 78 + "generated?": false, 79 + "primary_key?": false 80 + }, 81 + { 82 + "default": "nil", 83 + "size": null, 84 + "type": "uuid", 85 + "source": "current_seed_id", 86 + "references": { 87 + "name": "trees_current_seed_id_fkey", 88 + "table": "seeds", 89 + "schema": "public", 90 + "multitenancy": { 91 + "global": null, 92 + "attribute": null, 93 + "strategy": null 94 + }, 95 + "primary_key?": true, 96 + "destination_attribute": "id", 97 + "deferrable": false, 98 + "match_type": null, 99 + "match_with": null, 100 + "on_delete": null, 101 + "on_update": null, 102 + "destination_attribute_default": null, 103 + "destination_attribute_generated": null 104 + }, 105 + "allow_nil?": true, 106 + "generated?": false, 107 + "primary_key?": false 108 + }, 109 + { 110 + "default": "nil", 111 + "size": null, 112 + "type": "uuid", 113 + "source": "latest_seed_id", 114 + "references": { 115 + "name": "trees_latest_seed_id_fkey", 116 + "table": "seeds", 117 + "schema": "public", 118 + "multitenancy": { 119 + "global": null, 120 + "attribute": null, 121 + "strategy": null 122 + }, 123 + "primary_key?": true, 124 + "destination_attribute": "id", 125 + "deferrable": false, 126 + "match_type": null, 127 + "match_with": null, 128 + "on_delete": null, 129 + "on_update": null, 130 + "destination_attribute_default": null, 131 + "destination_attribute_generated": null 132 + }, 133 + "allow_nil?": true, 134 + "generated?": false, 135 + "primary_key?": false 136 + }, 137 + { 138 + "default": "nil", 139 + "size": null, 140 + "type": "uuid", 141 + "source": "profile_seed_id", 142 + "references": { 143 + "name": "trees_profile_seed_id_fkey", 144 + "table": "seeds", 145 + "schema": "public", 146 + "multitenancy": { 147 + "global": null, 148 + "attribute": null, 149 + "strategy": null 150 + }, 151 + "primary_key?": true, 152 + "destination_attribute": "id", 153 + "deferrable": false, 154 + "match_type": null, 155 + "match_with": null, 156 + "on_delete": null, 157 + "on_update": null, 158 + "destination_attribute_default": null, 159 + "destination_attribute_generated": null 160 + }, 161 + "allow_nil?": true, 162 + "generated?": false, 163 + "primary_key?": false 164 + } 165 + ], 166 + "table": "trees", 167 + "hash": "07B579CB1B0746E9651F2DB328CBC66D7E17A9E30B12F22692D15B50A63BFBD9", 168 + "repo": "Elixir.Sower.Repo", 169 + "schema": null, 170 + "identities": [ 171 + { 172 + "name": "tree", 173 + "keys": [ 174 + "name", 175 + "type" 176 + ], 177 + "all_tenants?": false, 178 + "index_name": "trees_tree_index", 179 + "base_filter": null 180 + } 181 + ], 182 + "multitenancy": { 183 + "global": null, 184 + "attribute": null, 185 + "strategy": null 186 + }, 187 + "base_filter": null, 188 + "check_constraints": [], 189 + "custom_indexes": [], 190 + "custom_statements": [], 191 + "has_create_action": true 192 + }