model_driven_api 3.6.3 → 3.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +832 -54
- data/Rakefile +3 -0
- data/app/controllers/api/v2/application_controller.rb +24 -49
- data/app/controllers/api/v2/info_controller.rb +2 -1308
- data/app/controllers/api/v3/application_controller.rb +132 -0
- data/app/controllers/api/v3/auth/oauth_controller.rb +4 -0
- data/app/controllers/api/v3/authentication_controller.rb +2 -0
- data/app/controllers/api/v3/info_controller.rb +37 -0
- data/app/controllers/api/v3/raw_controller.rb +14 -0
- data/app/controllers/api/v3/users_controller.rb +10 -0
- data/app/models/endpoints/push_subscriber.rb +110 -0
- data/config/routes.rb +44 -0
- data/lib/api/custom_action_dispatcher.rb +41 -0
- data/lib/api/model_resolver.rb +20 -0
- data/lib/api/open_api/base.rb +91 -0
- data/lib/api/open_api/v2.rb +1238 -0
- data/lib/api/open_api/v3.rb +349 -0
- data/lib/api/resource_attribute_set.rb +25 -0
- data/lib/api/v3/serializer_factory.rb +65 -0
- data/lib/concerns/api_exception_management.rb +4 -1
- data/lib/model_driven_api/engine.rb +7 -1
- data/lib/model_driven_api/version.rb +1 -1
- data/lib/model_driven_api.rb +8 -0
- data/lib/non_crud_endpoints.rb +18 -0
- metadata +78 -8
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
module Api
|
|
2
|
+
module OpenApi
|
|
3
|
+
class V3 < Base
|
|
4
|
+
def generate
|
|
5
|
+
paths = {}
|
|
6
|
+
paths.merge!(v3_authenticate_path)
|
|
7
|
+
paths.merge!(v3_raw_sql_paths)
|
|
8
|
+
paths.merge!(v3_info_paths)
|
|
9
|
+
ApplicationRecord.subclasses.sort_by(&:to_s).each do |model_class|
|
|
10
|
+
paths.merge!(v3_crud_paths(model_class))
|
|
11
|
+
paths.merge!(v3_custom_action_paths(model_class))
|
|
12
|
+
end
|
|
13
|
+
paths
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def description
|
|
17
|
+
v3_description
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def v3_authenticate_path
|
|
23
|
+
{
|
|
24
|
+
"/authenticate" => {
|
|
25
|
+
"post" => {
|
|
26
|
+
"summary" => "Authenticate",
|
|
27
|
+
"tags" => ["Authentication"],
|
|
28
|
+
"description" => "Exchange email/password for a JWT. The token is returned in the `Token` response header.",
|
|
29
|
+
"requestBody" => {
|
|
30
|
+
"required" => true,
|
|
31
|
+
"content" => {
|
|
32
|
+
"application/json" => {
|
|
33
|
+
"schema" => {
|
|
34
|
+
"type" => "object",
|
|
35
|
+
"properties" => {
|
|
36
|
+
"auth" => {
|
|
37
|
+
"type" => "object",
|
|
38
|
+
"properties" => {
|
|
39
|
+
"email" => { "type" => "string", "format" => "email" },
|
|
40
|
+
"password" => { "type" => "string", "format" => "password" },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
"responses" => {
|
|
49
|
+
"200" => {
|
|
50
|
+
"description" => "Authenticated — JWT in Token header",
|
|
51
|
+
"headers" => { "Token" => { "description" => "JWT", "schema" => { "type" => "string" } } },
|
|
52
|
+
},
|
|
53
|
+
"401" => { "description" => "Unauthorized" },
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def v3_raw_sql_paths
|
|
61
|
+
response_schema = {
|
|
62
|
+
"type" => "array",
|
|
63
|
+
"items" => { "type" => "object", "additionalProperties" => true },
|
|
64
|
+
}
|
|
65
|
+
query_param = {
|
|
66
|
+
"name" => "query",
|
|
67
|
+
"in" => "query",
|
|
68
|
+
"required" => true,
|
|
69
|
+
"schema" => { "type" => "string" },
|
|
70
|
+
"example" => "SELECT id, name FROM roles LIMIT 10",
|
|
71
|
+
}
|
|
72
|
+
body_schema = {
|
|
73
|
+
"type" => "object",
|
|
74
|
+
"properties" => { "query" => { "type" => "string" } },
|
|
75
|
+
}
|
|
76
|
+
{
|
|
77
|
+
"/raw/sql" => {
|
|
78
|
+
"get" => {
|
|
79
|
+
"summary" => "Raw SQL (GET)",
|
|
80
|
+
"tags" => ["Raw"],
|
|
81
|
+
"description" => "Execute a SELECT query. Returns rows as a plain JSON array (not JSON:API).",
|
|
82
|
+
"security" => [{ "bearerAuth" => [] }],
|
|
83
|
+
"parameters" => [query_param],
|
|
84
|
+
"responses" => {
|
|
85
|
+
"200" => { "description" => "Rows", "content" => { "application/json" => { "schema" => response_schema } } },
|
|
86
|
+
"400" => { "description" => "Only SELECT statements are allowed" },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
"post" => {
|
|
90
|
+
"summary" => "Raw SQL (POST)",
|
|
91
|
+
"tags" => ["Raw"],
|
|
92
|
+
"description" => "Execute a SELECT query. Returns rows as a plain JSON array (not JSON:API).",
|
|
93
|
+
"security" => [{ "bearerAuth" => [] }],
|
|
94
|
+
"requestBody" => { "required" => true, "content" => { "application/json" => { "schema" => body_schema } } },
|
|
95
|
+
"responses" => {
|
|
96
|
+
"200" => { "description" => "Rows", "content" => { "application/json" => { "schema" => response_schema } } },
|
|
97
|
+
"400" => { "description" => "Only SELECT statements are allowed" },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def v3_info_paths
|
|
105
|
+
{
|
|
106
|
+
"/info/version" => {
|
|
107
|
+
"get" => {
|
|
108
|
+
"summary" => "Version", "tags" => ["Info"],
|
|
109
|
+
"responses" => { "200" => { "description" => "App version string" } },
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
"/info/heartbeat" => {
|
|
113
|
+
"get" => {
|
|
114
|
+
"summary" => "Heartbeat", "tags" => ["Info"],
|
|
115
|
+
"security" => [{ "bearerAuth" => [] }],
|
|
116
|
+
"responses" => { "200" => { "description" => "Renews token, returns current user as plain JSON" } },
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
"/info/roles" => {
|
|
120
|
+
"get" => {
|
|
121
|
+
"summary" => "Roles", "tags" => ["Info"],
|
|
122
|
+
"security" => [{ "bearerAuth" => [] }],
|
|
123
|
+
"responses" => { "200" => { "description" => "All roles as plain JSON array" } },
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
"/info/schema" => {
|
|
127
|
+
"get" => {
|
|
128
|
+
"summary" => "Schema", "tags" => ["Info"],
|
|
129
|
+
"security" => [{ "bearerAuth" => [] }],
|
|
130
|
+
"responses" => { "200" => { "description" => "DB schema for models the user can read" } },
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
"/info/dsl" => {
|
|
134
|
+
"get" => {
|
|
135
|
+
"summary" => "DSL", "tags" => ["Info"],
|
|
136
|
+
"security" => [{ "bearerAuth" => [] }],
|
|
137
|
+
"responses" => { "200" => { "description" => "json_attrs DSL for each model" } },
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
"/info/translations" => {
|
|
141
|
+
"get" => {
|
|
142
|
+
"summary" => "Translations", "tags" => ["Info"],
|
|
143
|
+
"security" => [{ "bearerAuth" => [] }],
|
|
144
|
+
"parameters" => [{ "name" => "locale", "in" => "query", "schema" => { "type" => "string" } }],
|
|
145
|
+
"responses" => { "200" => { "description" => "Full i18n translation tree" } },
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
"/info/settings" => {
|
|
149
|
+
"get" => {
|
|
150
|
+
"summary" => "Settings", "tags" => ["Info"],
|
|
151
|
+
"security" => [{ "bearerAuth" => [] }],
|
|
152
|
+
"responses" => { "200" => { "description" => "All ThecoreSettings::Setting values" } },
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
"/info/swagger" => {
|
|
156
|
+
"get" => {
|
|
157
|
+
"summary" => "OpenAPI v3 spec", "tags" => ["Info"],
|
|
158
|
+
"responses" => { "200" => { "description" => "This OpenAPI 3.0 specification" } },
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def v3_crud_paths(model_class)
|
|
165
|
+
resource_type = model_class.model_name.plural
|
|
166
|
+
tag = model_class.model_name.name
|
|
167
|
+
|
|
168
|
+
attrs = begin
|
|
169
|
+
props = create_properties_from_model(model_class, model_class.json_attrs || {})
|
|
170
|
+
props.reject { |k, _| k.to_s == "id" }
|
|
171
|
+
rescue
|
|
172
|
+
{}
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
writable_attrs = begin
|
|
176
|
+
create_properties_from_model(model_class, {}, true)
|
|
177
|
+
rescue
|
|
178
|
+
{}
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
resource_schema = {
|
|
182
|
+
"type" => "object",
|
|
183
|
+
"properties" => {
|
|
184
|
+
"id" => { "type" => "string" },
|
|
185
|
+
"type" => { "type" => "string", "example" => resource_type },
|
|
186
|
+
"attributes" => { "type" => "object", "properties" => attrs },
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
collection_schema = {
|
|
190
|
+
"type" => "object",
|
|
191
|
+
"properties" => {
|
|
192
|
+
"data" => { "type" => "array", "items" => resource_schema },
|
|
193
|
+
"meta" => { "type" => "object", "properties" => { "total" => { "type" => "integer" } } },
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
single_schema = { "type" => "object", "properties" => { "data" => resource_schema } }
|
|
197
|
+
request_schema = {
|
|
198
|
+
"type" => "object",
|
|
199
|
+
"properties" => {
|
|
200
|
+
"data" => {
|
|
201
|
+
"type" => "object",
|
|
202
|
+
"properties" => {
|
|
203
|
+
"type" => { "type" => "string", "example" => resource_type },
|
|
204
|
+
"attributes" => { "type" => "object", "properties" => writable_attrs },
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
filter_params = begin
|
|
211
|
+
model_class.ransackable_attributes.map do |attr|
|
|
212
|
+
{ "name" => "filter[#{attr}]", "in" => "query", "schema" => { "type" => "string" } }
|
|
213
|
+
end
|
|
214
|
+
rescue
|
|
215
|
+
[]
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
id_param = { "name" => "id", "in" => "path", "required" => true, "schema" => { "type" => "integer" } }
|
|
219
|
+
sort_param = { "name" => "sort", "in" => "query", "schema" => { "type" => "string" }, "description" => "field or -field (descending), comma-separated" }
|
|
220
|
+
page_params = [
|
|
221
|
+
{ "name" => "page[number]", "in" => "query", "schema" => { "type" => "integer" } },
|
|
222
|
+
{ "name" => "page[size]", "in" => "query", "schema" => { "type" => "integer" } },
|
|
223
|
+
]
|
|
224
|
+
include_param = { "name" => "include", "in" => "query", "schema" => { "type" => "string" }, "description" => "Associations to sideload (empty to suppress defaults)" }
|
|
225
|
+
fields_param = { "name" => "fields[#{resource_type}]", "in" => "query", "schema" => { "type" => "string" }, "description" => "Sparse fieldsets" }
|
|
226
|
+
|
|
227
|
+
vnd = "application/vnd.api+json"
|
|
228
|
+
|
|
229
|
+
{
|
|
230
|
+
"/#{resource_type}" => {
|
|
231
|
+
"get" => {
|
|
232
|
+
"summary" => "Index #{tag}",
|
|
233
|
+
"tags" => [tag],
|
|
234
|
+
"security" => [{ "bearerAuth" => [] }],
|
|
235
|
+
"parameters" => [*page_params, *filter_params, sort_param, include_param, fields_param],
|
|
236
|
+
"responses" => {
|
|
237
|
+
"200" => { "description" => "Collection", "content" => { vnd => { "schema" => collection_schema } } },
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
"post" => {
|
|
241
|
+
"summary" => "Create #{tag}",
|
|
242
|
+
"tags" => [tag],
|
|
243
|
+
"security" => [{ "bearerAuth" => [] }],
|
|
244
|
+
"requestBody" => { "required" => true, "content" => { vnd => { "schema" => request_schema } } },
|
|
245
|
+
"responses" => {
|
|
246
|
+
"201" => { "description" => "Created", "content" => { vnd => { "schema" => single_schema } } },
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
"/#{resource_type}/{id}" => {
|
|
251
|
+
"get" => {
|
|
252
|
+
"summary" => "Show #{tag}",
|
|
253
|
+
"tags" => [tag],
|
|
254
|
+
"security" => [{ "bearerAuth" => [] }],
|
|
255
|
+
"parameters" => [id_param, include_param, fields_param],
|
|
256
|
+
"responses" => {
|
|
257
|
+
"200" => { "description" => "Resource", "content" => { vnd => { "schema" => single_schema } } },
|
|
258
|
+
"404" => { "description" => "Not found" },
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
"patch" => {
|
|
262
|
+
"summary" => "Update #{tag}",
|
|
263
|
+
"tags" => [tag],
|
|
264
|
+
"security" => [{ "bearerAuth" => [] }],
|
|
265
|
+
"parameters" => [id_param],
|
|
266
|
+
"requestBody" => { "required" => true, "content" => { vnd => { "schema" => request_schema } } },
|
|
267
|
+
"responses" => {
|
|
268
|
+
"200" => { "description" => "Updated", "content" => { vnd => { "schema" => single_schema } } },
|
|
269
|
+
"404" => { "description" => "Not found" },
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
"delete" => {
|
|
273
|
+
"summary" => "Destroy #{tag}",
|
|
274
|
+
"tags" => [tag],
|
|
275
|
+
"security" => [{ "bearerAuth" => [] }],
|
|
276
|
+
"parameters" => [id_param],
|
|
277
|
+
"responses" => {
|
|
278
|
+
"204" => { "description" => "No Content" },
|
|
279
|
+
"404" => { "description" => "Not found" },
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
}
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def v3_custom_action_paths(model_class)
|
|
287
|
+
paths = {}
|
|
288
|
+
resource_type = model_class.model_name.plural
|
|
289
|
+
tag = model_class.model_name.name
|
|
290
|
+
custom_actions = ("Endpoints::#{tag}".constantize.instance_methods(false) rescue [])
|
|
291
|
+
custom_actions.each do |action|
|
|
292
|
+
definition = ("Endpoints::#{tag}".constantize.definitions[tag][action.to_sym] rescue nil)
|
|
293
|
+
next unless definition
|
|
294
|
+
definition.each { |_verb, spec| spec[:tags] = [tag] if spec.is_a?(Hash) }
|
|
295
|
+
has_id = definition.any? { |_v, spec| spec.is_a?(Hash) && spec[:parameters]&.any? { |p| p[:in] == "path" } }
|
|
296
|
+
path = "/#{resource_type}/custom_action/#{action}#{has_id ? "/{id}" : ""}"
|
|
297
|
+
paths[path] = definition
|
|
298
|
+
end
|
|
299
|
+
paths
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def v3_description
|
|
303
|
+
<<~MD
|
|
304
|
+
## API v3 — JSON:API
|
|
305
|
+
|
|
306
|
+
All resource endpoints follow the [JSON:API 1.0](https://jsonapi.org) specification.
|
|
307
|
+
|
|
308
|
+
### Authentication
|
|
309
|
+
|
|
310
|
+
`POST /authenticate` → JWT in `Token` response header. Pass as `Authorization: Bearer <token>`.
|
|
311
|
+
Every successful request renews the token — always read and store the new `Token` header.
|
|
312
|
+
|
|
313
|
+
### Content negotiation
|
|
314
|
+
|
|
315
|
+
`Accept: application/vnd.api+json` (GET) · `Content-Type: application/vnd.api+json` (write requests).
|
|
316
|
+
|
|
317
|
+
### Filtering
|
|
318
|
+
|
|
319
|
+
`?filter[field]=value` — validated against each model's `ransackable_attributes`.
|
|
320
|
+
|
|
321
|
+
### Sorting
|
|
322
|
+
|
|
323
|
+
`?sort=field` (asc) · `?sort=-field` (desc) · `?sort=field1,-field2` (multi-field).
|
|
324
|
+
|
|
325
|
+
### Pagination
|
|
326
|
+
|
|
327
|
+
`?page[number]=N&page[size]=N` — response includes `meta.total`.
|
|
328
|
+
|
|
329
|
+
### Sparse fieldsets
|
|
330
|
+
|
|
331
|
+
`?fields[type]=field1,field2` — restrict attributes returned per resource type.
|
|
332
|
+
|
|
333
|
+
### Sideloading
|
|
334
|
+
|
|
335
|
+
Default sideloads come from each model's `json_attrs[:include]`.
|
|
336
|
+
Override with `?include=assoc1,assoc2` · suppress all with `?include=` (empty string).
|
|
337
|
+
|
|
338
|
+
### Info & utility endpoints
|
|
339
|
+
|
|
340
|
+
`GET /info/version|heartbeat|roles|schema|dsl|translations|settings|swagger` — return plain JSON (not JSON:API).
|
|
341
|
+
|
|
342
|
+
### Raw SQL escape hatch
|
|
343
|
+
|
|
344
|
+
`GET|POST /raw/sql` — SELECT-only; returns plain JSON array (not JSON:API).
|
|
345
|
+
MD
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Api
|
|
2
|
+
ResourceAttributeSet = Struct.new(:attributes, :methods_list, :includes) do
|
|
3
|
+
def self.for(model_class, jattrs: nil)
|
|
4
|
+
jattrs ||= model_class.respond_to?(:json_attrs) ? (model_class.json_attrs || {}) : {}
|
|
5
|
+
only = Array(jattrs[:only]).map(&:to_sym)
|
|
6
|
+
if only.empty?
|
|
7
|
+
except = Array(jattrs[:except]).map(&:to_sym)
|
|
8
|
+
only = model_class.column_names.map(&:to_sym) - except
|
|
9
|
+
end
|
|
10
|
+
new(only.reject { |a| a == :id }, Array(jattrs[:methods]).map(&:to_sym), jattrs[:include])
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Parse json_attrs[:include] into { assoc_name => spec_or_nil }.
|
|
14
|
+
# Handles both symbol items (:roles) and hash items (users: { only: [:id] }).
|
|
15
|
+
def parsed_includes
|
|
16
|
+
return {} unless includes
|
|
17
|
+
Array(includes).each_with_object({}) do |item, hash|
|
|
18
|
+
case item
|
|
19
|
+
when Hash then item.each { |k, v| hash[k] = v }
|
|
20
|
+
when Symbol then hash[item] = nil
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module Api
|
|
2
|
+
module V3
|
|
3
|
+
class SerializerFactory
|
|
4
|
+
# Generate (and cache) a JSONAPI::Serializer subclass for model_class.
|
|
5
|
+
#
|
|
6
|
+
# nested_attrs: when set, use these attrs instead of model_class.json_attrs.
|
|
7
|
+
# Used for included associations — avoids reading the associated model's own
|
|
8
|
+
# json_attrs and prevents infinite recursion.
|
|
9
|
+
# parent_name: when set, the generated constant is named
|
|
10
|
+
# "#{model_class.name}For#{parent_name}Serializer" and include: is NOT
|
|
11
|
+
# processed (one level deep only).
|
|
12
|
+
def self.serializer_for(model_class, nested_attrs: nil, parent_name: nil)
|
|
13
|
+
const_name = parent_name ? "#{model_class.name}For#{parent_name}Serializer" : "#{model_class.name}Serializer"
|
|
14
|
+
return Api::V3.const_get(const_name) if Api::V3.const_defined?(const_name)
|
|
15
|
+
|
|
16
|
+
jattrs = nested_attrs || (model_class.respond_to?(:json_attrs) ? (model_class.json_attrs || {}) : {})
|
|
17
|
+
attr_set = Api::ResourceAttributeSet.for(model_class, jattrs: jattrs)
|
|
18
|
+
|
|
19
|
+
# Nested serializers are always flat — no recursive includes — to prevent
|
|
20
|
+
# infinite loops on circular associations (e.g. Role↔User).
|
|
21
|
+
includes_map = parent_name ? {} : attr_set.parsed_includes
|
|
22
|
+
|
|
23
|
+
type = model_class.model_name.plural.to_sym
|
|
24
|
+
|
|
25
|
+
reflections = model_class.reflect_on_all_associations.each_with_object({}) { |r, h| h[r.name] = r }
|
|
26
|
+
nested_info = includes_map.each_with_object({}) do |(assoc_name, assoc_spec), hash|
|
|
27
|
+
reflection = reflections[assoc_name]
|
|
28
|
+
next unless reflection
|
|
29
|
+
nested_ser = assoc_spec \
|
|
30
|
+
? serializer_for(reflection.klass, nested_attrs: assoc_spec, parent_name: model_class.name) \
|
|
31
|
+
: serializer_for(reflection.klass)
|
|
32
|
+
hash[assoc_name] = { serializer: nested_ser, macro: reflection.macro }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
klass = Class.new do
|
|
36
|
+
include JSONAPI::Serializer
|
|
37
|
+
set_type type
|
|
38
|
+
attributes(*attr_set.attributes) if attr_set.attributes.any?
|
|
39
|
+
|
|
40
|
+
attr_set.methods_list.each do |method_name|
|
|
41
|
+
attribute(method_name) { |object| object.send(method_name) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
nested_info.each do |assoc_name, info|
|
|
45
|
+
ser = info[:serializer]
|
|
46
|
+
case info[:macro]
|
|
47
|
+
when :has_many then has_many assoc_name, serializer: ser
|
|
48
|
+
when :has_one then has_one assoc_name, serializer: ser
|
|
49
|
+
when :belongs_to then belongs_to assoc_name, serializer: ser
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
Api::V3.const_set(const_name, klass)
|
|
55
|
+
klass
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Parse json_attrs[:include] into { assoc_name => spec_or_nil }.
|
|
59
|
+
# Delegates to Api::ResourceAttributeSet#parsed_includes.
|
|
60
|
+
def self.extract_includes(include_spec)
|
|
61
|
+
Api::ResourceAttributeSet.new([], [], include_spec).parsed_includes
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -44,7 +44,10 @@ module ApiExceptionManagement
|
|
|
44
44
|
|
|
45
45
|
def api_error(status: 501, errors: [])
|
|
46
46
|
# puts errors.full_messages if !Rails.env.production? && errors.respond_to?(:full_messages)
|
|
47
|
-
|
|
47
|
+
if errors.blank?
|
|
48
|
+
head status
|
|
49
|
+
return
|
|
50
|
+
end
|
|
48
51
|
|
|
49
52
|
# For retrocompatibility, I try to send back only strings, as errors
|
|
50
53
|
errors_response = if errors.respond_to?(:full_messages)
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
module ModelDrivenApi
|
|
2
2
|
class Engine < ::Rails::Engine
|
|
3
3
|
# appending migrations to the main app's ones
|
|
4
|
+
initializer :register_json_api_mime_type do
|
|
5
|
+
Mime::Type.register "application/vnd.api+json", :json_api unless Mime[:json_api]
|
|
6
|
+
ActionDispatch::Request.parameter_parsers[:json_api] =
|
|
7
|
+
ActionDispatch::Request.parameter_parsers[:json]
|
|
8
|
+
end
|
|
9
|
+
|
|
4
10
|
initializer :append_migrations do |app|
|
|
5
|
-
unless app.root.to_s
|
|
11
|
+
unless app.root.to_s == root.to_s
|
|
6
12
|
config.paths["db/migrate"].expanded.each do |expanded_path|
|
|
7
13
|
app.config.paths["db/migrate"] << expanded_path
|
|
8
14
|
end
|
data/lib/model_driven_api.rb
CHANGED
|
@@ -6,6 +6,7 @@ require 'ransack'
|
|
|
6
6
|
require 'jwt'
|
|
7
7
|
require 'json_web_token'
|
|
8
8
|
require "kaminari"
|
|
9
|
+
require "pagy"
|
|
9
10
|
# require "multi_json"
|
|
10
11
|
require "simple_command"
|
|
11
12
|
|
|
@@ -16,6 +17,13 @@ require 'deep_merge/rails_compat'
|
|
|
16
17
|
require "model_driven_api/engine"
|
|
17
18
|
|
|
18
19
|
require "safe_sql_executor"
|
|
20
|
+
require "api/resource_attribute_set"
|
|
21
|
+
require "api/model_resolver"
|
|
22
|
+
require "api/custom_action_dispatcher"
|
|
23
|
+
require "api/open_api/base"
|
|
24
|
+
require "api/open_api/v2"
|
|
25
|
+
require "api/open_api/v3"
|
|
26
|
+
require "api/v3/serializer_factory"
|
|
19
27
|
|
|
20
28
|
module ModelDrivenApi
|
|
21
29
|
def self.smart_merge src, dest
|
data/lib/non_crud_endpoints.rb
CHANGED
|
@@ -2,6 +2,24 @@ class NonCrudEndpoints
|
|
|
2
2
|
attr_accessor :result
|
|
3
3
|
cattr_accessor :definitions
|
|
4
4
|
self.definitions = {}
|
|
5
|
+
|
|
6
|
+
# Registry of actions that do not require authentication.
|
|
7
|
+
# Populated by subclasses via `self.public_action :action_name`.
|
|
8
|
+
# Used by CustomActionDispatcher and ApplicationController to skip
|
|
9
|
+
# authenticate_request for these endpoints.
|
|
10
|
+
cattr_accessor :public_action_registry
|
|
11
|
+
self.public_action_registry = {}
|
|
12
|
+
|
|
13
|
+
def self.public_action(action_name)
|
|
14
|
+
self.public_action_registry[name] ||= []
|
|
15
|
+
self.public_action_registry[name] << action_name.to_sym
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.public_action?(model_name, action_name)
|
|
19
|
+
registry = public_action_registry["Endpoints::#{model_name}"] || []
|
|
20
|
+
registry.include?(action_name.to_sym)
|
|
21
|
+
end
|
|
22
|
+
|
|
5
23
|
# Add a validation method which will be inherited by all the instances, and automatically run before any method call
|
|
6
24
|
def initialize(m, params)
|
|
7
25
|
# Rails.logger.debug "Initializing NonCrudEndpoints"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: model_driven_api
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.7.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gabriele Tassoni
|
|
@@ -29,14 +29,14 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '
|
|
32
|
+
version: '3.0'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '
|
|
39
|
+
version: '3.0'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: simple_command
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -94,19 +94,75 @@ dependencies:
|
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
95
|
version: '1.2'
|
|
96
96
|
- !ruby/object:Gem::Dependency
|
|
97
|
-
name:
|
|
97
|
+
name: jsonapi-serializer
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '2.2'
|
|
103
|
+
type: :runtime
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '2.2'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: pagy
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '9.0'
|
|
117
|
+
type: :runtime
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '9.0'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: pg
|
|
98
126
|
requirement: !ruby/object:Gem::Requirement
|
|
99
127
|
requirements:
|
|
100
|
-
- - "
|
|
128
|
+
- - "~>"
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '1.1'
|
|
131
|
+
type: :development
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - "~>"
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '1.1'
|
|
138
|
+
- !ruby/object:Gem::Dependency
|
|
139
|
+
name: rspec-rails
|
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - "~>"
|
|
101
143
|
- !ruby/object:Gem::Version
|
|
102
|
-
version: '0'
|
|
144
|
+
version: '7.0'
|
|
103
145
|
type: :development
|
|
104
146
|
prerelease: false
|
|
105
147
|
version_requirements: !ruby/object:Gem::Requirement
|
|
106
148
|
requirements:
|
|
107
|
-
- - "
|
|
149
|
+
- - "~>"
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: '7.0'
|
|
152
|
+
- !ruby/object:Gem::Dependency
|
|
153
|
+
name: factory_bot_rails
|
|
154
|
+
requirement: !ruby/object:Gem::Requirement
|
|
155
|
+
requirements:
|
|
156
|
+
- - "~>"
|
|
157
|
+
- !ruby/object:Gem::Version
|
|
158
|
+
version: '6.4'
|
|
159
|
+
type: :development
|
|
160
|
+
prerelease: false
|
|
161
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
162
|
+
requirements:
|
|
163
|
+
- - "~>"
|
|
108
164
|
- !ruby/object:Gem::Version
|
|
109
|
-
version: '
|
|
165
|
+
version: '6.4'
|
|
110
166
|
description: Ruby on Rails REST APIs built by convention using the DB schema as the
|
|
111
167
|
foundation, please see README for mode of use.
|
|
112
168
|
email:
|
|
@@ -126,6 +182,13 @@ files:
|
|
|
126
182
|
- app/controllers/api/v2/info_controller.rb
|
|
127
183
|
- app/controllers/api/v2/raw_controller.rb
|
|
128
184
|
- app/controllers/api/v2/users_controller.rb
|
|
185
|
+
- app/controllers/api/v3/application_controller.rb
|
|
186
|
+
- app/controllers/api/v3/auth/oauth_controller.rb
|
|
187
|
+
- app/controllers/api/v3/authentication_controller.rb
|
|
188
|
+
- app/controllers/api/v3/info_controller.rb
|
|
189
|
+
- app/controllers/api/v3/raw_controller.rb
|
|
190
|
+
- app/controllers/api/v3/users_controller.rb
|
|
191
|
+
- app/models/endpoints/push_subscriber.rb
|
|
129
192
|
- app/models/endpoints/test_api.rb
|
|
130
193
|
- app/models/test_api.rb
|
|
131
194
|
- app/models/used_token.rb
|
|
@@ -138,6 +201,13 @@ files:
|
|
|
138
201
|
- config/routes.rb
|
|
139
202
|
- db/migrate/20210519145438_create_used_tokens.rb
|
|
140
203
|
- db/migrate/20210528111450_rename_valid_to_is_valid_in_used_token.rb
|
|
204
|
+
- lib/api/custom_action_dispatcher.rb
|
|
205
|
+
- lib/api/model_resolver.rb
|
|
206
|
+
- lib/api/open_api/base.rb
|
|
207
|
+
- lib/api/open_api/v2.rb
|
|
208
|
+
- lib/api/open_api/v3.rb
|
|
209
|
+
- lib/api/resource_attribute_set.rb
|
|
210
|
+
- lib/api/v3/serializer_factory.rb
|
|
141
211
|
- lib/concerns/api_exception_management.rb
|
|
142
212
|
- lib/concerns/model_driven_api_application_record.rb
|
|
143
213
|
- lib/concerns/model_driven_api_role.rb
|