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.
Files changed (124) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +168 -0
  4. data/Rakefile +15 -0
  5. data/app/assets/images/smriti/android-chrome-192x192.png +0 -0
  6. data/app/assets/images/smriti/android-chrome-512x512.png +0 -0
  7. data/app/assets/images/smriti/apple-touch-icon.png +0 -0
  8. data/app/assets/images/smriti/favicon-16x16.png +0 -0
  9. data/app/assets/images/smriti/favicon-32x32.png +0 -0
  10. data/app/assets/images/smriti/favicon-48x48.png +0 -0
  11. data/app/assets/images/smriti/favicon.ico +0 -0
  12. data/app/assets/images/smriti/favicon.svg +18 -0
  13. data/app/assets/images/smriti/logo.svg +18 -0
  14. data/app/assets/images/smriti/mask-icon.svg +5 -0
  15. data/app/assets/stylesheets/smriti/application.css +1040 -0
  16. data/app/controllers/smriti/admin/application_controller.rb +135 -0
  17. data/app/controllers/smriti/admin/dashboard_controller.rb +32 -0
  18. data/app/controllers/smriti/admin/mat_view_definitions_controller.rb +372 -0
  19. data/app/controllers/smriti/admin/mat_view_runs_controller.rb +185 -0
  20. data/app/controllers/smriti/admin/preferences_controller.rb +91 -0
  21. data/app/helpers/smriti/admin/datatable_helper.rb +249 -0
  22. data/app/helpers/smriti/admin/localized_digit_helper.rb +70 -0
  23. data/app/helpers/smriti/admin/ui_helper.rb +539 -0
  24. data/app/javascript/smriti/application.js +8 -0
  25. data/app/javascript/smriti/controllers/application.js +10 -0
  26. data/app/javascript/smriti/controllers/body_setup_controller.js +120 -0
  27. data/app/javascript/smriti/controllers/datatable_controller.js +351 -0
  28. data/app/javascript/smriti/controllers/details_controller.js +200 -0
  29. data/app/javascript/smriti/controllers/drawer_controller.js +470 -0
  30. data/app/javascript/smriti/controllers/flash_controller.js +112 -0
  31. data/app/javascript/smriti/controllers/index.js +10 -0
  32. data/app/javascript/smriti/controllers/mv_confirm_controller.js +435 -0
  33. data/app/javascript/smriti/controllers/tabs_controller.js +184 -0
  34. data/app/javascript/smriti/controllers/tooltip_controller.js +525 -0
  35. data/app/javascript/smriti/controllers/turbo_frame_lifecycle_controller.js +342 -0
  36. data/app/jobs/smriti/application_job.rb +144 -0
  37. data/app/jobs/smriti/create_view_job.rb +87 -0
  38. data/app/jobs/smriti/delete_view_job.rb +89 -0
  39. data/app/jobs/smriti/refresh_view_job.rb +94 -0
  40. data/app/models/concerns/smriti_i18n.rb +139 -0
  41. data/app/models/concerns/smriti_paginate.rb +70 -0
  42. data/app/models/concerns/smriti_query_helper.rb +36 -0
  43. data/app/models/smriti/application_record.rb +39 -0
  44. data/app/models/smriti/mat_view_definition.rb +254 -0
  45. data/app/models/smriti/mat_view_run.rb +275 -0
  46. data/app/views/layouts/smriti/_footer.html.erb +47 -0
  47. data/app/views/layouts/smriti/_header.html.erb +25 -0
  48. data/app/views/layouts/smriti/admin.html.erb +47 -0
  49. data/app/views/layouts/smriti/turbo_frame.html.erb +3 -0
  50. data/app/views/smriti/admin/dashboard/index.html.erb +38 -0
  51. data/app/views/smriti/admin/mat_view_definitions/_definition_actions.html.erb +94 -0
  52. data/app/views/smriti/admin/mat_view_definitions/_dt-index-empty-row.html.erb +11 -0
  53. data/app/views/smriti/admin/mat_view_definitions/_dt-index-row.html.erb +27 -0
  54. data/app/views/smriti/admin/mat_view_definitions/empty.html.erb +1 -0
  55. data/app/views/smriti/admin/mat_view_definitions/form.html.erb +79 -0
  56. data/app/views/smriti/admin/mat_view_definitions/index.html.erb +10 -0
  57. data/app/views/smriti/admin/mat_view_definitions/show.html.erb +40 -0
  58. data/app/views/smriti/admin/mat_view_runs/_dt-index-empty-row.html.erb +11 -0
  59. data/app/views/smriti/admin/mat_view_runs/_dt-index-row.html.erb +41 -0
  60. data/app/views/smriti/admin/mat_view_runs/index.html.erb +1 -0
  61. data/app/views/smriti/admin/mat_view_runs/show.html.erb +64 -0
  62. data/app/views/smriti/admin/preferences/show.html.erb +49 -0
  63. data/app/views/smriti/admin/ui/_card.html.erb +15 -0
  64. data/app/views/smriti/admin/ui/_datatable.html.erb +34 -0
  65. data/app/views/smriti/admin/ui/_datatable_filters.html.erb +45 -0
  66. data/app/views/smriti/admin/ui/_datatable_tbody.html.erb +11 -0
  67. data/app/views/smriti/admin/ui/_datatable_tfoot.html.erb +70 -0
  68. data/app/views/smriti/admin/ui/_datatable_thead.html.erb +105 -0
  69. data/app/views/smriti/admin/ui/_details.html.erb +10 -0
  70. data/app/views/smriti/admin/ui/_flash.html.erb +6 -0
  71. data/app/views/smriti/admin/ui/_table.html.erb +8 -0
  72. data/config/importmap.rb +9 -0
  73. data/config/locales/ar.yml +223 -0
  74. data/config/locales/de.yml +230 -0
  75. data/config/locales/en-AU-ocker.yml +223 -0
  76. data/config/locales/en-AU.yml +202 -0
  77. data/config/locales/en-BORK.yml +225 -0
  78. data/config/locales/en-CA.yml +223 -0
  79. data/config/locales/en-GB.yml +223 -0
  80. data/config/locales/en-LOL.yml +219 -0
  81. data/config/locales/en-SCOT.yml +223 -0
  82. data/config/locales/en-SHAKESPEARE.yml +225 -0
  83. data/config/locales/en-US-pirate.yml +222 -0
  84. data/config/locales/en-US.yml +225 -0
  85. data/config/locales/en-YODA.yml +221 -0
  86. data/config/locales/en.yml +223 -0
  87. data/config/locales/es.yml +226 -0
  88. data/config/locales/fa.yml +223 -0
  89. data/config/locales/fr-CA.yml +227 -0
  90. data/config/locales/fr.yml +227 -0
  91. data/config/locales/he.yml +218 -0
  92. data/config/locales/hi.yml +223 -0
  93. data/config/locales/it.yml +225 -0
  94. data/config/locales/ja-JP.yml +215 -0
  95. data/config/locales/pt.yml +225 -0
  96. data/config/locales/ru.yml +228 -0
  97. data/config/locales/ur.yml +225 -0
  98. data/config/locales/zh-CN.yml +214 -0
  99. data/config/locales/zh-TW.yml +214 -0
  100. data/config/routes.rb +36 -0
  101. data/lib/ext/exception.rb +20 -0
  102. data/lib/generators/smriti/install/install_generator.rb +86 -0
  103. data/lib/generators/smriti/install/templates/create_mat_view_definitions.rb +29 -0
  104. data/lib/generators/smriti/install/templates/create_mat_view_runs.rb +32 -0
  105. data/lib/generators/smriti/install/templates/smriti_initializer.rb +23 -0
  106. data/lib/smriti/admin/auth_bridge.rb +93 -0
  107. data/lib/smriti/admin/default_auth.rb +62 -0
  108. data/lib/smriti/configuration.rb +58 -0
  109. data/lib/smriti/engine.rb +82 -0
  110. data/lib/smriti/helpers/ui_test_ids.rb +49 -0
  111. data/lib/smriti/jobs/adapter.rb +81 -0
  112. data/lib/smriti/service_response.rb +75 -0
  113. data/lib/smriti/services/base_service.rb +471 -0
  114. data/lib/smriti/services/check_matview_exists.rb +76 -0
  115. data/lib/smriti/services/concurrent_refresh.rb +94 -0
  116. data/lib/smriti/services/create_view.rb +173 -0
  117. data/lib/smriti/services/delete_view.rb +111 -0
  118. data/lib/smriti/services/regular_refresh.rb +90 -0
  119. data/lib/smriti/services/swap_refresh.rb +181 -0
  120. data/lib/smriti/version.rb +21 -0
  121. data/lib/smriti.rb +64 -0
  122. data/lib/tasks/helpers.rb +185 -0
  123. data/lib/tasks/smriti_tasks.rake +151 -0
  124. 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