smriti 0.5.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +168 -0
- data/Rakefile +15 -0
- data/app/assets/images/smriti/android-chrome-192x192.png +0 -0
- data/app/assets/images/smriti/android-chrome-512x512.png +0 -0
- data/app/assets/images/smriti/apple-touch-icon.png +0 -0
- data/app/assets/images/smriti/favicon-16x16.png +0 -0
- data/app/assets/images/smriti/favicon-32x32.png +0 -0
- data/app/assets/images/smriti/favicon-48x48.png +0 -0
- data/app/assets/images/smriti/favicon.ico +0 -0
- data/app/assets/images/smriti/favicon.svg +18 -0
- data/app/assets/images/smriti/logo.svg +18 -0
- data/app/assets/images/smriti/mask-icon.svg +5 -0
- data/app/assets/stylesheets/smriti/application.css +1040 -0
- data/app/controllers/smriti/admin/application_controller.rb +135 -0
- data/app/controllers/smriti/admin/dashboard_controller.rb +32 -0
- data/app/controllers/smriti/admin/mat_view_definitions_controller.rb +372 -0
- data/app/controllers/smriti/admin/mat_view_runs_controller.rb +185 -0
- data/app/controllers/smriti/admin/preferences_controller.rb +91 -0
- data/app/helpers/smriti/admin/datatable_helper.rb +249 -0
- data/app/helpers/smriti/admin/localized_digit_helper.rb +70 -0
- data/app/helpers/smriti/admin/ui_helper.rb +539 -0
- data/app/javascript/smriti/application.js +8 -0
- data/app/javascript/smriti/controllers/application.js +10 -0
- data/app/javascript/smriti/controllers/body_setup_controller.js +120 -0
- data/app/javascript/smriti/controllers/datatable_controller.js +351 -0
- data/app/javascript/smriti/controllers/details_controller.js +200 -0
- data/app/javascript/smriti/controllers/drawer_controller.js +470 -0
- data/app/javascript/smriti/controllers/flash_controller.js +112 -0
- data/app/javascript/smriti/controllers/index.js +10 -0
- data/app/javascript/smriti/controllers/mv_confirm_controller.js +435 -0
- data/app/javascript/smriti/controllers/tabs_controller.js +184 -0
- data/app/javascript/smriti/controllers/tooltip_controller.js +525 -0
- data/app/javascript/smriti/controllers/turbo_frame_lifecycle_controller.js +342 -0
- data/app/jobs/smriti/application_job.rb +144 -0
- data/app/jobs/smriti/create_view_job.rb +87 -0
- data/app/jobs/smriti/delete_view_job.rb +89 -0
- data/app/jobs/smriti/refresh_view_job.rb +94 -0
- data/app/models/concerns/smriti_i18n.rb +139 -0
- data/app/models/concerns/smriti_paginate.rb +70 -0
- data/app/models/concerns/smriti_query_helper.rb +36 -0
- data/app/models/smriti/application_record.rb +39 -0
- data/app/models/smriti/mat_view_definition.rb +254 -0
- data/app/models/smriti/mat_view_run.rb +275 -0
- data/app/views/layouts/smriti/_footer.html.erb +47 -0
- data/app/views/layouts/smriti/_header.html.erb +25 -0
- data/app/views/layouts/smriti/admin.html.erb +47 -0
- data/app/views/layouts/smriti/turbo_frame.html.erb +3 -0
- data/app/views/smriti/admin/dashboard/index.html.erb +38 -0
- data/app/views/smriti/admin/mat_view_definitions/_definition_actions.html.erb +94 -0
- data/app/views/smriti/admin/mat_view_definitions/_dt-index-empty-row.html.erb +11 -0
- data/app/views/smriti/admin/mat_view_definitions/_dt-index-row.html.erb +27 -0
- data/app/views/smriti/admin/mat_view_definitions/empty.html.erb +1 -0
- data/app/views/smriti/admin/mat_view_definitions/form.html.erb +79 -0
- data/app/views/smriti/admin/mat_view_definitions/index.html.erb +10 -0
- data/app/views/smriti/admin/mat_view_definitions/show.html.erb +40 -0
- data/app/views/smriti/admin/mat_view_runs/_dt-index-empty-row.html.erb +11 -0
- data/app/views/smriti/admin/mat_view_runs/_dt-index-row.html.erb +41 -0
- data/app/views/smriti/admin/mat_view_runs/index.html.erb +1 -0
- data/app/views/smriti/admin/mat_view_runs/show.html.erb +64 -0
- data/app/views/smriti/admin/preferences/show.html.erb +49 -0
- data/app/views/smriti/admin/ui/_card.html.erb +15 -0
- data/app/views/smriti/admin/ui/_datatable.html.erb +34 -0
- data/app/views/smriti/admin/ui/_datatable_filters.html.erb +45 -0
- data/app/views/smriti/admin/ui/_datatable_tbody.html.erb +11 -0
- data/app/views/smriti/admin/ui/_datatable_tfoot.html.erb +70 -0
- data/app/views/smriti/admin/ui/_datatable_thead.html.erb +105 -0
- data/app/views/smriti/admin/ui/_details.html.erb +10 -0
- data/app/views/smriti/admin/ui/_flash.html.erb +6 -0
- data/app/views/smriti/admin/ui/_table.html.erb +8 -0
- data/config/importmap.rb +9 -0
- data/config/locales/ar.yml +223 -0
- data/config/locales/de.yml +230 -0
- data/config/locales/en-AU-ocker.yml +223 -0
- data/config/locales/en-AU.yml +202 -0
- data/config/locales/en-BORK.yml +225 -0
- data/config/locales/en-CA.yml +223 -0
- data/config/locales/en-GB.yml +223 -0
- data/config/locales/en-LOL.yml +219 -0
- data/config/locales/en-SCOT.yml +223 -0
- data/config/locales/en-SHAKESPEARE.yml +225 -0
- data/config/locales/en-US-pirate.yml +222 -0
- data/config/locales/en-US.yml +225 -0
- data/config/locales/en-YODA.yml +221 -0
- data/config/locales/en.yml +223 -0
- data/config/locales/es.yml +226 -0
- data/config/locales/fa.yml +223 -0
- data/config/locales/fr-CA.yml +227 -0
- data/config/locales/fr.yml +227 -0
- data/config/locales/he.yml +218 -0
- data/config/locales/hi.yml +223 -0
- data/config/locales/it.yml +225 -0
- data/config/locales/ja-JP.yml +215 -0
- data/config/locales/pt.yml +225 -0
- data/config/locales/ru.yml +228 -0
- data/config/locales/ur.yml +225 -0
- data/config/locales/zh-CN.yml +214 -0
- data/config/locales/zh-TW.yml +214 -0
- data/config/routes.rb +36 -0
- data/lib/ext/exception.rb +20 -0
- data/lib/generators/smriti/install/install_generator.rb +86 -0
- data/lib/generators/smriti/install/templates/create_mat_view_definitions.rb +29 -0
- data/lib/generators/smriti/install/templates/create_mat_view_runs.rb +32 -0
- data/lib/generators/smriti/install/templates/smriti_initializer.rb +23 -0
- data/lib/smriti/admin/auth_bridge.rb +93 -0
- data/lib/smriti/admin/default_auth.rb +62 -0
- data/lib/smriti/configuration.rb +58 -0
- data/lib/smriti/engine.rb +82 -0
- data/lib/smriti/helpers/ui_test_ids.rb +49 -0
- data/lib/smriti/jobs/adapter.rb +81 -0
- data/lib/smriti/service_response.rb +75 -0
- data/lib/smriti/services/base_service.rb +471 -0
- data/lib/smriti/services/check_matview_exists.rb +76 -0
- data/lib/smriti/services/concurrent_refresh.rb +94 -0
- data/lib/smriti/services/create_view.rb +173 -0
- data/lib/smriti/services/delete_view.rb +111 -0
- data/lib/smriti/services/regular_refresh.rb +90 -0
- data/lib/smriti/services/swap_refresh.rb +181 -0
- data/lib/smriti/version.rb +21 -0
- data/lib/smriti.rb +64 -0
- data/lib/tasks/helpers.rb +185 -0
- data/lib/tasks/smriti_tasks.rake +151 -0
- metadata +206 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Codevedas Inc. 2025-present
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* FrameLifecycleController
|
|
10
|
+
* ------------------------
|
|
11
|
+
* @classdesc Stimulus controller orchestrating Turbo Frame and Turbo Stream lifecycles.
|
|
12
|
+
*
|
|
13
|
+
* Responsibilities:
|
|
14
|
+
* - Add datatable context headers to outgoing Turbo Frame requests.
|
|
15
|
+
* - Show and clear a unified “busy” state on frames and <body>.
|
|
16
|
+
* - Handle both frame navigations and streamed responses.
|
|
17
|
+
* - Track multiple concurrent busy frames safely.
|
|
18
|
+
*
|
|
19
|
+
* Key Methods:
|
|
20
|
+
* - `_handleBeforeFetch`, `_handleComplete`: frame navigation lifecycle.
|
|
21
|
+
* - `_handleBeforeStreamRender`: stream render interception.
|
|
22
|
+
* - `_setFrameBusy`, `_setBodyBusy`: visual busy state management.
|
|
23
|
+
* - `_bindFrame`, `_unbindFrame`: frame event lifecycle binding.
|
|
24
|
+
*
|
|
25
|
+
* Events handled:
|
|
26
|
+
* - `turbo:before-fetch-request`
|
|
27
|
+
* - `turbo:frame-load`
|
|
28
|
+
* - `turbo:fetch-request-error`
|
|
29
|
+
* - `turbo:before-stream-render`
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { Controller } from "@hotwired/stimulus";
|
|
33
|
+
|
|
34
|
+
export default class extends Controller {
|
|
35
|
+
/** @type {string[]} Stimulus targets: one or more <turbo-frame> elements to observe. */
|
|
36
|
+
static targets = ["frame"];
|
|
37
|
+
|
|
38
|
+
/** @type {string[]} CSS classes toggled on frames and <body> when busy. */
|
|
39
|
+
static classes = ["busy"];
|
|
40
|
+
|
|
41
|
+
/** @type {object} Configurable values (ARIA attribute name for busy state). */
|
|
42
|
+
static values = {
|
|
43
|
+
busyAttribute: { type: String, default: "aria-busy" },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// ────────────────────────────────────────────────────────────────
|
|
47
|
+
// Initialization and Lifecycle
|
|
48
|
+
// ────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Initialize internal state and bind `this` for handlers.
|
|
52
|
+
*/
|
|
53
|
+
initialize() {
|
|
54
|
+
/** @type {Set<HTMLElement>} currently-busy frames */
|
|
55
|
+
this._busyFrames = new Set();
|
|
56
|
+
|
|
57
|
+
this._handleBeforeFetch = this._handleBeforeFetch.bind(this);
|
|
58
|
+
this._handleComplete = this._handleComplete.bind(this);
|
|
59
|
+
this._handleBeforeStreamRender = this._handleBeforeStreamRender.bind(this);
|
|
60
|
+
this._flushBusyFrames = this._flushBusyFrames.bind(this);
|
|
61
|
+
|
|
62
|
+
/** Symbol to temporarily store affected frames on a <turbo-stream>. */
|
|
63
|
+
this._affectedKey = Symbol("mvAffectedFrames");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Stimulus lifecycle: connect.
|
|
68
|
+
* - Cache <body> reference.
|
|
69
|
+
* - Bind Turbo events on all frames.
|
|
70
|
+
* - Subscribe to global stream render event.
|
|
71
|
+
*/
|
|
72
|
+
connect() {
|
|
73
|
+
this.bodyElement = document.body;
|
|
74
|
+
this.frameTargets.forEach((frame) => this._bindFrame(frame));
|
|
75
|
+
document.addEventListener("drawer:refresh", this._flushBusyFrames);
|
|
76
|
+
document.addEventListener(
|
|
77
|
+
"turbo:before-stream-render",
|
|
78
|
+
this._handleBeforeStreamRender,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Stimulus lifecycle: disconnect.
|
|
84
|
+
* - Unbind frame listeners.
|
|
85
|
+
* - Remove global stream listener.
|
|
86
|
+
* - Clear busy state.
|
|
87
|
+
*/
|
|
88
|
+
disconnect() {
|
|
89
|
+
this.frameTargets.forEach((frame) => this._unbindFrame(frame));
|
|
90
|
+
document.removeEventListener("drawer:refresh", this._flushBusyFrames);
|
|
91
|
+
document.removeEventListener(
|
|
92
|
+
"turbo:before-stream-render",
|
|
93
|
+
this._handleBeforeStreamRender,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
this._busyFrames.forEach((f) => this._setFrameBusy(f, false));
|
|
97
|
+
this._busyFrames.clear();
|
|
98
|
+
this._setBodyBusy(false);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Stimulus callback: when a frame target connects.
|
|
103
|
+
* @param {HTMLElement} frame
|
|
104
|
+
*/
|
|
105
|
+
frameTargetConnected(frame) {
|
|
106
|
+
this._bindFrame(frame);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Stimulus callback: when a frame target disconnects.
|
|
111
|
+
* @param {HTMLElement} frame
|
|
112
|
+
*/
|
|
113
|
+
frameTargetDisconnected(frame) {
|
|
114
|
+
this._unbindFrame(frame);
|
|
115
|
+
this._busyFrames.delete(frame);
|
|
116
|
+
if (this._busyFrames.size === 0) this._setBodyBusy(false);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ────────────────────────────────────────────────────────────────
|
|
120
|
+
// Turbo Frame Lifecycle Handlers
|
|
121
|
+
// ────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Handle outgoing Turbo Frame requests.
|
|
125
|
+
* Adds datatable headers and marks frame busy.
|
|
126
|
+
*
|
|
127
|
+
* @param {CustomEvent} event turbo:before-fetch-request
|
|
128
|
+
*/
|
|
129
|
+
_handleBeforeFetch(event) {
|
|
130
|
+
const frame = /** @type {HTMLElement} */ (event.currentTarget);
|
|
131
|
+
|
|
132
|
+
// Add datatable context headers
|
|
133
|
+
const params = new URL(window.location).searchParams;
|
|
134
|
+
event.detail.fetchOptions.headers["X-DtSearch"] =
|
|
135
|
+
params.get("dtsearch") || "";
|
|
136
|
+
event.detail.fetchOptions.headers["X-DtSort"] = params.get("dtsort") || "";
|
|
137
|
+
event.detail.fetchOptions.headers["X-DtFilter"] =
|
|
138
|
+
params.get("dtfilter") || "";
|
|
139
|
+
event.detail.fetchOptions.headers["X-DtPage"] = params.get("dtpage") || "1";
|
|
140
|
+
event.detail.fetchOptions.headers["X-DtPerPage"] =
|
|
141
|
+
params.get("dtperpage") || "25";
|
|
142
|
+
|
|
143
|
+
this._setFrameBusy(frame, true);
|
|
144
|
+
this._setBodyBusy(true);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Handle completed or failed Turbo Frame loads.
|
|
149
|
+
*
|
|
150
|
+
* @param {CustomEvent} event turbo:frame-load | turbo:fetch-request-error
|
|
151
|
+
*/
|
|
152
|
+
_handleComplete(event) {
|
|
153
|
+
const frame = /** @type {HTMLElement} */ (event.currentTarget);
|
|
154
|
+
this._setFrameBusy(frame, false);
|
|
155
|
+
if (this._busyFrames.size === 0) this._setBodyBusy(false);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ────────────────────────────────────────────────────────────────
|
|
159
|
+
// Turbo Stream Lifecycle Handlers
|
|
160
|
+
// ────────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Clear all busy states.
|
|
164
|
+
* Used when drawers or other UI elements may have interrupted frame loads.
|
|
165
|
+
*/
|
|
166
|
+
_flushBusyFrames() {
|
|
167
|
+
this._busyFrames.forEach((f) => this._setFrameBusy(f, false));
|
|
168
|
+
this._busyFrames.clear();
|
|
169
|
+
this._setBodyBusy(false);
|
|
170
|
+
|
|
171
|
+
// Notify datatables to refresh, since now no frames are busy
|
|
172
|
+
// and they may have been affected by the drawer action.
|
|
173
|
+
document.dispatchEvent(
|
|
174
|
+
new CustomEvent("datatable:refresh", {
|
|
175
|
+
bubbles: false,
|
|
176
|
+
cancelable: false,
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Handle streamed updates (no frame navigation).
|
|
183
|
+
* Marks affected frames busy before render and clears after.
|
|
184
|
+
*
|
|
185
|
+
* @param {CustomEvent} event turbo:before-stream-render
|
|
186
|
+
*/
|
|
187
|
+
_handleBeforeStreamRender(event) {
|
|
188
|
+
const streamEl = event.target; // <turbo-stream>
|
|
189
|
+
const affected = this._framesAffectedByStream(streamEl);
|
|
190
|
+
|
|
191
|
+
if (affected.length > 0) {
|
|
192
|
+
affected.forEach((f) => this._setFrameBusy(f, true));
|
|
193
|
+
this._setBodyBusy(true);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Stash affected frames for cleanup
|
|
197
|
+
streamEl[this._affectedKey] = affected;
|
|
198
|
+
|
|
199
|
+
// Wrap the render function for post-render cleanup
|
|
200
|
+
const originalRender = event.detail.render;
|
|
201
|
+
event.detail.render = (el) => {
|
|
202
|
+
try {
|
|
203
|
+
originalRender(el);
|
|
204
|
+
} finally {
|
|
205
|
+
const frames = streamEl[this._affectedKey] || [];
|
|
206
|
+
frames.forEach((f) => this._setFrameBusy(f, false));
|
|
207
|
+
delete streamEl[this._affectedKey];
|
|
208
|
+
if (this._busyFrames.size === 0) this._setBodyBusy(false);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Determine which frames a stream will affect.
|
|
215
|
+
*
|
|
216
|
+
* @param {HTMLElement} streamEl <turbo-stream> element
|
|
217
|
+
* @return {HTMLElement[]} affected frames
|
|
218
|
+
*/
|
|
219
|
+
_framesAffectedByStream(streamEl) {
|
|
220
|
+
const targetId = streamEl.getAttribute("target");
|
|
221
|
+
const targetsSelector = streamEl.getAttribute("targets");
|
|
222
|
+
const out = new Set();
|
|
223
|
+
|
|
224
|
+
// target="id"
|
|
225
|
+
if (targetId) {
|
|
226
|
+
const el = document.getElementById(targetId);
|
|
227
|
+
let frame = null;
|
|
228
|
+
if (el) {
|
|
229
|
+
frame = el.tagName === "TURBO-FRAME" ? el : el.closest("turbo-frame");
|
|
230
|
+
}
|
|
231
|
+
if (frame && this._isManagedFrame(frame)) out.add(frame);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// targets="selector"
|
|
235
|
+
if (targetsSelector) {
|
|
236
|
+
document.querySelectorAll(targetsSelector).forEach((node) => {
|
|
237
|
+
const frame = node.closest("turbo-frame");
|
|
238
|
+
if (frame && this._isManagedFrame(frame)) out.add(frame);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return Array.from(out);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ────────────────────────────────────────────────────────────────
|
|
246
|
+
// Binding Helpers
|
|
247
|
+
// ────────────────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Attach Turbo lifecycle events to a frame.
|
|
251
|
+
* @param {HTMLElement} frame
|
|
252
|
+
*/
|
|
253
|
+
_bindFrame(frame) {
|
|
254
|
+
if (frame.__mvLifecycleBound) return;
|
|
255
|
+
frame.addEventListener(
|
|
256
|
+
"turbo:before-fetch-request",
|
|
257
|
+
this._handleBeforeFetch,
|
|
258
|
+
);
|
|
259
|
+
frame.addEventListener("turbo:frame-load", this._handleComplete);
|
|
260
|
+
frame.addEventListener("turbo:fetch-request-error", this._handleComplete);
|
|
261
|
+
frame.__mvLifecycleBound = true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Detach Turbo lifecycle events from a frame.
|
|
266
|
+
* @param {HTMLElement} frame
|
|
267
|
+
*/
|
|
268
|
+
_unbindFrame(frame) {
|
|
269
|
+
if (!frame.__mvLifecycleBound) return;
|
|
270
|
+
frame.removeEventListener(
|
|
271
|
+
"turbo:before-fetch-request",
|
|
272
|
+
this._handleBeforeFetch,
|
|
273
|
+
);
|
|
274
|
+
frame.removeEventListener("turbo:frame-load", this._handleComplete);
|
|
275
|
+
frame.removeEventListener(
|
|
276
|
+
"turbo:fetch-request-error",
|
|
277
|
+
this._handleComplete,
|
|
278
|
+
);
|
|
279
|
+
delete frame.__mvLifecycleBound;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ────────────────────────────────────────────────────────────────
|
|
283
|
+
// Busy State Helpers
|
|
284
|
+
// ────────────────────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Determine if a frame belongs to this controller.
|
|
288
|
+
* @param {HTMLElement} frame
|
|
289
|
+
* @return {boolean}
|
|
290
|
+
*/
|
|
291
|
+
_isManagedFrame(frame) {
|
|
292
|
+
return this.frameTargets.includes
|
|
293
|
+
? this.frameTargets.includes(frame)
|
|
294
|
+
: this.frameTargets.indexOf?.(frame) >= 0;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Mark or unmark a frame as busy, coercing child elements to their frame.
|
|
299
|
+
*
|
|
300
|
+
* @param {HTMLElement} node - may be the frame or a child element.
|
|
301
|
+
* @param {boolean} busy
|
|
302
|
+
*/
|
|
303
|
+
_setFrameBusy(node, busy) {
|
|
304
|
+
if (!node) return;
|
|
305
|
+
|
|
306
|
+
// Normalize to the owning <turbo-frame>
|
|
307
|
+
let frame =
|
|
308
|
+
node.tagName === "TURBO-FRAME" ? node : node.closest?.("turbo-frame");
|
|
309
|
+
if (!frame || !this._isManagedFrame(frame)) return;
|
|
310
|
+
|
|
311
|
+
if (busy) {
|
|
312
|
+
this._busyFrames.add(frame);
|
|
313
|
+
frame.setAttribute(this.busyAttributeValue, "true");
|
|
314
|
+
frame.setAttribute("busy", "");
|
|
315
|
+
if (this.hasBusyClass) frame.classList.add(this.busyClass);
|
|
316
|
+
} else {
|
|
317
|
+
this._busyFrames.delete(frame);
|
|
318
|
+
frame.removeAttribute(this.busyAttributeValue);
|
|
319
|
+
frame.removeAttribute("busy");
|
|
320
|
+
if (this.hasBusyClass) frame.classList.remove(this.busyClass);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Apply or clear busy indicators on <body>.
|
|
326
|
+
*
|
|
327
|
+
* @param {boolean} busy
|
|
328
|
+
*/
|
|
329
|
+
_setBodyBusy(busy) {
|
|
330
|
+
if (!this.bodyElement) return;
|
|
331
|
+
|
|
332
|
+
if (busy) {
|
|
333
|
+
this.bodyElement.setAttribute(this.busyAttributeValue, "true");
|
|
334
|
+
this.bodyElement.setAttribute("busy", "");
|
|
335
|
+
if (this.hasBusyClass) this.bodyElement.classList.add(this.busyClass);
|
|
336
|
+
} else {
|
|
337
|
+
this.bodyElement.removeAttribute(this.busyAttributeValue);
|
|
338
|
+
this.bodyElement.removeAttribute("busy");
|
|
339
|
+
if (this.hasBusyClass) this.bodyElement.classList.remove(this.busyClass);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
|
4
|
+
#
|
|
5
|
+
# This source code is licensed under the MIT license found in the
|
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
|
7
|
+
|
|
8
|
+
##
|
|
9
|
+
# Top-level namespace for the smriti engine.
|
|
10
|
+
#
|
|
11
|
+
# All classes, modules, and services for materialised view management
|
|
12
|
+
# are defined under this namespace.
|
|
13
|
+
#
|
|
14
|
+
# @example Accessing a job
|
|
15
|
+
# Smriti::ApplicationJob
|
|
16
|
+
#
|
|
17
|
+
module Smriti
|
|
18
|
+
##
|
|
19
|
+
# Base class for all background jobs in the smriti engine.
|
|
20
|
+
#
|
|
21
|
+
# Inherits from {ActiveJob::Base} and provides a common superclass
|
|
22
|
+
# for engine jobs such as {Smriti::CreateViewJob} and {Smriti::RefreshViewJob}.
|
|
23
|
+
#
|
|
24
|
+
# @abstract
|
|
25
|
+
#
|
|
26
|
+
# @see Smriti::CreateViewJob
|
|
27
|
+
# @see Smriti::RefreshViewJob
|
|
28
|
+
# @see Smriti::DeleteViewJob
|
|
29
|
+
#
|
|
30
|
+
# @example Defining a custom job
|
|
31
|
+
# class MyCustomJob < Smriti::ApplicationJob
|
|
32
|
+
# def perform(mat_view_definition_id)
|
|
33
|
+
# # custom logic here
|
|
34
|
+
# end
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
class ApplicationJob < ActiveJob::Base
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def record_run(definition, operation, &)
|
|
41
|
+
start = monotime
|
|
42
|
+
run = start_run(definition, operation)
|
|
43
|
+
response = yield
|
|
44
|
+
finalize_run(run, response, elapsed_ms(start))
|
|
45
|
+
response.to_h
|
|
46
|
+
rescue StandardError => e
|
|
47
|
+
fail_run(run, e, elapsed_ms(start))
|
|
48
|
+
raise e
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
##
|
|
52
|
+
# Begin a {Smriti::MatViewRun} row for lifecycle tracking.
|
|
53
|
+
#
|
|
54
|
+
# @api private
|
|
55
|
+
#
|
|
56
|
+
# @return [Smriti::MatViewRun]
|
|
57
|
+
#
|
|
58
|
+
def start_run(definition, operation)
|
|
59
|
+
Smriti::MatViewRun.create!(
|
|
60
|
+
mat_view_definition: definition,
|
|
61
|
+
status: :running,
|
|
62
|
+
started_at: Time.current,
|
|
63
|
+
operation: operation
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
##
|
|
68
|
+
# Finalize the run with success/failure, timing, and meta from the response.
|
|
69
|
+
#
|
|
70
|
+
# @api private
|
|
71
|
+
#
|
|
72
|
+
# @param run [Smriti::MatViewRun]
|
|
73
|
+
# @param response [Smriti::ServiceResponse, nil] may be nil if exception raised
|
|
74
|
+
# @param duration_ms [Integer]
|
|
75
|
+
# @return [void]
|
|
76
|
+
#
|
|
77
|
+
def finalize_run(run, response, duration_ms)
|
|
78
|
+
base_attrs = {
|
|
79
|
+
finished_at: Time.current,
|
|
80
|
+
duration_ms: duration_ms,
|
|
81
|
+
meta: { request: response.request, response: response.response }.compact
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if response.success?
|
|
85
|
+
run.update!(base_attrs.merge(status: :success, error: nil))
|
|
86
|
+
else
|
|
87
|
+
run.update!(base_attrs.merge(status: :failed, error: response.error))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
##
|
|
92
|
+
# Mark the run failed due to an exception.
|
|
93
|
+
#
|
|
94
|
+
# @api private
|
|
95
|
+
#
|
|
96
|
+
# @param run [Smriti::MatViewRun]
|
|
97
|
+
# @param exception [Exception]
|
|
98
|
+
# @param duration_ms [Integer]
|
|
99
|
+
# @return [void]
|
|
100
|
+
#
|
|
101
|
+
def fail_run(run, exception, duration_ms)
|
|
102
|
+
run&.update!(
|
|
103
|
+
error: exception.mv_serialize_error,
|
|
104
|
+
finished_at: Time.current,
|
|
105
|
+
duration_ms: duration_ms,
|
|
106
|
+
status: :failed
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
##
|
|
111
|
+
# Monotonic clock getter (for elapsed-time measurement).
|
|
112
|
+
#
|
|
113
|
+
# @api private
|
|
114
|
+
# @return [Float] seconds
|
|
115
|
+
#
|
|
116
|
+
def monotime = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
117
|
+
|
|
118
|
+
##
|
|
119
|
+
# Convert monotonic start time to elapsed milliseconds.
|
|
120
|
+
#
|
|
121
|
+
# @api private
|
|
122
|
+
# @param start [Float]
|
|
123
|
+
# @return [Integer] elapsed ms
|
|
124
|
+
#
|
|
125
|
+
def elapsed_ms(start) = ((monotime - start) * 1000).round
|
|
126
|
+
|
|
127
|
+
##
|
|
128
|
+
# Normalize the strategy argument into a symbol or default.
|
|
129
|
+
#
|
|
130
|
+
# @api private
|
|
131
|
+
#
|
|
132
|
+
# @param arg [Symbol, String, nil]
|
|
133
|
+
# @return [Symbol] One of `:estimated`, `:exact`, or `:none` by default.
|
|
134
|
+
#
|
|
135
|
+
def normalize_strategy(arg)
|
|
136
|
+
case arg
|
|
137
|
+
when String, Symbol
|
|
138
|
+
arg.to_sym
|
|
139
|
+
else
|
|
140
|
+
:none
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
|
4
|
+
#
|
|
5
|
+
# This source code is licensed under the MIT license found in the
|
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
|
7
|
+
|
|
8
|
+
##
|
|
9
|
+
# Top-level namespace for the smriti engine.
|
|
10
|
+
module Smriti
|
|
11
|
+
##
|
|
12
|
+
# ActiveJob that handles *creation* of PostgreSQL materialised views for a
|
|
13
|
+
# given {Smriti::MatViewDefinition}.
|
|
14
|
+
#
|
|
15
|
+
# The job:
|
|
16
|
+
# 1. Normalizes the `force` argument.
|
|
17
|
+
# 2. Looks up the target {Smriti::MatViewDefinition}.
|
|
18
|
+
# 3. Starts a {Smriti::MatViewRun} row to track lifecycle/timing, with `operation: :create`.
|
|
19
|
+
# 4. Executes {Smriti::Services::CreateView}.
|
|
20
|
+
# 5. Finalizes the run with success/failure, duration, and meta.
|
|
21
|
+
#
|
|
22
|
+
# @see Smriti::Services::CreateView
|
|
23
|
+
# @see Smriti::MatViewDefinition
|
|
24
|
+
# @see Smriti::MatViewRun
|
|
25
|
+
#
|
|
26
|
+
# @example Enqueue a create job
|
|
27
|
+
# Smriti::CreateViewJob.perform_later(definition.id, force: true)
|
|
28
|
+
#
|
|
29
|
+
# @example Inline run (test/dev)
|
|
30
|
+
# Smriti::CreateViewJob.new.perform(definition.id, false)
|
|
31
|
+
#
|
|
32
|
+
class CreateViewJob < ApplicationJob
|
|
33
|
+
##
|
|
34
|
+
# Queue name for the job.
|
|
35
|
+
#
|
|
36
|
+
# Uses `Smriti.configuration.job_queue` when configured, otherwise `:default`.
|
|
37
|
+
#
|
|
38
|
+
# @return [void]
|
|
39
|
+
#
|
|
40
|
+
queue_as { Smriti.configuration.job_queue || :default }
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# Perform the create job for the given materialised view definition.
|
|
44
|
+
#
|
|
45
|
+
# @api public
|
|
46
|
+
#
|
|
47
|
+
# @param mat_view_definition_id [Integer, String] ID of {Smriti::MatViewDefinition}.
|
|
48
|
+
# @param force_arg [Boolean, Hash, nil] Optional flag or hash (`{ force: true }`)
|
|
49
|
+
# @param row_count_strategy_arg [:Symbol, String] One of: `:estimated`, `:exact`, `:none` or `nil`.
|
|
50
|
+
#
|
|
51
|
+
# @return [Hash] Serialized {Smriti::ServiceResponse#to_h}:
|
|
52
|
+
# - `:status` [Symbol]
|
|
53
|
+
# - `:error` [String, nil]
|
|
54
|
+
# - `:duration_ms` [Integer]
|
|
55
|
+
# - `:meta` [Hash]
|
|
56
|
+
#
|
|
57
|
+
# @raise [StandardError] Re-raised on unexpected failure after marking the run failed.
|
|
58
|
+
#
|
|
59
|
+
def perform(mat_view_definition_id, force_arg = nil, row_count_strategy_arg = nil)
|
|
60
|
+
definition = Smriti::MatViewDefinition.find(mat_view_definition_id)
|
|
61
|
+
|
|
62
|
+
record_run(definition, :create) do
|
|
63
|
+
Smriti::Services::CreateView.new(definition,
|
|
64
|
+
force: force?(force_arg),
|
|
65
|
+
row_count_strategy: normalize_strategy(row_count_strategy_arg)).call
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
##
|
|
72
|
+
# Normalize the `force` argument into a boolean.
|
|
73
|
+
#
|
|
74
|
+
# Accepts either a boolean-ish value or a Hash (e.g., `{ force: true }` or `{ "force" => true }`).
|
|
75
|
+
#
|
|
76
|
+
# @api private
|
|
77
|
+
#
|
|
78
|
+
# @param arg [Object] Raw argument; commonly `true/false`, `nil`
|
|
79
|
+
# @return [Boolean] Coerced force flag.
|
|
80
|
+
#
|
|
81
|
+
def force?(arg)
|
|
82
|
+
return false if arg.nil?
|
|
83
|
+
|
|
84
|
+
!!arg
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
|
4
|
+
#
|
|
5
|
+
# This source code is licensed under the MIT license found in the
|
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
|
7
|
+
|
|
8
|
+
##
|
|
9
|
+
# Top-level namespace for the smriti engine.
|
|
10
|
+
module Smriti
|
|
11
|
+
##
|
|
12
|
+
# ActiveJob that handles *deletion* of PostgreSQL materialised views via
|
|
13
|
+
# {Smriti::Services::DeleteView}.
|
|
14
|
+
#
|
|
15
|
+
# This job mirrors {Smriti::CreateViewJob} and {Smriti::RefreshViewJob}:
|
|
16
|
+
# it times the run and persists lifecycle state in {Smriti::MatViewRun}.
|
|
17
|
+
#
|
|
18
|
+
# @see Smriti::Services::DeleteView
|
|
19
|
+
# @see Smriti::MatViewDefinition
|
|
20
|
+
# @see Smriti::MatViewRun
|
|
21
|
+
#
|
|
22
|
+
# @example Enqueue a delete job
|
|
23
|
+
# Smriti::DeleteViewJob.perform_later(definition.id, cascade: true)
|
|
24
|
+
#
|
|
25
|
+
# @example Inline run (test/dev)
|
|
26
|
+
# Smriti::DeleteViewJob.new.perform(definition.id, false)
|
|
27
|
+
#
|
|
28
|
+
class DeleteViewJob < ApplicationJob
|
|
29
|
+
###
|
|
30
|
+
# cascade flag for the service call
|
|
31
|
+
# @return [Boolean]
|
|
32
|
+
attr_reader :cascade
|
|
33
|
+
|
|
34
|
+
##
|
|
35
|
+
# Queue name for the job.
|
|
36
|
+
#
|
|
37
|
+
# Uses `Smriti.configuration.job_queue` when configured, otherwise `:default`.
|
|
38
|
+
#
|
|
39
|
+
queue_as { Smriti.configuration.job_queue || :default }
|
|
40
|
+
|
|
41
|
+
##
|
|
42
|
+
# Perform the job for the given materialised view definition.
|
|
43
|
+
#
|
|
44
|
+
# @api public
|
|
45
|
+
#
|
|
46
|
+
# @param mat_view_definition_id [Integer, String] ID of {Smriti::MatViewDefinition}.
|
|
47
|
+
# @param cascade_arg [Boolean, String, Integer, Hash, nil] Cascade option.
|
|
48
|
+
# @param row_count_strategy_arg [:Symbol, String] One of: `:estimated`, `:exact`, `:none` or `nil`.
|
|
49
|
+
#
|
|
50
|
+
# @return [Hash] Serialized {Smriti::ServiceResponse#to_h}:
|
|
51
|
+
# - `:status` [Symbol]
|
|
52
|
+
# - `:error` [String, nil]
|
|
53
|
+
# - `:duration_ms` [Integer]
|
|
54
|
+
# - `:meta` [Hash]
|
|
55
|
+
#
|
|
56
|
+
# @raise [StandardError] Re-raised on unexpected failure after marking the run failed.
|
|
57
|
+
#
|
|
58
|
+
def perform(mat_view_definition_id, cascade_arg = nil, row_count_strategy_arg = nil)
|
|
59
|
+
definition = Smriti::MatViewDefinition.find(mat_view_definition_id)
|
|
60
|
+
record_run(definition, :drop) do
|
|
61
|
+
Smriti::Services::DeleteView.new(definition,
|
|
62
|
+
cascade: cascade?(cascade_arg),
|
|
63
|
+
row_count_strategy: normalize_strategy(row_count_strategy_arg)).call
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
##
|
|
70
|
+
# Evaluate if a value is "truthy" for cascade.
|
|
71
|
+
#
|
|
72
|
+
# @api private
|
|
73
|
+
# @param value [TrueClass, FalseClass, String, Integer, nil, Object]
|
|
74
|
+
# @return [Boolean]
|
|
75
|
+
#
|
|
76
|
+
def cascade?(value)
|
|
77
|
+
case value
|
|
78
|
+
when true
|
|
79
|
+
true
|
|
80
|
+
when String
|
|
81
|
+
%w[true 1 yes].include?(value.strip.downcase)
|
|
82
|
+
when Integer
|
|
83
|
+
value == 1
|
|
84
|
+
else
|
|
85
|
+
false
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|