@@ -3,163 +3,149 @@ defmodule Phoenix.Sync.PredefinedShape do
33
44 # A self-contained way to hold shape definition information, alongside stream
55 # configuration, compatible with both the embedded and HTTP API versions.
6+ # Defers to the client code to validate shape options, so we can keep up with
7+ # changes to the api without duplicating changes here
68
79 alias Electric.Client.ShapeDefinition
810
9- @ keys [
10- :relation ,
11- :where ,
12- :columns ,
13- :replica ,
14- :storage
15- ]
11+ shape_schema_gen = fn required? ->
12+ Keyword . take (
13+ [ table: [ type: :string , required: required? ] ] ++ ShapeDefinition . schema_definition ( ) ,
14+ ShapeDefinition . public_keys ( )
15+ )
16+ end
1617
17- @ schema NimbleOptions . new! (
18- table: [ type: :string ] ,
19- query: [ type: { :or , [ :atom , { :struct , Ecto.Query } ] } , doc: false ] ,
20- namespace: [ type: :string , default: "public" ] ,
21- where: [ type: :string ] ,
22- columns: [ type: { :list , :string } ] ,
23- replica: [ type: { :in , [ :default , :full ] } ] ,
24- storage: [ type: { :or , [ :map , nil ] } ]
25- )
18+ @ shape_definition_schema shape_schema_gen . ( false )
19+ @ keyword_shape_schema shape_schema_gen . ( true )
2620
27- defstruct [ :query | @ keys ]
21+ @ api_schema_opts [
22+ storage: [ type: { :or , [ :map , nil ] } ]
23+ ]
24+
25+ @ shape_schema NimbleOptions . new! ( @ shape_definition_schema )
26+ @ api_schema NimbleOptions . new! ( @ api_schema_opts )
27+ @ stream_schema Electric.Client.Stream . options_schema ( )
28+ @ public_schema NimbleOptions . new! ( @ shape_definition_schema ++ @ api_schema_opts )
29+
30+ @ api_schema_keys Keyword . keys ( @ api_schema_opts )
31+ @ stream_schema_keys Keyword . keys ( @ stream_schema . schema )
32+ @ shape_definition_keys ShapeDefinition . public_keys ( )
33+
34+ # we hold the query separate from the shape definition in order to allow
35+ # for transformation of a query to a shape definition at runtime rather
36+ # than compile time.
37+ defstruct [
38+ :shape_config ,
39+ :api_config ,
40+ :stream_config ,
41+ :query
42+ ]
2843
2944 @ type t :: % __MODULE__ { }
45+ @ type options ( ) :: [ unquote ( NimbleOptions . option_typespec ( @ public_schema ) ) ]
3046
31- def schema , do: @ schema
32- def keys , do: @ keys
47+ if Code . ensure_loaded? ( Ecto ) do
48+ @ type shape ( ) :: options ( ) | Electric.Client . ecto_shape ( )
49+ else
50+ @ type shape ( ) :: options ( )
51+ end
52+
53+ def schema , do: @ public_schema
3354
55+ @ spec new! ( shape ( ) , options ( ) ) :: t ( )
3456 def new! ( opts , config \\ [ ] )
3557
36- def new! ( shape , opts ) when is_list ( opts ) and is_list ( shape ) do
37- config = NimbleOptions . validate! ( Keyword . merge ( shape , opts ) , @ schema )
38- new ( Keyword . put ( config , :relation , build_relation! ( config ) ) )
58+ def new! ( shape , opts ) when is_list ( shape ) and is_list ( opts ) do
59+ shape
60+ |> Keyword . merge ( opts )
61+ |> split_and_validate_opts! ( mode: :keyword )
62+ |> new ( )
3963 end
4064
41- def new! ( schema , opts ) when is_atom ( schema ) do
42- new ( Keyword . put ( opts , :query , schema ) )
65+ def new! ( table , opts ) when is_binary ( table ) and is_list ( opts ) do
66+ new! ( [ table: table ] , opts )
4367 end
4468
45- def new! ( % Ecto.Query { } = query , opts ) do
46- new ( Keyword . put ( opts , :query , query ) )
69+ if Code . ensure_loaded? ( Ecto ) do
70+ def new! ( ecto_shape , opts )
71+ when is_atom ( ecto_shape ) or is_struct ( ecto_shape , Ecto.Query ) or
72+ is_function ( ecto_shape , 1 ) or
73+ is_struct ( ecto_shape , Ecto.Changeset ) do
74+ opts
75+ |> split_and_validate_opts! ( mode: :ecto )
76+ |> Keyword . merge ( query: ecto_shape )
77+ |> new ( )
78+ end
4779 end
4880
49- defp new ( opts ) do
50- struct ( __MODULE__ , opts )
51- end
81+ defp new ( opts ) , do: struct ( __MODULE__ , opts )
5282
53- defp build_relation! ( opts ) do
54- build_relation ( opts ) ||
55- raise ArgumentError ,
56- message: "missing relation or table in #{ inspect ( opts ) } "
57- end
83+ defp split_and_validate_opts! ( opts , mode ) do
84+ { shape_opts , other_opts } = Keyword . split ( opts , @ shape_definition_keys )
85+ { api_opts , other_opts } = Keyword . split ( other_opts , @ api_schema_keys )
5886
59- defp build_relation ( opts ) do
60- case Keyword . get ( opts , :relation ) do
61- { _namespace , _table } = relation ->
62- relation
87+ stream_opts =
88+ case Keyword . split ( other_opts , @ stream_schema_keys ) do
89+ { stream_opts , [ ] } ->
90+ stream_opts
6391
64- nil ->
65- case Keyword . get ( opts , :table ) do
66- table when is_binary ( table ) ->
67- namespace = Keyword . get ( opts , :namespace , "public" )
68- { namespace , table }
92+ { _stream_opts , invalid_opts } ->
93+ raise ArgumentError ,
94+ message: "received invalid options to a shape definition: #{ inspect ( invalid_opts ) } "
95+ end
6996
70- _ ->
71- nil
72- end
97+ shape_config = validate_shape_config ( shape_opts , mode )
98+ api_config = NimbleOptions . validate! ( api_opts , @ api_schema )
7399
74- _ ->
75- nil
76- end
100+ # remove replica value from the stream because it will override the shape
101+ # setting and since we've removed the `:replica` value earlier
102+ # it'll always be set to default
103+ stream_config =
104+ NimbleOptions . validate! ( stream_opts , @ stream_schema )
105+ |> Enum . reject ( & is_nil ( elem ( & 1 , 1 ) ) )
106+ |> Enum . reject ( & ( elem ( & 1 , 0 ) == :replica ) )
107+
108+ [ shape_config: shape_config , api_config: api_config , stream_config: stream_config ]
109+ end
110+
111+ # If we're defining a shape with a keyword list then we need at least the
112+ # `table`. Coming from some ecto value, the table is already present
113+ defp validate_shape_config ( shape_opts , mode: :keyword ) do
114+ NimbleOptions . validate! ( shape_opts , @ keyword_shape_schema )
115+ end
116+
117+ defp validate_shape_config ( shape_opts , _mode ) do
118+ NimbleOptions . validate! ( shape_opts , @ shape_schema )
77119 end
78120
79121 def client ( % Electric.Client { } = client , % __MODULE__ { } = predefined_shape ) do
80122 Electric.Client . merge_params ( client , to_client_params ( predefined_shape ) )
81123 end
82124
83- defp to_client_params ( % __MODULE__ { } = predefined_shape ) do
84- { { namespace , table } , shape } =
85- predefined_shape
86- |> resolve_query ( )
87- |> to_list ( )
88- |> Keyword . pop! ( :relation )
89-
90- # Remove storage as it's not currently supported as a query param
91- shape
92- |> Keyword . put ( :table , ShapeDefinition . url_table_name ( namespace , table ) )
93- |> Keyword . delete ( :storage )
94- |> columns_to_query_param ( )
125+ def to_client_params ( % __MODULE__ { } = predefined_shape ) do
126+ predefined_shape
127+ |> to_shape_definition ( )
128+ |> ShapeDefinition . params ( )
95129 end
96130
97131 def to_api_params ( % __MODULE__ { } = predefined_shape ) do
98132 predefined_shape
99- |> resolve_query ( )
100- |> to_list ( )
133+ |> to_shape_definition ( )
134+ |> ShapeDefinition . params ( format: :keyword )
135+ |> Keyword . merge ( predefined_shape . api_config )
101136 end
102137
103138 def to_stream_params ( % __MODULE__ { } = predefined_shape ) do
104- { { namespace , table } , shape } =
105- predefined_shape
106- |> resolve_query ( )
107- |> to_list ( )
108- |> Keyword . pop! ( :relation )
109-
110- { shape_opts , stream_opts } = Keyword . split ( shape , ShapeDefinition . public_keys ( ) )
111-
112- { :ok , shape_definition } =
113- ShapeDefinition . new ( table , Keyword . merge ( shape_opts , namespace: namespace ) )
114-
115- { shape_definition , stream_opts }
139+ { to_shape_definition ( predefined_shape ) , predefined_shape . stream_config }
116140 end
117141
118- defp resolve_query ( % __MODULE__ { query: nil } = predefined_shape ) do
119- predefined_shape
142+ defp to_shape_definition ( % __MODULE__ { query: nil , shape_config: shape_config } ) do
143+ ShapeDefinition . new! ( shape_config )
120144 end
121145
122146 # we resolve the query at runtime to avoid compile-time dependencies in
123147 # router modules
124- defp resolve_query ( % __MODULE__ { } = predefined_shape ) do
125- from_queryable! ( predefined_shape )
126- end
127-
128- defp from_queryable! ( % { query: queryable } = predefined_shape ) do
129- queryable
130- |> Electric.Client.EctoAdapter . shape_from_query! ( )
131- |> from_shape_definition ( predefined_shape )
132- end
133-
134- defp from_shape_definition ( % ShapeDefinition { } = shape_definition , predefined_shape ) do
135- % {
136- namespace: namespace ,
137- table: table ,
138- where: where ,
139- columns: columns
140- } = shape_definition
141-
142- % { predefined_shape | relation: { namespace || "public" , table } , columns: columns }
143- |> put_if ( :where , where )
144- end
145-
146- defp put_if ( shape , _key , nil ) , do: shape
147- defp put_if ( shape , key , value ) , do: Map . put ( shape , key , value )
148-
149- defp to_list ( % __MODULE__ { } = shape ) do
150- Enum . flat_map ( @ keys , fn key ->
151- value = Map . fetch! ( shape , key )
152-
153- if ! is_nil ( value ) ,
154- do: [ { key , value } ] ,
155- else: [ ]
156- end )
157- end
158-
159- defp columns_to_query_param ( shape ) do
160- case Keyword . get ( shape , :columns ) do
161- columns when is_list ( columns ) -> Keyword . put ( shape , :columns , Enum . join ( columns , "," ) )
162- _ -> shape
163- end
148+ defp to_shape_definition ( % __MODULE__ { query: queryable , shape_config: shape_config } ) do
149+ Electric.Client.EctoAdapter . shape! ( queryable , shape_config )
164150 end
165151end
0 commit comments