active_record_in_time_scope 0.1.7

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.
@@ -0,0 +1,457 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordInTimeScope
4
+ # Class methods added to ActiveRecord models when ActiveRecordInTimeScope is included
5
+ module ClassMethods
6
+ # Defines time-window scopes for the model.
7
+ #
8
+ # This method creates both a class-level scope and an instance method
9
+ # to check if records fall within a specified time window.
10
+ #
11
+ # @param scope_name [Symbol] The name of the scope (default: :in_time)
12
+ # When not :in_time, columns default to +<scope_name>_start_at+ and +<scope_name>_end_at+
13
+ #
14
+ # @param start_at [Hash] Configuration for the start column
15
+ # @option start_at [Symbol, nil] :column Column name (nil to disable start boundary)
16
+ # @option start_at [Boolean] :null Whether the column allows NULL values
17
+ # (auto-detected from schema if not specified)
18
+ #
19
+ # @param end_at [Hash] Configuration for the end column
20
+ # @option end_at [Symbol, nil] :column Column name (nil to disable end boundary)
21
+ # @option end_at [Boolean] :null Whether the column allows NULL values
22
+ # (auto-detected from schema if not specified)
23
+ #
24
+ # @raise [ColumnNotFoundError] When a specified column doesn't exist (at class load time)
25
+ # @raise [ConfigurationError] When both columns are nil, or when using start-only/end-only
26
+ # pattern with a nullable column (at scope call time)
27
+ #
28
+ # @return [void]
29
+ #
30
+ # == Examples
31
+ #
32
+ # Default scope with nullable columns:
33
+ #
34
+ # in_time_scope
35
+ # # Creates: Model.in_time, model.in_time?
36
+ #
37
+ # Named scope:
38
+ #
39
+ # in_time_scope :published
40
+ # # Creates: Model.in_time_published, model.in_time_published?
41
+ # # Uses: published_start_at, published_end_at columns
42
+ #
43
+ # Custom columns:
44
+ #
45
+ # in_time_scope start_at: { column: :available_at }, end_at: { column: :expired_at }
46
+ #
47
+ # Start-only pattern (for history tracking):
48
+ #
49
+ # in_time_scope start_at: { null: false }, end_at: { column: nil }
50
+ # # Also creates: Model.latest_in_time(:foreign_key), Model.earliest_in_time(:foreign_key)
51
+ #
52
+ # End-only pattern (for expiration):
53
+ #
54
+ # in_time_scope start_at: { column: nil }, end_at: { null: false }
55
+ # # Also creates: Model.latest_in_time(:foreign_key), Model.earliest_in_time(:foreign_key)
56
+ #
57
+ def in_time_scope(scope_name = :in_time, start_at: {}, end_at: {})
58
+ table_column_hash = columns_hash
59
+ time_column_prefix = scope_name == :in_time ? "" : "#{scope_name}_"
60
+
61
+ start_at_column = start_at.fetch(:column, :"#{time_column_prefix}start_at")
62
+ end_at_column = end_at.fetch(:column, :"#{time_column_prefix}end_at")
63
+
64
+ start_at_null = fetch_null_option(start_at, start_at_column, table_column_hash)
65
+ end_at_null = fetch_null_option(end_at, end_at_column, table_column_hash)
66
+
67
+ define_scope_methods(
68
+ scope_name == :in_time ? "" : "_#{scope_name}",
69
+ start_at_column: start_at_column,
70
+ start_at_null: start_at_null,
71
+ end_at_column: end_at_column,
72
+ end_at_null: end_at_null
73
+ )
74
+ end
75
+
76
+ private
77
+
78
+ # Fetches the null option for a column, auto-detecting from schema if not specified
79
+ #
80
+ # @param config [Hash] Configuration hash with optional :null key
81
+ # @param column [Symbol, nil] Column name
82
+ # @param table_column_hash [Hash] Hash of column metadata from ActiveRecord
83
+ # @return [Boolean, nil] Whether the column allows NULL values
84
+ # @raise [ColumnNotFoundError] When the column doesn't exist in the table
85
+ # @api private
86
+ def fetch_null_option(config, column, table_column_hash)
87
+ return nil if column.nil?
88
+ return config[:null] if config.key?(:null)
89
+
90
+ column_info = table_column_hash[column.to_s]
91
+ raise ColumnNotFoundError, "Column '#{column}' does not exist on table '#{table_name}'" if column_info.nil?
92
+
93
+ column_info.null
94
+ end
95
+
96
+ # Returns true when only the start column is configured (history tracking pattern)
97
+ #
98
+ # @param start_at_column [Symbol, nil] Start column name
99
+ # @param end_at_column [Symbol, nil] End column name
100
+ # @return [Boolean]
101
+ # @api private
102
+ def start_only_pattern?(start_at_column, end_at_column)
103
+ !start_at_column.nil? && end_at_column.nil?
104
+ end
105
+
106
+ # Returns true when only the end column is configured (expiration pattern)
107
+ #
108
+ # @param start_at_column [Symbol, nil] Start column name
109
+ # @param end_at_column [Symbol, nil] End column name
110
+ # @return [Boolean]
111
+ # @api private
112
+ def end_only_pattern?(start_at_column, end_at_column)
113
+ start_at_column.nil? && !end_at_column.nil?
114
+ end
115
+
116
+ # Defines the appropriate scope methods based on configuration
117
+ #
118
+ # @param suffix [String] The suffix for method names ("" or "_#{scope_name}")
119
+ # @param start_at_column [Symbol, nil] Start column name
120
+ # @param start_at_null [Boolean, nil] Whether start column allows NULL
121
+ # @param end_at_column [Symbol, nil] End column name
122
+ # @param end_at_null [Boolean, nil] Whether end column allows NULL
123
+ # @return [void]
124
+ # @api private
125
+ def define_scope_methods(suffix, start_at_column:, start_at_null:, end_at_column:, end_at_null:)
126
+ # Define class-level scope and instance method
127
+ if start_at_column.nil? && end_at_column.nil?
128
+ define_error_scope_and_method(suffix,
129
+ "At least one of start_at or end_at must be specified")
130
+ elsif start_only_pattern?(start_at_column, end_at_column)
131
+ # Start-only pattern (history tracking) - requires non-nullable column
132
+ if start_at_null
133
+ define_error_scope_and_method(suffix,
134
+ "Start-only pattern requires non-nullable column. " \
135
+ "Set `start_at: { null: false }` or add an end_at column")
136
+ else
137
+ define_start_only_scope(suffix, start_at_column)
138
+ define_instance_method(suffix, start_at_column, start_at_null, end_at_column, end_at_null)
139
+ define_latest_one_scope(suffix, start_at_column)
140
+ define_earliest_one_scope(suffix, start_at_column)
141
+ define_before_scope(suffix, start_at_column, start_at_null)
142
+ define_after_scope(suffix, end_at_column, end_at_null)
143
+ define_out_of_time_scope(suffix)
144
+ end
145
+ elsif end_only_pattern?(start_at_column, end_at_column)
146
+ # End-only pattern (expiration) - requires non-nullable column
147
+ if end_at_null
148
+ define_error_scope_and_method(suffix,
149
+ "End-only pattern requires non-nullable column. " \
150
+ "Set `end_at: { null: false }` or add a start_at column")
151
+ else
152
+ define_end_only_scope(suffix, end_at_column)
153
+ define_instance_method(suffix, start_at_column, start_at_null, end_at_column, end_at_null)
154
+ define_latest_one_scope(suffix, end_at_column)
155
+ define_earliest_one_scope(suffix, end_at_column)
156
+ define_before_scope(suffix, start_at_column, start_at_null)
157
+ define_after_scope(suffix, end_at_column, end_at_null)
158
+ define_out_of_time_scope(suffix)
159
+ end
160
+ else
161
+ # Both start and end
162
+ define_full_scope(suffix, start_at_column, start_at_null, end_at_column, end_at_null)
163
+ define_instance_method(suffix, start_at_column, start_at_null, end_at_column, end_at_null)
164
+ define_before_scope(suffix, start_at_column, start_at_null)
165
+ define_after_scope(suffix, end_at_column, end_at_null)
166
+ define_out_of_time_scope(suffix)
167
+ end
168
+ end
169
+
170
+ # Defines a scope and instance method that raise ConfigurationError
171
+ #
172
+ # @param suffix [String] The suffix for method names
173
+ # @param message [String] The error message
174
+ # @return [void]
175
+ # @api private
176
+ def define_error_scope_and_method(suffix, message)
177
+ method_names = [
178
+ :"in_time#{suffix}",
179
+ :"before_in_time#{suffix}",
180
+ :"after_in_time#{suffix}",
181
+ :"out_of_time#{suffix}"
182
+ ]
183
+
184
+ method_names.each do |method_name|
185
+ scope method_name, ->(_time = Time.current) {
186
+ raise ActiveRecordInTimeScope::ConfigurationError, message
187
+ }
188
+
189
+ define_method("#{method_name}?") do |_time = Time.current|
190
+ raise ActiveRecordInTimeScope::ConfigurationError, message
191
+ end
192
+ end
193
+ end
194
+
195
+ # Defines a start-only scope (for history tracking pattern)
196
+ #
197
+ # @param suffix [String] The suffix for method names
198
+ # @param column [Symbol] The start column name
199
+ # @return [void]
200
+ # @api private
201
+ def define_start_only_scope(suffix, column)
202
+ # Simple scope - WHERE only, no ORDER BY
203
+ # Users can add .order(start_at: :desc) externally if needed
204
+ scope :"in_time#{suffix}", ->(time = Time.current) {
205
+ where(column => ..time)
206
+ }
207
+ end
208
+
209
+ # Defines the latest_in_time scope using NOT EXISTS subquery
210
+ #
211
+ # This scope efficiently finds the latest record per foreign key,
212
+ # suitable for use with has_one associations and includes.
213
+ #
214
+ # @note When multiple records share the same timestamp for a given foreign key,
215
+ # all of them will be returned. This is safe for +has_one+ associations
216
+ # (ActiveRecord picks one), but callers using this as a standalone scope
217
+ # should be aware that the result may contain multiple records per foreign key
218
+ # in the case of timestamp ties.
219
+ #
220
+ # @param suffix [String] The suffix for method names
221
+ # @param column [Symbol] The timestamp column name
222
+ # @return [void]
223
+ #
224
+ # @example Usage with has_one
225
+ # has_one :current_price, -> { latest_in_time(:user_id) }, class_name: 'Price'
226
+ #
227
+ # @api private
228
+ def define_latest_one_scope(suffix, column)
229
+ # NOT EXISTS approach: select records where no later record exists for the same foreign key
230
+ scope :"latest_in_time#{suffix}", ->(foreign_key, time = Time.current) {
231
+ p2 = arel_table.alias("p2")
232
+
233
+ subquery = Arel::SelectManager.new(arel_table)
234
+ .from(p2)
235
+ .project(Arel.sql("1"))
236
+ .where(p2[foreign_key].eq(arel_table[foreign_key]))
237
+ .where(p2[column].lteq(time))
238
+ .where(p2[column].gt(arel_table[column]))
239
+ .where(p2[:id].not_eq(arel_table[:id]))
240
+
241
+ # Propagate simple equality conditions from the current scope into the NOT EXISTS
242
+ # subquery. This ensures that chained scopes like `.approved.latest_in_time(:user_id)`
243
+ # only consider approved records when searching for "a newer record exists",
244
+ # preventing a newer rejected record from shadowing the latest approved one.
245
+ where_values_hash.each do |col, val|
246
+ next if col.to_s == column.to_s # time column already handled above
247
+ next if col.to_s == foreign_key.to_s # foreign key already handled above
248
+
249
+ subquery = if val.nil?
250
+ subquery.where(p2[col].eq(nil))
251
+ elsif val.is_a?(Array)
252
+ subquery.where(p2[col].in(val))
253
+ else
254
+ subquery.where(p2[col].eq(val))
255
+ end
256
+ end
257
+
258
+ not_exists = Arel::Nodes::Not.new(Arel::Nodes::Exists.new(subquery.ast))
259
+
260
+ where(column => ..time).where(not_exists)
261
+ }
262
+ end
263
+
264
+ # Defines the earliest_in_time scope using NOT EXISTS subquery
265
+ #
266
+ # This scope efficiently finds the earliest record per foreign key,
267
+ # suitable for use with has_one associations and includes.
268
+ #
269
+ # @note When multiple records share the same timestamp for a given foreign key,
270
+ # all of them will be returned. This is safe for +has_one+ associations
271
+ # (ActiveRecord picks one), but callers using this as a standalone scope
272
+ # should be aware that the result may contain multiple records per foreign key
273
+ # in the case of timestamp ties.
274
+ #
275
+ # @param suffix [String] The suffix for method names
276
+ # @param column [Symbol] The timestamp column name
277
+ # @return [void]
278
+ #
279
+ # @example Usage with has_one
280
+ # has_one :first_price, -> { earliest_in_time(:user_id) }, class_name: 'Price'
281
+ #
282
+ # @api private
283
+ def define_earliest_one_scope(suffix, column)
284
+ # NOT EXISTS approach: select records where no earlier record exists for the same foreign key
285
+ scope :"earliest_in_time#{suffix}", ->(foreign_key, time = Time.current) {
286
+ p2 = arel_table.alias("p2")
287
+
288
+ subquery = Arel::SelectManager.new(arel_table)
289
+ .from(p2)
290
+ .project(Arel.sql("1"))
291
+ .where(p2[foreign_key].eq(arel_table[foreign_key]))
292
+ .where(p2[column].lteq(time))
293
+ .where(p2[column].lt(arel_table[column]))
294
+ .where(p2[:id].not_eq(arel_table[:id]))
295
+
296
+ # Propagate simple equality conditions from the current scope into the NOT EXISTS
297
+ # subquery (same reasoning as define_latest_one_scope).
298
+ where_values_hash.each do |col, val|
299
+ next if col.to_s == column.to_s
300
+ next if col.to_s == foreign_key.to_s
301
+
302
+ subquery = if val.nil?
303
+ subquery.where(p2[col].eq(nil))
304
+ elsif val.is_a?(Array)
305
+ subquery.where(p2[col].in(val))
306
+ else
307
+ subquery.where(p2[col].eq(val))
308
+ end
309
+ end
310
+
311
+ not_exists = Arel::Nodes::Not.new(Arel::Nodes::Exists.new(subquery.ast))
312
+
313
+ where(column => ..time).where(not_exists)
314
+ }
315
+ end
316
+
317
+ # Defines an end-only scope (for expiration pattern)
318
+ #
319
+ # @param suffix [String] The suffix for method names
320
+ # @param column [Symbol] The end column name
321
+ # @return [void]
322
+ # @api private
323
+ def define_end_only_scope(suffix, column)
324
+ scope :"in_time#{suffix}", ->(time = Time.current) {
325
+ where.not(column => ..time)
326
+ }
327
+ end
328
+
329
+ # Defines a full scope with both start and end columns
330
+ #
331
+ # @param suffix [String] The suffix for method names
332
+ # @param start_column [Symbol] The start column name
333
+ # @param start_null [Boolean] Whether start column allows NULL
334
+ # @param end_column [Symbol] The end column name
335
+ # @param end_null [Boolean] Whether end column allows NULL
336
+ # @return [void]
337
+ # @api private
338
+ def define_full_scope(suffix, start_column, start_null, end_column, end_null)
339
+ scope :"in_time#{suffix}", ->(time = Time.current) {
340
+ start_scope = if start_null
341
+ where(start_column => nil).or(where(start_column => ..time))
342
+ else
343
+ where(start_column => ..time)
344
+ end
345
+
346
+ end_scope = if end_null
347
+ where(end_column => nil).or(where.not(end_column => ..time))
348
+ else
349
+ where.not(end_column => ..time)
350
+ end
351
+
352
+ start_scope.merge(end_scope)
353
+ }
354
+
355
+ # NOTE: latest_in_time / earliest_in_time are NOT defined for full scope (both start and end)
356
+ # because the concept of "latest" or "earliest" is ambiguous when there's a time range.
357
+ # These scopes are only available for start-only or end-only patterns.
358
+ end
359
+
360
+ # Defines the instance method to check if a record is within the time window
361
+ #
362
+ # @param suffix [String] The suffix for method names
363
+ # @param start_column [Symbol, nil] The start column name
364
+ # @param start_null [Boolean, nil] Whether start column allows NULL
365
+ # @param end_column [Symbol, nil] The end column name
366
+ # @param end_null [Boolean, nil] Whether end column allows NULL
367
+ # @return [void]
368
+ # @api private
369
+ def define_instance_method(suffix, start_column, start_null, end_column, end_null)
370
+ define_method("in_time#{suffix}?") do |time = Time.current|
371
+ start_ok = if start_column.nil?
372
+ true
373
+ elsif start_null
374
+ send(start_column).nil? || send(start_column) <= time
375
+ else
376
+ send(start_column) <= time
377
+ end
378
+
379
+ end_ok = if end_column.nil?
380
+ true
381
+ elsif end_null
382
+ send(end_column).nil? || send(end_column) > time
383
+ else
384
+ send(end_column) > time
385
+ end
386
+
387
+ start_ok && end_ok
388
+ end
389
+ end
390
+
391
+ # Defines before_in_time scope (records not yet started: start_at > time)
392
+ #
393
+ # @param suffix [String] The suffix for method names
394
+ # @param start_column [Symbol, nil] The start column name
395
+ # @param start_null [Boolean, nil] Whether start column allows NULL
396
+ # @return [void]
397
+ # @api private
398
+ def define_before_scope(suffix, start_column, start_null)
399
+ # No start column means always started (never before)
400
+ # start_at > time means not yet started
401
+ # NULL start_at is treated as "already started" (not before)
402
+ scope :"before_in_time#{suffix}", ->(time = Time.current) {
403
+ start_column.nil? ? none : where.not(start_column => ..time)
404
+ }
405
+
406
+ define_method("before_in_time#{suffix}?") do |time = Time.current|
407
+ return false if start_column.nil?
408
+
409
+ val = send(start_column)
410
+ return false if val.nil? && start_null
411
+
412
+ val > time
413
+ end
414
+ end
415
+
416
+ # Defines after_in_time scope (records already ended: end_at <= time)
417
+ #
418
+ # @param suffix [String] The suffix for method names
419
+ # @param end_column [Symbol, nil] The end column name
420
+ # @param end_null [Boolean, nil] Whether end column allows NULL
421
+ # @return [void]
422
+ # @api private
423
+ def define_after_scope(suffix, end_column, end_null)
424
+ # No end column means never ends (never after)
425
+ # end_at <= time means already ended
426
+ # NULL end_at is treated as "never ends" (not after)
427
+ scope :"after_in_time#{suffix}", ->(time = Time.current) {
428
+ end_column.nil? ? none : where(end_column => ..time)
429
+ }
430
+
431
+ define_method("after_in_time#{suffix}?") do |time = Time.current|
432
+ return false if end_column.nil?
433
+
434
+ val = send(end_column)
435
+ return false if val.nil? && end_null
436
+
437
+ val <= time
438
+ end
439
+ end
440
+
441
+ # Defines out_of_time scope (records outside time window: before OR after)
442
+ #
443
+ # @param suffix [String] The suffix for method names
444
+ # @return [void]
445
+ # @api private
446
+ def define_out_of_time_scope(suffix)
447
+ # out_of_time = before_in_time OR after_in_time
448
+ scope :"out_of_time#{suffix}", ->(time = Time.current) {
449
+ send(:"before_in_time#{suffix}", time).or(send(:"after_in_time#{suffix}", time))
450
+ }
451
+
452
+ define_method("out_of_time#{suffix}?") do |time = Time.current|
453
+ send("before_in_time#{suffix}?", time) || send("after_in_time#{suffix}?", time)
454
+ end
455
+ end
456
+ end
457
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordInTimeScope
4
+ # The current version of the ActiveRecordInTimeScope gem
5
+ VERSION = "0.1.7"
6
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require_relative "active_record_in_time_scope/version"
5
+ require_relative "active_record_in_time_scope/class_methods"
6
+
7
+ # ActiveRecordInTimeScope provides time-window scopes for ActiveRecord models.
8
+ #
9
+ # It allows you to easily query records that fall within specific time periods,
10
+ # with support for nullable columns, custom column names, and multiple scopes per model.
11
+ #
12
+ # ActiveRecordInTimeScope is automatically included in ActiveRecord::Base, so you can use
13
+ # +in_time_scope+ directly in your models without explicit include.
14
+ #
15
+ # == Basic usage
16
+ #
17
+ # class Event < ActiveRecord::Base
18
+ # in_time_scope
19
+ # end
20
+ #
21
+ # Event.in_time # Records active at current time
22
+ # Event.in_time(some_time) # Records active at specific time
23
+ # event.in_time? # Check if record is active now
24
+ #
25
+ # == Patterns
26
+ #
27
+ # === Full pattern (both start and end)
28
+ #
29
+ # Default pattern with both +start_at+ and +end_at+ columns.
30
+ # Supports nullable columns (NULL means "no limit").
31
+ #
32
+ # class Event < ActiveRecord::Base
33
+ # in_time_scope # Uses start_at and end_at columns
34
+ # end
35
+ #
36
+ # === Start-only pattern (history tracking)
37
+ #
38
+ # For versioned records where each row is valid from +start_at+ until the next row.
39
+ # Requires non-nullable column.
40
+ #
41
+ # class Price < ActiveRecord::Base
42
+ # in_time_scope start_at: { null: false }, end_at: { column: nil }
43
+ # end
44
+ #
45
+ # # Additional scopes created:
46
+ # Price.latest_in_time(:user_id) # Latest record per user
47
+ # Price.earliest_in_time(:user_id) # Earliest record per user
48
+ #
49
+ # === End-only pattern (expiration)
50
+ #
51
+ # For records that are always active until they expire.
52
+ # Requires non-nullable column.
53
+ #
54
+ # class Coupon < ActiveRecord::Base
55
+ # in_time_scope start_at: { column: nil }, end_at: { null: false }
56
+ # end
57
+ #
58
+ # == Using with has_one associations
59
+ #
60
+ # The +latest_in_time+ and +earliest_in_time+ scopes are optimized for
61
+ # +has_one+ associations with +includes+, using NOT EXISTS subqueries.
62
+ #
63
+ # class Price < ActiveRecord::Base
64
+ # belongs_to :user
65
+ # in_time_scope start_at: { null: false }, end_at: { column: nil }
66
+ # end
67
+ #
68
+ # class User < ActiveRecord::Base
69
+ # has_many :prices
70
+ #
71
+ # # Efficient: uses NOT EXISTS subquery
72
+ # has_one :current_price,
73
+ # -> { latest_in_time(:user_id) },
74
+ # class_name: "Price"
75
+ #
76
+ # has_one :first_price,
77
+ # -> { earliest_in_time(:user_id) },
78
+ # class_name: "Price"
79
+ # end
80
+ #
81
+ # # Works efficiently with includes
82
+ # User.includes(:current_price).each do |user|
83
+ # puts user.current_price&.amount
84
+ # end
85
+ #
86
+ # == Named scopes
87
+ #
88
+ # Define multiple time windows per model using named scopes.
89
+ #
90
+ # class Article < ActiveRecord::Base
91
+ # in_time_scope :published # Uses published_start_at, published_end_at
92
+ # in_time_scope :featured # Uses featured_start_at, featured_end_at
93
+ # end
94
+ #
95
+ # Article.in_time_published
96
+ # Article.in_time_featured
97
+ # article.in_time_published?
98
+ #
99
+ # == Custom columns
100
+ #
101
+ # class Event < ActiveRecord::Base
102
+ # in_time_scope start_at: { column: :available_at },
103
+ # end_at: { column: :expired_at }
104
+ # end
105
+ #
106
+ # == Error handling
107
+ #
108
+ # - ColumnNotFoundError: Raised at class load time if column doesn't exist
109
+ # - ConfigurationError: Raised at scope call time for invalid configurations
110
+ #
111
+ # @see ClassMethods#in_time_scope
112
+ module ActiveRecordInTimeScope
113
+ # Base error class for ActiveRecordInTimeScope errors
114
+ class Error < StandardError; end
115
+
116
+ # Raised when a specified column does not exist on the table
117
+ # @note This error is raised at class load time
118
+ class ColumnNotFoundError < Error; end
119
+
120
+ # Raised when the scope configuration is invalid
121
+ # @note This error is raised when the scope or instance method is called
122
+ class ConfigurationError < Error; end
123
+
124
+ # @api private
125
+ def self.included(model)
126
+ model.extend ClassMethods
127
+ end
128
+ end
129
+
130
+ ActiveSupport.on_load(:active_record) do
131
+ include ActiveRecordInTimeScope
132
+ end
data/mise.toml ADDED
@@ -0,0 +1,2 @@
1
+ [tools]
2
+ ruby = "4.0.1"
@@ -0,0 +1,16 @@
1
+ # RBS collection configuration
2
+ # Run `rbs collection install` to download external gem signatures
3
+
4
+ sources:
5
+ - type: git
6
+ name: ruby/gem_rbs_collection
7
+ remote: https://github.com/ruby/gem_rbs_collection.git
8
+ revision: main
9
+ repo_dir: gems
10
+
11
+ path: .gem_rbs_collection
12
+
13
+ gems:
14
+ - name: activerecord
15
+ - name: activesupport
16
+ - name: activemodel
data/rulesync.jsonc ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/dyoshikawa/rulesync/refs/heads/main/config-schema.json",
3
+ "targets": ["claudecode"],
4
+ "features": ["rules", "commands"],
5
+ "baseDirs": ["."]
6
+ }