@@ -3,163 +3,149 @@ defmodule Phoenix.Sync.PredefinedShape do
3
3
4
4
# A self-contained way to hold shape definition information, alongside stream
5
5
# 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
6
8
7
9
alias Electric.Client.ShapeDefinition
8
10
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
16
17
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 )
26
20
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
+ ]
28
43
29
44
@ type t :: % __MODULE__ { }
45
+ @ type options ( ) :: [ unquote ( NimbleOptions . option_typespec ( @ public_schema ) ) ]
30
46
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
33
54
55
+ @ spec new! ( shape ( ) , options ( ) ) :: t ( )
34
56
def new! ( opts , config \\ [ ] )
35
57
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 ( )
39
63
end
40
64
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 )
43
67
end
44
68
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
47
79
end
48
80
49
- defp new ( opts ) do
50
- struct ( __MODULE__ , opts )
51
- end
81
+ defp new ( opts ) , do: struct ( __MODULE__ , opts )
52
82
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 )
58
86
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
63
91
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
69
96
70
- _ ->
71
- nil
72
- end
97
+ shape_config = validate_shape_config ( shape_opts , mode )
98
+ api_config = NimbleOptions . validate! ( api_opts , @ api_schema )
73
99
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 )
77
119
end
78
120
79
121
def client ( % Electric.Client { } = client , % __MODULE__ { } = predefined_shape ) do
80
122
Electric.Client . merge_params ( client , to_client_params ( predefined_shape ) )
81
123
end
82
124
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 ( )
95
129
end
96
130
97
131
def to_api_params ( % __MODULE__ { } = predefined_shape ) do
98
132
predefined_shape
99
- |> resolve_query ( )
100
- |> to_list ( )
133
+ |> to_shape_definition ( )
134
+ |> ShapeDefinition . params ( format: :keyword )
135
+ |> Keyword . merge ( predefined_shape . api_config )
101
136
end
102
137
103
138
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 }
116
140
end
117
141
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 )
120
144
end
121
145
122
146
# we resolve the query at runtime to avoid compile-time dependencies in
123
147
# 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 )
164
150
end
165
151
end
0 commit comments