Last active
February 25, 2021 05:17
-
-
Save yoshida-eth0/25487c425ff613a9b9cf3d4a59c76b15 to your computer and use it in GitHub Desktop.
DynamoidのHash-Range Tableでhas_many/has_one/belongs_to/embeds_many/embeds_oneできるようにした。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
DynamoidのHash-Range Tableでhas_many/has_one/belongs_to/embeds_many/embeds_oneできるようにした。 | |
Dynamoid標準のhas_many/has_one/belongs_toは双方のドキュメントに相手のHash IDを保持する構成だけど、 | |
これはActiveRecord同様にbelongs_toを定義した側にidを持つ。 | |
hr_belongs_toで指定したhash_foreign_key/range_foreign_keyでwhere検索するのでsecondary indexを貼っておく。 | |
DynamoDBではsecondary indexをuniqueにできないのでhas_oneはアクセサとしては1件だけど複数件保存できてしまうので事前に自分で削除する必要あり。 | |
embedded_inは未対応。 | |
dependency: | |
yoshida-eth0/dynamoid_embeds.rb | |
https://gist.github.com/yoshida-eth0/2d47dd43438282fad686dd2b84a5b1d1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'dynamoid' | |
module Dynamoid | |
module HashRangeAssociations | |
extend ActiveSupport::Concern | |
included do | |
class_attribute :hr_options, instance_accessor: false | |
self.hr_options = {} | |
end | |
module ClassMethods | |
# @params [name] attribute name | |
# @params [options] | |
# hash_foreign_key: #{hash_key}_hash_key | |
# range_foreign_key: #{range_key}_range_key | |
# inverse_of: #{document_name} | |
# class: target class | |
# class_name: target classname | |
# where: lambda | |
def hr_has_many(name, options = {}) | |
option = HasOption.new(self, name, :plural, options) | |
hr_association(name, option, HasMany) | |
end | |
# @params [name] attribute name | |
# @params [options] | |
# hash_foreign_key: #{hash_key}_hash_key | |
# range_foreign_key: #{range_key}_range_key | |
# inverse_of: #{document_name} | |
# class: target class | |
# class_name: target classname | |
# where: lambda | |
def hr_has_one(name, options = {}) | |
option = HasOption.new(self, name, :single, options) | |
hr_association(name, option, HasOne) | |
end | |
# @params [name] attribute name | |
# @params [options] | |
# hash_foreign_key: #{hash_key}_hash_key | |
# range_foreign_key: #{range_key}_range_key | |
# inverse_of: #{document_name} | |
# class: target class | |
# class_name: target classname | |
# where: lambda | |
def hr_belongs_to(name, options = {}) | |
option = BelongsOption.new(self, name, :single, options) | |
hr_association(name, option, BelongsTo) | |
if !self.instance_methods.include?(option.hash_foreign_key) | |
field option.hash_foreign_key, option.hash_foreign_type | |
end | |
if option.target_class.range_key && !self.instance_methods.include?(option.range_foreign_key) | |
field option.range_foreign_key, option.range_foreign_type | |
end | |
end | |
# @params [name] attribute name | |
# @params [options] | |
# inverse_of: #{document_name} | |
# class: target class | |
# class_name: target classname | |
# autobuild: bool | |
def embeds_many(name, options = {}) | |
option = HasOption.new(self, name, :plural, options) | |
options[:default] = options[:autobuild] ? -> { [] } : nil | |
self.hr_options[name] = option | |
field(name, Dynamoid::EmbedsMany(option.target_class), options) | |
end | |
# @params [name] attribute name | |
# @params [options] | |
# inverse_of: #{document_name} | |
# class: target class | |
# class_name: target classname | |
# autobuild: bool | |
def embeds_one(name, options = {}) | |
option = HasOption.new(self, name, :single, options) | |
options[:default] = options[:autobuild] ? -> { option.target_class.new } : nil | |
self.hr_options[name] = option | |
field(name, option.target_class, options) | |
end | |
# @params [name] attribute name | |
# @params [options] | |
# inverse_of: #{document_name} | |
def embedded_in(name, options = {}) | |
option = BelongsOption.new(self, name, :single, options) | |
self.hr_options[name] = option | |
# TODO: embeds側を参照できるようにする | |
end | |
def hr_association(name, option, association_class) | |
self.hr_options[name] = option | |
define_method(name) do | |
@hr_associations ||= {} | |
@hr_associations[name] ||= association_class.new(self, name) | |
end | |
define_method("#{name}=") do |value| | |
self.send(name).setter(value) | |
end | |
end | |
end | |
class Association < Delegator | |
def initialize(source, name) | |
@source = source | |
@name = name | |
@option = source.class.hr_options[name] | |
@target = nil | |
@loaded = false | |
end | |
def loaded? | |
@loaded | |
end | |
def target | |
unless loaded? | |
@target = find_target | |
@loaded = true | |
update_reference | |
end | |
@target | |
end | |
def reset | |
@target = nil | |
@loaded = false | |
end | |
def __getobj__ | |
target | |
end | |
def __setobj__(obj) | |
setter(obj) | |
end | |
end | |
class ManyAssociation < Association | |
def add_loaded_target(object) | |
if loaded? | |
@target << object | |
end | |
end | |
end | |
class SingleAssociation < Association | |
def set_loaded_target(object) | |
@target = object | |
@loaded = true | |
end | |
end | |
class HasMany < ManyAssociation | |
def find_target | |
query = {} | |
query[@option.hash_foreign_key] = @source.hash_key | |
if @source.class.range_key | |
query[@option.range_foreign_key] = @source.range_value | |
end | |
@option.target_chain(@source).where(query).all.to_a | |
end | |
def setter(object) | |
while 0<target.length | |
delete(target.first) | |
end | |
self << object | |
object | |
end | |
def delete(object) | |
target.delete(object) | |
object.delete | |
end | |
def <<(object) | |
Array(object).each {|o| | |
o.send(@option.target_field_name).setter(@source) | |
@target << o | |
} | |
end | |
def update_reference | |
@target.each {|object| | |
object.send(@option.target_field_name).set_loaded_target(@source) | |
} | |
end | |
end | |
class HasOne < SingleAssociation | |
def find_target | |
query = {} | |
query[@option.hash_foreign_key] = @source.hash_key | |
if @source.class.range_key | |
query[@option.range_foreign_key] = @source.range_value | |
end | |
@option.target_chain(@source).where(query).first | |
end | |
def setter(object) | |
if target | |
delete(target) | |
end | |
self << object | |
object | |
end | |
def delete(object) | |
target.delete(object) | |
object.delete | |
end | |
def <<(object) | |
if object | |
object.send(@option.target_field_name).setter(@source) | |
end | |
@target = object | |
end | |
def update_reference | |
@target.send(@option.target_field_name).set_loaded_target(@source) | |
end | |
end | |
class BelongsTo < SingleAssociation | |
def find_target | |
query = {} | |
query[@option.target_class.hash_key] = @source.send(@option.hash_foreign_key) | |
if @option.target_class.range_key | |
query[@option.target_class.range_key] = @source.send(@option.range_foreign_key) | |
end | |
@option.target_chain(@source).where(query).first | |
end | |
def setter(object) | |
@target = object | |
if object | |
@source.send("#{@option.hash_foreign_key}=", object.hash_key) | |
if @option.target_class.range_key | |
@source.send("#{@option.range_foreign_key}=", object.range_value) | |
end | |
target_association = object.send(@option.target_field_name) | |
target_option = @option.target_option | |
if target_option.type==:plural | |
target_association.add_loaded_target(@source) | |
elsif target_option.type==:single | |
target_association.set_loaded_target(@source) | |
end | |
end | |
@loaded = true | |
object | |
end | |
def update_reference | |
target_option = @option.target_option | |
if target_option.type==:single | |
@target.send(@option.target_field_name).set_loaded_target(@source) | |
end | |
end | |
end | |
module Option | |
attr_reader :source_class | |
attr_reader :name | |
attr_reader :type | |
attr_reader :options | |
def initialize(source_class, name, type, options={}) | |
@source_class = source_class | |
@name = name | |
@type = type | |
@options = options | |
end | |
def target_class | |
options[:class] || (options[:class_name] || name).to_s.classify.constantize | |
end | |
def where_lambda | |
options[:where] | |
end | |
def target_chain(source) | |
chain = Dynamoid::Criteria::Chain.new(target_class) | |
if where_lambda | |
if 0<where_lambda.arity | |
chain.instance_exec(source, &where_lambda) | |
else | |
chain.instance_exec(&where_lambda) | |
end | |
end | |
chain | |
end | |
end | |
class HasOption | |
include Option | |
def target_field_name | |
single_key_name = options[:inverse_of] || source_class.to_s.underscore.to_sym | |
if target_class.hr_options[single_key_name]&.type==:single | |
return single_key_name | |
end | |
raise "undefined association: #{target_class.to_s}##{single_key_name}" | |
end | |
def target_option | |
target_class.hr_options[target_field_name] | |
end | |
def hash_foreign_key | |
target_option.hash_foreign_key | |
end | |
def range_foreign_key | |
target_option.range_foreign_key | |
end | |
def autobuild? | |
!!options[:autobuild] | |
end | |
end | |
class BelongsOption | |
include Option | |
def target_field_name | |
plural_key_name = options[:inverse_of] || source_class.to_s.underscore.pluralize.to_sym | |
single_key_name = options[:inverse_of] || source_class.to_s.underscore.to_sym | |
if target_class.hr_options[plural_key_name]&.type==:plural | |
return plural_key_name | |
end | |
if target_class.hr_options[single_key_name]&.type==:single | |
return single_key_name | |
end | |
raise "undefined association: #{target_class.to_s}##{plural_key_name} or #{target_class.to_s}##{single_key_name}" | |
end | |
def target_option | |
target_class.hr_options[target_field_name] | |
end | |
def hash_foreign_key | |
options[:hash_foreign_key] || target_option.options[:hash_foreign_key] || "#{target_class.to_s.underscore}_hash_key" | |
end | |
def range_foreign_key | |
options[:range_foreign_key] || target_option.options[:range_foreign_key] || "#{target_class.to_s.underscore}_range_key" | |
end | |
def hash_foreign_type | |
target_class.attributes[target_class.hash_key][:type] | |
end | |
def range_foreign_type | |
target_class.attributes[target_class.range_key][:type] | |
end | |
end | |
end | |
module Components | |
include Dynamoid::HashRangeAssociations | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'dynamoid' | |
module Dynamoid | |
module Document | |
def serializable_hash(options = nil) | |
attribute_names = attributes.keys.map(&:to_s) | |
return serializable_attributes(attribute_names) if options.blank? | |
if only = options[:only] | |
attribute_names &= Array(only).map(&:to_s) | |
elsif except = options[:except] | |
attribute_names -= Array(except).map(&:to_s) | |
end | |
hash = serializable_attributes(attribute_names) | |
Array(options[:methods]).each { |m| hash[m.to_s] = send(m) } | |
serializable_add_includes(options) do |association, records, opts| | |
hash[association.to_s] = if records.respond_to?(:to_ary) | |
records.to_ary.map { |a| a.serializable_hash(opts) } | |
else | |
records.serializable_hash(opts) | |
end | |
end | |
hash | |
end | |
def serializable_add_includes(options = {}) #:nodoc: | |
return unless includes = options[:include] | |
unless includes.is_a?(Hash) | |
includes = Hash[Array(includes).flat_map { |n| n.is_a?(Hash) ? n.to_a : [[n, {}]] }] | |
end | |
includes.each do |association, opts| | |
records = send(association) | |
if Dynamoid::HashRangeAssociations::HasMany===records || Dynamoid::HashRangeAssociations::HasOne===records || Dynamoid::HashRangeAssociations::BelongsTo===records | |
records = records.target | |
end | |
if records | |
yield association, records, opts | |
end | |
end | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# path: /user/{user_name}/{item_title} | |
def show(event:, context:) | |
user_name = event["pathParameters"]["user_name"] | |
item_title = event["pathParameters"]["item_title"] | |
begin | |
item = Item.find(title: item_title, user_range_key: user_name) | |
{ | |
statusCode: 200, | |
body: { | |
item: item, | |
user: item&.user, | |
}.to_json | |
} | |
rescue Dynamoid::Errors::RecordNotFound => e | |
{ | |
statusCode: 404, | |
body: { | |
type: e.class.name, | |
message: e.message, | |
}.to_json | |
} | |
end | |
end | |
# path: /user/{user_name}/{item_title} | |
def create(event:, context:) | |
user_name = event["pathParameters"]["user_name"] | |
item_title = event["pathParameters"]["item_title"] | |
begin | |
user = User.find("DUMMY_POOL_ID", range_key: user_name) | |
item = Item.new(pool_id: "DUMMY_POOL_ID", title: item_title) | |
item.user = user | |
item.save | |
{ | |
statusCode: 200, | |
body: { | |
item: item, | |
user: user, | |
items: user.items, | |
}.to_json | |
} | |
rescue Dynamoid::Errors::RecordNotFound => e | |
{ | |
statusCode: 404, | |
body: { | |
type: e.class.name, | |
message: e.message, | |
}.to_json | |
} | |
end | |
end | |
# path: /user/{user_name}/{item_title} | |
def delete(event:, context:) | |
user_name = event["pathParameters"]["user_name"] | |
item_title = event["pathParameters"]["item_title"] | |
begin | |
item = Item.where(title: item_title, user_name: user_name).first | |
raise Dynamoid::Errors::RecordNotFound unless item | |
item.delete | |
{ | |
statusCode: 200, | |
body: { | |
item: item, | |
}.to_json | |
} | |
rescue Dynamoid::Errors::RecordNotFound => e | |
{ | |
statusCode: 404, | |
body: { | |
type: e.class.name, | |
message: e.message, | |
}.to_json | |
} | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# path: /users | |
def index(event:, context:) | |
{ | |
statusCode: 200, | |
body: { | |
users: User.all, | |
items: Item.all, | |
}.to_json | |
} | |
end | |
# path: /users/{user_name} | |
def show(event:, context:) | |
user_name = event["pathParameters"]["user_name"] | |
begin | |
user = User.find("DUMMY_POOL_ID", range_key: user_name) | |
{ | |
statusCode: 200, | |
body: { | |
user: user, | |
items: user&.items, | |
}.to_json | |
} | |
rescue Dynamoid::Errors::RecordNotFound => e | |
{ | |
statusCode: 404, | |
body: { | |
type: e.class.name, | |
message: e.message, | |
}.to_json | |
} | |
end | |
end | |
# path: /users/{user_name} | |
def create(event:, context:) | |
user_name = event["pathParameters"]["user_name"] | |
user = User.new(pool_id: "DUMMY_POOL_ID", name: user_name) | |
user.save | |
{ | |
statusCode: 200, | |
body: { | |
user: user, | |
items: user&.items, | |
}.to_json | |
} | |
end | |
# path: /users/{user_name} | |
def delete(event:, context:) | |
user_name = event["pathParameters"]["user_name"] | |
begin | |
user = User.find("DUMMY_POOL_ID", range_key: user_name) | |
user.items.each {|item| | |
item.delete | |
} | |
user.delete | |
{ | |
statusCode: 200, | |
body: { | |
user: user, | |
items: user&.items, | |
}.to_json | |
} | |
rescue Dynamoid::Errors::RecordNotFound => e | |
{ | |
statusCode: 404, | |
body: { | |
type: e.class.name, | |
message: e.message, | |
}.to_json | |
} | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$ curl -X POST http://localhost:3000/local/users/tanaka | jq . | |
% Total % Received % Xferd Average Speed Time Time Time Current | |
Dload Upload Total Spent Left Speed | |
100 153 100 153 0 0 105 0 0:00:01 0:00:01 --:--:-- 105 | |
{ | |
"user": { | |
"pool_id": "DUMMY_POOL_ID", | |
"name": "tanaka", | |
"created_at": "2021-02-18T16:39:48.682+09:00", | |
"updated_at": "2021-02-18T16:39:48.682+09:00" | |
}, | |
"items": [] | |
} | |
$ curl -X POST http://localhost:3000/local/users/satou | jq . | |
% Total % Received % Xferd Average Speed Time Time Time Current | |
Dload Upload Total Spent Left Speed | |
100 152 100 152 0 0 114 0 0:00:01 0:00:01 --:--:-- 114 | |
{ | |
"user": { | |
"pool_id": "DUMMY_POOL_ID", | |
"name": "satou", | |
"created_at": "2021-02-18T16:39:52.970+09:00", | |
"updated_at": "2021-02-18T16:39:52.970+09:00" | |
}, | |
"items": [] | |
} | |
$ curl -X POST http://localhost:3000/local/users/tanaka/apple | jq . | |
% Total % Received % Xferd Average Speed Time Time Time Current | |
Dload Upload Total Spent Left Speed | |
100 469 100 469 0 0 325 0 0:00:01 0:00:01 --:--:-- 325 | |
{ | |
"item": { | |
"pool_id": "DUMMY_POOL_ID", | |
"title": "apple", | |
"user_name": "tanaka", | |
"created_at": "2021-02-18T16:39:55.930+09:00", | |
"updated_at": "2021-02-18T16:39:55.930+09:00" | |
}, | |
"user": { | |
"created_at": "2021-02-18T07:39:48.682+00:00", | |
"updated_at": "2021-02-18T07:39:48.682+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"name": "tanaka" | |
}, | |
"items": [ | |
{ | |
"created_at": "2021-02-18T07:39:55.930+00:00", | |
"updated_at": "2021-02-18T07:39:55.930+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"title": "apple", | |
"user_name": "tanaka" | |
} | |
] | |
} | |
$ curl -X POST http://localhost:3000/local/users/tanaka/orange | jq . | |
% Total % Received % Xferd Average Speed Time Time Time Current | |
Dload Upload Total Spent Left Speed | |
100 626 100 626 0 0 450 0 0:00:01 0:00:01 --:--:-- 450 | |
{ | |
"item": { | |
"pool_id": "DUMMY_POOL_ID", | |
"title": "orange", | |
"user_name": "tanaka", | |
"created_at": "2021-02-18T16:39:58.798+09:00", | |
"updated_at": "2021-02-18T16:39:58.798+09:00" | |
}, | |
"user": { | |
"created_at": "2021-02-18T07:39:48.682+00:00", | |
"updated_at": "2021-02-18T07:39:48.682+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"name": "tanaka" | |
}, | |
"items": [ | |
{ | |
"created_at": "2021-02-18T07:39:58.798+00:00", | |
"updated_at": "2021-02-18T07:39:58.798+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"title": "orange", | |
"user_name": "tanaka" | |
}, | |
{ | |
"created_at": "2021-02-18T07:39:55.930+00:00", | |
"updated_at": "2021-02-18T07:39:55.930+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"title": "apple", | |
"user_name": "tanaka" | |
} | |
] | |
} | |
$ curl -X POST http://localhost:3000/local/users/satou/banana | jq . | |
% Total % Received % Xferd Average Speed Time Time Time Current | |
Dload Upload Total Spent Left Speed | |
100 468 100 468 0 0 342 0 0:00:01 0:00:01 --:--:-- 342 | |
{ | |
"item": { | |
"pool_id": "DUMMY_POOL_ID", | |
"title": "banana", | |
"user_name": "satou", | |
"created_at": "2021-02-18T16:40:01.607+09:00", | |
"updated_at": "2021-02-18T16:40:01.607+09:00" | |
}, | |
"user": { | |
"created_at": "2021-02-18T07:39:52.970+00:00", | |
"updated_at": "2021-02-18T07:39:52.970+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"name": "satou" | |
}, | |
"items": [ | |
{ | |
"created_at": "2021-02-18T07:40:01.607+00:00", | |
"updated_at": "2021-02-18T07:40:01.607+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"title": "banana", | |
"user_name": "satou" | |
} | |
] | |
} | |
$ curl -X POST http://localhost:3000/local/users/satou/grape | jq . | |
% Total % Received % Xferd Average Speed Time Time Time Current | |
Dload Upload Total Spent Left Speed | |
100 621 100 621 0 0 430 0 0:00:01 0:00:01 --:--:-- 430 | |
{ | |
"item": { | |
"pool_id": "DUMMY_POOL_ID", | |
"title": "grape", | |
"user_name": "satou", | |
"created_at": "2021-02-18T16:40:04.612+09:00", | |
"updated_at": "2021-02-18T16:40:04.612+09:00" | |
}, | |
"user": { | |
"created_at": "2021-02-18T07:39:52.970+00:00", | |
"updated_at": "2021-02-18T07:39:52.970+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"name": "satou" | |
}, | |
"items": [ | |
{ | |
"created_at": "2021-02-18T07:40:04.612+00:00", | |
"updated_at": "2021-02-18T07:40:04.612+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"title": "grape", | |
"user_name": "satou" | |
}, | |
{ | |
"created_at": "2021-02-18T07:40:01.607+00:00", | |
"updated_at": "2021-02-18T07:40:01.607+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"title": "banana", | |
"user_name": "satou" | |
} | |
] | |
} | |
$ curl -X GET http://localhost:3000/local/users | jq . | |
% Total % Received % Xferd Average Speed Time Time Time Current | |
Dload Upload Total Spent Left Speed | |
100 908 100 908 0 0 654 0 0:00:01 0:00:01 --:--:-- 654 | |
{ | |
"users": [ | |
{ | |
"created_at": "2021-02-18T07:39:52.970+00:00", | |
"updated_at": "2021-02-18T07:39:52.970+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"name": "satou" | |
}, | |
{ | |
"created_at": "2021-02-18T07:39:48.682+00:00", | |
"updated_at": "2021-02-18T07:39:48.682+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"name": "tanaka" | |
} | |
], | |
"items": [ | |
{ | |
"created_at": "2021-02-18T07:39:55.930+00:00", | |
"updated_at": "2021-02-18T07:39:55.930+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"title": "apple", | |
"user_name": "tanaka" | |
}, | |
{ | |
"created_at": "2021-02-18T07:40:01.607+00:00", | |
"updated_at": "2021-02-18T07:40:01.607+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"title": "banana", | |
"user_name": "satou" | |
}, | |
{ | |
"created_at": "2021-02-18T07:40:04.612+00:00", | |
"updated_at": "2021-02-18T07:40:04.612+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"title": "grape", | |
"user_name": "satou" | |
}, | |
{ | |
"created_at": "2021-02-18T07:39:58.798+00:00", | |
"updated_at": "2021-02-18T07:39:58.798+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"title": "orange", | |
"user_name": "tanaka" | |
} | |
] | |
} | |
$ curl -X GET http://localhost:3000/local/users/tanaka | jq . | |
% Total % Received % Xferd Average Speed Time Time Time Current | |
Dload Upload Total Spent Left Speed | |
100 463 100 463 0 0 336 0 0:00:01 0:00:01 --:--:-- 336 | |
{ | |
"user": { | |
"created_at": "2021-02-18T07:39:48.682+00:00", | |
"updated_at": "2021-02-18T07:39:48.682+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"name": "tanaka" | |
}, | |
"items": [ | |
{ | |
"created_at": "2021-02-18T07:39:58.798+00:00", | |
"updated_at": "2021-02-18T07:39:58.798+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"title": "orange", | |
"user_name": "tanaka" | |
}, | |
{ | |
"created_at": "2021-02-18T07:39:55.930+00:00", | |
"updated_at": "2021-02-18T07:39:55.930+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"title": "apple", | |
"user_name": "tanaka" | |
} | |
] | |
} | |
$ curl -X DELETE http://localhost:3000/local/users/tanaka/orange | jq . | |
% Total % Received % Xferd Average Speed Time Time Time Current | |
Dload Upload Total Spent Left Speed | |
100 164 100 164 0 0 119 0 0:00:01 0:00:01 --:--:-- 119 | |
{ | |
"item": { | |
"created_at": "2021-02-18T07:39:58.798+00:00", | |
"updated_at": "2021-02-18T07:39:58.798+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"title": "orange", | |
"user_name": "tanaka" | |
} | |
} | |
$ curl -X GET http://localhost:3000/local/users/tanaka | jq . | |
% Total % Received % Xferd Average Speed Time Time Time Current | |
Dload Upload Total Spent Left Speed | |
100 307 100 307 0 0 225 0 0:00:01 0:00:01 --:--:-- 225 | |
{ | |
"user": { | |
"created_at": "2021-02-18T07:39:48.682+00:00", | |
"updated_at": "2021-02-18T07:39:48.682+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"name": "tanaka" | |
}, | |
"items": [ | |
{ | |
"created_at": "2021-02-18T07:39:55.930+00:00", | |
"updated_at": "2021-02-18T07:39:55.930+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"title": "apple", | |
"user_name": "tanaka" | |
} | |
] | |
} | |
$ curl -X DELETE http://localhost:3000/local/users/tanaka | jq . | |
% Total % Received % Xferd Average Speed Time Time Time Current | |
Dload Upload Total Spent Left Speed | |
100 307 100 307 0 0 231 0 0:00:01 0:00:01 --:--:-- 231 | |
{ | |
"user": { | |
"created_at": "2021-02-18T07:39:48.682+00:00", | |
"updated_at": "2021-02-18T07:39:48.682+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"name": "tanaka" | |
}, | |
"items": [ | |
{ | |
"created_at": "2021-02-18T07:39:55.930+00:00", | |
"updated_at": "2021-02-18T07:39:55.930+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"title": "apple", | |
"user_name": "tanaka" | |
} | |
] | |
} | |
$ curl -X GET http://localhost:3000/local/users | jq . | |
% Total % Received % Xferd Average Speed Time Time Time Current | |
Dload Upload Total Spent Left Speed | |
100 463 100 463 0 0 351 0 0:00:01 0:00:01 --:--:-- 351 | |
{ | |
"users": [ | |
{ | |
"created_at": "2021-02-18T07:39:52.970+00:00", | |
"updated_at": "2021-02-18T07:39:52.970+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"name": "satou" | |
} | |
], | |
"items": [ | |
{ | |
"created_at": "2021-02-18T07:40:01.607+00:00", | |
"updated_at": "2021-02-18T07:40:01.607+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"title": "banana", | |
"user_name": "satou" | |
}, | |
{ | |
"created_at": "2021-02-18T07:40:04.612+00:00", | |
"updated_at": "2021-02-18T07:40:04.612+00:00", | |
"pool_id": "DUMMY_POOL_ID", | |
"title": "grape", | |
"user_name": "satou" | |
} | |
] | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Item | |
include Dynamoid::Document | |
table key: :pool_id | |
range :title, :string | |
hr_belongs_to :user, hash_foreign_key: :pool_id, range_foreign_key: :user_name | |
local_secondary_index range_key: :user_name, projected_attributes: :all | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class User | |
include Dynamoid::Document | |
table key: :pool_id | |
range :name, :string | |
hr_has_many :items | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment