jsonb-accessor-1.3.10/0000755000175000017500000000000014513544510013175 5ustar raviravijsonb-accessor-1.3.10/lib/0000755000175000017500000000000014513544510013743 5ustar raviravijsonb-accessor-1.3.10/lib/jsonb_accessor/0000755000175000017500000000000014513544510016740 5ustar raviravijsonb-accessor-1.3.10/lib/jsonb_accessor/version.rb0000644000175000017500000000011514513544510020747 0ustar raviravi# frozen_string_literal: true module JsonbAccessor VERSION = "1.3.10" end jsonb-accessor-1.3.10/lib/jsonb_accessor/query_helper.rb0000644000175000017500000000735714513544510022005 0ustar raviravi# frozen_string_literal: true module JsonbAccessor module QueryHelper # Errors InvalidColumnName = Class.new(StandardError) InvalidFieldName = Class.new(StandardError) InvalidDirection = Class.new(StandardError) NotSupported = Class.new(StandardError) # Constants GREATER_THAN = ">" GREATER_THAN_OR_EQUAL_TO = ">=" LESS_THAN = "<" LESS_THAN_OR_EQUAL_TO = "<=" NUMBER_OPERATORS_MAP = { GREATER_THAN => GREATER_THAN, "greater_than" => GREATER_THAN, "gt" => GREATER_THAN, GREATER_THAN_OR_EQUAL_TO => GREATER_THAN_OR_EQUAL_TO, "greater_than_or_equal_to" => GREATER_THAN_OR_EQUAL_TO, "gte" => GREATER_THAN_OR_EQUAL_TO, LESS_THAN => LESS_THAN, "less_than" => LESS_THAN, "lt" => LESS_THAN, LESS_THAN_OR_EQUAL_TO => LESS_THAN_OR_EQUAL_TO, "less_than_or_equal_to" => LESS_THAN_OR_EQUAL_TO, "lte" => LESS_THAN_OR_EQUAL_TO }.freeze NUMBER_OPERATORS = NUMBER_OPERATORS_MAP.keys.freeze TIME_OPERATORS_MAP = { "after" => GREATER_THAN, "before" => LESS_THAN }.freeze TIME_OPERATORS = TIME_OPERATORS_MAP.keys.freeze ORDER_DIRECTIONS = [:asc, :desc, "asc", "desc"].freeze class << self def validate_column_name!(query, column_name) raise InvalidColumnName, "a column named `#{column_name}` does not exist on the `#{query.model.table_name}` table" if query.model.columns.none? { |column| column.name == column_name.to_s } end def validate_field_name!(query, column_name, field_name) store_keys = query.model.public_send("jsonb_store_key_mapping_for_#{column_name}").values if store_keys.exclude?(field_name.to_s) valid_field_names = store_keys.map { |key| "`#{key}`" }.join(", ") raise InvalidFieldName, "`#{field_name}` is not a valid field name, valid field names include: #{valid_field_names}" end end def validate_direction!(option) raise InvalidDirection, "`#{option}` is not a valid direction for ordering, only `asc` and `desc` are accepted" if ORDER_DIRECTIONS.exclude?(option) end def number_query_arguments?(arg) arg.is_a?(Hash) && arg.keys.map(&:to_s).all? { |key| NUMBER_OPERATORS.include?(key) } end def time_query_arguments?(arg) arg.is_a?(Hash) && arg.keys.map(&:to_s).all? { |key| TIME_OPERATORS.include?(key) } end def convert_number_ranges(attributes) attributes.each_with_object({}) do |(name, value), new_attributes| is_range = value.is_a?(Range) new_attributes[name] = if is_range && value.first.is_a?(Numeric) && value.exclude_end? { greater_than_or_equal_to: value.first, less_than: value.end } elsif is_range && value.first.is_a?(Numeric) { greater_than_or_equal_to: value.first, less_than_or_equal_to: value.end } else value end end end def convert_time_ranges(attributes) attributes.each_with_object({}) do |(name, value), new_attributes| is_range = value.is_a?(Range) if is_range && (value.first.is_a?(Time) || value.first.is_a?(Date)) start_time = value.first end_time = value.end new_attributes[name] = { before: end_time, after: start_time } else new_attributes[name] = value end end end def convert_ranges(attributes) %i[convert_number_ranges convert_time_ranges].reduce(attributes) do |new_attributes, converter_method| public_send(converter_method, new_attributes) end end end end end jsonb-accessor-1.3.10/lib/jsonb_accessor/query_builder.rb0000644000175000017500000001014714513544510022143 0ustar raviravi# frozen_string_literal: true module JsonbAccessor module QueryBuilder extend ActiveSupport::Concern included do scope(:jsonb_contains, lambda do |column_name, attributes| JsonbAccessor::QueryHelper.validate_column_name!(all, column_name) where("#{table_name}.#{column_name} @> (?)::jsonb", attributes.to_json) end) scope(:jsonb_excludes, lambda do |column_name, attributes| JsonbAccessor::QueryHelper.validate_column_name!(all, column_name) where.not("#{table_name}.#{column_name} @> (?)::jsonb", attributes.to_json) end) scope(:jsonb_number_where, lambda do |column_name, field_name, given_operator, value| JsonbAccessor::QueryHelper.validate_column_name!(all, column_name) operator = JsonbAccessor::QueryHelper::NUMBER_OPERATORS_MAP.fetch(given_operator.to_s) where("(#{table_name}.#{column_name} ->> ?)::float #{operator} ?", field_name, value) end) scope(:jsonb_number_where_not, lambda do |column_name, field_name, given_operator, value| JsonbAccessor::QueryHelper.validate_column_name!(all, column_name) operator = JsonbAccessor::QueryHelper::NUMBER_OPERATORS_MAP.fetch(given_operator.to_s) where.not("(#{table_name}.#{column_name} ->> ?)::float #{operator} ?", field_name, value) end) scope(:jsonb_time_where, lambda do |column_name, field_name, given_operator, value| JsonbAccessor::QueryHelper.validate_column_name!(all, column_name) operator = JsonbAccessor::QueryHelper::TIME_OPERATORS_MAP.fetch(given_operator.to_s) where("(#{table_name}.#{column_name} ->> ?)::timestamp #{operator} ?", field_name, value) end) scope(:jsonb_time_where_not, lambda do |column_name, field_name, given_operator, value| JsonbAccessor::QueryHelper.validate_column_name!(all, column_name) operator = JsonbAccessor::QueryHelper::TIME_OPERATORS_MAP.fetch(given_operator.to_s) where.not("(#{table_name}.#{column_name} ->> ?)::timestamp #{operator} ?", field_name, value) end) scope(:jsonb_where, lambda do |column_name, attributes| query = all contains_attributes = {} JsonbAccessor::QueryHelper.convert_ranges(attributes).each do |name, value| if JsonbAccessor::QueryHelper.number_query_arguments?(value) value.each { |operator, query_value| query = query.jsonb_number_where(column_name, name, operator, query_value) } elsif JsonbAccessor::QueryHelper.time_query_arguments?(value) value.each { |operator, query_value| query = query.jsonb_time_where(column_name, name, operator, query_value) } else contains_attributes[name] = value end end query.jsonb_contains(column_name, contains_attributes) end) scope(:jsonb_where_not, lambda do |column_name, attributes| query = all excludes_attributes = {} attributes.each do |name, value| raise JsonbAccessor::QueryHelper::NotSupported, "`jsonb_where_not` scope does not accept ranges as arguments. Given `#{value}` for `#{name}` field" if value.is_a?(Range) if JsonbAccessor::QueryHelper.number_query_arguments?(value) value.each { |operator, query_value| query = query.jsonb_number_where_not(column_name, name, operator, query_value) } elsif JsonbAccessor::QueryHelper.time_query_arguments?(value) value.each { |operator, query_value| query = query.jsonb_time_where_not(column_name, name, operator, query_value) } else excludes_attributes[name] = value end end excludes_attributes.empty? ? query : query.jsonb_excludes(column_name, excludes_attributes) end) scope(:jsonb_order, lambda do |column_name, field_name, direction| JsonbAccessor::QueryHelper.validate_column_name!(all, column_name) JsonbAccessor::QueryHelper.validate_field_name!(all, column_name, field_name) JsonbAccessor::QueryHelper.validate_direction!(direction) order(Arel.sql("(#{table_name}.#{column_name} -> '#{field_name}') #{direction}")) end) end end end jsonb-accessor-1.3.10/lib/jsonb_accessor/macro.rb0000644000175000017500000001362514513544510020375 0ustar raviravi# frozen_string_literal: true module JsonbAccessor module Macro module ClassMethods def jsonb_accessor(jsonb_attribute, field_types) names_and_store_keys = field_types.each_with_object({}) do |(name, type), mapping| _type, options = Array(type) mapping[name.to_s] = (options.try(:delete, :store_key) || name).to_s end # Defines virtual attributes for each jsonb field. field_types.each do |name, type| next attribute name, type unless type.is_a?(Array) next attribute name, *type unless type.last.is_a?(Hash) *args, keyword_args = type attribute name, *args, **keyword_args end store_key_mapping_method_name = "jsonb_store_key_mapping_for_#{jsonb_attribute}" # Defines methods on the model class class_methods = Module.new do # Allows us to get a mapping of field names to store keys scoped to the column define_method(store_key_mapping_method_name) do superclass_mapping = superclass.try(store_key_mapping_method_name) || {} superclass_mapping.merge(names_and_store_keys) end end # We extend with class methods here so we can use the results of methods it defines to define more useful methods later extend class_methods # Get field names to default values mapping names_and_defaults = field_types.each_with_object({}) do |(name, type), mapping| _type, options = Array(type) field_default = options.try(:delete, :default) mapping[name.to_s] = field_default unless field_default.nil? end # Get store keys to default values mapping store_keys_and_defaults = JsonbAccessor::Helpers.convert_keys_to_store_keys(names_and_defaults, public_send(store_key_mapping_method_name)) # Define jsonb_defaults_mapping_for_ defaults_mapping_method_name = "jsonb_defaults_mapping_for_#{jsonb_attribute}" class_methods.instance_eval do define_method(defaults_mapping_method_name) do superclass_mapping = superclass.try(defaults_mapping_method_name) || {} superclass_mapping.merge(store_keys_and_defaults) end end all_defaults_mapping = public_send(defaults_mapping_method_name) # Fields may have procs as default value. This means `all_defaults_mapping` may contain procs as values. To make this work # with the attributes API, we need to wrap `all_defaults_mapping` with a proc itself, making sure it returns a plain hash # each time it is evaluated. all_defaults_mapping_proc = if all_defaults_mapping.present? -> { all_defaults_mapping.transform_values { |value| value.respond_to?(:call) ? value.call : value }.to_h.compact } end attribute jsonb_attribute, :jsonb, default: all_defaults_mapping_proc if all_defaults_mapping_proc.present? # Setters are in a module to allow users to override them and still be able to use `super`. setters = Module.new do # Overrides the setter created by `attribute` above to make sure the jsonb attribute is kept in sync. names_and_store_keys.each do |name, store_key| define_method("#{name}=") do |value| super(value) # If enum was defined, take the value from the enum and not what comes out directly from the getter attribute_value = defined_enums[name].present? ? defined_enums[name][value] : public_send(name) # Rails always saves time based on `default_timezone`. Since #as_json considers timezone, manual conversion is needed if attribute_value.acts_like?(:time) attribute_value = (JsonbAccessor::Helpers.active_record_default_timezone == :utc ? attribute_value.utc : attribute_value.in_time_zone).strftime("%F %R:%S.%L") end new_values = (public_send(jsonb_attribute) || {}).merge(store_key => attribute_value) write_attribute(jsonb_attribute, new_values) end end # Overrides the jsonb attribute setter to make sure the jsonb fields are kept in sync. define_method("#{jsonb_attribute}=") do |value| value ||= {} names_to_store_keys = self.class.public_send(store_key_mapping_method_name) # this is the raw hash we want to save in the jsonb_attribute value_with_store_keys = JsonbAccessor::Helpers.convert_keys_to_store_keys(value, names_to_store_keys) write_attribute(jsonb_attribute, value_with_store_keys) # this maps attributes to values value_with_named_keys = JsonbAccessor::Helpers.convert_store_keys_to_keys(value, names_to_store_keys) empty_named_attributes = names_to_store_keys.transform_values { nil } empty_named_attributes.merge(value_with_named_keys).each do |name, attribute_value| # Only proceed if this attribute has been defined using `jsonb_accessor`. next unless names_to_store_keys.key?(name) write_attribute(name, attribute_value) end end end include setters # Makes sure new objects have the appropriate values in their jsonb fields. after_initialize do next unless has_attribute? jsonb_attribute jsonb_values = public_send(jsonb_attribute) || {} jsonb_values.each do |store_key, value| name = names_and_store_keys.key(store_key) next unless name write_attribute( name, JsonbAccessor::Helpers.deserialize_value(value, self.class.type_for_attribute(name).type) ) clear_attribute_change(name) if persisted? end end JsonbAccessor::AttributeQueryMethods.new(self).define(store_key_mapping_method_name, jsonb_attribute) end end end end jsonb-accessor-1.3.10/lib/jsonb_accessor/helpers.rb0000644000175000017500000000211314513544510020724 0ustar raviravi# frozen_string_literal: true module JsonbAccessor module Helpers module_function def active_record_default_timezone ActiveRecord.try(:default_timezone) || ActiveRecord::Base.default_timezone end # Replaces all keys in `attributes` that have a defined store_key with the store_key def convert_keys_to_store_keys(attributes, store_key_mapping) attributes.stringify_keys.transform_keys do |key| store_key_mapping[key] || key end end # Replaces all keys in `attributes` that have a defined store_key with the named key (alias) def convert_store_keys_to_keys(attributes, store_key_mapping) convert_keys_to_store_keys(attributes, store_key_mapping.invert) end def deserialize_value(value, attribute_type) return value if value.blank? if attribute_type == :datetime value = if active_record_default_timezone == :utc Time.find_zone("UTC").parse(value).in_time_zone else Time.zone.parse(value) end end value end end end jsonb-accessor-1.3.10/lib/jsonb_accessor/attribute_query_methods.rb0000644000175000017500000000312514513544510024241 0ustar raviravi# frozen_string_literal: true module JsonbAccessor class AttributeQueryMethods def initialize(klass) @klass = klass end def define(store_key_mapping_method_name, jsonb_attribute) return if klass.superclass.respond_to? store_key_mapping_method_name # _where scope klass.define_singleton_method "#{jsonb_attribute}_where" do |attributes| store_key_attributes = JsonbAccessor::Helpers.convert_keys_to_store_keys(attributes, all.model.public_send(store_key_mapping_method_name)) jsonb_where(jsonb_attribute, store_key_attributes) end # _where_not scope klass.define_singleton_method "#{jsonb_attribute}_where_not" do |attributes| store_key_attributes = JsonbAccessor::Helpers.convert_keys_to_store_keys(attributes, all.model.public_send(store_key_mapping_method_name)) jsonb_where_not(jsonb_attribute, store_key_attributes) end # _order scope klass.define_singleton_method "#{jsonb_attribute}_order" do |*args| ordering_options = args.extract_options! order_by_defaults = args.each_with_object({}) { |attribute, config| config[attribute] = :asc } store_key_mapping = all.model.public_send(store_key_mapping_method_name) order_by_defaults.merge(ordering_options).reduce(all) do |query, (name, direction)| key = store_key_mapping[name.to_s] order_query = jsonb_order(jsonb_attribute, key, direction) query.merge(order_query) end end end private attr_reader :klass end end jsonb-accessor-1.3.10/lib/jsonb_accessor.rb0000644000175000017500000000106314513544510017265 0ustar raviravi# frozen_string_literal: true require "active_record" require "active_record/connection_adapters/postgresql_adapter" require "jsonb_accessor/version" require "jsonb_accessor/helpers" require "jsonb_accessor/macro" require "jsonb_accessor/query_helper" require "jsonb_accessor/query_builder" require "jsonb_accessor/attribute_query_methods" module JsonbAccessor extend ActiveSupport::Concern include Macro end ActiveSupport.on_load(:active_record) do ActiveRecord::Base.include JsonbAccessor ActiveRecord::Base.include JsonbAccessor::QueryBuilder end jsonb-accessor-1.3.10/jsonb_accessor.gemspec0000644000175000017500000000362314513544510017543 0ustar raviravi# frozen_string_literal: true lib = File.expand_path("lib", __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "jsonb_accessor/version" is_java = RUBY_PLATFORM == "java" Gem::Specification.new do |spec| spec.name = "jsonb_accessor" spec.version = JsonbAccessor::VERSION spec.authors = ["Michael Crismali", "Joe Hirn", "Jason Haruska"] spec.email = ["michael@crismali.com", "joe@devmynd.com", "jason@haruska.com"] spec.platform = "java" if is_java spec.summary = "Adds typed jsonb backed fields to your ActiveRecord models." spec.description = "Adds typed jsonb backed fields to your ActiveRecord models." spec.homepage = "https://github.com/devmynd/jsonb_accessor" spec.license = "MIT" spec.required_ruby_version = ">= 2" spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) || f.match(/png\z/) } spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] spec.add_dependency "activerecord", ">= 5.0" spec.add_dependency "activesupport", ">= 5.0" if is_java spec.add_dependency "activerecord-jdbcpostgresql-adapter", ">= 50.0" else spec.add_dependency "pg", ">= 0.18.1" end spec.add_development_dependency "appraisal", "~> 2.2.0" spec.add_development_dependency "awesome_print" spec.add_development_dependency "database_cleaner", "~> 1.6.0" spec.add_development_dependency "pry" spec.add_development_dependency "pry-doc" spec.add_development_dependency "pry-nav" spec.add_development_dependency "psych", "~> 3" spec.add_development_dependency "rake", ">= 12.3.3" spec.add_development_dependency "rspec", "~> 3.6.0" spec.add_development_dependency "rubocop", "~> 1" end jsonb-accessor-1.3.10/gemfiles/0000755000175000017500000000000014513544510014770 5ustar raviravijsonb-accessor-1.3.10/gemfiles/activerecord_7.0.1.gemfile0000644000175000017500000000017314513544510021520 0ustar raviravi# This file was generated by Appraisal source "https://rubygems.org" gem "activerecord", "~> 7.0.1" gemspec path: "../" jsonb-accessor-1.3.10/gemfiles/activerecord_6.1.0.gemfile0000644000175000017500000000017114513544510021515 0ustar raviravi# This file was generated by Appraisal source "https://rubygems.org" gem "activerecord", "~> 6.1" gemspec path: "../" jsonb-accessor-1.3.10/gemfiles/activerecord_6.0.0.gemfile0000644000175000017500000000017314513544510021516 0ustar raviravi# This file was generated by Appraisal source "https://rubygems.org" gem "activerecord", "~> 6.0.6" gemspec path: "../" jsonb-accessor-1.3.10/gemfiles/activerecord_5.2.0.gemfile0000644000175000017500000000017314513544510021517 0ustar raviravi# This file was generated by Appraisal source "https://rubygems.org" gem "activerecord", "~> 5.2.8" gemspec path: "../" jsonb-accessor-1.3.10/gemfiles/activerecord_5.1.0.gemfile0000644000175000017500000000017314513544510021516 0ustar raviravi# This file was generated by Appraisal source "https://rubygems.org" gem "activerecord", "~> 5.1.3" gemspec path: "../" jsonb-accessor-1.3.10/gemfiles/activerecord_5.0.0.gemfile0000644000175000017500000000017314513544510021515 0ustar raviravi# This file was generated by Appraisal source "https://rubygems.org" gem "activerecord", "~> 5.0.0" gemspec path: "../" jsonb-accessor-1.3.10/docker-compose.yml0000644000175000017500000000134714513544510016637 0ustar raviraviversion: '3' services: ruby: environment: - DATABASE_HOST=postgres build: args: - RUBY_VERSION=${RUBY_VERSION:-3.2.2} - RUBY_PLATFORM=${RUBY_PLATFORM:-ruby} context: . volumes: - '.:/usr/src/app' depends_on: - postgres postgres: image: postgres:12 environment: - POSTGRES_HOST_AUTH_METHOD=trust - POSTGRES_DB=jsonb_accessor - PGDATA=/var/lib/postgresql/data/pgdata volumes: - pg_data:/var/lib/postgresql/data/pgdata ports: - 5432:5432 volumes: pg_data: jsonb-accessor-1.3.10/db/0000755000175000017500000000000014513544510013562 5ustar raviravijsonb-accessor-1.3.10/db/schema.rb0000644000175000017500000000256014513544510015352 0ustar raviravi# frozen_string_literal: true # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # # Note that this schema.rb definition is the authoritative source for your # database schema. If you need to create the application database on another # system, you should be using db:schema:load, not running all the migrations # from scratch. The latter is a flawed and unsustainable approach (the more migrations # you'll amass, the slower it'll run and the greater likelihood for issues). # # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema.define(version: 20_150_407_031_737) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" create_table "product_categories", id: :serial, force: :cascade do |t| t.jsonb "options" end create_table "products", id: :serial, force: :cascade do |t| t.jsonb "options" t.jsonb "data" t.string "string_type" t.integer "integer_type" t.integer "product_category_id" t.boolean "boolean_type" t.float "float_type" t.time "time_type" t.date "date_type" t.datetime "datetime_type" t.decimal "decimal_type" end end jsonb-accessor-1.3.10/db/migrate/0000755000175000017500000000000014513544510015212 5ustar raviravijsonb-accessor-1.3.10/db/migrate/20150407031737_set_up_testing_db.rb0000644000175000017500000000102114513544510022701 0ustar raviravi# frozen_string_literal: true class SetUpTestingDb < ActiveRecord::Migration[5.0] def change create_table :products do |t| t.jsonb :options t.jsonb :data t.string :string_type t.integer :integer_type t.integer :product_category_id t.boolean :boolean_type t.float :float_type t.time :time_type t.date :date_type t.datetime :datetime_type t.decimal :decimal_type end create_table :product_categories do |t| t.jsonb :options end end end jsonb-accessor-1.3.10/db/config.yml0000644000175000017500000000024414513544510015552 0ustar raviravidefault: &default adapter: postgresql database: jsonb_accessor host: <%= ENV.fetch("DATABASE_HOST") { "127.0.0.1" } %> user: postgres test: <<: *default jsonb-accessor-1.3.10/bin/0000755000175000017500000000000014513544510013745 5ustar raviravijsonb-accessor-1.3.10/bin/setup0000755000175000017500000000012214513544510015026 0ustar raviravi#!/bin/bash set -euo pipefail IFS=$'\n\t' bundle rake db:create rake db:migrate jsonb-accessor-1.3.10/bin/console0000755000175000017500000000070214513544510015334 0ustar raviravi#!/usr/bin/env ruby # frozen_string_literal: true require "bundler/setup" require "jsonb_accessor" require "rspec" require File.expand_path("../spec/spec_helper.rb", __dir__) dbconfig = YAML.safe_load(ERB.new(File.read(File.join("db", "config.yml"))).result, aliases: true) ActiveRecord::Base.establish_connection(dbconfig["development"]) # rubocop:disable Lint/UselessAssignment x = Product.new # rubocop:enable Lint/UselessAssignment Pry.start jsonb-accessor-1.3.10/UPGRADE_GUIDE.md0000644000175000017500000000377314513544510015535 0ustar raviravi# Upgrading from 0.X.X to 1.0.0 ## Jsonb Accessor declaration In 0.X.X you would write: ```ruby class Product < ActiveRecord::Base jsonb_accessor :data, :count, # doesn't specify a type title: :string, external_id: :integer, reviewed_at: :date_time, # snake cased previous_rankings: :integer_array, # `:type_array` key external_rankings: :array # plain array end ``` In 1.0.0 you would write: ```ruby class Product < ActiveRecord::Base jsonb_accessor :data, count: :value, # all fields must specify a type title: :string, external_id: :integer, reviewed_at: :datetime, # `:date_time` is now `:datetime` previous_rankings: [:integer, array: true], # now just the type followed by `array: true` external_rankings: [:value, array: true] # now the value type is specified as well as `array: true` end ``` There are several important differences. All fields must now specify a type, `:date_time` is now `:datetime`, and arrays are specified using a type and `array: true` instead of `type_array`. Also, in order to use the `value` type you need to register it: ```ruby # in an initializer ActiveRecord::Type.register(:value, ActiveRecord::Type::Value) ``` ### Deeply nested objects In 0.X.X you could write: ```ruby class Product < ActiveRecord::Base jsonb_accessor :data, ranking_info: { original_rank: :integer, current_rank: :integer, metadata: { ranked_on: :date } } end ``` Which would allow you to use getter and setter methods at any point in the structure. ```ruby Product.new(ranking_info: { original_rank: 3, current_rank: 5, metadata: { ranked_on: Date.today } }) product.ranking_info.original_rank # 3 product.ranking_info.metadata.ranked_on # Date.today ``` 1.0.0 does not support this syntax. If you need these sort of methods, you can create your own type `class` and register it with `ActiveRecord::Type`. [Here's an example](http://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html#method-i-attribute). jsonb-accessor-1.3.10/Rakefile0000644000175000017500000000164514513544510014650 0ustar raviravi# frozen_string_literal: true require "rubygems" require "bundler/setup" require "bundler/gem_tasks" require "rspec/core/rake_task" require "rubocop/rake_task" require "active_record" require "erb" RSpec::Core::RakeTask.new RuboCop::RakeTask.new # rubocop:disable Style/MixinUsage include ActiveRecord::Tasks # rubocop:enable Style/MixinUsage root = File.expand_path __dir__ db_dir = File.join(root, "db") DatabaseTasks.root = root DatabaseTasks.db_dir = db_dir DatabaseTasks.database_configuration = YAML.safe_load(ERB.new(File.read(File.join(db_dir, "config.yml"))).result, aliases: true) DatabaseTasks.migrations_paths = [File.join(db_dir, "migrate")] DatabaseTasks.env = "test" task :environment do ActiveRecord::Base.configurations = DatabaseTasks.database_configuration ActiveRecord::Base.establish_connection DatabaseTasks.env.to_sym end load "active_record/railties/databases.rake" task(default: %i[rubocop spec]) jsonb-accessor-1.3.10/README.md0000644000175000017500000002651014513544510014460 0ustar raviravi# JSONb Accessor Created by     [Tandem Logo](https://www.madeintandem.com/) [![Gem Version](https://badge.fury.io/rb/jsonb_accessor.svg)](http://badge.fury.io/rb/jsonb_accessor)    ![CI](https://github.com/madeintandem/jsonb_accessor/actions/workflows/ci.yml/badge.svg) JSONb Accessor Logo Adds typed `jsonb` backed fields as first class citizens to your `ActiveRecord` models. This gem is similar in spirit to [HstoreAccessor](https://github.com/madeintandem/hstore_accessor), but the `jsonb` column in PostgreSQL has a few distinct advantages, mostly around nested documents and support for collections. It also adds generic scopes for querying `jsonb` columns. ## Table of Contents - [Installation](#installation) - [Usage](#usage) - [Scopes](#scopes) - [Single-Table Inheritance](#single-table-inheritance) - [Dependencies](#dependencies) - [Validations](#validations) - [Upgrading](#upgrading) - [Development](#development) - [Contributing](#contributing) ## Installation Add this line to your application's `Gemfile`: ```ruby gem "jsonb_accessor" ``` And then execute: $ bundle install ## Usage First we must create a model which has a `jsonb` column available to store data into it: ```ruby class CreateProducts < ActiveRecord::Migration def change create_table :products do |t| t.jsonb :data end end end ``` We can then declare the `jsonb` fields we wish to expose via the accessor: ```ruby class Product < ActiveRecord::Base jsonb_accessor :data, title: :string, external_id: :integer, reviewed_at: :datetime end ``` Any type the [`attribute` API](http://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html#method-i-attribute) supports. You can also implement your own type by following the example in the `attribute` documentation. To pass through options like `default` and `array` to the `attribute` API, just put them in an array. ```ruby class Product < ActiveRecord::Base jsonb_accessor :data, title: [:string, default: "Untitled"], previous_titles: [:string, array: true, default: []] end ``` The `default` option works pretty much as you would expect in practice; if no values are set for the attributes, a hash of the specified default values is saved to the jsonb column. You can also pass in a `store_key` option. ```ruby class Product < ActiveRecord::Base jsonb_accessor :data, title: [:string, store_key: :t] end ``` This allows you to use `title` for your getters and setters, but use `t` as the key in the `jsonb` column. ```ruby product = Product.new(title: "Foo") product.title #=> "Foo" product.data #=> { "t" => "Foo" } ``` ## Scopes Jsonb Accessor provides several scopes to make it easier to query `jsonb` columns. `jsonb_contains`, `jsonb_number_where`, `jsonb_time_where`, and `jsonb_where` are available on all `ActiveRecord::Base` subclasses and don't require that you make use of the `jsonb_accessor` declaration. If a class does have a `jsonb_accessor` declaration, then we define one custom scope. So, let's say we have a class that looks like this: ```ruby class Product < ActiveRecord::Base jsonb_accessor :data, name: :string, price: [:integer, store_key: :p], price_in_cents: :integer, reviewed_at: :datetime end ``` Jsonb Accessor will add a `scope` to `Product` called like the json column with `_where` suffix, in our case `data_where`. ```ruby Product.all.data_where(name: "Granite Towel", price: 17) ``` Similarly, it will also add a `data_where_not` `scope` to `Product`. ```ruby Product.all.data_where_not(name: "Plasma Fork") ``` For number fields you can query using `<` or `>`or use plain english if that's what you prefer. ```ruby Product.all.data_where(price: { <: 15 }) Product.all.data_where(price: { <=: 15 }) Product.all.data_where(price: { less_than: 15 }) Product.all.data_where(price: { less_than_or_equal_to: 15 }) Product.all.data_where(price: { >: 15 }) Product.all.data_where(price: { >=: 15 }) Product.all.data_where(price: { greater_than: 15 }) Product.all.data_where(price: { greater_than_or_equal_to: 15 }) Product.all.data_where(price: { greater_than: 15, less_than: 30 }) ``` For time related fields you can query using `before` and `after`. ```ruby Product.all.data_where(reviewed_at: { before: Time.current.beginning_of_week, after: 4.weeks.ago }) ``` If you want to search for records within a certain time, date, or number range, just pass in the range (Note: this is just shorthand for the above mentioned `before`/`after`/`less_than`/`less_than_or_equal_to`/`greater_than_or_equal_to`/etc options). ```ruby Product.all.data_where(price: 10..20) Product.all.data_where(price: 10...20) Product.all.data_where(reviewed_at: Time.current..3.days.from_now) ``` This scope is a convenient wrapper around the `jsonb_where` `scope` that saves you from having to convert the given keys to the store keys and from specifying the column. ### `jsonb_where` Works just like the [`scope` above](#scopes) except that it does not convert the given keys to store keys and you must specify the column name. For example: ```ruby Product.all.jsonb_where(:data, reviewed_at: { before: Time.current }, p: { greater_than: 5 }) # instead of Product.all.data_where(reviewed_at: { before: Time.current }, price: { greater_than: 5 }) ``` This scope makes use of the `jsonb_contains`, `jsonb_number_where`, and `jsonb_time_where` `scope`s. ### `jsonb_where_not` Just the opposite of `jsonb_where`. Note that this will automatically exclude all records that contain `null` in their jsonb column (the `data` column, in the example below). ```ruby Product.all.jsonb_where_not(:data, reviewed_at: { before: Time.current }, p: { greater_than: 5 }) ``` ### `_order` Orders your query according to values in the Jsonb Accessor fields similar to ActiveRecord's `order`. ```ruby Product.all.data_order(:price) Product.all.data_order(:price, :reviewed_at) Product.all.data_order(:price, reviewed_at: :desc) ``` It will convert your given keys into store keys if necessary. ### `jsonb_order` Allows you to order by a Jsonb Accessor field. ```ruby Product.all.jsonb_order(:data, :price, :asc) Product.all.jsonb_order(:data, :price, :desc) ``` ### `jsonb_contains` Returns all records that contain the given JSON paths. ```ruby Product.all.jsonb_contains(:data, title: "foo") Product.all.jsonb_contains(:data, reviewed_at: 10.minutes.ago, p: 12) # Using the store key ``` **Note:** Under the hood, `jsonb_contains` uses the [`@>` operator in Postgres](https://www.postgresql.org/docs/9.5/static/functions-json.html) so when you include an array query, the stored array and the array used for the query do not need to match exactly. For example, when queried with `[1, 2]`, records that have arrays of `[2, 1, 3]` will be returned. ### `jsonb_excludes` Returns all records that exclude the given JSON paths. Pretty much the opposite of `jsonb_contains`. Note that this will automatically exclude all records that contain `null` in their jsonb column (the `data` column, in the example below). ```ruby Product.all.jsonb_excludes(:data, title: "foo") Product.all.jsonb_excludes(:data, reviewed_at: 10.minutes.ago, p: 12) # Using the store key ``` ### `jsonb_number_where` Returns all records that match the given criteria. ```ruby Product.all.jsonb_number_where(:data, :price_in_cents, :greater_than, 300) ``` It supports: - `>` - `>=` - `greater_than` - `greater_than_or_equal_to` - `<` - `<=` - `less_than` - `less_than_or_equal_to` and it is indifferent to strings/symbols. ### `jsonb_number_where_not` Returns all records that do not match the given criteria. It's the opposite of `jsonb_number_where`. Note that this will automatically exclude all records that contain `null` in their jsonb column (the `data` column, in the example below). ```ruby Product.all.jsonb_number_where_not(:data, :price_in_cents, :greater_than, 300) ``` ### `jsonb_time_where` Returns all records that match the given criteria. ```ruby Product.all.jsonb_time_where(:data, :reviewed_at, :before, 2.days.ago) ``` It supports `before` and `after` and is indifferent to strings/symbols. ### `jsonb_time_where_not` Returns all records that match the given criteria. The opposite of `jsonb_time_where`. Note that this will automatically exclude all records that contain `null` in their jsonb column (the `data` column, in the example below). ```ruby Product.all.jsonb_time_where_not(:data, :reviewed_at, :before, 2.days.ago) ``` ## Single-Table Inheritance One of the big issues with `ActiveRecord` single-table inheritance (STI) is sparse columns. Essentially, as sub-types of the original table diverge further from their parent more columns are left empty in a given table. Postgres' `jsonb` type provides part of the solution in that the values in an `jsonb` column does not impose a structure - different rows can have different values. We set up our table with an `jsonb` field: ```ruby # db/migration/_create_players.rb class CreateVehicles < ActiveRecord::Migration def change create_table :vehicles do |t| t.string :make t.string :model t.integer :model_year t.string :type t.jsonb :data end end end ``` And for our models: ```ruby # app/models/vehicle.rb class Vehicle < ActiveRecord::Base end # app/models/vehicles/automobile.rb class Automobile < Vehicle jsonb_accessor :data, axle_count: :integer, weight: :float end # app/models/vehicles/airplane.rb class Airplane < Vehicle jsonb_accessor :data, engine_type: :string, safety_rating: :integer end ``` From here any attributes specific to any sub-class can be stored in the `jsonb` column avoiding sparse data. Indices can also be created on individual fields in an `jsonb` column. This approach was originally conceived by Joe Hirn in [this blog post](https://madeintandem.com/blog/2013-3-single-table-inheritance-hstore-lovely-combination/). ## Validations Because this gem promotes attributes nested into the JSON column to first level attributes, most validations should just work. Please leave us feedback if they're not working as expected. ## Dependencies - ActiveRecord >= 5.0 - Postgres >= 9.4 (in order to use the [jsonb column type](http://www.postgresql.org/docs/9.4/static/datatype-json.html)). ## Upgrading See the [upgrade guide](UPGRADE_GUIDE.md). ## Development ### On your local machine After checking out the repo, run `bin/setup` to install dependencies (make sure postgres is running first). Run `bin/console` for an interactive prompt that will allow you to experiment. `rake` will run Rubocop and the specs. ### With Docker ``` # setup docker-compose build docker-compose run ruby rake db:migrate # run test suite docker-compose run ruby rake spec ``` ## Contributing 1. [Fork it](https://github.com/madeintandem/jsonb_accessor/fork) 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Add tests and changes (run the tests with `rake`) 4. Commit your changes (`git commit -am 'Add some feature'`) 5. Push to the branch (`git push origin my-new-feature`) 6. Create a new Pull Request ## Alternatives - https://github.com/DmitryTsepelev/store_model 💪 - https://github.com/palkan/store_attribute ❤️ - https://github.com/jrochkind/attr_json 🤩 jsonb-accessor-1.3.10/Makefile0000644000175000017500000000071514513544510014640 0ustar raviravibuild-gem: @docker build --build-arg RUBY_PLATFORM=ruby --build-arg RUBY_VERSION=3.2.2 -t jsonb_accessor-ruby:3.2.2 . @docker run --rm -v $(PWD):/usr/src/app -w /usr/src/app jsonb_accessor-ruby:3.2.2 gem build build-gem-java: @docker build --build-arg RUBY_PLATFORM=jruby --build-arg RUBY_VERSION=9.4.2-jdk -t jsonb_accessor-jruby:9.4.2-jdk . @docker run --rm -v $(PWD):/usr/src/app -w /usr/src/app jsonb_accessor-jruby:9.4.2-jdk gem build --platform java jsonb-accessor-1.3.10/LICENSE.txt0000644000175000017500000000207314513544510015022 0ustar raviraviThe MIT License (MIT) Copyright (c) 2015 Michael Crismali Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. jsonb-accessor-1.3.10/Gemfile0000644000175000017500000000020214513544510014462 0ustar raviravi# frozen_string_literal: true source "https://rubygems.org" # Specify your gem's dependencies in jsonb_accessor.gemspec gemspec jsonb-accessor-1.3.10/Dockerfile0000644000175000017500000000045714513544510015175 0ustar raviraviARG RUBY_VERSION ARG RUBY_PLATFORM FROM ${RUBY_PLATFORM}:${RUBY_VERSION} RUN apt-get update && apt-get install -y --no-install-recommends git WORKDIR /usr/src/app COPY lib/jsonb_accessor/version.rb ./lib/jsonb_accessor/version.rb COPY jsonb_accessor.gemspec Gemfile ./ # RUN bundle install COPY . ./ jsonb-accessor-1.3.10/CODE_OF_CONDUCT.md0000644000175000017500000000261514513544510016000 0ustar raviravi# Contributor Code of Conduct As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) jsonb-accessor-1.3.10/CHANGELOG.md0000644000175000017500000000254114513544510015010 0ustar raviravi# Changelog ## [Unreleased] ## [1.3.10] - 2023-05-30 ### No changes A new release was necessary to fix the corrupted 1.3.9 Java release on RubyGems. ## [1.3.9] - 2023-05-30 ### No changes A new release was necessary to fix the corrupted 1.3.8 Java release on RubyGems. ## [1.3.8] - 2023-05-29 ### Fixes - Support for ActiveRecord::Enum. [#163](https://github.com/madeintandem/jsonb_accessor/pull/163) ## [1.3.7] - 2022-12-29 - jruby support. jsonb_accessor now depends on `activerecord-jdbcpostgresql-adapter` instead of `pg` when the RUBY_PLATFORM is java. [#157](https://github.com/madeintandem/jsonb_accessor/pull/157) ## [1.3.6] - 2022-09-23 ### Fixed - Bug fix: Datetime values were not properly deserialized [#155](https://github.com/madeintandem/jsonb_accessor/pull/155) ## [1.3.5] - 2022-07-23 ### Fixed - Bug fix: Attributes defined outside of jsonb_accessor are not written [#149](https://github.com/madeintandem/jsonb_accessor/pull/149) ## [1.3.4] - 2022-02-02 ### Fixed - Bug fix: Raised ActiveModel::MissingAttributeError when model was initialized without the jsonb_accessor field [#145](https://github.com/madeintandem/jsonb_accessor/issues/145) ## [1.3.3] - 2022-01-29 ### Fixed - Bug fix: DateTime objects are now correctly written without timezone information [#137](https://github.com/madeintandem/jsonb_accessor/pull/137). Thanks @caiohsramos jsonb-accessor-1.3.10/Appraisals0000644000175000017500000000070614513544510015222 0ustar raviravi# frozen_string_literal: true appraise "activerecord-5.0.0" do gem "activerecord", "~> 5.0.0" end appraise "activerecord-5.1.0" do gem "activerecord", "~> 5.1.3" end appraise "activerecord-5.2.0" do gem "activerecord", "~> 5.2.8" end appraise "activerecord-6.0.0" do gem "activerecord", "~> 6.0.6" end appraise "activerecord-6.1.0" do gem "activerecord", "~> 6.1" end appraise "activerecord-7.0.1" do gem "activerecord", "~> 7.0.1" end jsonb-accessor-1.3.10/.ruby-version0000644000175000017500000000000614513544510015636 0ustar raviravi3.2.1 jsonb-accessor-1.3.10/.rubocop.yml0000644000175000017500000000246614513544510015457 0ustar raviraviAllCops: NewCops: enable TargetRubyVersion: 2.7.2 SuggestExtensions: false Exclude: - "db/**/*" - "gemfiles/**/*" - "vendor/**/*" Layout/SpaceBeforeFirstArg: Enabled: false Layout/LineLength: Enabled: false Layout/SpaceAroundEqualsInParameterDefault: Enabled: false Lint/UnusedBlockArgument: Enabled: false Lint/UnusedMethodArgument: Enabled: false Metrics/AbcSize: Enabled: false Metrics/ClassLength: Enabled: false Metrics/CyclomaticComplexity: Enabled: false Metrics/MethodLength: Enabled: false Metrics/ModuleLength: Enabled: false Metrics/PerceivedComplexity: Enabled: false Metrics/BlockLength: Enabled: false Style/ClassAndModuleChildren: Enabled: false Style/ClassVars: Enabled: false Style/Documentation: Enabled: false Style/DoubleNegation: Enabled: false Naming/FileName: Enabled: false Style/GuardClause: Enabled: false Style/NilComparison: Enabled: false Style/RescueModifier: Enabled: false Style/SignalException: Enabled: false Style/SingleLineMethods: Enabled: false Style/StringLiterals: EnforcedStyle: double_quotes Naming/BinaryOperatorParameterName: Enabled: false Naming/VariableNumber: Enabled: false Gemspec/RequiredRubyVersion: Enabled: false Gemspec/RequireMFA: Enabled: false Gemspec/DevelopmentDependencies: EnforcedStyle: gemspec jsonb-accessor-1.3.10/.rspec0000644000175000017500000000003214513544510014305 0ustar raviravi--format progress --color jsonb-accessor-1.3.10/.gitignore0000644000175000017500000000020014513544510015155 0ustar raviravi/.bundle/ /.yardoc /Gemfile.lock /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ gemfiles/.bundle gemfiles/*.gemfile.lock jsonb-accessor-1.3.10/.github/0000755000175000017500000000000014513544510014535 5ustar raviravijsonb-accessor-1.3.10/.github/workflows/0000755000175000017500000000000014513544510016572 5ustar raviravijsonb-accessor-1.3.10/.github/workflows/ci.yml0000644000175000017500000000645214513544510017717 0ustar raviraviname: CI on: push: branches: [master] pull_request: branches: ['**'] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: bundler-cache: true - name: Rubocop run: bundle exec rubocop tests: needs: lint services: db: image: postgres:9.4 env: POSTGRES_HOST_AUTH_METHOD: trust POSTGRES_DB: jsonb_accessor ports: ['5432:5432'] options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - gemfile: activerecord_5.0.0 ruby: '2.4' - gemfile: activerecord_5.1.0 ruby: '2.4' - gemfile: activerecord_5.2.0 ruby: '2.4' - gemfile: activerecord_5.0.0 ruby: '2.5' - gemfile: activerecord_5.1.0 ruby: '2.5' - gemfile: activerecord_5.2.0 ruby: '2.5' - gemfile: activerecord_6.0.0 ruby: '2.5' - gemfile: activerecord_6.1.0 ruby: '2.5' - gemfile: activerecord_5.0.0 ruby: '2.6' - gemfile: activerecord_5.1.0 ruby: '2.6' - gemfile: activerecord_5.2.0 ruby: '2.6' - gemfile: activerecord_6.0.0 ruby: '2.6' - gemfile: activerecord_6.1.0 ruby: '2.6' - gemfile: activerecord_5.0.0 ruby: '2.7' - gemfile: activerecord_5.1.0 ruby: '2.7' - gemfile: activerecord_5.2.0 ruby: '2.7' - gemfile: activerecord_6.0.0 ruby: '2.7' - gemfile: activerecord_6.1.0 ruby: '2.7' - gemfile: activerecord_7.0.1 ruby: '2.7' - gemfile: activerecord_6.1.0 ruby: '3.0' - gemfile: activerecord_6.1.0 ruby: '3.1' - gemfile: activerecord_6.1.0 ruby: '3.2' - gemfile: activerecord_7.0.1 ruby: '3.0' - gemfile: activerecord_7.0.1 ruby: '3.1' - gemfile: activerecord_7.0.1 ruby: '3.2' - gemfile: activerecord_5.1.0 ruby: 'jruby-9.3' - gemfile: activerecord_5.2.0 ruby: 'jruby-9.3' - gemfile: activerecord_6.0.0 ruby: 'jruby-9.3' - gemfile: activerecord_6.1.0 ruby: 'jruby-9.3' # waiting for https://github.com/jruby/activerecord-jdbc-adapter/issues/1125 to be addressed # - gemfile: activerecord_7.0.1 # ruby: 'jruby-9.4' name: ${{ matrix.gemfile }}, ruby ${{ matrix.ruby }} steps: - uses: actions/checkout@v3 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - name: Bundle install run: | bundle config set gemfile "${GITHUB_WORKSPACE}/gemfiles/${{ matrix.gemfile }}.gemfile" bundle install --jobs 4 --retry 3 - name: Setup DB run: | bundle exec rake db:schema:load - name: Run tests run: | bundle exec rake spec