model_driven_api 3.7.4 → 3.9.0
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d0955834f57dc25b51a326caf3a64d2da728c635eacb2b01cf1e9f656e559ea5
|
|
4
|
+
data.tar.gz: 677af97d19ed54c6cba486f0d99bce4736e4e06e3f916c4d0c896a13eef8c865
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 07f3a5338e772e90baf5687dc38b6b038b55b97da94bae909d544408767b67927da667507f3e84e52a80f4cc7ff26c275757d9700f1bbfb0ce8914784da206fd
|
|
7
|
+
data.tar.gz: ec926ebae3447ad6ba9f8231dd60b2992f75b2838b2b49be14378390fe63f99b0af3085f9759ea0e2d580477463fa10d092b651dc29452597f0801c420b4eb18
|
data/README.md
CHANGED
|
@@ -449,6 +449,22 @@ When composing `json_attrs` across multiple concerns, use `ModelDrivenApi.smart_
|
|
|
449
449
|
self.json_attrs = ModelDrivenApi.smart_merge((json_attrs || {}), { only: [:id, :name] })
|
|
450
450
|
```
|
|
451
451
|
|
|
452
|
+
### Default `json_attrs` — no `Api::ModelName` concern required
|
|
453
|
+
|
|
454
|
+
A model doesn't need an explicit `Api::ModelName` concern just to get a working API shape.
|
|
455
|
+
`ModelDrivenApiDefaultJsonAttrs` is registered into
|
|
456
|
+
[`ThecoreBackendCommons::DefaultModuleRegistry`](../thecore_backend_commons/README.md#defaultmoduleregistry--shared-applicationrecordinherited-hook)
|
|
457
|
+
and automatically `include`d into every `ApplicationRecord` subclass as it's defined, setting
|
|
458
|
+
`self.json_attrs = { except: [] }` (every column, no `methods`, no sideloaded `include`) — the
|
|
459
|
+
simplest safe default. A model with its own `Api::ModelName` concern (or `ModelDrivenApiUser`/
|
|
460
|
+
`ModelDrivenApiRole`) is unaffected: that concern's `include` always runs after the default and
|
|
461
|
+
freely overrides or merges on top of it via `ModelDrivenApi.smart_merge`. Only a model with no
|
|
462
|
+
concern at all keeps the bare default.
|
|
463
|
+
|
|
464
|
+
**Temporary dependency note**: this currently requires `thecore_backend_commons` from a Gemfile
|
|
465
|
+
`git:` pin to its `release/3` branch, since the RubyGems release hasn't caught up with the
|
|
466
|
+
registry yet — see this repo's `CLAUDE.md` for the exact commit/removal condition.
|
|
467
|
+
|
|
452
468
|
---
|
|
453
469
|
|
|
454
470
|
## Raw SQL endpoint
|
|
@@ -57,30 +57,45 @@ class Endpoints::PushSubscriber < NonCrudEndpoints
|
|
|
57
57
|
|
|
58
58
|
self.desc 'PushSubscriber', :send_push, {
|
|
59
59
|
post: {
|
|
60
|
-
summary: 'Send
|
|
60
|
+
summary: 'Send Web Push to one subscriber (push_subscriber_id) or many (push_subscriber_ids array, async).',
|
|
61
61
|
parameters: [],
|
|
62
62
|
responses: {
|
|
63
|
-
'201' => { description: '
|
|
63
|
+
'201' => { description: 'Single: message JSON. Bulk: { created: [...], failed: [...] }' },
|
|
64
64
|
'422' => { description: 'Validation error' },
|
|
65
|
-
'404' => { description: 'Subscriber not found' }
|
|
65
|
+
'404' => { description: 'Subscriber not found (single mode only)' }
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
def send_push(params)
|
|
71
|
-
|
|
72
|
-
return [{ error: '
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
)
|
|
79
|
-
unless message.save
|
|
80
|
-
return [{ error: message.errors.full_messages.join(', ') }, 422]
|
|
71
|
+
return [{ error: 'title is required' }, 422] if params[:title].blank?
|
|
72
|
+
return [{ error: 'body is required' }, 422] if params[:body].blank?
|
|
73
|
+
|
|
74
|
+
if params[:push_subscriber_ids].present?
|
|
75
|
+
send_push_bulk(params)
|
|
76
|
+
else
|
|
77
|
+
send_push_single(params)
|
|
81
78
|
end
|
|
82
|
-
|
|
83
|
-
[message,
|
|
79
|
+
rescue => e
|
|
80
|
+
[{ error: e.message }, 500]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
self.desc 'PushSubscriber', :broadcast_push, {
|
|
84
|
+
post: {
|
|
85
|
+
summary: 'Send Web Push to all active subscribers.',
|
|
86
|
+
parameters: [],
|
|
87
|
+
responses: {
|
|
88
|
+
'201' => { description: '{ enqueued: N } — number of jobs enqueued' },
|
|
89
|
+
'422' => { description: 'Validation error (title or body missing)' }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
def broadcast_push(params)
|
|
95
|
+
return [{ error: 'title is required' }, 422] if params[:title].blank?
|
|
96
|
+
return [{ error: 'body is required' }, 422] if params[:body].blank?
|
|
97
|
+
|
|
98
|
+
broadcast_push_all(params)
|
|
84
99
|
rescue => e
|
|
85
100
|
[{ error: e.message }, 500]
|
|
86
101
|
end
|
|
@@ -103,8 +118,89 @@ class Endpoints::PushSubscriber < NonCrudEndpoints
|
|
|
103
118
|
now = Time.current
|
|
104
119
|
message.update!(received_at: now) if params[:received] && message.received_at.nil?
|
|
105
120
|
message.update!(read_at: now) if params[:read] && message.read_at.nil?
|
|
106
|
-
[message, 200]
|
|
121
|
+
[message.as_json(PushMessage.json_attrs), 200]
|
|
107
122
|
rescue ActiveRecord::RecordInvalid => e
|
|
108
123
|
[{ error: e.message }, 422]
|
|
109
124
|
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def send_push_single(params) # rubocop:disable Metrics/MethodLength
|
|
129
|
+
subscriber = PushSubscriber.active.find_by(id: params[:push_subscriber_id])
|
|
130
|
+
return [{ error: 'Subscriber not found' }, 404] unless subscriber
|
|
131
|
+
|
|
132
|
+
message = subscriber.push_messages.build(
|
|
133
|
+
title: params[:title],
|
|
134
|
+
body: params[:body],
|
|
135
|
+
url: params[:url],
|
|
136
|
+
icon: params[:icon]
|
|
137
|
+
)
|
|
138
|
+
return [{ error: message.errors.full_messages.join(', ') }, 422] unless message.save
|
|
139
|
+
|
|
140
|
+
ThecoreBackendCommons::PushNotificationService.dispatch(subscriber, message)
|
|
141
|
+
[message.as_json(PushMessage.json_attrs), 201]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def broadcast_push_all(params) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
145
|
+
active_ids = PushSubscriber.active.pluck(:id)
|
|
146
|
+
return [{ enqueued: 0 }, 201] if active_ids.empty?
|
|
147
|
+
|
|
148
|
+
now = Time.current
|
|
149
|
+
records = active_ids.map do |sub_id|
|
|
150
|
+
{
|
|
151
|
+
push_subscriber_id: sub_id,
|
|
152
|
+
title: params[:title],
|
|
153
|
+
body: params[:body],
|
|
154
|
+
url: params[:url].presence,
|
|
155
|
+
icon: params[:icon].presence,
|
|
156
|
+
message_type: params[:message_type].presence || 'communication',
|
|
157
|
+
sender_user_id: params[:current_user_id].presence,
|
|
158
|
+
created_at: now,
|
|
159
|
+
updated_at: now
|
|
160
|
+
}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
returning_cols = %w[id push_subscriber_id title body url icon
|
|
164
|
+
message_type sent_at received_at read_at
|
|
165
|
+
created_at updated_at sender_user_id]
|
|
166
|
+
result = PushMessage.insert_all(records, returning: returning_cols)
|
|
167
|
+
|
|
168
|
+
created = result.to_a
|
|
169
|
+
created.each { |r| PushDispatchJob.perform_later(r['id']) }
|
|
170
|
+
|
|
171
|
+
[{ enqueued: created.length }, 201]
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def send_push_bulk(params) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
175
|
+
ids = Array(params[:push_subscriber_ids]).map(&:to_i).uniq
|
|
176
|
+
valid_ids = PushSubscriber.active.where(id: ids).pluck(:id)
|
|
177
|
+
failed = ids - valid_ids
|
|
178
|
+
|
|
179
|
+
return [{ created: [], failed: failed }, 201] if valid_ids.empty?
|
|
180
|
+
|
|
181
|
+
now = Time.current
|
|
182
|
+
records = valid_ids.map do |sub_id|
|
|
183
|
+
{
|
|
184
|
+
push_subscriber_id: sub_id,
|
|
185
|
+
title: params[:title],
|
|
186
|
+
body: params[:body],
|
|
187
|
+
url: params[:url].presence,
|
|
188
|
+
icon: params[:icon].presence,
|
|
189
|
+
message_type: params[:message_type].presence || 'communication',
|
|
190
|
+
sender_user_id: params[:sender_user_id].presence,
|
|
191
|
+
created_at: now,
|
|
192
|
+
updated_at: now
|
|
193
|
+
}
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
returning_cols = %w[id push_subscriber_id title body url icon
|
|
197
|
+
message_type sent_at received_at read_at
|
|
198
|
+
created_at updated_at sender_user_id]
|
|
199
|
+
result = PushMessage.insert_all(records, returning: returning_cols)
|
|
200
|
+
|
|
201
|
+
created = result.to_a
|
|
202
|
+
created.each { |r| PushDispatchJob.perform_later(r['id']) }
|
|
203
|
+
|
|
204
|
+
[{ created: created, failed: failed }, 201]
|
|
205
|
+
end
|
|
110
206
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require "concerns/model_driven_api_default_json_attrs"
|
|
2
|
+
|
|
3
|
+
# Registers the generic default `json_attrs` module (ADR 0001) into
|
|
4
|
+
# ThecoreBackendCommons's shared DefaultModuleRegistry so it is `include`d
|
|
5
|
+
# into every `ApplicationRecord` subclass as it is defined.
|
|
6
|
+
#
|
|
7
|
+
# Installed from `config.to_prepare` -- not `config.after_initialize` -- for
|
|
8
|
+
# the same reason `ThecoreBackendCommons::DefaultModuleRegistry.install!` is:
|
|
9
|
+
# Rails runs `to_prepare` callbacks *before* `eager_load!`, so the module must
|
|
10
|
+
# already be registered before eager loading defines every model class in
|
|
11
|
+
# production. `to_prepare` also re-runs on every class reload in development;
|
|
12
|
+
# `DefaultModuleRegistry.register` is idempotent for the same module object,
|
|
13
|
+
# so re-running this block on reload is safe.
|
|
14
|
+
Rails.application.configure do
|
|
15
|
+
config.to_prepare do
|
|
16
|
+
ThecoreBackendCommons::DefaultModuleRegistry.register(ModelDrivenApiDefaultJsonAttrs)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Registered into ThecoreBackendCommons::DefaultModuleRegistry (see
|
|
2
|
+
# config/initializers/default_json_attrs_registration.rb) so it is
|
|
3
|
+
# automatically `include`d into *every* `ApplicationRecord` subclass at
|
|
4
|
+
# class-definition time -- not just the handful of named classes
|
|
5
|
+
# (ModelDrivenApiUser, ModelDrivenApiRole, ...) that get a bespoke concern.
|
|
6
|
+
#
|
|
7
|
+
# See vendor/external/thecore/docs/adr/0001-application-record-defaults-over-generated-concerns.md
|
|
8
|
+
# in the host app for the design rationale: default model behavior should
|
|
9
|
+
# not require a generated per-model `Api::ModelName` concern file for the
|
|
10
|
+
# no-customization case.
|
|
11
|
+
#
|
|
12
|
+
# A model that *does* need custom serialization still gets (or keeps) an
|
|
13
|
+
# explicit `Api::ModelName` concern, `include`d directly in the model file
|
|
14
|
+
# exactly as today -- because `ApplicationRecord.inherited` (which applies
|
|
15
|
+
# this default) fires *before* the subclass's own body executes, that later
|
|
16
|
+
# explicit `include` always runs after this default has already set
|
|
17
|
+
# `json_attrs`, so it freely overrides (or, via `ModelDrivenApi.smart_merge`,
|
|
18
|
+
# merges on top of) the default's value. See `ModelDrivenApiUser`/
|
|
19
|
+
# `ModelDrivenApiRole` for that pattern.
|
|
20
|
+
module ModelDrivenApiDefaultJsonAttrs
|
|
21
|
+
extend ActiveSupport::Concern
|
|
22
|
+
|
|
23
|
+
included do
|
|
24
|
+
## DSL (AKA what to show in the returned JSON)
|
|
25
|
+
# Use self.json_attrs to drive json rendering for
|
|
26
|
+
# API model responses (index, show and update ones).
|
|
27
|
+
# For reference:
|
|
28
|
+
# https://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
|
|
29
|
+
# The object passed accepts only these keys:
|
|
30
|
+
# - only: list [] of model field names in symbol notation to be shown in JSON
|
|
31
|
+
# serialization.
|
|
32
|
+
# - except: exclude these fields from the JSON serialization, is a list []
|
|
33
|
+
# of model field names in symbol notation.
|
|
34
|
+
# - methods: include the result of some methods defined in the model (virtual
|
|
35
|
+
# fields).
|
|
36
|
+
# - include: include associated models, it's a list [] of hashes {} which also
|
|
37
|
+
# accepts the [:only, :except, :methods, :include] keys.
|
|
38
|
+
#
|
|
39
|
+
# Default shape: no `only`/`except` restriction beyond the empty array
|
|
40
|
+
# below (so every column is serialized), no `methods`, no `include` (no
|
|
41
|
+
# associations sideloaded) -- the simplest safe default for a model that
|
|
42
|
+
# has no customization needs. `cattr_accessor` (not a plain method
|
|
43
|
+
# definition) is required so `json_attrs` lands as an *own* method on
|
|
44
|
+
# each concrete model class -- `Api::V2::InfoController#schema`/`#dsl`
|
|
45
|
+
# check `instance_methods(false).include?(:json_attrs)` while walking
|
|
46
|
+
# `ApplicationRecord.subclasses`, and a merely-inherited method would
|
|
47
|
+
# silently fail that check and drop the model from introspection output.
|
|
48
|
+
cattr_accessor :json_attrs
|
|
49
|
+
self.json_attrs = { except: [] }
|
|
50
|
+
end
|
|
51
|
+
end
|
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.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gabriele Tassoni
|
|
@@ -195,6 +195,7 @@ files:
|
|
|
195
195
|
- config/initializers/after_initialize_for_model_driven_api.rb
|
|
196
196
|
- config/initializers/auto_include_json.rb
|
|
197
197
|
- config/initializers/cors_api_thecore.rb
|
|
198
|
+
- config/initializers/default_json_attrs_registration.rb
|
|
198
199
|
- config/initializers/knock.rb
|
|
199
200
|
- config/initializers/time_with_zone.rb
|
|
200
201
|
- config/initializers/wrap_parameters.rb
|
|
@@ -210,6 +211,7 @@ files:
|
|
|
210
211
|
- lib/api/v3/serializer_factory.rb
|
|
211
212
|
- lib/concerns/api_exception_management.rb
|
|
212
213
|
- lib/concerns/model_driven_api_application_record.rb
|
|
214
|
+
- lib/concerns/model_driven_api_default_json_attrs.rb
|
|
213
215
|
- lib/concerns/model_driven_api_push_message.rb
|
|
214
216
|
- lib/concerns/model_driven_api_push_subscriber.rb
|
|
215
217
|
- lib/concerns/model_driven_api_role.rb
|